# 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](https://learn.microsoft.com/azure/cosmos-db/time-to-live)).
   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`.
