Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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. Capable modern MCP clients use Tasks; other clients receive the normal synchronous result. |
| `.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
35 changes: 35 additions & 0 deletions docs/mcp-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ 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 not build on
> native MCP roots and can use [soft roots](#soft-roots-fallback) or explicit command
> parameters instead. See
> [mcp-reference.md](mcp-reference.md#sdk-and-protocol-versions) for the version posture.

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 Down Expand Up @@ -173,6 +180,34 @@ Even a weak client can then discover the real tools manually and call them via `
| Client supports tools but misses dynamic refreshes | Enable `DiscoverAndCallShim` |
| Client has both issues | Use soft roots and the dynamic tool shim |

## Tasks for long-running commands

`.LongRunning()` opts a tool into the modern MCP Tasks extension:

```csharp
app.Map("deploy", DeployAsync)
.LongRunning();
```

For a client that negotiates the `io.modelcontextprotocol/tasks` extension, the call creates a task. The client can poll it with `tasks/get` and cancel it with `tasks/cancel`; the command's `CancellationToken` is cancelled too. Other tools remain synchronous.

Clients that do not negotiate the extension — including legacy clients — receive the normal synchronous `tools/call` result. Repl does not implement the retired 2025 Tasks wire format (`Tool.Execution` / `taskSupport`); it uses the current extension only.

By default Repl keeps task state in an in-memory store, which is the right fit for a stdio server process. For a stateless HTTP server, multiple server instances, or task recovery after a restart, supply a shared durable implementation of `IMcpTaskStore`:

```csharp
using ModelContextProtocol.Extensions.Tasks;

app.UseMcpServer(options =>
{
options.TaskStore = taskStore;
});
```

`taskStore` must be safe for concurrent access and outlive the individual request. A task can be cancelled by a capable client, but a process shutdown still cancels any in-memory work.

> **Interaction limitation:** the current official SDK cannot compose MCP's multi-round-trip requests (MRTR) with a task-backed `McpServerTool`. Keep task-backed commands non-interactive for now. A client that falls back to synchronous execution can still use Repl's existing interaction features.

## MCP Apps advanced patterns

For the basic MCP Apps setup, start with [mcp-overview.md](mcp-overview.md#mcp-apps) and [mcp-reference.md](mcp-reference.md#mcp-apps). This section covers patterns for more complex UIs.
Expand Down
8 changes: 8 additions & 0 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
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()` | Uses MCP Tasks with capable modern clients; other clients receive the normal synchronous result. See [advanced configuration](mcp-advanced.md#tasks-for-long-running-commands). |
| `.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
6 changes: 6 additions & 0 deletions docs/mcp-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,12 @@ 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) is available starting with SDK 2.0; Repl has not adopted it yet.
- **MCP Tasks**: `.LongRunning()` uses the SDK's `ModelContextProtocol.Extensions.Tasks` extension and its current `tasks/get` / `tasks/update` / `tasks/cancel` lifecycle. Clients that do not negotiate the extension keep the normal synchronous `tools/call` behavior. Repl intentionally does not implement the retired per-tool `Tool.Execution` / `taskSupport` wire format; see [MCP Tasks for long-running commands](mcp-advanced.md#tasks-for-long-running-commands).

| Feature | Claude Desktop | Claude Code | Codex | VS Code Copilot | Cursor | Continue |
|---|---|---|---|---|---|---|
| Tools | Yes | Yes | Yes | Yes | Yes | Yes |
Expand Down
3 changes: 2 additions & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
<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="ModelContextProtocol.Extensions.Tasks" 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
7 changes: 7 additions & 0 deletions src/Repl.Mcp/IMcpFeedback.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
using ModelContextProtocol.Protocol;
using Repl.Interaction;

// 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, available 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;

/// <summary>
Expand Down
30 changes: 20 additions & 10 deletions src/Repl.Mcp/McpClientRootsService.cs
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, available 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 = [];
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 +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;
Expand Down
14 changes: 6 additions & 8 deletions src/Repl.Mcp/McpElicitationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ namespace Repl.Mcp;
/// a multi-field variant would build the <see cref="ElicitRequestParams.RequestSchema"/> with
/// multiple properties instead of one.
/// </remarks>
internal sealed class McpElicitationService : IMcpElicitation
internal sealed class McpElicitationService(McpRequestServerAccessor servers) : IMcpElicitation
{
private const string FieldName = "value";

private McpServer? _server;

public bool IsSupported => _server?.ClientCapabilities?.Elicitation is not null;
public bool IsSupported => servers.Effective?.ClientCapabilities?.Elicitation is not null;

public async ValueTask<string?> ElicitTextAsync(
string message,
Expand Down Expand Up @@ -99,19 +97,19 @@ internal sealed class McpElicitationService : IMcpElicitation
: null;
}

internal void AttachServer(McpServer server) => _server = server;

private async ValueTask<ElicitResult?> ElicitSingleFieldAsync(
string message,
ElicitRequestParams.PrimitiveSchemaDefinition schema,
CancellationToken cancellationToken)
{
if (!IsSupported)
// Single read: the effective server must not change between the support check and
// the call (a concurrent request re-binding the accessor must not be observed).
if (servers.Effective is not { ClientCapabilities.Elicitation: not null } server)
{
return null;
}

var result = await _server!.ElicitAsync(
var result = await server.ElicitAsync(
new ElicitRequestParams
{
Message = message,
Expand Down
30 changes: 18 additions & 12 deletions src/Repl.Mcp/McpFeedbackService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,45 @@
using ModelContextProtocol.Server;
using Repl.Interaction;

// 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, available 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;

/// <summary>
/// Internal implementation of <see cref="IMcpFeedback"/> backed by a live <see cref="McpServer"/> session.
/// </summary>
internal sealed class McpFeedbackService : IMcpFeedback
internal sealed class McpFeedbackService(McpRequestServerAccessor servers) : IMcpFeedback
{
private const string LoggerName = "repl.interaction";

private readonly AsyncLocal<ProgressToken?> _progressToken = new();
// This service is created per McpServerHandler/session and overlaid into the
// per-connection service provider, so the attached server reference is not shared
// across concurrent MCP connections.
private McpServer? _server;

public bool IsProgressSupported => _server is not null && _progressToken.Value is not null;
public bool IsProgressSupported => servers.Effective is not null && _progressToken.Value is not null;

public bool IsLoggingSupported => _server is not null;
public bool IsLoggingSupported => servers.Effective is not null;

public async ValueTask ReportProgressAsync(
ReplProgressEvent progress,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(progress);

if (!IsProgressSupported || progress.State == ReplProgressState.Clear || _progressToken.Value is not { } progressToken)
// Single read: the effective server must not change between the support check and
// the send (a concurrent request re-binding the accessor must not be observed).
if (servers.Effective is not { } server
|| progress.State == ReplProgressState.Clear
|| _progressToken.Value is not { } progressToken)
{
return;
}

var percent = progress.ResolvePercent();
await _server!.NotifyProgressAsync(
await server.NotifyProgressAsync(
progressToken,
new ProgressNotificationValue
{
Expand All @@ -52,12 +59,12 @@ public async ValueTask SendMessageAsync(
object? data,
CancellationToken cancellationToken = default)
{
if (!IsLoggingSupported)
if (servers.Effective is not { } server)
{
return;
}

await _server!.SendNotificationAsync(
await server.SendNotificationAsync(
NotificationMethods.LoggingMessageNotification,
new LoggingMessageNotificationParams
{
Expand All @@ -68,7 +75,6 @@ public async ValueTask SendMessageAsync(
cancellationToken: cancellationToken).ConfigureAwait(false);
}

internal void AttachServer(McpServer server) => _server = server;

internal IDisposable PushProgressToken(ProgressToken? progressToken) =>
new ProgressTokenScope(_progressToken, progressToken);
Expand Down
7 changes: 7 additions & 0 deletions src/Repl.Mcp/McpInteractionChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
using ModelContextProtocol.Server;
using Repl.Interaction;

// 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, available in SDK 2.0 as MrtrContext/MrtrExchange)
// is not adopted by Repl yet, and hosts still rely on these features, so Repl keeps
// these features, so Repl keeps supporting them until the SDK removes the surface (#51).
#pragma warning disable MCP9005

namespace Repl.Mcp;

/// <summary>
Expand Down
31 changes: 31 additions & 0 deletions src/Repl.Mcp/McpRequestServerAccessor.cs
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;
}
Loading
Loading