Skip to content

Adopt MCP 2026-07-28 and Explicit Conversation IDs

  • Status: Accepted
  • Deciders: Emmitt Johnson
  • Date: 2026-08-09

Context and Problem Statement

Forge needs to move its MCP servers and bridge from ModelContextProtocol 1.4.1 / ModelContextProtocol.AspNetCore 1.2.0 to csharp-sdk 2.1.0 so Smithy and the CLI can speak MCP 2026-07-28 (src/dotnet/Directory.Packages.props). That protocol revision removes HTTP protocol sessions and Mcp-Session-Id, and csharp-sdk 2.1.0 defaults HTTP transport to stateless mode where transport session identity is no longer available for durable keying (docs/_internal/mcp-conventions.md; SEP-2567).

Smithy's MCP ask tool historically derived durable Cosmos history keys from transport identity. That design was already broken before the protocol change, because almost no MCP clients resume a prior transport session: per-tool-call clients like ChatGPT never accumulated working history, and per-process clients like VS Code bled unrelated threads together until restart (SEP-2567). After the 2.1.0 bump, leaving the old server.SessionId ?? "default" fallback in place would silently collapse each authenticated user into one permanent, unbounded shared history bucket (docs/_internal/mcp-conventions.md).

Forge therefore needs a Smithy history model that works under MCP 2026-07-28, matches the consumers Forge actually has, and bounds persisted conversation state without depending on transport-derived identifiers.

Decision Drivers

  • Adopt MCP 2026-07-28 for the consumers Forge actually has (VS Code, GitHub Copilot), while keeping dual-era negotiation so CLI installs that haven't self-updated yet still work (docs/_internal/mcp-conventions.md; docs/guides/development/install-saif-cli.md; https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning).
  • Remove transport-derived history keying that the new protocol forbids and that SEP-2567 showed was already misaligned with real client behavior.
  • Make the post-upgrade failure mode impossible by eliminating the ?? "default" storage bucket.
  • Keep the Smithy ask surface forward-compatible with a future server-minted conversation handle without committing to that design now.
  • Reuse existing Forge MCP conventions where they are already established in Smithy, including JSON tool payloads and camelCase tool parameter names (src/smithy/src/Smithy/Mcp/SmithyTools.cs).

Considered Options

  • Pin McpServerOptions.ProtocolVersion to 2026-07-28 on both servers, rather than negotiate across the SDK's dual-era support, since Forge's only known consumers (VS Code, GitHub Copilot) are already confirmed on that revision (docs/_internal/mcp-conventions.md).
  • Keep history keyed from transport identity via server.SessionId.
  • Continue persisting history when either identity or conversation context is missing by falling back to "default" or "anonymous".
  • Keep ask returning a bare string.
  • Introduce a create_conversation or other server-minted handle tool now.
  • Rename the new parameter to SEP-style snake_case such as conversation_id.
  • Purge old Cosmos conversation documents proactively.
  • Move APIM registration to native MCP support as part of this change.

Decision Outcome

Chosen option: adopt MCP 2026-07-28 via csharp-sdk 2.1.0, leave ProtocolVersion unset so the SDK negotiates dual-era (2024-11-05 through 2026-07-28), and move Smithy history from transport-derived keys to an optional caller-supplied conversationId on ask, because that is the lowest-commitment change that fixes the existing history bug, avoids the post-upgrade shared-bucket failure mode, and stays compatible with CLI installs that haven't self-updated yet (docs/_internal/mcp-conventions.md; docs/guides/development/install-saif-cli.md).

Forge records the following as part of this decision:

  1. Adopt MCP 2026-07-28 and csharp-sdk 2.1.0 on both Smithy's HTTP server and the CLI's stdio server, and leave ProtocolVersion unset (dual-era negotiation) on both. Forge's only known MCP consumers, VS Code and GitHub Copilot, are already confirmed on 2026-07-28 (docs/_internal/mcp-conventions.md), but pinning to that revision would hard-reject any CLI install that hasn't self-updated past this change when it acts as a client against Smithy's endpoint via RemoteMcpBridge: saif doctor fix --self is opt-in, not automatic, and a legacy client on the wire has no fall-forward mechanism when it receives -32022 UnsupportedProtocolVersion back. The session-history fix this ADR makes (item 2 below) is independent of which era gets negotiated — Smithy no longer keys state off server.SessionId regardless — so there is no reason to force the narrower pin during rollout. Revisit pinning once CLI self-update adoption is high enough that the compatibility window no longer matters (docs/guides/development/install-saif-cli.md; https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning).
  2. Stop deriving Smithy conversation history from transport identity and use an explicit optional conversationId parameter on ask instead. The old design was not merely outdated. It never worked reliably under the client behaviors SEP-2567 documented: per-tool-call clients never got a working history, per-process clients mixed unrelated threads together, and after the SDK bump a null SessionId plus ?? "default" would have created one permanent unbounded shared history bucket per authenticated user (docs/_internal/mcp-conventions.md; src/smithy/src/Smithy/Mcp/SmithyTools.cs).
  3. Gate history on one rule only: Smithy loads and persists history only when both an authenticated Entra oid and a caller-supplied conversationId are present. Everything else is one-shot and touches no storage (src/smithy/src/Smithy/Mcp/SmithyTools.cs).
  4. Return the ask answer as tool text content and expose a structured envelope alongside it: the plain text answer remains in the tool content, while MCP structuredContent carries { answer, conversationId, note }. This preserves existing text consumers and still makes dropped or ignored conversationId values visible to callers that read the structured payload (src/smithy/src/Smithy/Mcp/SmithyTools.cs).
  5. Defer a create_conversation or other server-minted handle tool. ask is already forward-compatible because it now accepts conversationId, so adding a minting tool later would be additive only (src/smithy/src/Smithy/Mcp/SmithyTools.cs). This keeps commitment low in a space with no strong public prior art for handle-based conversational MCP servers under this spec revision.
  6. Use the camelCase name conversationId. SEP-2567's examples use snake_case names such as basket_id, browser_id, and connection_id, but those examples are non-normative, and Smithy already uses camelCase tool parameters such as searchText and maxResults (https://modelcontextprotocol.io/seps/2567-sessionless-mcp; src/smithy/src/Smithy/Mcp/SmithyTools.cs). Forge records this as a deliberate divergence for internal consistency.
  7. Rely on the existing Cosmos retention policy and do not add a purge. The conversations container already has default_ttl = 2592000 or 30 days in src/smithy/infra/agent/main.tf, committed on 2026-05-07 and unrelated to this ADR (src/smithy/infra/agent/main.tf). Azure Cosmos DB TTL is a delta from each item's last-modified _ts, so old documents under the previous mcp:{oid}:{sessionId} key scheme will age out 30 days after their final write with no migration or purge required (Azure Cosmos DB TTL docs, accessed 2026-08-09). Forge also records the opposite implication: TTL alone would not have bounded the old post-bump mcp:{oid}:default failure mode, because an actively written shared bucket would refresh _ts on every call and never expire. The keying change is what bounds history, not the TTL.
  8. Defer APIM native MCP support. Forge stays on OpenAPI-spec-based APIM registration for now. That work is out of scope for this ADR and will be handled in a separate ADR later.

Consequences

  • Good, because Forge adopts MCP 2026-07-28 cleanly for the consumers it actually has, while dual-era negotiation keeps CLI installs that haven't self-updated yet working against Smithy.
  • Good, because Smithy history now follows an explicit caller contract instead of transport behavior that SEP-2567 removed and clients never implemented consistently.
  • Good, because the storage interaction is bounded to authenticated, caller-opted-in conversations, and one-shot calls now perform zero history reads or writes.
  • Good, because the response envelope makes dropped or ignored conversationId values visible to the caller instead of silently resetting context.
  • Good, because old Cosmos documents self-expire without a migration or purge once the old key paths stop receiving writes.
  • Bad, because callers now need to thread conversationId correctly if they want continuity, and the fallback behavior is intentionally one-shot when they do not.
  • Bad, because dual-era negotiation means Forge doesn't get the fail-fast diagnostic a pin would give against a truly unexpected client; this is deliberately accepted while CLI self-update adoption for this change is still ramping up.
  • Bad, because Forge is deliberately diverging from SEP-2567's snake_case examples for this one parameter name.
  • Bad, because APIM native MCP support remains deferred work and not part of this adoption.

More Information

  • SEP-2567 makes the protocol change normative but leaves explicit state handles as a documented tool-design pattern rather than a wire-level feature, which is why Forge treats conversationId as an ordinary tool parameter instead of inventing protocol state (https://modelcontextprotocol.io/seps/2567-sessionless-mcp).
  • Smithy's persisted key format is mcp:{oid}:{conversationId} in src/smithy/src/Smithy/Mcp/McpConversationState.cs.