feat(mcp): migrate to ModelContextProtocol 2.2.0 - #71
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3cb6d8d09b
ℹ️ 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".
|
Adversarial review pass applied:
|
autocarl
left a comment
There was a problem hiding this comment.
I validated the current head locally: strict Release build is clean (0 warnings / 0 errors), and the full suite passes with 1,314 total / 1,313 passed / 1 known Inspector skip. The SDK migration itself is small, but the 2.0 request model exposes one blocking concurrency regression plus three contract/documentation gaps.
The blocker is the shared mutable MCP service overlay: with the default 2026-07-28 protocol, every request has its own destination-bound request.Server, but AttachServer overwrites _roots, _sampling, _elicitation, and _feedback shared by concurrent invocations. I reproduced this deterministically: request A advertised Sampling and paused; request B without Sampling ran; when A resumed it observed not-supported.
The existing Codex thread is also still partially unresolved: CommandAnnotations.LongRunning, CommandAnnotationsBuilder.LongRunning(), and CommandBuilder.LongRunning() continue to promise task-based programmatic execution, although this PR deliberately emits no Tasks/Execution signal. Please describe it as Repl-local metadata until #72 is implemented.
Non-blocking release note: a stable package with ModelContextProtocol 2.0.0-preview.3 fails pack with NU5104. The PR caveat already acknowledges the required re-pin before stable 0.12; keeping main on 0.12.0-dev.* is fine, but this should remain an explicit release gate.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c235f9a90e
ℹ️ 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".
| var server = _servers.Effective; | ||
| if (server?.ClientCapabilities?.Roots is null) | ||
| { | ||
| return Current; |
There was a problem hiding this comment.
Key cached roots by the destination session
When one McpServerHandler serves two root-capable sessions, this now resolves the correct request-bound server but still returns the single shared _hardRoots cache as soon as _hardRootsLoaded is true. Thus, if client A loads its roots first, a command invoked by client B receives A's root URIs without issuing roots/list to B; this can both expose another client's workspace roots and generate B's root-dependent tool snapshot from A's workspace. Keep the hard-root cache/version state per McpServer (or per session) rather than per handler.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed independently on 3cc415ae. A deterministic two-session probe gave client A root-a and client B root-b; after A populated the shared handler, B returned "root-a:file:///root-a" instead of its own root. This is a real cross-session data leak. The hard-roots state must be keyed by destination/session; because GetSnapshotAsync also caches one _snapshot for the handler, the root-dependent generated snapshot needs the same session affinity rather than only fixing the McpClientRootsService array.
There was a problem hiding this comment.
Fixed in the follow-up commit. Hard roots are now session state: cache entries are keyed by the destination server in a ConditionalWeakTable (weak keys — entries die with their session), with a global version stamp handling roots-list-changed invalidation (coarse but correct; the event is rare). Client B now performs its own roots/list round-trip and can no longer observe client A's workspace roots, and the root-dependent snapshot builds from the right workspace. Regression: When_TwoRootCapableClientsShareHandler_Then_EachSeesOwnRoots (RED observed with exactly your scenario — B received A's file:///ga).
There was a problem hiding this comment.
Revalidated on exact head ddce4d408466630cacc59d5c67b53cb92f6d8653. Two Roots-capable clients on the RunAsync path now receive their own file:///ga and file:///bu roots, and the targeted regression passes. The original hard-roots leak on that path is fixed. A separate verified leak remains when the documented public API reuses one BuildMcpServerOptions() result across servers.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3cc415ae43
ℹ️ 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".
autocarl
left a comment
There was a problem hiding this comment.
Re-reviewed current head 3cc415ae. The original request-binding blocker is fixed: the new AsyncLocal regression passes, the pinned 2025-11-25 legacy handshake/list/call regression passes, the strict MCP test build is clean (0 warnings / 0 errors), and the documentation/.LongRunning() contract corrections are present. CI is green, including the required Build, Test, Pack check.
Two cross-session lifecycle defects remain and are independently reproduced in the existing threads:
- Roots cache/snapshot affinity: after session A loads
root-a, session B configured withroot-breceives"root-a:file:///root-a". This leaks one client workspace into another and can contaminate root-dependent tool discovery. - Routing subscription lifetime: after session A closes, routing invalidation no longer produces
tools/list_changedfor still-active session B; the deterministic probe times out after 2 seconds.
Please key Roots and root-dependent snapshots by destination/session, and track active session notification/subscription lifetimes so one session cleanup cannot disable another. The migration is materially better, but these remain blocking before merge.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b094d05506
ℹ️ 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".
| // version stamp invalidates every entry on any roots-list change (coarse, but the | ||
| // event is rare and correctness beats granularity here). | ||
| private readonly System.Runtime.CompilerServices.ConditionalWeakTable<McpServer, SessionRoots> _sessionRoots = []; | ||
| private McpClientRoot[] _softRoots = []; |
There was a problem hiding this comment.
Scope soft-root state per MCP session
When one handler serves two clients that lack native Roots support, this single _softRoots array is shared by both sessions: after client A calls SetSoftRoots, client B sees A's URI through Current and HasSoftRoots, and its soft-root-dependent modules can be enabled. This exposes one client's workspace context to another and contradicts the documented current-session semantics; store fallback roots per effective server/session just as hard roots are scoped.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed independently on b094d055. Two sessions without native Roots shared one handler; A called SetSoftRoots(file:///root-a), then B called a separate soft_show tool and received "file:///root-a". This directly violates the current-session contract and leaks workspace context. Please key soft roots by the effective destination/session just like hard roots, including HasSoftRoots, Current, SetSoftRoots, and ClearSoftRoots.
There was a problem hiding this comment.
Revalidated on exact head ddce4d408466630cacc59d5c67b53cb92f6d8653 with the same deterministic two-session probe on RunAsync. After A sets file:///root-a, B returns none and never observes A's root. The original soft-root leak is fixed on that path; the public static-options path is covered by a separate verified blocker.
| var snapshotVersion = Volatile.Read(ref _snapshotVersion); | ||
| if (Volatile.Read(ref _builtSnapshotVersion) == snapshotVersion |
There was a problem hiding this comment.
Cache generated snapshots per MCP session
The earlier hard-root cache fix is insufficient because this handler-level cache is returned before _roots.GetAsync() and BuildSnapshotCore() run for a later session. If client A first lists tools with a module predicate based on IMcpClientRoots.Current (or IsSupported), client B receives A's generated tool/resource/prompt snapshot; B can therefore be shown unavailable tools or miss its own workspace-specific tools. Key snapshots/version state by the request's session/destination server, or rebuild session-aware snapshots per request.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed independently on b094d055. I registered roots_supported when IMcpClientRoots.IsSupported is true and roots_unsupported otherwise, then started A with Roots and B without Roots on one handler. A first tools/list returned only roots_unsupported, showing that B session state already influenced A snapshot generation; the handler-global _snapshot then preserves that cross-session result. Please ensure BuildSnapshotCore runs with the originating request/session context and key snapshot/version/gate state per destination session.
There was a problem hiding this comment.
Revalidated on exact head ddce4d408466630cacc59d5c67b53cb92f6d8653. Snapshot/gate/version state is now owned by each McpSessionContext, and the original cross-session snapshot contamination is fixed. I am raising a separate protocol-era finding because the new green test intentionally varies the modern tools/list catalog per connection.
| // One handler can serve several concurrent sessions; session-scoped concerns (routing | ||
| // notifications, roots list-changed handler) track EVERY active session, not a single | ||
| // last- or first-attached server. Guarded by _attachLock. | ||
| private readonly List<McpServer> _sessions = []; |
There was a problem hiding this comment.
Track compatibility bootstrap state per MCP session
With DiscoverAndCallShim enabled, _compatibilityIntroServed remains handler-global even though this change allows the handler to serve every session in _sessions. Once client A consumes the shim, client B's first tools/list receives the real list instead of discover_tools/call_tool; after a later routing invalidation, whichever session lists first consumes the only re-armed shim. A weak client in any other session then cannot use the documented fallback to discover newly session-specific tools, so the bootstrap flag must be maintained per session.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed independently on b094d055. With DiscoverAndCallShim, A first tools/list returned discover_tools and call_tool, but B first tools/list returned only the real echo tool. The handler-global _compatibilityIntroServed lets one client consume another client bootstrap. Please track/re-arm this state per session.
There was a problem hiding this comment.
Revalidated on exact head ddce4d408466630cacc59d5c67b53cb92f6d8653. Each RunAsync transport context now owns its compatibility-intro flag, so one client no longer consumes another client's bootstrap. The original handler-global state bug is fixed; modern list-invariance/subscription behavior is addressed separately.
autocarl
left a comment
There was a problem hiding this comment.
Re-reviewed current head b094d055. The two previous blockers are genuinely fixed: the new hard-roots isolation regression and surviving-session routing-notification regression both pass locally; strict MCP test build is clean (0 warnings / 0 errors); CI is green, including the required Build, Test, Pack check.
Three remaining handler-global states still violate the new multi-session model, and all three are independently reproduced in the existing threads:
- Soft roots leak: after session A sets
file:///root-a, session B reads the same URI through its own command. - Generated snapshot affinity: with A advertising Roots and B not advertising Roots, A first
tools/listreturns theroots_unsupportedmodule, showing B session state influenced A generation; the single_snapshotalso caches the cross-session result. - Compatibility bootstrap affinity: in
DiscoverAndCallShimmode, A receivesdiscover_tools/call_tool, while B first list receives onlyechobecause A consumed the handler-global bootstrap flag.
Please move all session-sensitive state — hard and soft roots, generated snapshots/versioning, and compatibility intro state — behind one coherent per-session context. The latest commit is another real improvement, but these remain blocking cross-session correctness/data-isolation issues.
Architecture follow-up: relation to #70There is a strong architectural relationship between this PR's multi-session findings and #70, but they cover different lifetime boundaries:
A hosted/MCP session can process multiple requests concurrently, so a DI scope per session does not replace request-level binding to Recommended splitRequired in this PRIntroduce one coherent internal MCP session container, for example: internal sealed class McpSessionContext
{
public McpClientRootsService Roots { get; }
public McpSnapshotCache Snapshot { get; }
public McpCompatibilityState Compatibility { get; }
public IServiceProvider Services { get; }
}The exact shape is flexible, but it should own all state that currently assumes one handler equals one session:
For Externally hosted Remains in #70#70 should still implement the general Repl lifetime contract:
Once #70 exists, Sequencing recommendationThe current cross-session leaks should not be merged with the expectation that #70 will repair them later: they are observable correctness/data-isolation bugs introduced by sharing one MCP handler across sessions. The pragmatic sequence is:
Implementing #70 first and rebasing this PR on it would also be architecturally valid, but #70 has a much broader surface and would significantly delay the SDK migration. The key requirement either way is that this PR must not merge while known MCP session state remains handler-global. |
|
Agreed on the layering table and the sequencing — this matches the plan on #70. Concretely:
You are right that the snapshot cache and |
|
McpSessionContext landed in 1f8950f, per the recommended split:
Both remaining handler-global leaks were RED-observed before the refactor with dedicated regressions: the roots-less session saw the roots-gated tool of the other session (snapshot cache), and only the first session received the Linked as Related to #70 — PR #74 implements the general session-scope contract, and this context will construct from the session-scoped provider once both merge. |
- Bump ModelContextProtocol 1.4.1 -> 2.0.0-preview.3. - Remove the Tool.Execution mapping for .LongRunning() commands: SDK 2.0 dropped the experimental MCP Tasks tool augmentation (Tasks SEP deferred out of the 2.0 protocol release). The annotation stays in Repl's model; protocol-level task support returns with the SDK Tasks runtime. - Keep supporting Roots, Sampling, and Logging: deprecated by spec 2026-07-28 (SEP-2577, MCP9005) with no replacement, still relied on by current hosts. Scoped, documented pragmas at the feature touchpoints. - Document the SDK/protocol version posture in docs/mcp-reference.md. Full suite green against the new SDK (1312 passed, 1 known skip), including all MCP capability, tool-call, roots, sampling, and logging regressions. Note: re-pin to the stable 2.0.0 release before cutting stable 0.12. Refs #51
…e shape (review) - Correct the migration rationale: MCP Tasks was EXTRACTED to ModelContextProtocol.Extensions.Tasks (store, task results, client polling), not removed; the per-tool Tool.Execution augmentation is gone from the protocol surface. Comments and docs now say so, and Repl still deliberately does not advertise task support without the runtime. - Name the designated successor (SEP-2322 multi-round-trip requests) in the deprecation pragmas instead of claiming 'no replacement API'. - Narrow MCP9005 pragmas to their touchpoints in McpServerHandler and Given_McpIntegration (file-scoped kept only where usage is dense). - Lock the SDK-2.0 tools/list wire shape: a .LongRunning() tool serializes its annotations and emits no task/execution augmentation. - Align remaining .LongRunning() doc mentions (overview, coding-agents guide, package README) with the current no-advertisement posture.
…ed server field SDK 2.0's 2026-07-28 protocol path hands each request a destination-bound McpServer, and one handler can serve several sessions. The four capability services (roots, sampling, elicitation, feedback) stored the last-attached server in a shared mutable field, so a concurrent request from another session could cross-wire capabilities mid-call (IsSupported flipping while a handler was awaiting). - McpRequestServerAccessor: AsyncLocal request binding flowing with the invocation, session-level server as fallback for code outside a request (routing notifications, roots list-changed handler). - Services resolve the effective server through the accessor with a single read per operation (no torn check-then-use). - McpServerHandler splits session-level attach (RunAsync, once) from request-level binding (every handler); externally hosted servers adopt the first observed server for session concerns. - Deterministic regression: two sessions on one handler, sampling-capable client pauses mid-call while a sampling-less client is served — the paused call must keep observing ITS client's capabilities (RED observed: 'True|False' on the pre-fix code, exactly the reported repro).
… Tasks wording (review) - Regression pinning the last initialize-era protocol revision (2025-11-25): asserts the negotiated version and a tool list + call — the default client negotiates 2026-07-28 and never exercised the fallback path. - Roots/Sampling/Logging documented as legacy-compatibility only: deprecation notices in mcp-agent-capabilities.md and mcp-advanced.md steer new applications toward IReplInteractionChannel / soft roots; mcp-reference.md no longer reads as an endorsement. - Tasks wording corrected everywhere: the SDK has shipped the Tasks extension (ModelContextProtocol.Extensions.Tasks); what is pending is Repl's integration (issue #72) — including the CommandAnnotations.LongRunning XML doc that still promised task-based execution.
…le (review) - Hard roots are now SESSION state: entries keyed by destination server in a ConditionalWeakTable (weak keys die with the session), with a global version stamp for roots-list-changed invalidation. One session can no longer receive another session's cached workspace roots, and the root-dependent snapshot builds from the right workspace (RED observed: client B received client A's roots). - Session attachment is reference-counted: the handler tracks every active session, discovery notifications fan out to ALL of them, the accessor fallback moves to a surviving session on close, and the routing subscription is dropped only when the LAST session ends. A first-session close no longer silences the survivors (RED observed: surviving session timed out waiting for tools/list_changed). - Roots list-changed handler registered once per session (per-server registration replaces the single global flag).
One handler serves several sessions; everything that varied per client was still handler-global after the earlier point fixes. McpSessionContext now owns it all, per the architecture review: - hard AND soft roots: McpClientRootsService is one instance per session (plain fields again — the ConditionalWeakTable keying is gone); a session's 'workspace init' no longer sets another session's workspace. - generated snapshot + version + gate: the tool graph can be gated on session capabilities, so each session caches its own build against the handler-global routing version (RED observed: the roots-less session saw the roots-gated tool of the other session). - compatibility-shim intro: per-session flag, reset for every active session on routing invalidation (RED observed: only the first session received the discover_tools/call_tool intro). - per-session service overlay handed to McpServer.Create; request handlers recover their session through request.Server.Services instead of using a destination-bound per-request server as a surrogate session key. - externally hosted servers (BuildDynamicServerOptions) share one explicit lazy fallback context instead of racing a last-attached field. Request-bound OUTBOUND capabilities (sampling/elicitation/feedback) keep flowing through the per-request AsyncLocal accessor — finer than the session, unchanged. Related to #70 (per-session DI scopes generalize the lifetime contract; this context will construct from the session-scoped provider once both merge).
…in SDK 2.0 The deprecation pragmas and the reference doc claimed the SEP-2322 multi-round-trip successor was 'not yet consumable in the SDK'; preview.3 actually ships it experimentally (MrtrContext/MrtrContinuation/MrtrExchange). Reworded to 'shipped experimentally, not adopted by Repl yet' — adoption is a follow-up under the compliance track.
b362736 to
ddce4d4
Compare
autocarl
left a comment
There was a problem hiding this comment.
Re-reviewed the exact current head ddce4d408466630cacc59d5c67b53cb92f6d8653 against base d4f039841568c27dbb820ddfece9ec488106cf48.
The previous request-binding, RunAsync hard/soft-roots isolation, snapshot contamination, compatibility-intro ownership, first-session-close lifecycle, legacy handshake, Tasks wording, and deprecation-guidance findings are fixed. I independently rebuilt with warnings as errors, ran the full solution (1,467 passed / 0 failed / 1 external-toolchain smoke skipped), packed Repl.Mcp, ran the concurrency tests repeatedly, and verified the exact-head CI provenance.
I still cannot approve the MCP 2026-07-28 migration. Four blockers remain:
- the documented reusable
BuildMcpServerOptions()path captures one context and demonstrably leaks soft roots across servers while losing request capabilities; - modern list results still vary by connection/first-call state;
- list-change notifications bypass
subscriptions/listen; - logging notifications ignore the modern per-request
logLevelopt-in.
Two additional medium findings cover a teardown false-green and modern soft-roots guidance. Each inline comment includes a deterministic reproduction and acceptance-test shape.
Primary references: MCP 2026-07-28 key changes and the exact ModelContextProtocol SDK 2.2.0 implementation.
| var serverName = _options.ServerName ?? ResolveAppName() ?? "repl-mcp-server"; | ||
| var serverVersion = _options.ServerVersion ?? "1.0.0"; | ||
| var snapshot = BuildSnapshotCore(); | ||
| var snapshot = BuildSnapshotCore(CreateSessionContext()); |
There was a problem hiding this comment.
[blocking] Do not capture one session context in reusable static options
BuildMcpServerOptions() is public and the transport guide explicitly tells hosts to build it once and reuse it across concurrent McpServer.Create(...) calls. This line now builds one McpSessionContext; its tool/resource/prompt adapters capture that context's service overlay. Static ReplMcpServerTool.InvokeAsync then calls the adapter directly, bypassing BindRequestServer/ResolveContext.
I reproduced both consequences on this exact head:
- server A calls
SetSoftRoots(file:///root-a); server B, created from the same options, readsfile:///root-ainstead ofnone; - a Sampling-capable client calls a tool that returns
IMcpSampling.IsSupported; it receivesFalsebecause the static path never binds the destination server.
This is cross-connection state exposure on the documented integration path. Please arrange per-connection/request scoping for static options (or stop documenting them as reusable), bind each invocation's request server, and add two-server soft-root isolation plus capability-binding regressions using one shared options instance.
|
|
||
| toolsWithRoots.Should().Contain(tool => string.Equals(tool.Name, "gated", StringComparison.Ordinal)); | ||
| toolsWithoutRoots.Should().Contain(tool => string.Equals(tool.Name, "always", StringComparison.Ordinal)); | ||
| toolsWithoutRoots.Should().NotContain(tool => string.Equals(tool.Name, "gated", StringComparison.Ordinal)); |
There was a problem hiding this comment.
[blocking] Keep 2026-07-28 catalogs invariant across connections
Both clients in this test negotiate 2026-07-28 (verified with explicit assertions in a local probe), but the test intentionally expects different tools/list results based on the connection's Roots capability. The final spec removed protocol-level sessions and explicitly says tools/list, resources/list, and prompts/list no longer vary per connection. Replacing these expectations with list equality fails deterministically: the Roots client receives { always, gated }, while the other receives { always }.
Please keep request-scoped capabilities for invocation behavior, but do not use McpSessionContext to vary modern catalog membership. If connection-specific catalogs and the first-list compatibility shim are retained for legacy clients, pin those tests/paths to 2025-11-25; add a modern regression asserting invariant lists and use explicit ordinary arguments or server-minted handles for cross-call state.
| try | ||
| { | ||
| using var timeoutCts = new CancellationTokenSource(NotificationSendTimeout); | ||
| await server.SendNotificationAsync(method, timeoutCts.Token).ConfigureAwait(false); |
There was a problem hiding this comment.
[blocking] Route modern list changes through subscriptions/listen
This raw broadcast bypasses the SDK 2.2.0 subscription pipeline. For 2026-07-28, */list_changed may be delivered only for notification types explicitly requested through subscriptions/listen, and each notification must carry the listen request id in _meta/io.modelcontextprotocol/subscriptionId. RegisterNotificationHandler only installs a local callback; it does not subscribe.
I added NegotiatedProtocolVersion == "2026-07-28" to When_FirstSessionCloses_Then_SurvivingSessionStillReceivesRoutingNotifications; it still passes even though that client never opens subscriptions/listen, proving the current test accepts an unsolicited modern notification.
Please preserve direct broadcasts only for initialize-era clients. For modern clients, either drive SDK primitive collections so its built-in fan-out runs, or implement SubscriptionsListenHandler with acknowledgement, filtering, tagging, and lifetime handling. Add regressions for no delivery without opt-in and tagged delivery with opt-in.
| } | ||
|
|
||
| await _server!.SendNotificationAsync( | ||
| await server.SendNotificationAsync( |
There was a problem hiding this comment.
[blocking] Honor the per-request modern log level before emitting messages
In 2026-07-28, _meta/io.modelcontextprotocol/logLevel replaces logging/setLevel; when it is absent, the server must not emit notifications/message for that request. Here IsLoggingSupported is true whenever any effective server exists, and SendMessageAsync sends unconditionally without checking the request's log level or severity threshold.
The existing When_ToolEmitsUserFeedback_Then_McpReceivesNotifications client supplies notification handlers but no logLevel. I added an assertion that it negotiated 2026-07-28; the test remains green and receives all messages, reproducing the violation.
Please carry the request context/log level through the request binding, return unsupported when modern metadata is absent, filter by the requested threshold, and keep legacy logging/setLevel semantics only on legacy revisions. Add negative and threshold regressions.
| await clientA.DisposeAsync().ConfigureAwait(false); | ||
| await clientB.DisposeAsync().ConfigureAwait(false); | ||
| await cts.CancelAsync().ConfigureAwait(false); | ||
| _ = serverTaskA; |
There was a problem hiding this comment.
[medium] Await server tasks so teardown failures cannot stay green
Discarding these tasks means the test validates assertions but not server termination. I inserted an unconditional InvalidOperationException at the end of McpServerHandler.RunAsync; When_TwoRootCapableClientsShareHandler_Then_EachSeesOwnRoots still passed because its server tasks are ignored. The same pattern occurs in the other new multi-session tests, while the shared fixture also swallows a teardown timeout.
Please cancel/close the transports, await every RunAsync task, accept only the expected cancellation outcome, and fail on fault or timeout. A teardown mutation like the one above should turn at least one regression red.
| > **⚠️ Deprecation notice (SEP-2577):** the MCP specification (2026-07-28) deprecates the | ||
| > Roots feature, and the SDK may remove it in a future version. Repl keeps supporting it | ||
| > **for existing hosts and applications only** — new applications should not build on | ||
| > native MCP roots and can use [soft roots](#soft-roots-fallback) or explicit command |
There was a problem hiding this comment.
[medium] Do not recommend implicit soft-root session state to new 2026-07-28 applications
This replaces deprecated native Roots with another connection-scoped state mechanism (SetSoftRoots lives in McpSessionContext and can change routing/catalog membership). The 2026-07-28 architecture removed protocol sessions and directs cross-call state through explicit ordinary arguments or server-minted handles.
Please recommend explicit parameters/handles for new applications. If soft roots remain as a compatibility feature, label them as legacy/stateful-only and update the transport/session-isolation guidance accordingly.
autocarl
left a comment
There was a problem hiding this comment.
Follow-up to review #4933578205 on the unchanged head. A late independent concurrency pass identified one additional cache-publication regression; the inline comment records it separately without duplicating the existing blockers.
| if (observedState.Version == snapshotVersion) | ||
| { | ||
| Volatile.Write(ref _builtSnapshotVersion, snapshotVersion); | ||
| context.BuiltSnapshotVersion = snapshotVersion; |
There was a problem hiding this comment.
[medium] Publish the snapshot and its version with release/acquire semantics
Before this change, the lock-free fast path used Volatile.Read(ref _builtSnapshotVersion) and the writer published with Volatile.Write(ref _builtSnapshotVersion, snapshotVersion) after storing _snapshot. Moving the fields into McpSessionContext replaced both sides with ordinary property access (BuiltSnapshotVersion / Snapshot) while readers at lines 369-371 remain outside SnapshotGate.
There is therefore no release/acquire edge publishing this pair: a concurrent fast-path reader may observe the new version while still observing the previous snapshot and return stale discovery state. The semaphore only orders readers that take the slow path.
Please either publish an immutable (version, snapshot) holder atomically, or restore a Volatile.Write/Volatile.Read backing-field protocol (snapshot first, version last). Keep the exception/fallback path dirty as intended, and add a concurrent invalidation/read regression around the publication boundary.
Summary
Updates Repl.Mcp to the stable official
ModelContextProtocolSDK 2.2.0 and adapts the MCP server to its per-request, multi-session model.SDK migration
ModelContextProtocolfrom 1.4.1 to 2.2.0.initializecompatibility for older hosts..LongRunning()remains a Repl annotation in this PR. Modern MCP Tasks integration is handled separately in Implement MCP Tasks runtime for .LongRunning() commands (ModelContextProtocol.Extensions.Tasks) #72.Session isolation and concurrency
One handler can serve multiple MCP sessions. This PR makes MCP-local state session-bound:
This prevents capability cross-wiring, workspace roots leaking between clients, stale session-specific tool graphs, and notifications stopping when one of several sessions closes.
Broader application DI scoping remains out of scope and is tracked in #70.
Validation
Refs #51