-
Notifications
You must be signed in to change notification settings - Fork 2
feat(mcp): migrate to ModelContextProtocol 2.0 line (2.0.0-preview.3) #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3cb6d8d
46792e9
c235f9a
3cc415a
b094d05
1f8950f
b362736
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,38 @@ | ||
| using ModelContextProtocol.Protocol; | ||
| using ModelContextProtocol.Server; | ||
|
|
||
| // Roots, Sampling, and Logging are deprecated by MCP spec 2026-07-28 (SEP-2577, SDK | ||
| // diagnostic MCP9005); the designated successor for server-initiated flows (SEP-2322, | ||
| // multi-round-trip requests, shipped experimentally in SDK 2.0 as MrtrContext/MrtrExchange) | ||
| // is not adopted by Repl yet, and hosts still rely on these features, so Repl keeps | ||
| // supporting them until the SDK removes the surface (#51). | ||
| #pragma warning disable MCP9005 | ||
|
|
||
| namespace Repl.Mcp; | ||
|
|
||
| // One instance PER SESSION (owned by McpSessionContext): hard roots, soft roots, and | ||
| // their cache/version state are session state — one handler can serve several sessions, | ||
| // and serving session A's roots to session B would expose A's workspace URIs and build | ||
| // B's root-dependent snapshot from the wrong workspace. Outbound transport still goes | ||
| // through the request-bound accessor (the destination is per request, finer than the | ||
| // session). | ||
| internal sealed class McpClientRootsService : IMcpClientRoots | ||
| { | ||
| private readonly ICoreReplApp _app; | ||
| private readonly McpRequestServerAccessor _servers; | ||
| private readonly Lock _syncRoot = new(); | ||
| private McpServer? _server; | ||
| private McpClientRoot[] _hardRoots = []; | ||
| private McpClientRoot[] _softRoots = []; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When one handler serves two clients that lack native Roots support, this single Useful? React with 👍 / 👎.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed independently on |
||
| private bool _hardRootsLoaded; | ||
| private long _hardRootsVersion; | ||
|
|
||
| public McpClientRootsService(ICoreReplApp app) | ||
| public McpClientRootsService(ICoreReplApp app, McpRequestServerAccessor servers) | ||
| { | ||
| _app = app; | ||
| _servers = servers; | ||
| } | ||
|
|
||
| public bool IsSupported => _server?.ClientCapabilities?.Roots is not null; | ||
| public bool IsSupported => _servers.Effective?.ClientCapabilities?.Roots is not null; | ||
|
|
||
| public bool HasSoftRoots | ||
| { | ||
|
|
@@ -42,15 +56,11 @@ public IReadOnlyList<McpClientRoot> Current | |
| } | ||
| } | ||
|
|
||
| public void AttachServer(McpServer server) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(server); | ||
| _server = server; | ||
| } | ||
|
|
||
| public async ValueTask<IReadOnlyList<McpClientRoot>> GetAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| var server = _server; | ||
| // Single read: the effective server must not change between the support check and | ||
| // the roots request (a concurrent request re-binding the accessor must not be observed). | ||
| var server = _servers.Effective; | ||
| if (server?.ClientCapabilities?.Roots is null) | ||
| { | ||
| return Current; | ||
|
Comment on lines
+63
to
66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When one Useful? React with 👍 / 👎.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed independently on
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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: |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| using ModelContextProtocol.Server; | ||
|
|
||
| namespace Repl.Mcp; | ||
|
|
||
| /// <summary> | ||
| /// Resolves the <see cref="McpServer"/> a capability call must target. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// SDK 2.0's <c>2026-07-28</c> protocol path hands each request a destination-bound | ||
| /// <see cref="McpServer"/>, and one handler can serve several sessions. The capability | ||
| /// services are singletons (exposed through DI to command handlers), so the effective | ||
| /// server must be the one bound to the FLOWING request — a shared mutable field would be | ||
| /// overwritten by whichever request attached last, cross-wiring capabilities between | ||
| /// concurrent calls. <see cref="AsyncLocal{T}"/> flows with the invocation and cannot | ||
| /// leak across requests; the session-level server remains the fallback for code running | ||
| /// outside a request (e.g. routing-change notifications). | ||
| /// </remarks> | ||
| internal sealed class McpRequestServerAccessor | ||
| { | ||
| private readonly AsyncLocal<McpServer?> _current = new(); | ||
| private McpServer? _session; | ||
|
|
||
| /// <summary>Server for the flowing request, falling back to the session server.</summary> | ||
| public McpServer? Effective => _current.Value ?? _session; | ||
|
|
||
| /// <summary>Binds the flowing async context to the request's destination server.</summary> | ||
| public void BindRequest(McpServer server) => _current.Value = server; | ||
|
|
||
| /// <summary>Records the session-level server used outside request flows (null when the last session ends).</summary> | ||
| public void AttachSession(McpServer? server) => _session = server; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.