@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
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
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: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/tracestatefromparams._metafirst, then the HTTPtraceparentheader. Clients must inject_meta— a header alone is not enough in practice, because proxies in front of the server (the harness/api/mcp-proxyamong them) forward only an allow-list of headers and droptraceparent. Header-only runs show up as orphan client spans. CheckAI does this incheckai/agent/lib/mcpClientTracing.ts. Neither present means a new root span. - Attributes:
mcp.method.name,mcp.tool.name/gen_ai.tool.name(ontools/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 whenYYYY-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/ toolerror.typerather than a guessed one. - Notifications produce no span.
mcp.confirmation_requiredand toolerror.typecome from the mutating-tool invocation hook, so read-only tools always reportfalse. - 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:
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 anerrorinvocation.
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 inmcp-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.
handleMcpRequestalways registers the Confirm Card, Plan Review, Staffing Draft, and Gigs resources, the app-onlyreject_confirmationtool, the card link on gated member tools, and the view links intoolViews.ts. The public tier never gets MCP Apps. The ENG-854 hello-world tool and resource are registered only withhandleMcpRequest({ mcpAppsSpike: true }): api-events sets that frommcpConfig().mcpAppsSpikeEnabled = !isDeployedEnvironment()→runtime.mcpAppsSpike, so qa/prodtools/listnever listsshow_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, andCallToolResult: the tool has_meta.ui.resourceUri(plus the legacy_meta["ui/resourceUri"]key), and the resource istext/html;profile=mcp-appwith_meta.ui.csp/prefersBorder.@modelcontextprotocol/ext-apps2.x needs SDK v2 (@modelcontextprotocol/server) and zod 4. 1.7.x works with SDK^1.29and zod 3 if we later want itsAppclass or React hooks in the view.@mcp-ui/server6.x still pins ext-apps^0.3; skip it. - Views are self-contained HTML built from
viewRuntime.ts.VIEW_BRIDGE_JSis a hand-rolled postMessage JSON-RPC client (scCreateBridge):ui/initialize→ui/notifications/initialized, thentool-input/tool-result/host-context-changed/size-changed, andtools/call/ui/update-model-contextback through the host.VIEW_THEME_CSSmaps 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 withtextContentonly. - Text fallback. Every app tool returns normal
contenttext plusstructuredContent. Clients without MCP Apps support ignore_metaand read the text. The server cannot detect UI support per client, because the transport is stateless andinitializecapabilities 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 aconfirm 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/listgains_metaon gated tools plus thereject_confirmationentry. Hosts that honourvisibilityhide 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 resultcontentandstructuredContentdo not change. - Result
_meta, not the body. The card’s data rides inCallToolResult._meta["soundcheck/confirm"] = { tool, arguments (without confirmation_token), preview? }.contentandstructuredContentare 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 ofrequireConfirmation) is{ title, facts: [{ label, value }], recipients: [{ name, detail? }] }. It is kept beside the confirmation payload (aWeakMap), so it never touchesparts,items, the args hash, or the payload.send_message,send_crew_offer, andcreate_event_invitationsbuild one from data their confirm step already has (no extra backend calls;send_messagerecipients are user ids). Other gated tools show the summary only. - Approve. Only a click sends
tools/callthrough the host with the same arguments plusconfirmation_token. The view uses the host’sui/notifications/tool-inputarguments and falls back to_metaarguments; the tool name comes from_metathenhostContext.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 withui/update-model-context(orui/messagewhen the host only supports that). - Reject burns the token. Reject calls the app-only tool
reject_confirmation(_meta.ui.visibility: ["app"], accepted by preflight throughmcpAppTools()) with{ tool, confirmation_token, organization_id? }.Gate.rejectchecks the token is authentic, unexpired, and bound to this caller, organization, and tool. It then claims the action row and marks it failed withREJECTED_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_secondsthe 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_JSin a view and callscRenderToolResult(container, bridge, { tool, arguments, preview, result, onSettled? })orscConfirmStep(container, bridge, { tool, arguments, preview, required, onSettled? })to run the same Approve/Reject flow inside a larger widget.onSettled(outcome, result)fires once withapproved,failed,rejected,reject-failed, orexpired, 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, andupdate_event_planresults carry_meta["soundcheck/plan"]: a compact model built byplanReviewModel()(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_plangets the result_metabut no view link.contentandstructuredContentare unchanged, so tools-only clients see the same JSON as before. - Answer / Dismiss. Each open question gets a select built from its own
optionswhen it has them, otherwise a text, number, date, or time input. Save sends oneupdate_event_planwith the plan’s currentplan_versionand[{ op: "answer_question", question_id, value }](Dismiss sendsdismiss_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_REQUIREDanswer makes the view callget_event_planonce, show the fresh version, and tell the user nothing was saved. It never retries the write. - Mark reviewed calls
review_event_planand then shows the proposed gig. Create draft gig is enabled only when the plan is REVIEWED andreadiness.can_create_draft; otherwise it is disabled and the view lists the blocking reasons. The click callscreate_draft_event_from_planwithout a token, renders the confirm step from theconfirmation_requiredresult, and only Approve re-sends the same arguments plusconfirmation_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(orui/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) andv1OrganizationsPositionsList(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),sendableplus a hint, gig totals, and anext_stepline. Names come from thename/username/emailthe offer rows already carry, so there is no per-person lookup. Both ops were already in api-eventsDIRECT_OPS. - Data. The roster rides in
_meta["soundcheck/staffing"].get_crew_offerandsave_crew_offerresults get a single-shift roster there, built from the offer they return with no extra read; theircontentandstructuredContentare unchanged. - Send. A shift is
sendablewhen 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 callssend_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 withconfirmation_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(orget_crew_offerfor 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"].entrypointsis[{ 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-callsopen_gigs. - Icon.
iconsholds one 20×20 monochrome stroke SVG (currentColor, 1.33px strokes) as a base64data:URI. The MCPIcontype allowsdata:URIs, and an inline icon needs no hosting, CORS, or same-origin trust. SDK 1.30 does not serialize toolicons, sodecorateToolListIconsadds them totools/listresponses on the member transport. - Display modes. Gigs declares
availableDisplayModes: ["inline", "fullscreen"]andpreferredDisplayMode: "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/messagewith the gig’s title, date, andevent_idwhen the host advertisesmessage; otherwise it tells the user what to ask. Open in Soundcheck usesui/open-linkfor{appUrl}/gigs/{event_id}when the host advertisesopenLinks, and shows the URL otherwise. - Gig detail.
open_gig(title Gig, input{ event_id }) links toui://soundcheck/gig.html(prefers fullscreen). It readsv1EventsDetail(normal event view access checks and server-side redaction), then, failing soft, crew offers and positions for the gig’s org,v1EventsRunOfShowListwhen the org adopted Run of Show, andv1EventsLedgerListonly whenviewer_capabilities.can_view_financialsis 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_gigfrom 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 readshostContext["openai/deepLink"]({ url }, or the older{ path, query }) at initialize and onhost-context-changed;/gigs/{event_id}opens the detail,/and/gigsshow the list, anything else is ignored. - Model context. While a gig is on screen the detail sends
ui/update-model-contextwith one titled text block (_meta["openai/title"]) naming the gig and itsevent_id, and sends{ content: [] }on back. Hosts withoutupdateModelContextget nothing. - Composer @-mentions.
search_gig_mentions(_meta["openai/extensions"]["mentions/search"],ui.visibility: ["app"]) takes{ query }and returnsstructuredContent.itemsasresource_links tosoundcheck://gig/{event_id}(text/markdown). That template returns the same short summary as theopen_gigtext fallback, with the same finance gate.soundcheck://event/{event_id}is unchanged (raw event JSON) for other clients. - MCP Apps off.
registerToolsadds the OpenAI metadata only withmcpApps: true, sotools/listfor 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 emptyconnectDomainsandresourceDomains, 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/callon the member MCP, under the user’s existing session and the normal confirmation gate. The Confirm Card’s Approve therefore sendsconfirmation_tokenlike any other caller. - The view must not assume the host answers quickly or at all. Show useful content before
ui/initializeresolves.
Verifying locally
The mcp-ts unit tests covertools/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 returnscontext_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_idfor imports,request_idfor availability requests;event_idisnulloff a gig page), andselection({type, ids}ornull). A test checks that every key is a real argument in themcp-tscatalog. Records with no member MCP tool (hotels, run-of-show templates) come back asentityonly. The route table isDETAIL_ROUTESinweb/src/lib/webmcp/context.ts. A list page reports its first path segment assurface, 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.tsxregisters the tool once, besideAskCheckAIDockin the authenticated tree. It reads the route, the active org (only onceisOrgReady), 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()prefersdocument.modelContext(the current spec, which removes a tool when its registrationAbortSignalaborts) and falls back tonavigator.modelContext(Chrome’s earlier preview, which usesunregisterToolor a handle returned byregisterTool). 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_requestedin PostHog with the samecontext_idthe agent receives, plus the surface, entity type, and selection size. It records no ids and no route. The member MCP does not accept acontext_idyet, 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— onechecksjob (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.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 inmcp-ts/src/handler.ts (SERVER_TITLE, SERVER_VERSION, INSTRUCTIONS / PUBLIC_INSTRUCTIONS) and served on the MCP host:
Implementation.Title= “Soundcheck - Live Event Staffing”,WebsiteURL(fromMCP_WEBSITE_URL, default product apex), andVersion(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-resourceand/.well-known/oauth-protected-resource/mcp, RFC 9728):resource,authorization_servers(production listshttps://clerk.soundchecklive.io),bearer_methods_supported,scopes_supported,resource_name, and, when the env vars are set,resource_documentation(MCP_DOCS_URL) andresource_policy_uri(MCP_POLICY_URL). PointMCP_DOCS_URLat /integrations/mcp-server on the Mintlify docs host andMCP_POLICY_URLat https://soundchecklive.io/privacy. Older MCP clients also read RFC 8414 at/.well-known/oauth-authorization-serverand/.well-known/oauth-authorization-server/mcpon the same origin (live Clerk AS metadata, includingclient_id_metadata_document_supportedwhen Clerk advertises it). Treat DCR metadata as informational; do not claim naive clients can finish member OAuth via DCR today, and do not invent aclient_id. OpenAI Apps domain verification is served at/.well-known/openai-apps-challenge(OPENAI_APPS_CHALLENGE).
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/(schema2025-12-11), generated from the constants inmcp-ts/src/handler.tsbynpm run generate:registry. CI runsnpm run check:registry, so a constant change without a regenerated listing fails. mcp-publisher publishreadsserver.jsonfrom the working directory, so publish each listing with its file copied in asserver.json.- Keep the reverse-DNS namespace verified for the product domain.
soundchecklive.iocarriesv=MCPv1; k=ed25519TXT records;mcp-publisher login dns --domain soundchecklive.ioneeds 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].urlonhttps://mcp.soundchecklive.io/mcp. Never invent a host, never point atqa-mcp/dev-mcp, and never reuse retired Azure App Service leftovers. - Registry package
version(today 0.1.1-ts) is not the same field asinitializeSERVER_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-publisherCLI (mcp-publisher login→mcp-publisher publish) when the remote URL or version changes.
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/mcphttps://mcp.soundchecklive.io/.well-known/mcp.jsonhttps://mcp.soundchecklive.io/.well-known/mcp/server-card.json
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(andwww) — same content asmcp.jsonhttps://soundchecklive.io/.well-known/mcp.json(andwww)https://soundchecklive.io/.well-known/mcp-server-card.json(andwww) — flat path; replaces the older nested marketing path/.well-known/mcp/server-card.json
/.well-known/mcp/server-card.json paths unchanged.
For agents
- Product: https://soundchecklive.io
- Public MCP:
https://mcp.soundchecklive.io/public/mcp(soundcheck-public0.1.0-ts; 18 tools includingsearch_businesses,get_business,send_feedback,normalize_positions,read_event_details,get_import_format,check_import_file, places catalog, and inventory taxonomy) - Member MCP:
https://mcp.soundchecklive.io/mcp(OAuth via Clerk PRM) - Registry:
io.soundchecklive/soundchecklatest 0.1.1-ts - MCP docs: https://docs.soundchecklive.io/integrations/mcp-server
- Connect spokes: Claude, Claude Code, ChatGPT, Cursor
- This playbook: https://docs.soundchecklive.io/integrations/mcp-discovery
- Official AI-info: https://docs.soundchecklive.io/ai-info
- Privacy: https://soundchecklive.io/privacy
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.Serverinstance 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 noAuthorizationheader. Advertisehttps://mcp.soundchecklive.io/public/mcp(soundcheck-public0.1.0-ts). Prefer that host over anyapi.soundchecklive.iopath.- Per-source rate limiting (DONE, app-level).
/public/mcpis wrapped by a per-client-IP token-bucket limiter in the MCP server (mcp-ts/src/ratelimit.ts), configured viaPUBLIC_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/mcptraffic 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/mcpwith the tool name in the JSON-RPC body, not the URL path. - Tool-specific hardening (expensive analyze).
read_event_detailscalls 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 toPUBLIC_RATELIMIT_TRUSTED_PROXY_COUNTfor proxy parsing). Direct MCP (v1PublicNormalizeEventDataCreate) shares the same singleton limiter viaallowPublicNormalizewith HTTP client context — REST and Direct MCP each charge once at their transport boundary;normalizeEventDataOperationdoes not rate-limit. This composes with the shared/public/mcplimiter so expensive extraction calls are constrained even when ingress limits are relaxed. - Tool-specific hardening (pricing lookup).
lookup_market_pricingcallsPOST /api/v1/public/pricing/lookup, which has its own per-route token bucket (PUBLIC_PRICING_RATELIMIT_RPSdefault 2,PUBLIC_PRICING_RATELIMIT_BURSTdefault 10,PUBLIC_PRICING_RATELIMIT_TRUSTED_PROXY_COUNT; fallback toPUBLIC_RATELIMIT_TRUSTED_PROXY_COUNTfor proxy parsing). Direct MCP (v1PublicPricingLookupCreate) shares the same singleton limiter viaallowPricingLookup, 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_KEYfor analysis to succeed. Without it,POST /api/v1/public/normalize-event-datareturns 503, Direct MCP returns 503, and the public MCP tool still appears intools/listbut 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, lowerRPS/BURSTto tolerate the fan-out, or move enforcement to the edge. TuneRPS/BURSTper environment before a wide launch.
- Keying is per-source IP only (not per-tool). The public MCP ingress bucket is still shared per IP because MCP calls are all
- Do not spam intake.
request_bookingandrequest_sponsorshipcreate 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/*). SetPUBLIC_RATELIMIT_TRUSTED_PROXY_COUNTto 2 (ALB appends client IP then forwarding-rule IP). Do not advertiseqa-mcp.soundchecklive.ioordev-mcp.soundchecklive.io(internal qa and dev environments, not for customers). - Document member
/mcpon 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.
- Per-source rate limiting (DONE, app-level).
- Phase 2 adds public gig/event search. This is blocked on an API change:
GET /storefront/eventscurrently returns full rosters/assignments/invitations to anonymous callers; it needs a public-safe event projection and anisPublic/org-consent flag before any agent exposure.
Catalog and deployment provenance
The generated catalog is built from SDK protocol listings bymcp-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.