MCP Server

The Business Dash MCP (Model Context Protocol) server lets AI coding assistants — Claude, Cursor, ChatGPT, Goose, VSCode, and others — interact with your organization's data directly. The assistant can read your business profile, browse your catalog, search services, check your booking link, and even submit quote requests, all through structured tool calls.


How It Works

Each organization's published site hosts its own MCP endpoint at /.well-known/mcp.json. AI clients that support MCP discovery find it automatically when pointed at your site URL.

The server implements the Streamable HTTP Transport specification (JSON-RPC 2.0 over HTTP POST).

Before the MCP endpoint responds, the organization must opt in by enabling the MCP App feature in the dashboard settings.


Enabling MCP

  1. Go to Settings → AI & Integrations in the organization dashboard
  2. Toggle MCP App to enabled
  3. Optionally configure which write tools are allowed (see below)

Once enabled, the MCP endpoint is live at your published site's URL:

https://<your-site>.businessdash.us/.well-known/mcp.json

Or on your custom domain:

https://<your-domain>/.well-known/mcp.json

SDK-Hosted Sites (your own Next.js app)

The host-resolved endpoint above exists only on platform-served sites. If your domain hosts your own app built with the SDK, mount the proxy and your domain gains the identical connector surface:

// app/api/mcp/route.ts
import { mcpHandler } from '@businessdash/sdk/mcp'
export const { POST, GET } = mcpHandler({
  siteId: process.env.BD_SITE_ID!,
  baseUrl: process.env.NEXT_PUBLIC_BD_APP_URL!, // https://www.businessdash.us
})

// app/.well-known/mcp.json/route.ts
import { mcpManifestHandler } from '@businessdash/sdk/mcp'
export const GET = mcpManifestHandler({
  siteId: process.env.BD_SITE_ID!,
  baseUrl: process.env.NEXT_PUBLIC_BD_APP_URL!,
})

The proxy forwards verbatim to /api/public/mcp/{siteId} on the platform — the org's MCP opt-in and per-tool write gates are enforced there, so the proxy can't widen anything.


Connecting an AI Assistant

Claude Cowork / claude.ai (custom connector)

Cowork and claude.ai use the same connector system. The org's MCP endpoint is a standard remote connector URL:

  1. Open Settings → Connectors in Claude (or Cowork)
  2. Choose Add custom connector
  3. Paste the connector URL:
https://<your-domain>/api/mcp

Claude can then read the business profile, browse and search the catalog, fetch the booking link, and file quote requests straight into the CRM — using the same per-tool gating configured in the dashboard. Write tools stay off unless the org enables them.

The dashboard shows this URL pre-filled with the org's verified domain under Site Builder → SEO + AI discoverability once the MCP App is enabled.

The endpoint answers only on a verified custom domain — verify the domain before handing the URL to Claude.

ChatGPT

ChatGPT's connectors are MCP too. In Settings → Connectors (enable developer mode if the option is hidden), choose Add connector and paste the same URL:

https://<your-domain>/api/mcp

Deep Research and chat can then call the read tools; write tools follow the same per-tool dashboard gating as every other client.

Gemini CLI / Gemini Code Assist

The consumer Gemini app doesn't take custom connectors yet, but Google's agent surfaces do. For Gemini CLI, add to ~/.gemini/settings.json:

{
  "mcpServers": {
    "bd": {
      "httpUrl": "https://<your-domain>/api/mcp"
    }
  }
}

Gemini Code Assist reads the same mcpServers shape. For the org connector (https://www.businessdash.us/api/mcp/org), Gemini CLI's remote-MCP OAuth support walks the same authorization flow Claude and ChatGPT use — run /mcp auth if it doesn't prompt automatically.

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "bd": {
      "type": "streamable-http",
      "url": "https://<your-site>.businessdash.us/api/mcp"
    }
  }
}

Claude Code / Cursor / VSCode

No dedicated config needed — these clients auto-discover MCP via the .well-known/mcp.json endpoint when pointed at your site URL. Just give your assistant the site URL and it will find the tools.

Goose

Use the Streamable HTTP transport type:

# In your Goose session config
mcp_servers:
  bd:
    type: streamable-http
    url: https://<your-site>.businessdash.us/api/mcp

Available Tools

Read Tools (always on)

ToolDescription
get_business_infoReturns name, address, hours, contact info
get_company_profileFull profile — about, SEO metadata, JSON-LD, OG tags
list_servicesServices catalog with title, price, price type
list_productsProducts catalog
search_catalogSearch products and services by keyword
request_quoteSubmit a quote request (creates a CRM lead)
book_meeting_linkReturns the public scheduling/booking page URL

Write Tools (disabled by default)

ToolDescription
update_company_profilePatch website, phone, email, about text, share image
generate_blog_imageGenerate AI blog-cover image variations
generate_blog_image_dark_companionGenerate dark-theme companion for a light image

Write tools are gated per-tool. Enable them in Settings → AI & Integrations → MCP App Tools.


Write Tool Configuration

Write tools are individually gated through mcp_app_tools_config. To enable a write tool:

  1. Navigate to Settings → AI & Integrations → MCP App
  2. Toggle the specific write tool you want to allow
  3. Changes take effect immediately — no restart needed

An AI assistant calling a disabled write tool receives:

{ "ok": false, "error": "tool_disabled" }

This gives you fine-grained control. For example, you might allow update_company_profile for onboarding assistance while keeping blog image generation off.


Tool Examples

Get business info

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_business_info"
  }
}

Search catalog

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "search_catalog",
    "arguments": {
      "q": "water heater"
    }
  }
}

Submit a quote request

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "request_quote",
    "arguments": {
      "name": "Jane Smith",
      "email": "jane@example.com",
      "phone": "555-0123",
      "message": "Looking to install a tankless water heater"
    }
  }
}

Protocol Details

  • Transport: Streamable HTTP (JSON-RPC 2.0)
  • Endpoint: https://<your-site>/api/mcp
  • Discovery: https://<your-site>/.well-known/mcp.json
  • Methods supported: initialize, tools/list, tools/call, resources/list, resources/read, ping
  • Auth: Phase 2 (planned) — currently gated by organization opt-in + hostname resolution

Security

  • The MCP endpoint only responds for organizations that have explicitly enabled the feature
  • Requests are resolved per-hostname — only your published site's domain can reach your MCP endpoint
  • Write tools are individually gated and disabled by default
  • Every write action is logged with an audit trail (source, timestamp, changed fields)
  • No authentication token is required in Phase 1 — security relies on hostname resolution and the opt-in gate

Org Connector (assistant acts as staff)

Everything above is the public connector — the assistant sees what a site visitor sees. The org connector is the private counterpart: Claude or ChatGPT operates inside the workspace, holding a revocable credential an org admin explicitly approved.

https://www.businessdash.us/api/mcp/org

Adding that URL as a custom connector triggers a standard OAuth flow:

  1. The client discovers the authorization server via RFC 9728 resource metadata (/.well-known/oauth-protected-resource/api/mcp/org) and registers itself (RFC 7591 dynamic client registration — open, grants nothing by itself).
  2. The admin lands on /oauth/authorize, signed into their org, and approves — read scope always, write scope only via an explicit switch on the consent screen.
  3. The token exchanged (authorization code + PKCE S256) is minted as an agent credential — the same table auth.md agents live in, so it shows up in the org's credential list and revokes the same way.

Org-only tools on top of the full public catalog:

ToolDescription
get_org_overviewOrg name, sites, verified domains, distribution counts
get_site_trafficPageviews + visitors for a window (human traffic only)
get_ai_visibilityWhich AI crawlers fetched which pages (AEO signal)
list_ai_distribution_itemsThe resolved answer-engine distribution list

Public write tools (update_company_profile, blog image generation) become callable only when the approval included write scope — and each still passes through the org's per-tool mcp_app_tools_config gate. An OAuth approval never bypasses the org's own switches.

Mounting it on your framework

mcpHandler returns a Web-standard (Request) => Promise<Response>, which is exactly what Next's App Router, Astro, Remix and TanStack Start want — and exactly what SvelteKit, Nuxt, Qwik City and Express do not. Those four get adapters:

import {
  sveltekitMcpHandler,   // also astroMcpHandler
  nitroMcpHandler,       // Nuxt / Nitro
  qwikMcpHandler,        // Qwik City
  expressMcpHandler,     // Express, Angular SSR, Analog
} from '@businessdash/sdk/mcp-adapters'

SvelteKit — src/routes/api/mcp/+server.ts:

export const { POST, GET } = sveltekitMcpHandler({ siteId, baseUrl })

Nuxt / Nitro — server/routes/api/mcp.post.ts:

export default defineEventHandler(nitroMcpHandler({ siteId, baseUrl }))

Qwik City — src/routes/api/mcp/index.ts. Qwik answers by calling send() rather than returning, which the adapter handles:

const mcp = qwikMcpHandler({ siteId, baseUrl })
export const onPost = mcp.onPost
export const onGet = mcp.onGet

Express / Angular SSR:

const mcp = expressMcpHandler({ siteId, baseUrl })
app.post('/api/mcp', mcp)
app.get('/.well-known/mcp.json', expressMcpManifestHandler({ siteId, baseUrl }))

Each adapter is a shape-change over the same handler — none reimplements the proxy, the error mapping or the manifest caching.

Mount the manifest route too. It is what tells the platform which domain to advertise, so the connector URL an org hands to Claude or ChatGPT points at their site. On Node the origin is rebuilt from the Host header and x-forwarded-proto — behind a proxy that does not set them, the published URL will be wrong.