Skip to content

fix(mcp): keep one cached catalog per protocol era - #109

Open
carldebilly wants to merge 2 commits into
mainfrom
dev/cdb/mcp-per-era-snapshot-cache
Open

carldebilly wants to merge 2 commits into
mainfrom
dev/cdb/mcp-per-era-snapshot-cache

Conversation

@carldebilly

@carldebilly carldebilly commented Sep 22, 2026

Copy link
Copy Markdown
Member

Closes #101.

McpSessionContext kept one cached catalog per session, and publishing it for one protocol era overwrote the other era's entry. A connection is served modern requests, then an initialize and legacy ones. A modern request accepted before initialize can still be in flight, because the SDK dispatches one connection's requests concurrently, and its build then publishes after the legacy one. That replaced the legacy entry the availability fallback reads. A legacy build that then failed transiently found only a modern entry and failed closed, on a connection that had been serving a legacy catalog a moment earlier.

The context now keeps one entry per era, read through GetSnapshotCache(sessionless), and each publication writes only its own era's slot. The per-read era checks are gone, since the slot now is the era. ThrowSanitizedIfAClientAlreadyHasASchema asks HasServedSnapshot (either era), so a client that has seen any schema still gets the sanitised message.

History: this PR first closed #101 as unreachable. Its test showed that a newly submitted modern request is rejected once initialize has negotiated the legacy version. Codex pointed out that this misses a modern request already in flight, which is right, so the second commit implements the fix and drops that test.

Test plan

  • When_BothErasPublishOnOneSession_Then_EachKeepsItsOwnEntry was written against the new lookup while it still sat on the single slot, and was red there (the modern publication evicted the legacy entry). It went green once the slots were split.
  • The existing When_OneConnectionIsServedBothEras_Then_EachGetsItsOwnCatalog still passes.
  • Repl.McpTests 324 (+1 skipped, run twice).

Issue #101 reports that one snapshot slot per session lets publishing one era's catalog
evict the other's: alternating requests would rebuild on every switch, and a legacy build
that failed transiently would find only a modern entry and lose its availability fallback.
Both consequences need a modern catalog to be published after a legacy one on the same
connection, and the SDK does not allow that.

McpServerImpl (2.2.0) sets a session's protocol version on its first request that carries
one. initialize supersedes a version set earlier, and after that SetNegotiatedProtocolVersion
rejects any request naming a different version with InvalidRequest. So one connection goes
through modern requests, then an initialize, then legacy requests only. Nothing can evict the
legacy entry the fallback reads, there is one switch at most, and a legacy build that fails
before any legacy success has nothing to fall back to with one slot or two.

The new test, written as #101's regression, could not reproduce it: the modern request after
initialize came back as InvalidRequest (-32600). It now pins that premise instead. If the SDK
ever lets a connection return to the modern era, the test fails and the cache needs one slot
per era. Pointing the fourth request at no version at all turns it red, so the assertion is
not swallowed by its null-conditional chain. SnapshotCacheEntry's documentation now says why
one slot is enough.

Refs #101
@carldebilly carldebilly added this to the 0.12.0 milestone Sep 22, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-22T22:39:53.221558Z e84eef7 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce942d6347

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Repl.McpTests/Given_McpConcurrentSessions.cs Outdated
The previous commit closed #101 as unreachable: after initialize, the SDK rejects a new
modern request, so it looked as if no modern catalog could ever be published after a legacy
one. Review on #109 showed the hole in that. A modern request accepted before initialize
can still be in flight, since the SDK dispatches one connection's requests concurrently, and
its build then publishes after the legacy one. With one slot, that replaces the legacy entry
the availability fallback reads, which is exactly what #101 describes. The window is narrow
but real.

McpSessionContext now keeps one entry per era, reached through GetSnapshotCache(sessionless),
and publishing writes only its own era's slot. The era checks at each read are gone, since
the slot already is the era. ThrowSanitizedIfAClientAlreadyHasASchema asks HasServedSnapshot,
meaning either era, so a client that has seen any schema still gets the sanitised message. The
test that pinned the one-way premise is removed, along with the documentation paragraph built
on it.

TDD: the new When_BothErasPublishOnOneSession_Then_EachKeepsItsOwnEntry was written against
the new lookup first implemented over the single slot, and was red there — the modern
publication evicted the legacy entry — before the slots were split.

Refs #101
@carldebilly carldebilly changed the title test(mcp): pin the one-way era order that makes one snapshot slot enough fix(mcp): keep one cached catalog per protocol era Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp: one snapshot slot per session drops the other era's availability fallback

1 participant