Skip to main content
Soundcheck MCP is hosted in-process on api-events. The @soundcheck/mcp package (mcp-ts/) is the catalog + Streamable HTTP handler library. api-events executes tools via DirectBackend → existing *Operation functions. CheckAI calls the same catalog out of process via HttpToolBackend against API_BASE_URL. End-user setup for Claude Code and Cursor lives in Soundcheck MCP server. This page covers what an operator needs.

Architecture

The MCP transport is stateless. It does not own a database; sessions exist only inside the MCP transport.

Source layout

Opt-in 1:1 tools are annotated with sibling mcpTool() next to openapiRoute(). Emit JSON Schema ToolDefs with cd api-events && npm run mcp:emit.

Local development

Then point MCP Inspector at http://localhost:4000/mcp. CheckAI uses API_BASE_URL=http://localhost:4000 (no Soundcheck MCP_URL).

OAuth 2.0 Protected Resource

api-events exposes RFC 9728 PRM at:
and RFC 8414 authorization-server metadata (live Clerk AS document, including CIMD when Clerk advertises it) at:
OpenAI Apps domain verification (plain-text token, no auth):
Override the token with OPENAI_APPS_CHALLENGE (production defaults to the pinned verification value when unset). Spec-compliant MCP clients read PRM on 401 and run the OAuth flow against Clerk. Older clients that fetch AS metadata from the resource origin get the same Clerk authorize, token, and DCR endpoints. Hosted API and MCP hosts fill authorization_servers from the resource URL when OAUTH_ISSUER is unset (HOSTED_MCP_OAUTH_ISSUERS in api-events/src/config/config.ts).

Public MCP hostname (same Cloud Run)

MCP is in-process on api-events. The public connect hostname is a third cert + host rule on the existing api-events ALB (mcp_domain_name on infra-gcp/modules/events). CheckAI does not use these hosts; it talks to api-events via API_BASE_URL. There is no leftover dedicated MCP Cloud Run in any GCP env state. The MCP host is path-restricted to /mcp, /public/mcp, and /.well-known/*. Member connect stays https://<host>/mcp (not the origin root). Do not advertise these URLs until Squarespace CNAMEs exist and the managed cert is ACTIVE (ENG-613). Apply order (Cloud Agent or human only) is in infra-gcp/README.md.

Server tracing (telemetry.dev)

api-events emits one OpenTelemetry SERVER span per JSON-RPC request on /mcp and /public/mcp (api-events/src/observability/mcpServerSpans.ts), exported over OTLP/HTTP to telemetry.dev so client and server spans pair up. The MCP SDK is v1, so the vendor’s @telemetry-dev/mcp wrapper (v2 only) is not used.
  • Parent context: W3C traceparent / tracestate from params._meta first, then the HTTP traceparent header. Clients must inject _meta — a header alone is not enough in practice, because proxies in front of the server (the harness /api/mcp-proxy among them) forward only an allow-list of headers and drop traceparent. Header-only runs show up as orphan client spans. CheckAI does this in checkai/agent/lib/mcpClientTracing.ts. Neither present means a new root span.
  • Attributes: mcp.method.name, mcp.tool.name / gen_ai.tool.name (on tools/call), gen_ai.operation.name, mcp.tier (member / public), mcp.confirmation_required (the call returned a confirmation prompt), jsonrpc.request.id, mcp.protocol.version, http.response.status_code, error.type.
  • Never captured: tool arguments or results, tool error messages, confirmation_token, bearer tokens, org ids.
  • Client-supplied values are validated before export: methods must be known MCP methods, tool names must be in the tier’s catalog (otherwise the span is just tools/call), request ids are kept only when integer or UUID, and protocol versions only when YYYY-MM-DD. At most 16 messages per request are traced.
  • Batches: the invocation hook has no request id, so when one batch calls the same tool twice those spans carry no mcp.confirmation_required / tool error.type rather than a guessed one.
  • Notifications produce no span. mcp.confirmation_required and tool error.type come from the mutating-tool invocation hook, so read-only tools always report false.
  • The provider is private (never registered globally), so Sentry’s trace sampling and the PostHog trace exporter are unaffected.
Only dev enables it (enable_telemetry_dev_api_key = true in infra-gcp/environments/dev). After terraform apply, add the key and roll a revision:
qa/prod stay off until the flag is set there after a redaction review.

Tool-call errors agents can act on

tools/call is checked before the MCP SDK sees it (mcp-ts/src/tools/preflight.ts), because the SDK answers an unknown tool or a schema miss with bare prose and nothing is recorded. Both now come back as the usual JSON envelope with isError and structuredContent:
  • Unknown tool → {"code":"unknown_tool","tool":"list_org_call_lists","did_you_mean":["get_staffing_plan"]}. Nothing ran, so no audit row is written.
  • Bad arguments → {"code":"invalid_arguments","tool":"…","missing_fields":[…],"issues":[{"field":"…","message":"…"}]}, recorded as an error invocation.
create_event_invitations is event-scoped (no organization_id) and takes invitations:[{email|user_id|phone_number, …}], or the emails:["sam@example.com"] shorthand. With valid arguments it returns confirmation_required while MCP_CONFIRMATION_SECRET is set, never a silent failure.

MCP Apps (SEP-1865)

The member tier can serve MCP Apps: interactive HTML views that hosts implementing the extension render in a sandboxed iframe next to a tool result. Code lives in mcp-ts/src/apps/: viewRuntime.ts (shared bridge, theme, widget styles, and confirm step), confirmCard.ts (ENG-855 Confirm Card), planReview.ts (ENG-856 Plan Review), staffingDraft.ts (ENG-857 Staffing Draft), gigs.ts (Gigs, with OpenAI entrypoints), toolViews.ts (which catalog tools link to which view and which results carry view data), helloWorld.ts (ENG-854 spike: tool show_hello_world_app renders ui://soundcheck/hello-world.html), and register.ts.
  • Product views are on in every environment; the hello-world spike is dev-only. handleMcpRequest always registers the Confirm Card, Plan Review, Staffing Draft, and Gigs resources, the app-only reject_confirmation tool, the card link on gated member tools, and the view links in toolViews.ts. The public tier never gets MCP Apps. The ENG-854 hello-world tool and resource are registered only with handleMcpRequest({ mcpAppsSpike: true }): api-events sets that from mcpConfig().mcpAppsSpikeEnabled = !isDeployedEnvironment() → runtime.mcpAppsSpike, so qa/prod tools/list never lists show_hello_world_app. There is no feature flag.
  • No SDK upgrade. The wire contract is plain _meta, which SDK v1.30 already passes through on tools, resources, content items, and CallToolResult: the tool has _meta.ui.resourceUri (plus the legacy _meta["ui/resourceUri"] key), and the resource is text/html;profile=mcp-app with _meta.ui.csp / prefersBorder. @modelcontextprotocol/ext-apps 2.x needs SDK v2 (@modelcontextprotocol/server) and zod 4. 1.7.x works with SDK ^1.29 and zod 3 if we later want its App class or React hooks in the view. @mcp-ui/server 6.x still pins ext-apps ^0.3; skip it.
  • Views are self-contained HTML built from viewRuntime.ts. VIEW_BRIDGE_JS is a hand-rolled postMessage JSON-RPC client (scCreateBridge): ui/initialize → ui/notifications/initialized, then tool-input / tool-result / host-context-changed / size-changed, and tools/call / ui/update-model-context back through the host. VIEW_THEME_CSS maps host variables onto --sc-* tokens for light and dark. mcpAppHtml() assembles a page. No build step, no external scripts, styles, or fonts. Views set dynamic text with textContent only.
  • Text fallback. Every app tool returns normal content text plus structuredContent. Clients without MCP Apps support ignore _meta and read the text. The server cannot detect UI support per client, because the transport is stateless and initialize capabilities are gone by the next request. So the same response has to work both ways.

Confirm Card (ENG-855)

Every confirmation-gated member tool (any tool with a confirm callback or a confirmation_token argument, including the custom-gated send_message, broadcast_event_reminder, and reply_to_mailbox_thread) links to ui://soundcheck/confirm-card.html. When the tool returns confirmation_required, hosts that support MCP Apps (Cursor, Claude) render the card: the summary, an optional structured preview, a live “Expires in …” countdown, “Nothing is sent or changed until you approve.”, and Approve / Reject.
  • What changes for tools-only clients. tools/list gains _meta on gated tools plus the reject_confirmation entry. Hosts that honour visibility hide that tool from the model. Other clients may show it, but its description says it is not for agents, and calling it can only void an unused token, never perform an action. Tool result content and structuredContent do not change.
  • Result _meta, not the body. The card’s data rides in CallToolResult._meta["soundcheck/confirm"] = { tool, arguments (without confirmation_token), preview? }. content and structuredContent are unchanged, so tools-only clients see exactly what they saw before. The token only arrives at runtime inside the tool result; the static HTML never contains one.
  • Previews. ToolConfirmResult.preview (and the optional last argument of requireConfirmation) is { title, facts: [{ label, value }], recipients: [{ name, detail? }] }. It is kept beside the confirmation payload (a WeakMap), so it never touches parts, items, the args hash, or the payload. send_message, send_crew_offer, and create_event_invitations build one from data their confirm step already has (no extra backend calls; send_message recipients are user ids). Other gated tools show the summary only.
  • Approve. Only a click sends tools/call through the host with the same arguments plus confirmation_token. The view uses the host’s ui/notifications/tool-input arguments and falls back to _meta arguments; the tool name comes from _meta then hostContext.toolInfo. A busy flag disables both buttons on the first click, so a double-click sends one call. The card then shows done, the error, or a fresh card if the server asks for confirmation again. It tells the model with ui/update-model-context (or ui/message when the host only supports that).
  • Reject burns the token. Reject calls the app-only tool reject_confirmation (_meta.ui.visibility: ["app"], accepted by preflight through mcpAppTools()) with { tool, confirmation_token, organization_id? }. Gate.reject checks the token is authentic, unexpired, and bound to this caller, organization, and tool. It then claims the action row and marks it failed with REJECTED_ACTION_ERROR. A later approve with that token fails with “was rejected by the user … get a fresh approval” instead of the generic unresolved-outcome error. Rejecting an action that already ran, is running, or failed returns an error and changes nothing. The card also tells the model the user rejected, so the agent stops.
  • Expiry. After expires_in_seconds the card disables Approve and Reject and says “This approval expired. Ask the agent to prepare it again.” The click handler re-checks expiry too.
  • Executed results. With the gate disabled (local dev without MCP_CONFIRMATION_SECRET) or after the tool ran, the card shows “Done” or the error.
  • Reuse (ENG-856 / ENG-857). Embed VIEW_BRIDGE_JS + CONFIRM_STEP_JS in a view and call scRenderToolResult(container, bridge, { tool, arguments, preview, result, onSettled? }) or scConfirmStep(container, bridge, { tool, arguments, preview, required, onSettled? }) to run the same Approve/Reject flow inside a larger widget. onSettled(outcome, result) fires once with approved, failed, rejected, reject-failed, or expired, so the widget can refresh or give its own controls back.

Plan Review (ENG-856)

get_event_plan and review_event_plan link to ui://soundcheck/plan-review.html. A tool can link to one view only, so the gated create_draft_event_from_plan keeps the Confirm Card link; the Plan Review runs the same confirm step inline instead.
  • Data. get_event_plan, review_event_plan, and update_event_plan results carry _meta["soundcheck/plan"]: a compact model built by planReviewModel() (title, dated details with provenance, venue/client, template, counts, readiness, open questions with draft-blocking ones first, open conflicts, the review’s proposed changes, status, version, created gig id). update_event_plan gets the result _meta but no view link. content and structuredContent are unchanged, so tools-only clients see the same JSON as before.
  • Answer / Dismiss. Each open question gets a select built from its own options when it has them, otherwise a text, number, date, or time input. Save sends one update_event_plan with the plan’s current plan_version and [{ op: "answer_question", question_id, value }] (Dismiss sends dismiss_question). The view submits only what the user typed or picked, never an empty answer, and re-renders from the plan in the result.
  • Version conflicts. A 412 / PLAN_VERSION_MISMATCH / PLAN_CHANGED_REVIEW_REQUIRED answer makes the view call get_event_plan once, show the fresh version, and tell the user nothing was saved. It never retries the write.
  • Mark reviewed calls review_event_plan and then shows the proposed gig. Create draft gig is enabled only when the plan is REVIEWED and readiness.can_create_draft; otherwise it is disabled and the view lists the blocking reasons. The click calls create_draft_event_from_plan without a token, renders the confirm step from the confirmation_required result, and only Approve re-sends the same arguments plus confirmation_token. Reject burns the token. A refusal from the tool’s confirm step (plan not ready) shows as an error line. A CREATED plan shows “Draft gig created” with the gig id and no controls.
  • Model sync. After each write the view sends a one-line factual summary through ui/update-model-context (or ui/message), including the new plan version, so the agent’s next edit uses it.

Staffing Draft (ENG-857)

review_staffing_draft, get_crew_offer, and save_crew_offer link to ui://soundcheck/staffing-draft.html. send_crew_offer keeps its Confirm Card link; the Staffing Draft runs the same confirm step inline.
  • New read-only tool. review_staffing_draft { event_id, organization_id? } (mcp-ts/src/tools/staffingRoster.ts) makes two backend reads, v1OrganizationsOffersList (event-scoped, complete) and v1OrganizationsPositionsList (active_only: false, for position names), and returns a model-shaped roster: shifts grouped by position, each with title, type, status, days with call times formatted in the gig’s timezone, fee, seats_needed / max_acceptances, per-person name / status / delivery / fee, counts (seats, attached, accepted, declined, pending, not sent), sendable plus a hint, gig totals, and a next_step line. Names come from the name / username / email the offer rows already carry, so there is no per-person lookup. Both ops were already in api-events DIRECT_OPS.
  • Data. The roster rides in _meta["soundcheck/staffing"]. get_crew_offer and save_crew_offer results get a single-shift roster there, built from the offer they return with no extra read; their content and structuredContent are unchanged.
  • Send. A shift is sendable when it is DRAFT with people attached (or a linked call list), or OPEN with attached PENDING people whose delivery is NOT_SENT. Its “Send…” button calls send_crew_offer { offer_id, organization_id? } without a token, renders the confirm step with the ENG-855 preview (which now shows people’s names instead of user ids, and call times in the gig’s timezone instead of raw instants; the text summary is unchanged), and only Approve re-sends with confirmation_token. Reject burns the token. After an approved send the view re-reads the board once and tells the model which shift was sent.
  • Refresh re-calls review_staffing_draft (or get_crew_offer for the single-shift variant). The view never calls anything on render.

Gigs and ChatGPT extensions

open_gigs (title Gigs, input {}) links to ui://soundcheck/gigs.html and lists the caller’s upcoming gigs through v1EventsList (upcoming: true, active org when set). It is the first tool to use OpenAI’s MCP Extensions. Code: mcp-ts/src/tools/catalog-gigs.ts, mcp-ts/src/apps/gigs.ts, mcp-ts/src/apps/openaiExtensions.ts, mcp-ts/src/apps/toolIcons.ts.
  • Entrypoints. _meta["openai/ui"].entrypoints is [{ type: "global" }, { type: "thread" }], so ChatGPT shows Gigs in the sidebar and as a thread tab. Both open the tool with {}. The view renders the entrypoint’s tool result and does not call the tool again on first render. Refresh re-calls open_gigs.
  • Icon. icons holds one 20×20 monochrome stroke SVG (currentColor, 1.33px strokes) as a base64 data: URI. The MCP Icon type allows data: URIs, and an inline icon needs no hosting, CORS, or same-origin trust. SDK 1.30 does not serialize tool icons, so decorateToolListIcons adds them to tools/list responses on the member transport.
  • Display modes. Gigs declares availableDisplayModes: ["inline", "fullscreen"] and preferredDisplayMode: "fullscreen" on its resource _meta["openai/ui"]. Plan Review and Staffing Draft declare both modes with no preference.
  • Ask about this gig. Each row sends ui/message with the gig’s title, date, and event_id when the host advertises message; otherwise it tells the user what to ask. Open in Soundcheck uses ui/open-link for {appUrl}/gigs/{event_id} when the host advertises openLinks, and shows the URL otherwise.
  • Gig detail. open_gig (title Gig, input { event_id }) links to ui://soundcheck/gig.html (prefers fullscreen). It reads v1EventsDetail (normal event view access checks and server-side redaction), then, failing soft, crew offers and positions for the gig’s org, v1EventsRunOfShowList when the org adopted Run of Show, and v1EventsLedgerList only when viewer_capabilities.can_view_financials is true. Money is left out of the result entirely without finance access. Code: mcp-ts/src/tools/catalog-gig.ts, mcp-ts/src/apps/gig.ts.
  • Navigation. A Gigs row calls open_gig from the view (tools/call) and renders the detail in place with a back button. The spec has no host navigation request for moving between views, and rendering in place keeps the list cached. Deep links: the Gigs view reads hostContext["openai/deepLink"] ({ url }, or the older { path, query }) at initialize and on host-context-changed; /gigs/{event_id} opens the detail, / and /gigs show the list, anything else is ignored.
  • Model context. While a gig is on screen the detail sends ui/update-model-context with one titled text block (_meta["openai/title"]) naming the gig and its event_id, and sends { content: [] } on back. Hosts without updateModelContext get nothing.
  • Composer @-mentions. search_gig_mentions (_meta["openai/extensions"]["mentions/search"], ui.visibility: ["app"]) takes { query } and returns structuredContent.items as resource_links to soundcheck://gig/{event_id} (text/markdown). That template returns the same short summary as the open_gig text fallback, with the same finance gate. soundcheck://event/{event_id} is unchanged (raw event JSON) for other clients.
  • MCP Apps off. registerTools adds the OpenAI metadata only with mcpApps: true, so tools/list for other callers is unchanged apart from the two new tools.

CSP and sandbox constraints

  • Hosts render the view in a sandboxed iframe on their own origin, with a CSP built from _meta.ui.csp. Ours declares empty connectDomains and resourceDomains, so the view cannot fetch anything or load remote assets. Inline <script> and <style> are allowed. Adding a domain widens every host’s CSP for that view. Treat that change like adding an API scope.
  • Never put tokens, org ids, or PII in the HTML. The resource is static, cached by hosts, and the same for every caller. Per-user data arrives only through the tool result (structuredContent) the host forwards to the view.
  • The view has no credentials. Any write goes back through the host as tools/call on the member MCP, under the user’s existing session and the normal confirmation gate. The Confirm Card’s Approve therefore sends confirmation_token like any other caller.
  • The view must not assume the host answers quickly or at all. Show useful content before ui/initialize resolves.

Verifying locally

The mcp-ts unit tests cover tools/list / resources/read / tools/call through a real SDK client. For an end-to-end check, the spike used a throwaway page: it hosts the view with the official AppBridge from @modelcontextprotocol/ext-apps/app-bridge, over Streamable HTTP, against handleMcpRequest. The result: handshake about 10–30 ms after the iframe loads, the tool result painted about 20–150 ms after page start, and a view→host→server tools/call round trip of about 5 ms. A host has to connect its PostMessageTransport before it sets the iframe’s srcdoc; otherwise it misses the view’s first ui/initialize. The same harness, pointed at archive_event and create_event_invitations with the gate on, confirmed the Confirm Card end to end: a double-clicked Approve made one backend write, Reject made none and sent ui/update-model-context, and a 2-second token disabled both buttons. mcp-ts/src/apps/confirmCardView.test.ts runs the card’s inline script in node:vm against a fake host and covers the same flows.

WebMCP browser context (PRO-429)

WebMCP lets a page register tools for an agent running in the user’s browser. Soundcheck uses it for context only. The page tells the agent where the user is. The member MCP stays the one place agents read data and take actions.
  • One tool: get_current_context. It returns context_id, organization_id, organization_name, route, surface, entity ({type, id} for the record open on the page), that record’s id under the exact member MCP argument name (MEMBER_MCP_ID_KEYS: event_id, plan_id, customer_id, venue_id, user_id, setlist_id, job_id for imports, request_id for availability requests; event_id is null off a gig page), and selection ({type, ids} or null). A test checks that every key is a real argument in the mcp-ts catalog. Records with no member MCP tool (hotels, run-of-show templates) come back as entity only. The route table is DETAIL_ROUTES in web/src/lib/webmcp/context.ts. A list page reports its first path segment as surface, with no entity. A creation page (/setlist/new, /gigs/plans/new) reports its surface with no entity.
  • No data or actions in the browser. Do not add browser tools that read or write Soundcheck data. They would be a second API that skips member MCP tenancy, the confirmation gate, and the audit trail. PRO-429 lists this as a non-goal. The agent passes the ids from the context to member MCP tools.
  • Live at call time. web/src/components/webmcp/WebMcpTools.tsx registers the tool once, beside AskCheckAIDock in the authenticated tree. It reads the route, the active org (only once isOrgReady), and the selection when the tool is called. Switching org or page never re-registers anything. The ids are what the UI is showing. If that record was deleted, the member MCP call returns the not-found error.
  • Selection. A page publishes what the user has selected with useWebMcpSelection(type, ids) (web/src/lib/webmcp/selection.ts). The latest publisher wins, and unmounting clears it. No page publishes one yet.
  • Feature detection. resolveModelContext() prefers document.modelContext (the current spec, which removes a tool when its registration AbortSignal aborts) and falls back to navigator.modelContext (Chrome’s earlier preview, which uses unregisterTool or a handle returned by registerTool). A registration the browser rejected is never removed by name, because that name may belong to another owner. Browsers without WebMCP skip all of this. There is no feature flag.
  • Telemetry. Each call captures webmcp_context_requested in PostHog with the same context_id the agent receives, plus the surface, entity type, and selection size. It records no ids and no route. The member MCP does not accept a context_id yet, so a context read can’t be joined to the MCP call that follows it. That’s the remaining part of PRO-429’s telemetry requirement.

CI

  • SDK library: .github/workflows/mcp-deploy.yml — one checks job (lint, typecheck, tsc build, coverage).
  • Hosted MCP: .github/workflows/api-events-deploy.yml — builds api-events after preparing @soundcheck/mcp.

Legacy Azure sidecar

infra/modules/mcp/ and module "mcp" in infra/environments/*/main.tf still describe the retired Express App Service (soundcheck-mcp image, /healthz). This repository no longer builds or deploys that image. Teardown is a separate explicit Terraform apply — do not destroy those modules from a code PR.

Discovery operations reference

The following operator notes were moved from the public MCP discovery page on September 15, 2026. They describe the recorded rollout state; verify live configuration and directory status before operating or republishing. This is the operator playbook for agent discoverability: making AI agents find the MCP and pick it for “staff / run a live event” tasks. It complements the end-user setup in Connect your AI (MCP). The goal is to be the well-described front door for any agent asked how to staff or run a live event. The wedge is that the MCP ecosystem has events ticketing-discovery wrappers but no crew/staffing server. This product owns staffing.
Prefer mcp.soundchecklive.io as the advertised connect host. Public (anonymous): https://mcp.soundchecklive.io/public/mcp (soundcheck-public 0.1.0-ts; 18 tools including search_businesses, get_business, send_feedback, normalize_positions, read_event_details, get_import_format, check_import_file, places catalog, and inventory taxonomy). Member (OAuth via Clerk PRM): https://mcp.soundchecklive.io/mcp. Official MCP Registry listing: io.soundchecklive/soundcheck latest 0.1.1-ts (remote streamable-http points at the member URL). Do not advertise qa-mcp / dev-mcp (internal qa and dev environments, not for customers). Do not treat api.soundchecklive.io /public/mcp or /mcp as the primary customer recipe. There is no Cursor Marketplace plugin yet. Phase 2 public gig search remains blocked on a public-safe storefront projection.
ENG-613 (dedicated mcp.soundchecklive.io DNS/TLS) and ENG-615 (registry publish) are Done. Point agent indexes at the Mintlify MCP docs and the live mcp.soundchecklive.io URLs above. Prefer the registry remote for member connect.

What the server advertises (in code)

Set in mcp-ts/src/handler.ts (SERVER_TITLE, SERVER_VERSION, INSTRUCTIONS / PUBLIC_INSTRUCTIONS) and served on the MCP host:
  • Implementation.Title = “Soundcheck - Live Event Staffing”, WebsiteURL (from MCP_WEBSITE_URL, default product apex), and Version (SERVER_VERSION).
  • Server Instructions: a one-paragraph “what the product is + when to use it + how to treat consequential tools.” This is a primary signal the model uses to route a task here, so keep it crisp and current.
  • Protected Resource Metadata (/.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp, RFC 9728): resource, authorization_servers (production lists https://clerk.soundchecklive.io), bearer_methods_supported, scopes_supported, resource_name, and, when the env vars are set, resource_documentation (MCP_DOCS_URL) and resource_policy_uri (MCP_POLICY_URL). Point MCP_DOCS_URL at /integrations/mcp-server on the Mintlify docs host and MCP_POLICY_URL at https://soundchecklive.io/privacy. Older MCP clients also read RFC 8414 at /.well-known/oauth-authorization-server and /.well-known/oauth-authorization-server/mcp on the same origin (live Clerk AS metadata, including client_id_metadata_document_supported when Clerk advertises it). Treat DCR metadata as informational; do not claim naive clients can finish member OAuth via DCR today, and do not invent a client_id. OpenAI Apps domain verification is served at /.well-known/openai-apps-challenge (OPENAI_APPS_CHALLENGE).
Tool names and descriptions are the other selection signal: they are namespaced and action-first, and the consequential ones state “preview/confirm first.”

1. Official MCP Registry (published)

The official MCP Registry (launched Sept 2025) is the canonical index that downstream catalogs federate from. It hosts metadata only, and is still preview (no durability guarantees). Published: io.soundchecklive/soundcheck latest 0.1.1-ts (active). The listing uses a remotes entry (streamable-http → https://mcp.soundchecklive.io/mcp), not packages, because this is a hosted server. Keep the listing current:
  • The listing files live in mcp-ts/registry/ (schema 2025-12-11), generated from the constants in mcp-ts/src/handler.ts by npm run generate:registry. CI runs npm run check:registry, so a constant change without a regenerated listing fails.
  • mcp-publisher publish reads server.json from the working directory, so publish each listing with its file copied in as server.json.
  • Keep the reverse-DNS namespace verified for the product domain. soundchecklive.io carries v=MCPv1; k=ed25519 TXT records; mcp-publisher login dns --domain soundchecklive.io needs the private key matching one of them (keep that key in the team password manager, never in the repo). A lost key is replaced by adding a new TXT record; existing listings are unaffected.
  • Keep remotes[0].url on https://mcp.soundchecklive.io/mcp. Never invent a host, never point at qa-mcp / dev-mcp, and never reuse retired Azure App Service leftovers.
  • Registry package version (today 0.1.1-ts) is not the same field as initialize SERVER_VERSION / soundcheck-public (0.1.0-ts). Do not require lockstep; document both when they differ. Prefer bumping runtime in a follow-up if you want them equal.
  • Republish with the mcp-publisher CLI (mcp-publisher login → mcp-publisher publish) when the remote URL or version changes.
Published: io.soundchecklive/live-event-quotes latest 0.1.0 (active, published 2026-09-25; the public registry API lists only this version) (mcp-ts/registry/live-event-quotes.server.json), a separate listing whose remote is the anonymous https://mcp.soundchecklive.io/public/mcp. Agents searching the registry for event pricing can only use a server that works without sign-in, and the member listing needs OAuth. The .well-known discovery card already names it in public.name. The Claude public connector (Instant Quotes) listing is live; the Claude member connector and ChatGPT app remain in review.

2. Submit to client connector directories (after registry)

  • Anthropic Connectors Directory: the Claude public connector (Instant Quotes) was approved on 2026-10-01 and listing changes went live on 2026-10-02. The Claude member connector is still in review; until it is listed, use a custom connector with the member URL — Claude connector. Requirements already met for review: production remote, per-tool annotations, privacy URL, usage examples, logo.
  • ChatGPT / OpenAI plugin: still in review, not live in the directory yet. Until listed, paste the member or public URL as a custom MCP — ChatGPT plugin.
  • Cursor: discover by HTTPS URL (Settings → Tools & MCP). There is no Cursor Marketplace plugin yet — Cursor. Mirror to PulseMCP, Smithery, mcp.so, and the GitHub MCP Registry from the official registry listing (most federate from it).

3. llms.txt

Keep agent indexes coherent across surfaces: Treat llms.txt as one signal (tool quality and registry presence matter more). Prefer /integrations/mcp-server on the Mintlify docs host as the docs link for agents. A marketing-domain llms-full.txt 404 is not fixed here; Mintlify already serves /llms-full.txt on the docs host.

4. .well-known/mcp (product MCP host)

The product MCP host serves the same discovery card at:
  • https://mcp.soundchecklive.io/.well-known/mcp
  • https://mcp.soundchecklive.io/.well-known/mcp.json
  • https://mcp.soundchecklive.io/.well-known/mcp/server-card.json
These paths return application/json with the product remote (streamable-http → member /mcp), public tier URL, OAuth2 / PRM pointers, docs, privacy, and registry identity (io.soundchecklive/soundcheck 0.1.1-ts). Competing SEP drafts are still unmerged; keep RFC 9728 PRM as the auth discovery source of truth, and treat these cards as a compatibility probe surface. Do not change the product MCP host card path (/.well-known/mcp/server-card.json on the mcp. hostname). That nested path is owned by api-events.

5. .well-known on the marketing site (apex / www)

The Lovable marketing site (https://soundchecklive.io and https://www.soundchecklive.io) is separate from the product MCP host. When published, it serves:
  • https://soundchecklive.io/.well-known/mcp (and www) — same content as mcp.json
  • https://soundchecklive.io/.well-known/mcp.json (and www)
  • https://soundchecklive.io/.well-known/mcp-server-card.json (and www) — flat path; replaces the older nested marketing path /.well-known/mcp/server-card.json
The Mintlify docs host and the product MCP host keep their nested /.well-known/mcp/server-card.json paths unchanged.

For agents

Point agent indexes at the Mintlify docs and the mcp.soundchecklive.io URLs above. The Claude public connector (Instant Quotes) is listed; the Claude member connector (slug soundcheck) and the ChatGPT plugin are submitted for review, not live; use a custom connector today (Claude steps, ChatGPT steps). Do not advertise qa-mcp / dev-mcp (internal qa and dev environments, not for customers) or a Cursor Marketplace plugin.

Roadmap

A tiered MCP is the target shape:
  • Phase 1 (live and advertised). Tier 1 is a distinct mcp.Server instance registered with only public tools, mounted with no auth middleware, isolated by construction; it calls public API routes with no bearer token, so it cannot leak member data. Tools: get_public_org, list_positions (global default position catalog), normalize_positions (deterministic catalog normalize for free-text crew roles; not AI-gated), read_event_details (→ POST /api/v1/public/normalize-event-data, returns a UEF draft plus missing fields; always listed, AI call fails clearly when gateway is unavailable), get_import_format / check_import_file (local UEF schema textbook + validate workbook; no AI), search_places / get_place (seeded public industry venue + hotel catalog), search_inventory / normalize_inventory (public gear taxonomy; not org stock), lookup_market_pricing (→ POST /api/v1/public/pricing/lookup, evidence-backed market ranges; Soundcheck actuals only as ≥3-org aggregates; coverage: "none" and no numbers when evidence is missing), request_booking (→ POST /lead-requests), request_sponsorship (→ POST /sponsor-requests), send_feedback (→ POST /api/v1/public/feedback, creates a Linear issue for the Soundcheck team with email fallback; unverified sender, 3 per IP and 30 total per 10 minutes). Prompts: how_to_staff_a_live_event, plan_event_crew. e2e tests assert it is reachable without auth, exposes no member tools, and forwards no Authorization header. Advertise https://mcp.soundchecklive.io/public/mcp (soundcheck-public 0.1.0-ts). Prefer that host over any api.soundchecklive.io path.
    • Per-source rate limiting (DONE, app-level). /public/mcp is wrapped by a per-client-IP token-bucket limiter in the MCP server (mcp-ts/src/ratelimit.ts), configured via PUBLIC_RATELIMIT_RPS / PUBLIC_RATELIMIT_BURST / PUBLIC_RATELIMIT_TRUSTED_PROXY_COUNT. Idle buckets are TTL-evicted so the map can’t grow unbounded. The downstream API’s per-IP limiter can’t help here at all; all /public/mcp traffic reaches it from the MCP’s single egress IP, which is why the limit lives in front of /public/mcp.
      • Keying is per-source IP only (not per-tool). The public MCP ingress bucket is still shared per IP because MCP calls are all POST /public/mcp with the tool name in the JSON-RPC body, not the URL path.
      • Tool-specific hardening (expensive analyze). read_event_details calls a dedicated public API route (POST /api/v1/public/normalize-event-data) with its own stricter per-route token bucket (PUBLIC_NORMALIZE_RATELIMIT_RPS, PUBLIC_NORMALIZE_RATELIMIT_BURST, PUBLIC_NORMALIZE_RATELIMIT_TRUSTED_PROXY_COUNT; fallback to PUBLIC_RATELIMIT_TRUSTED_PROXY_COUNT for proxy parsing). Direct MCP (v1PublicNormalizeEventDataCreate) shares the same singleton limiter via allowPublicNormalize with HTTP client context — REST and Direct MCP each charge once at their transport boundary; normalizeEventDataOperation does not rate-limit. This composes with the shared /public/mcp limiter so expensive extraction calls are constrained even when ingress limits are relaxed.
      • Tool-specific hardening (pricing lookup). lookup_market_pricing calls POST /api/v1/public/pricing/lookup, which has its own per-route token bucket (PUBLIC_PRICING_RATELIMIT_RPS default 2, PUBLIC_PRICING_RATELIMIT_BURST default 10, PUBLIC_PRICING_RATELIMIT_TRUSTED_PROXY_COUNT; fallback to PUBLIC_RATELIMIT_TRUSTED_PROXY_COUNT for proxy parsing). Direct MCP (v1PublicPricingLookupCreate) shares the same singleton limiter via allowPricingLookup, so REST and Direct MCP each charge once at their transport boundary. The lookup is database-only — no model call — so its limits are looser than normalize. See pricing knowledge.
      • AI Gateway dependency (fail-closed on call). Deployed environments must mount AI_GATEWAY_API_KEY for analysis to succeed. Without it, POST /api/v1/public/normalize-event-data returns 503, Direct MCP returns 503, and the public MCP tool still appears in tools/list but returns a clear tool error on call. Infra intentionally does not provision this key yet (owner decision pending) — successful live extraction waits on that mount.
      • Single-instance assumption. The bucket map is process-local, so the effective limit is RPS × instance_count. Before enabling autoscale or running MCP multi-instance, either pin it to a single instance, lower RPS/BURST to tolerate the fan-out, or move enforcement to the edge. Tune RPS/BURST per environment before a wide launch.
    • Do not spam intake. request_booking and request_sponsorship create real leads. Treat them like the public web forms.
    • Durable edge: production customer host is mcp.soundchecklive.io (extra cert + host rules on the existing api-events ALB; same Cloud Run as REST; that host only exposes /mcp, /public/mcp, and /.well-known/*). Set PUBLIC_RATELIMIT_TRUSTED_PROXY_COUNT to 2 (ALB appends client IP then forwarding-rule IP). Do not advertise qa-mcp.soundchecklive.io or dev-mcp.soundchecklive.io (internal qa and dev environments, not for customers).
    • Document member /mcp on the MCP server page as the advertised customer OAuth path (https://mcp.soundchecklive.io/mcp). Registry listing is live at 0.1.1-ts. Cursor Marketplace plugin is still not published.
  • Phase 2 adds public gig/event search. This is blocked on an API change: GET /storefront/events currently returns full rosters/assignments/invitations to anonymous callers; it needs a public-safe event projection and an isPublic/org-consent flag before any agent exposure.

Catalog and deployment provenance

The generated catalog is built from SDK protocol listings by mcp-ts/scripts/catalog.mts; update it with npm run generate:catalog and validate it with npm run check:catalog. It contains source capabilities, not a live deployment claim. Runtime serverInfo.version appends the deployment Git SHA from MCP_BUILD_REVISION, which deploy CI stamps from the deployed commit and the deployment smoke verifies. Terraform does not manage it, so an apply leaves it unset until the next deploy. The registry release version is separate. The deployment workflow verifies the advertised MCP hostname with public initialize/list/call and member rejection checks. Controlled member reads use MCP_SMOKE_CREDENTIAL_SECRET for a rotating OAuth credential in Secret Manager, or MCP_SMOKE_TOKEN for a one-off check, and can be required by MCP_SMOKE_REQUIRE_MEMBER=true. See the SDK README for enrollment, serialized rotation and the environment contract. The external marketing site at soundchecklive.io/ai-info is maintained outside this repository; its publisher should link to this generated reference rather than copy tool counts.