Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
207e390
feat(mcp): migrate to ModelContextProtocol 2.0 line (2.0.0-preview.3)
carldebilly Jul 16, 2026
aeeb8df
fix(mcp): correct Tasks narrative, scope pragmas, lock tools/list wir…
carldebilly Jul 16, 2026
b7bf5c3
fix(mcp): bind capability services to the flowing request, not a shar…
carldebilly Jul 17, 2026
d4ecc96
fix(mcp): legacy handshake regression, legacy-compat warnings, honest…
carldebilly Jul 17, 2026
69898e5
fix(mcp): per-session roots cache + reference-counted session lifecyc…
carldebilly Jul 17, 2026
48bf7fd
refactor(mcp): McpSessionContext owns all per-session state (review)
carldebilly Jul 17, 2026
9c1b8ce
docs(mcp): correct SEP-2322 availability — MRTR ships experimentally …
carldebilly Jul 19, 2026
ddce4d4
chore(mcp): update SDK to 2.2.0
carldebilly Aug 13, 2026
01b2403
test(mcp): own server tasks and pin the negotiated revision
carldebilly Aug 22, 2026
3f98670
fix(mcp): publish the session snapshot cache as one atomic value
carldebilly Aug 22, 2026
84d73d7
fix(mcp): bind the flowing request on the pre-built options path
carldebilly Aug 22, 2026
3190574
fix(mcp): let the SDK fan out list-changed to actual subscribers
carldebilly Aug 23, 2026
c6b4cae
fix(mcp): honour the per-request log level and own the message contract
carldebilly Aug 24, 2026
4c9fd42
refactor(mcp): drop the unreachable external session fallback
carldebilly Aug 24, 2026
31953dc
docs(mcp): correct the isolation, soft-roots and notification claims
carldebilly Aug 24, 2026
f561022
chore(mcp): repair the deprecation banners and record the SDK migration
carldebilly Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,43 @@ Nerdbank.GitVersioning at pack time; this file groups changes by theme instead o

## Unreleased

### Changed — MCP SDK 2.2.0

- `Repl.Mcp` now builds on `ModelContextProtocol` **2.2.0** (from 1.4.1). The SDK is a transitively
public dependency, so a consumer referencing `Repl.Mcp` must move to the 2.x line. The server stays
multi-revision: it keeps the `initialize` handshake for existing hosts while also serving the
sessionless `2026-07-28` revision.
- **Breaking:** `IMcpFeedback.SendMessageAsync` takes a Repl-owned `McpMessageLevel` instead of the
SDK's `LoggingLevel`. `LoggingLevel` carries the SDK's `MCP9005` deprecation, and Repl's internal
`#pragma` never covered a *consumer's* compilation — anyone building with warnings as errors got a
hard error on a Repl signature. The new enum has the same members and numeric values.
- **User feedback delivery depends on the negotiated revision.** `2026-07-28` removed
`logging/setLevel` and forbids emitting `notifications/message` for a request that declared no
`_meta/io.modelcontextprotocol/logLevel` (SEP-2575), so Repl now honours that. Messages that cannot
be sent as notifications are appended to the **tool result** after the command's own payload, so no
host loses feedback. Initialize-era clients keep the previous session-wide behaviour, unchanged.
A caller reading only the first content block, or `StructuredContent`, is unaffected.
- **Discovery notifications are delivered by the SDK's own fan-out** rather than broadcast by Repl.
Initialize-era clients keep receiving unsolicited `*/list_changed`; a `2026-07-28` client receives
only the types it requested through `subscriptions/listen`, tagged with its listen request id. A
modern client that opens no subscription receives none — as the specification requires. List
results carry `ttlMs: 0`, so such a client re-lists on demand instead of caching.
- `.LongRunning()` remains Repl-local metadata and emits nothing on the protocol surface; SDK 2.x
removed the per-tool `Tool.Execution` augmentation. Protocol-level task support returns once Repl
integrates `ModelContextProtocol.Extensions.Tasks` (tracked in issue #72).

### Compatibility notes — MCP

- **Known limitation.** Soft roots ([`docs/mcp-advanced.md`](docs/mcp-advanced.md#soft-roots-fallback))
set by one connection are visible to every other connection created from the same
`BuildMcpServerOptions()` result. Client capabilities *are* isolated per request on that path;
cross-call state is not, because `2026-07-28` removed protocol sessions and that path has no
per-connection identity. Host one server per process (`mcp serve`), or take the workspace as an
explicit command argument.
- `docs/mcp-transports.md` previously claimed each connection has "its own I/O capture" and "its own
session-aware routing state". I/O capture is per *invocation*, and session-aware routing state
exists only under `mcp serve`. The doc now states what is isolated at which boundary.

### Added — option visibility

- `.Hidden(bool isHidden = true)` on the option builder (`WithOption(name, option => option.Hidden())`)
Expand Down
2 changes: 1 addition & 1 deletion docs/for-coding-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Use these annotations to help agents make safer decisions:
| `.Destructive()` | May delete or mutate important state; ask for confirmation. |
| `.Idempotent()` | Safe to retry. |
| `.OpenWorld()` | Talks to external systems; expect latency and failures. |
| `.LongRunning()` | May take time; use call-now / poll-later patterns. |
| `.LongRunning()` | May take time. Documentation hint for now — no protocol-level task advertisement until Repl integrates the SDK Tasks extension. |
| `.AutomationHidden()` | Do not expose this command to MCP automation. |
| `.WithOption(name, o => o.AutomationHidden())` | Keep this one option out of the tool schema; the command stays visible. |

Expand Down
14 changes: 13 additions & 1 deletion docs/mcp-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ If your tool list is static, stay with the default setup from [mcp-overview.md](

## Client roots

> **⚠️ 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 take the workspace as an
> **explicit command parameter**, or mint a handle from a setup command and pass it back — that is
> what SEP-2567 prescribes now that the protocol has no sessions to hang such state on. See
> [mcp-reference.md](mcp-reference.md#sdk-and-protocol-versions) for the version posture.
>
> [Soft roots](#soft-roots-fallback) are **not** the modern answer: they are the same
> connection-scoped state by another name, and they are scoped to the process rather than the
> connection when a host reuses one `BuildMcpServerOptions()` result. Treat them as a legacy
> compatibility feature for clients that lack native roots.

A **root** is a URI the client declares as being in scope for the session — typically an opened project folder, a working directory, or a boundary for what the agent should inspect or modify.

Roots give the server session-specific workspace context without inventing a custom protocol. When the client supports native MCP roots, `Repl.Mcp` exposes them through `IMcpClientRoots`.
Expand All @@ -42,7 +54,7 @@ app.Map("workspace roots", async (IMcpClientRoots roots, CancellationToken ct) =
| `Current` | Current effective roots for the session |
| `GetAsync()` | Refreshes native roots if supported |
| `HasSoftRoots` | Fallback roots were initialized manually |
| `SetSoftRoots()` / `ClearSoftRoots()` | Manage fallback roots for the current session |
| `SetSoftRoots()` / `ClearSoftRoots()` | Manage fallback roots — per connection under `mcp serve`, per process when a host reuses one `BuildMcpServerOptions()` result |

> **Why `IMcpClientRoots` is MCP-only:** Roots are session-scoped MCP data. They don't make sense as a generic `Repl.Core` concept for terminal or non-MCP execution. That's why the interface lives in `Repl.Mcp` and is injected only for MCP sessions.

Expand Down
25 changes: 19 additions & 6 deletions docs/mcp-agent-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@

See also: [sample 08-mcp-server](../samples/08-mcp-server/) for a working example that uses all three in a CSV import and feedback workflow.

> **⚠️ Deprecation notice (SEP-2577):** the MCP specification (2026-07-28) deprecates the
> Sampling and Logging features that `IMcpSampling` and `IMcpFeedback` build on, and the
> SDK may remove them in a future version. Repl keeps supporting them **for existing hosts
> and applications only** — new applications should not adopt these interfaces directly and
> should prefer the portable `IReplInteractionChannel`, which degrades gracefully across
> CLI, REPL, hosted sessions, and MCP. See
> [mcp-reference.md](mcp-reference.md#sdk-and-protocol-versions) for the version posture.

## Overview

Repl provides three MCP-oriented injectable interfaces:
Expand Down Expand Up @@ -238,12 +246,15 @@ public interface IMcpFeedback
CancellationToken cancellationToken = default);

ValueTask SendMessageAsync(
LoggingLevel level,
McpMessageLevel level,
object? data,
CancellationToken cancellationToken = default);
}
```

`McpMessageLevel` is Repl's own enum (`Debug` … `Emergency`), so this signature does not expose the
SDK's deprecated `LoggingLevel` to your build.

Use it when:

- you need to control MCP progress/message notifications directly
Expand All @@ -256,10 +267,10 @@ Use it when:
app.Map("sync contacts",
async (IMcpFeedback feedback, CancellationToken ct) =>
{
if (feedback.IsLoggingSupported)
{
await feedback.SendMessageAsync(LoggingLevel.Info, "Starting sync.", ct);
}
// Sending unconditionally is fine: a message the client cannot receive as a notification
// is carried back in the tool result instead. Check IsLoggingSupported only when you want
// to skip work that would otherwise be wasted.
await feedback.SendMessageAsync(McpMessageLevel.Info, "Starting sync.", ct);

if (feedback.IsProgressSupported)
{
Expand Down Expand Up @@ -322,7 +333,9 @@ if (!elicitation.IsSupported)
For `IMcpFeedback`, the same idea applies:

- check `IsProgressSupported` before sending MCP-only progress directly
- check `IsLoggingSupported` before sending MCP-only messages directly
- `IsLoggingSupported` tells you whether a message would arrive as a **notification**; it is `false`
on `2026-07-28` unless the request declared a log level. Messages are never dropped for that
reason — they ride back in the tool result — so treat it as a hint, not a gate
- prefer `IReplInteractionChannel` when the feedback should still render well outside MCP

## Client compatibility
Expand Down
2 changes: 1 addition & 1 deletion docs/mcp-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ app.Map("deploy", handler).Destructive().LongRunning().OpenWorld();
| `.Destructive()` | Ask user for confirmation, sequential |
| `.Idempotent()` | Safe to retry, can parallelize |
| `.OpenWorld()` | Reaches external systems — expect latency and transient failures |
| `.LongRunning()` | Enables call-now/poll-later pattern |
| `.LongRunning()` | Slow-operation hint (protocol-level task advertisement returns once Repl integrates the SDK Tasks extension — see [mcp-reference.md](mcp-reference.md#sdk-and-protocol-versions)) |
| `.AutomationHidden()` | Not visible to agents |

**Annotate every command exposed to agents.** Unannotated tools force agents to assume the worst: confirm everything, no parallelism, no retries.
Expand Down
8 changes: 8 additions & 0 deletions docs/mcp-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,14 @@ side-channel command output and are not included in `resources/read` bodies.

Feature support varies across agents. Check [mcp-availability.com](https://mcp-availability.com/) for current data.

### SDK and protocol versions

- Repl.Mcp builds on the official C# SDK (`ModelContextProtocol`), currently at **2.2.0**. The SDK negotiates the protocol version with each client, including fallback to the legacy `initialize` handshake for older hosts.
- **Roots, Sampling, and Logging** are deprecated by MCP specification 2026-07-28 (SEP-2577). Repl.Mcp keeps supporting them **for existing hosts and applications only** — new applications should not adopt these features (the SDK may remove them) and should prefer Repl's portable abstractions such as `IReplInteractionChannel`. The designated successor for server-initiated flows (SEP-2322, multi-round-trip requests) shipped experimentally in the SDK 2.0 preview line and is stable as of 2.2.0; Repl has not adopted it yet.
- **Discovery notifications** follow the negotiated revision. Repl drives the SDK's own fan-out rather than broadcasting itself, so an initialize-era client keeps receiving unsolicited `*/list_changed` while a `2026-07-28` client receives only the notification types it requested through `subscriptions/listen`, each tagged with its listen request id (SEP-2575). A modern client that opens no subscription receives none — which is what the specification requires. List results carry `ttlMs: 0`, so such a client re-lists on demand rather than caching.
- **User feedback** (`notice` / `warning` / `problem`, and `IMcpFeedback.SendMessageAsync`) follows the same split. On `2026-07-28`, `logging/setLevel` is gone and a server must not emit `notifications/message` for a request that declared no `_meta/io.modelcontextprotocol/logLevel`. Messages that cannot be delivered as notifications are appended to the **tool result** instead, after the command's own payload, so no host loses them. Initialize-era clients keep the session-wide `logging/setLevel` behaviour unchanged. Note that the SDK's own client cannot request a level on `2026-07-28` at all, so in practice modern hosts see feedback in the tool result.
- **MCP Tasks**: the SDK reorganized Tasks into `ModelContextProtocol.Extensions.Tasks` and dropped the per-tool execution augmentation (`Tool.Execution`) from the protocol surface, so `.LongRunning()` commands no longer advertise task support at the protocol level. The annotation stays in Repl's own model (help/docs); protocol-level task support can return once Repl integrates the Tasks extension, store, and get/update/cancel lifecycle (tracked in issue #72).

| Feature | Claude Desktop | Claude Code | Codex | VS Code Copilot | Cursor | Continue |
|---|---|---|---|---|---|---|
| Tools | Yes | Yes | Yes | Yes | Yes | Yes |
Expand Down
27 changes: 21 additions & 6 deletions docs/mcp-transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ async Task HandleConnectionAsync(Stream input, Stream output, CancellationToken
}
```

Client capabilities (sampling, elicitation, roots) resolve per **request** on this path, so each
connection sees its own — that is a property of the request, not of the options instance.

> **Known limitation:** cross-call state does not. The options carry one command catalog, so
> [soft roots](mcp-advanced.md#soft-roots-fallback) set by one connection are visible to every other
> connection built from the same options. The `2026-07-28` revision removed protocol-level sessions,
> and this path has no per-connection identity to hang that state on. If your commands rely on soft
> roots, host one server per process (`mcp serve`) or pass the workspace as an explicit command
> argument.

## Scenario B: MCP-over-HTTP

The MCP spec also defines an HTTP transport. For that, you typically host MCP inside ASP.NET Core rather than through `mcp serve`.
Expand All @@ -64,14 +74,19 @@ var mcpOptions = app.Core.BuildMcpServerOptions(configure: o =>

You can then pass those options to the MCP SDK's HTTP integration.

## Session isolation
## What is isolated, and at which boundary

Each connection or HTTP session is isolated:
The `2026-07-28` revision removed protocol-level sessions: the client declares its capabilities on
every request rather than once per connection. So the boundaries are not all the same size.

- its own MCP session
- its own I/O capture
- its own session-aware routing state
| Isolated per | What |
|---|---|
| Request | Client capabilities, the requested log level, and the destination for sampling, elicitation and progress |
| Invocation | I/O capture — each tool call gets its own capture scope, not one per connection |
| Connection (`mcp serve` only) | The MCP session object, the native roots cache, soft roots, and session-aware routing state |

That matters especially when using dynamic tools, roots, or session-specific modules.
A server created from a reused `BuildMcpServerOptions()` result has the first two but not the third;
see the known limitation above. That matters especially when using dynamic tools, roots, or
session-specific modules.

For those higher-level patterns, see [mcp-advanced.md](mcp-advanced.md).
2 changes: 1 addition & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5"/>
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.5"/>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5"/>
<PackageVersion Include="ModelContextProtocol" Version="1.4.1"/>
<PackageVersion Include="ModelContextProtocol" Version="2.2.0"/>
<PackageVersion Include="Spectre.Console" Version="0.55.0"/>
</ItemGroup>

Expand Down
5 changes: 3 additions & 2 deletions src/Repl.Core/CommandAnnotations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ public sealed record CommandAnnotations
public bool OpenWorld { get; init; }

/// <summary>
/// Indicates the command may take a long time to complete.
/// Enables task-based execution in programmatic clients.
/// Indicates the command may take a long time to complete, so programmatic clients
/// should expect a slow call. Protocol-level task-based execution (MCP Tasks) is not
/// advertised until Repl integrates the SDK's Tasks extension.
/// </summary>
public bool LongRunning { get; init; }

Expand Down
15 changes: 11 additions & 4 deletions src/Repl.Mcp/IMcpFeedback.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using ModelContextProtocol.Protocol;
using Repl.Interaction;

namespace Repl.Mcp;
Expand All @@ -17,8 +16,15 @@ public interface IMcpFeedback
bool IsProgressSupported { get; }

/// <summary>
/// Gets a value indicating whether the connected MCP client can receive logging/message notifications.
/// Gets a value indicating whether a message sent for the current request would reach the
/// connected MCP client as a notification.
/// </summary>
/// <remarks>
/// On the <c>2026-07-28</c> revision this is <see langword="false"/> unless the request declared
/// a log level in its metadata, because the specification forbids emitting message notifications
/// for a request that did not ask for them. A message sent while this is <see langword="false"/>
/// is not lost: it is carried back in the tool result instead.
/// </remarks>
bool IsLoggingSupported { get; }

/// <summary>
Expand All @@ -29,10 +35,11 @@ ValueTask ReportProgressAsync(
CancellationToken cancellationToken = default);

/// <summary>
/// Sends a structured MCP message notification to the connected client.
/// Sends a message to the connected client, as a notification when the request asked for one and
/// otherwise as part of the tool result.
/// </summary>
ValueTask SendMessageAsync(
LoggingLevel level,
McpMessageLevel level,
object? data,
CancellationToken cancellationToken = default);
}
27 changes: 17 additions & 10 deletions src/Repl.Mcp/McpClientRootsService.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

// Deprecated by MCP spec 2026-07-28 (SEP-2577, MCP9005); kept for existing hosts.
// Rationale and successor: docs/mcp-reference.md#sdk-and-protocol-versions (#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 = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

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
{
Expand All @@ -42,15 +53,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 +60 to 63

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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: When_TwoRootCapableClientsShareHandler_Then_EachSeesOwnRoots (RED observed with exactly your scenario — B received A's file:///ga).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Expand Down
Loading
Loading