ABOUTME: Redacted local debugging and client-smoke runbook for the API-hosted MCP adapter. ABOUTME: Provides secret-free Inspector, VS Code, Copilot, JSON-RPC, and compatibility guidance.
Audience: Contributors | Operators | AI agents Status: Implemented for local/manual smoke and automated contract tests Last Verified: 2026-06-25 Source Anchors:
Explore.API/Program.cs,Explore.API/Mcp/*,Event.API.IntegrationTests/Features/McpProtocolContractTests.cs,Event.API.IntegrationTests/Features/EventManagementMcpPublicReadTests.cs,Event.API.IntegrationTests/Features/EventManagementMcpAuthenticatedReadTests.cs,Event.API.IntegrationTests/Features/EventManagementMcpRedactionTests.cs,Event.API.IntegrationTests/Features/McpAuthorizationTests.cs,Event.Architecture.Tests/McpArchitectureTests.cs,Explore.Diagnostic/AiReplay/AiReplayReportGenerator.cs,Explore.Diagnostic/AiEvaluation/AiEvaluationReportGenerator.cs,docs/adr/ADR-010-mcp-adapter-hosting-strategy.md
This runbook is for debugging the optional API-hosted Model Context Protocol (MCP) adapter. It does not change the product boundary: MCP is mapped by default at /mcp, stateless, API-key-first for external clients, curated, and proposal-first for mutations. No credentials, a blank API-key header, or invalid API keys may see only explicitly anonymous-safe surfaces: registry discovery and public event reads. Scoped tools/resources/prompts still require a valid authenticated context. API-key callers need mcp:read plus event read-equivalent scope authority for authenticated event-management reads such as list_my_events, get_event_creation_context, get_event_publish_readiness, event_management_context, and the Phase 5 program/custom-property/registration/team/template/sync context tools. mcp:propose is required for manage_event_with_confirmation, propose_ai_tool_action, and every registry-projected propose_* event-management tool.
Use only fake or disposable data when connecting interactive MCP clients.
Required configuration for local MCP debugging:
| Setting | Required value | Why |
|---|---|---|
Mcp:Enabled |
true or unset default |
Maps the local /mcp endpoint. Set false only when you intentionally want the endpoint unmapped. |
mcp.enabled |
true or unset default |
Runtime governance must also allow the adapter. If a mapped endpoint returns 404, check this instance/tenant setting before debugging tools. |
Mcp:Stateless |
true |
Keeps Streamable HTTP stateless; no session affinity or Mcp-Session-Id. |
Mcp:EnableLegacySse |
true or unset default |
Legacy SSE remains unavailable at runtime. The true startup value is only a ceiling for future governance, not an automatic runtime transport change. |
mcp.enable_legacy_sse |
false or unset default |
Records runtime intent only; it does not expose legacy SSE in the current adapter. |
| Auth | prefer one X-API-Key |
Normal external MCP smoke uses a disposable API key. Bearer tokens are allowed only for user-delegated local smoke. Do not send both. |
| Tenant binding | normal API edge binding | Anonymous or invalid-key reads still need a resolved tenant in multi-tenant mode. A valid tenant-bound API key may provide tenant context. |
Example local API launch with redacted values:
ASPNETCORE_ENVIRONMENT=Development \
Mcp__Enabled=true \
Mcp__EndpointPath=/mcp \
Mcp__Stateless=true \
Mcp__EnableLegacySse=true \
dotnet run --project Explore.API/Explore.API.csproj --configuration Debug --urls http://127.0.0.1:<redacted-port>Runtime governance checks:
Mcp:Enabled=falsemeans/mcpis not mapped and requires an API restart after changing startup configuration.Mcp:Enabled=trueplusmcp.enabled=falsemeans the path is mapped but the runtime gate returns404without exposing MCP details.- MCP clients that render an optional API-key field as
"X-API-Key": ""are treated the same as no API key. Remove the header or leave it blank for anonymous-safe discovery. - Invalid, revoked, or missing API keys can still reach anonymous-safe discovery only, but they remain rate-limited by remote IP. Repeated bad-key smoke attempts should produce
429/Retry-Afterwithout echoing the credential. - Instance admins configure
mcp.enabled,mcp.enable_legacy_sse,governance.lock_tenant_mcp, andgovernance.lock_tenant_mcp_legacy_ssefrom instance administration. Tenant admins see MCP overrides only when the corresponding lock is open. - Endpoint path and stateless mode are never runtime settings. Legacy SSE remains unavailable even when both startup and runtime legacy-SSE values are true.
/healthexposes only bounded MCP booleans such asstartupEnabled,runtimeEnabled,legacySseRuntimeRequested, andlegacySseRuntimeEnabled; it must not include tenant IDs, endpoint URLs, keys, prompts, or raw protocol bodies.
Before diagnosing missing tools, rebuild and restart both the API and the MCP client:
dotnet build Explore.API/Explore.API.csproj --configuration Debug --verbosity quietSet breakpoints in:
Explore.API/Mcp/AiToolRegistryMcpTools.csforlist_ai_tool_contracts;Explore.API/Mcp/EventManagementMcpTools.csholds the[McpServerTool]entry points themselves: public event reads, authenticated event-management reads, program/custom-property/registration/team/template contexts, and sync contexts. Tool discovery is attribute-based, so tool methods stay on this class. Its collaborators are:EventMcpBounds.cs— every size and truncation ceiling in one place. These are one disclosure budget, not thirty independent numbers; raising one widens what an assistant can pull in a single turn.EventMcpDescriptorMappers.cs— pure DTO→descriptor projections. No I/O, no authorization, no ambient state, so a response shape can be reasoned about without a request or a tenant.EventMcpLocationDisclosureGuard.cs— the fail-closed AI location-disclosure boundary. It runs every location value through the AI context gateway and throws unless the gateway confirms the exact expected disclosure.EventMcpTextFilters.cs— blank-entry filtering applied before bounds, so truncation counts reflect real content;
Explore.API/Mcp/EventManagementMcpResources.csfor the scopedevent_management_contextresource template;Explore.API/Mcp/AiAssistantMcpTools.csfor generic proposal calls;Explore.API/Mcp/AiMcpProjectedToolFactory.csfor projectedpropose_*tools;- relevant native CQS handlers such as
ProposeAiToolActionCommandHandler.
Before exposing MCP outside local development:
- Serve
/mcponly through the same production TLS boundary as the API. Do not document or supportcurl -kas a production workaround; fix certificate trust instead. - Confirm
/healthreports the boundedmcp-adapterposture and does not include endpoint URLs, API keys, tenant IDs, prompts, payloads, model IDs, or raw exceptions. - Confirm rate limiting is enabled in the target environment. Valid API keys must partition by key ID; no-key, blank-key, invalid-key, revoked-key, or inactive-key traffic must stay in the anonymous/IP partition.
- Confirm tenant binding matches the API edge. A tenant-bound key with a conflicting trusted tenant hint must fail closed rather than falling back to another tenant.
- Generate a disposable scoped API key with the minimum required scopes for the smoke scenario. Use
mcp:readplus event read-equivalent scope for protected reads, and addmcp:proposeonly when proposal smoke is needed. - Keep tool metadata trusted by source. Do not connect production clients to unreviewed MCP servers or accept remote tool import/execution as a substitute for this API-hosted registry.
- Keep rollback simple: set runtime
mcp.enabled=falsefor immediate shutdown of the mapped endpoint, or set startupMcp:Enabled=falseand restart when the route must be unmapped.
Do not commit .vscode/mcp.json with real tokens, tenant slugs, endpoint URLs, API keys, or copied response bodies. Root .mcp.json is git-ignored for local secrets; documentation must keep only placeholders.
{
"servers": {
"islamu-event-local-mcp": {
"type": "http",
"url": "http://127.0.0.1:<redacted-port>/mcp",
"headers": {
"X-API-Key": "${env:ISLAMU_EVENT_API_KEY}",
"X-Tenant-Slug": "${input:islamu_mcp_tenant_slug}"
}
}
},
"inputs": [
{
"type": "promptString",
"id": "islamu_mcp_tenant_slug",
"description": "Disposable tenant slug for local MCP smoke",
"password": false
}
]
}Set ISLAMU_EVENT_API_KEY only in your local shell or secret manager. Leave it unset to smoke anonymous-safe list_ai_tool_contracts, search_public_events, get_public_event, get_public_event_program_summary, and list_public_event_sessions. Use a disposable key with mcp:read plus event read-equivalent scope authority when smoking scoped event-management reads such as list_my_events, get_event_creation_context, get_event_publish_readiness, or event_management_context; never configure both Authorization and X-API-Key in the same client entry.
Use a solution-local .mcp.json only with placeholders or prompt-backed values. Do not commit real credentials.
{
"servers": {
"islamu-event-local-mcp": {
"type": "http",
"url": "http://127.0.0.1:<redacted-port>/mcp",
"headers": {
"X-API-Key": "<redacted-disposable-api-key>",
"X-Tenant-Slug": "<redacted-disposable-tenant-slug>"
}
}
}
}Run deterministic replay first; it is the CI-safe contract check and does not contact live clients or providers:
dotnet run --project Explore.Diagnostic/Explore.Diagnostic.csproj --configuration Release --no-restore -- ai-replay-report --output /tmp/explore-ai-replay-mcp-smokeThen start Inspector manually:
npx -y @modelcontextprotocol/inspectorInspector checklist:
- Connect to
http://127.0.0.1:<redacted-port>/mcp. - Configure
X-API-Keyfor scoped smoke, leave credentials blank for anonymous-safe discovery, or use a bearer token only for user-delegated local smoke. Do not configure both bearer and API key. Valid keys are rate-limited by key ID; missing/invalid/revoked keys are rate-limited by remote IP. - Include the same tenant binding used by the API edge when multi-tenant routing is active. A valid tenant-bound API key can provide tenant context; anonymous/no-key smoke still needs trusted tenant binding outside single-tenant mode.
- If the client receives
404, check the startup ceiling (Mcp:Enabled) and runtime setting (mcp.enabled) before investigating client headers. - Initialize the connection and list tools, resources, resource templates, and prompts.
- Anonymous, invalid-key, or revoked-key expected tool surface:
list_ai_tool_contracts,search_public_events,get_public_event,get_public_event_program_summary, andlist_public_event_sessionsonly. - Valid scoped-key expected surface:
mcp:readcan discover generic MCP read resources, andmcp:readplus event read-equivalent scope authority can discoverevent_management_context,list_my_events,get_event_creation_context,get_event_publish_readiness,get_event_program_management_context,get_event_custom_properties_context,get_event_registrations_context,get_event_team_context,get_event_template_catalog_context,get_event_template_sync_context, andget_event_session_template_sync_context.mcp:proposeis required to discover or callpropose_ai_tool_action,manage_event_with_confirmation, projected core lifecycle/aspect tools such aspropose_create_event_draft,propose_update_event_draft,propose_publish_event,propose_delete_event,propose_upsert_event_islamic_aspect, and Phase 5 sub-resource proposal tools such aspropose_create_event_session,propose_set_event_custom_property_value, andpropose_apply_event_template_sync. - Call
list_ai_tool_contractsand verify proposal/confirmation metadata is present. - Optional anonymous-safe public event read smoke: call
search_public_eventswith a disposable public search term,get_public_eventwith a known published public event id, or the program/session tools for the same published public event. Hidden, draft, archived, private, unknown, and cross-tenant events and their program/session data must not be disclosed. - Optional scoped event-management read smoke: call
list_my_eventsonly with a disposable bearer user or user-owned API key, callget_event_creation_contextonly against disposable tenant/user publisher data, callget_event_publish_readinessonly for a disposable draft event where REST HAL exposespublish-readiness, and readevent_management_contextonly for a disposable event. For Phase 5 reads, call at most one representative context for the scenario, such asget_event_program_management_context,get_event_registrations_context,get_event_team_context, or a template sync context, and verify it is gated by the same event HAL/domain authority as REST. Do not retain private event content, tenant/user identifiers, registration data, or raw IDs beyond the pass/fail note. - Optional scoped proposal smoke: prefer a low-risk proposal such as
propose_create_event_draftorpropose_update_event_draftwith a valid scoped key against a disposable conversation. Destructive or fanout-like tools such as delete, revoke, purge, and sync-apply require their destructive confirmation metadata but still stop after the proposed action is returned. - Do not call confirm/reject endpoints from Inspector and do not assert that an event was created.
Retain only scenario code, pass/fail status, redacted endpoint path, redacted auth mode, and bounded failure category. Discard protocol exports, browser storage, copied JSON bodies, and screenshots that contain private content.
- Rebuild the API and restart the MCP client before investigating stale tools.
- Open Copilot Chat in Agent mode and choose Select Tools.
- With a valid scoped API key, verify the expected MCP tools, resources, and prompts are visible. Protected event-management reads require
mcp:readplus event read-equivalent scope authority for API-key callers; proposal tools andmanage_event_with_confirmationrequiremcp:propose. Without a key, verify onlylist_ai_tool_contracts,search_public_events,get_public_event,get_public_event_program_summary, andlist_public_event_sessionsare visible. - Use a disposable prompt that asks for a proposal only, not direct event creation.
- Confirm that Copilot asks for tool approval and that the result says confirmation is required before side effects.
- If tools are missing, restart the API and the IDE MCP server entry, then verify the client config URL and auth headers.
Do not store Copilot transcripts, screenshots, raw tool payloads, tenant/user identifiers, bearer/API-key values, or model/provider output as evidence.
Use this only when Inspector is unavailable. Keep output local and redacted. Omit X-API-Key to smoke anonymous-safe discovery only.
curl --fail-with-body \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-H 'ProtocolVersion: 2025-06-18' \
-H 'X-API-Key: <redacted-disposable-api-key-or-env-placeholder>' \
-H 'X-Tenant-Slug: <redacted-disposable-tenant-slug>' \
--data '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
'http://127.0.0.1:<redacted-port>/mcp'Do not paste raw responses into tickets. Summarize only the method, pass/fail status, and bounded failure category.
Event.API.IntegrationTests/Features/McpProtocolContractTests.cs is the automated counterpart to this manual runbook. It uses WebApplicationFactory, authenticated test principals, a minimal stateless JSON-RPC helper, and fake/disposable in-memory AI conversation data to prove:
- anonymous and invalid-key
tools/listexpose only the anonymous-safe registry discovery and public event read surfaces; - valid scoped API-key
initialize,tools/list,resources/list,resources/templates/list, andprompts/listexpose the bounded authenticated surface; - public event MCP reads match REST visibility for published public event list/detail/program/session data and hide draft, archived, private, unknown, and cross-tenant data;
- authenticated event MCP reads call
list_my_events,get_event_creation_context,get_event_publish_readiness, andevent_management_contextthrough the JSON-RPC/mcpsurface, match REST ownership/publisher-context/publish-readiness/HAL affordance behavior, avoid caller-supplied user or tenant ids, and omit internal role ids from MCP output; - Phase 5 authenticated event MCP reads call program, custom-property, registration, team, template, and sync context tools through the JSON-RPC
/mcpsurface with bounded descriptors and HAL/domain-authority gates; tools/callfor registry discovery is redacted;- generic and projected proposal tools persist proposed actions only;
- proposal calls do not create events;
- malformed, unknown, hidden-field, and disabled-endpoint paths fail safely without echoing sensitive markers.
EventManagementMcpRedactionTestscovers event MCP error redaction,McpAuthorizationTestscovers API-key/bearer conflict handling and key/IP rate-limit partitioning, andMcpArchitectureTestskeeps MCP SDK dependencies and repository access inside the API adapter boundary.
The harness intentionally does not run MCP Inspector, GitHub Copilot, live AI providers, or product confirmation endpoints in normal CI.
The adapter emits bounded OpenTelemetry dimensions for MCP tool calls only:
- ActivitySource:
Explore.Mcp; - Meter:
Explore.Mcp; - metrics:
explore.mcp.tool_callsandexplore.mcp.tool_call_duration; - allowed tags: known MCP tool name, projected-tool flag, bounded outcome, and bounded failure code.
These traces/metrics must never include prompts, selected-reference content, tool payload JSON, provider responses, tenant/user identifiers, bearer/API-key values, endpoint URLs, model IDs, or raw exceptions.
Use the read-only doctor when reviewing MCP debug readiness:
dotnet run --project Explore.Diagnostic/Explore.Diagnostic.csproj --configuration Release --no-restore -- --timeout-seconds 30McpDebugReadinessDoctorCheck verifies that this runbook, protocol tests, deterministic replay/evaluation coverage, .gitignore local-secret posture, and the stdio decision ADR are present. It does not start servers, call live MCP clients, create tokens, persist config, run migrations, or print secrets.
Deterministic replay/evaluation evidence now includes MCP proposal-vs-execution checks:
dotnet run --project Explore.Diagnostic/Explore.Diagnostic.csproj --configuration Release --no-restore -- ai-replay-report --output /tmp/explore-ai-replay-mcp-smoke
dotnet run --project Explore.Diagnostic/Explore.Diagnostic.csproj --configuration Release --no-restore -- ai-eval-report --output /tmp/explore-ai-eval-mcp-smokeProduct MCP stdio hosting remains out of scope. ADR-011 records the current decision: no stdio host is implemented in Phase 12. Any future local-only diagnostic host must be separate from Explore.API, keep logs on stderr, simulate auth/tenant context explicitly, remain proposal-first, and ship its own tests/runbook before use.
| Client / path | Supported use | Evidence | Unsupported |
|---|---|---|---|
| MCP Inspector | Manual local/staging discovery and proposal-only smoke | Replay report + redacted manual checklist | Saved screenshots, protocol exports, confirmation calls |
| VS Code / GitHub Copilot Agent Mode | Manual tool visibility and approval-flow smoke | Redacted pass/fail notes only | Stored transcripts, direct mutation claims |
| WebApplicationFactory JSON-RPC tests | CI-safe protocol contract coverage | McpProtocolContractTests |
Live clients, live providers, raw protocol artifacts |
| Doctor / replay / evaluation | Read-only readiness and advisory MCP proposal-flow evidence | McpDebugReadinessDoctorCheck, ai-replay-report, ai-eval-report |
Starting servers, creating tokens, live providers |
| Official C# SDK client | Future upgrade target when test-host transport is practical | ADR/update checklist | Product behavior changes without review |
| curl / JSON-RPC fallback | Local troubleshooting only | Method/status/failure category | Pasted credentials or raw response bodies |
Before upgrading ModelContextProtocol.AspNetCore or supporting new client-visible behavior, complete this gate:
- Review SDK/protocol release notes.
- Rerun focused MCP API integration tests.
- Rerun
ai-replay-reportand advisory evals when relevant. - Repeat redacted Inspector smoke if endpoint behavior changed.
- Verify docs still say startup default
/mcp, runtime-governed, stateless, API-key-first for external clients, anonymous-safe only without a valid key, registry-backed, and proposal-first. - Keep rollback simple: set runtime
mcp.enabled=falsefor immediate shutdown or setMcp:Enabled=falseplus restart to unmap the endpoint.
Unsupported without a new ADR/task: stateful sessions, legacy SSE, server-to-client requests, sampling, elicitation, roots, completions, subscriptions, list-changed notifications, remote tool import/execution, direct repository mutation, and product stdio hosting.