forked from admin/VolcanicAgenticCore
feat(mcp): per-session server connection and MCP tools
McpServerConnection starts the session's server lazily through PrepareProcessAsync, applies the call timeout, reports exits with the stderr tail and restarts on the next call, stops when idle, and supports StopAsync (reusable) and idempotent DisposeAsync. McpTool exposes a catalogue entry as an ITool and maps results with McpResultMapper. Spec change (§5, Decisions): ModelContextProtocol.Core 2.2.0 does not send notifications/cancelled when a request's token fires (verified against the fake server), so calls go out through SendRequestAsync with their own request id and the connection sends notifications/cancelled itself on abort and timeout. Also documents exit detection, StopAsync vs DisposeAsync and JSON-RPC errors as ordinary error results, per the plan.
This commit is contained in:
@@ -3194,6 +3194,8 @@ internal sealed class McpServerConnection : IAsyncDisposable
|
||||
}
|
||||
```
|
||||
|
||||
(As implemented: SDK 2.2.0 does not send `notifications/cancelled` when the call's token fires, so `CallAsync` sends the request through `SendRequestAsync<CallToolRequestParams, CallToolResult>` with its own id `volcanic-<n>` and sends the notification itself on abort and timeout; the idle stop re-checks the in-flight count under the gate. The spec §5 says so.)
|
||||
|
||||
(If `ExitCode` is a non-nullable `int`, drop the `?.` before `ToString`. If `McpProtocolException` lives in another namespace, adjust the using. A JSON-RPC error from the server, e.g. invalid params, surfaces as that exception: it becomes an ordinary error result and doesn't mark the server dead.)
|
||||
|
||||
Create `extensions/Volcanic.AgenticCore.Extensions.Mcp/McpTool.cs`:
|
||||
|
||||
@@ -29,8 +29,8 @@ two meet only in tool naming (see "Names").
|
||||
(`o.AddExtension<McpExtension>()`) or dropped into an extensions directory. A small contract gap is closed in
|
||||
Abstractions (see Components §8). No runtime slice changes.
|
||||
- **Official SDK, not hand-rolled JSON-RPC.** Use the `ModelContextProtocol.Core` package (the client/transport
|
||||
core, without the hosting package). It handles the initialize handshake, protocol version negotiation, the
|
||||
`notifications/cancelled` notification on cancellation, pagination of `tools/list`, content-block types and
|
||||
core, without the hosting package). It handles the initialize handshake, protocol version negotiation,
|
||||
pagination of `tools/list`, content-block types and
|
||||
process shutdown (`StdioClientTransport`: `Command`, `Arguments`, `WorkingDirectory`, `EnvironmentVariables`,
|
||||
`StandardErrorLines`, `ShutdownTimeout`). The `ClaudeCode.Bridge` hand-rolls its protocol because it answers only
|
||||
three methods. A general client would have to reimplement much more. SDK types never leave the extension. Its
|
||||
@@ -237,18 +237,27 @@ One per server per extension instance (that is, per session):
|
||||
start: expand `${workspace}` from `ctx.Cwd` and `${env:…}`, build the command, cwd and environment through
|
||||
`ctx.PrepareProcessAsync` (so session-level `ProcessSpawnEvent` handlers run and `TZ` is the chat's zone), create `StdioClientTransport` + `McpClient` under `startupTimeout`, and route
|
||||
`StandardErrorLines` to `api.Logger` at `stderrLevel` with the prefix `"<name> mcp: "`, keeping a 20-line tail.
|
||||
- Calls use a linked token: the tool's `CancellationToken` (a session abort) plus `CancelAfter(callTimeout)`. The
|
||||
SDK sends `notifications/cancelled` for the request. A timeout becomes
|
||||
- Calls use a linked token: the tool's `CancellationToken` (a session abort) plus `CancelAfter(callTimeout)`. On an
|
||||
abort or a timeout the connection sends `notifications/cancelled` for the request itself: SDK 2.2.0 doesn't send it
|
||||
when a request's token fires (checked against the fake server), so each call goes out with its own request id
|
||||
(`volcanic-<n>`) through `SendRequestAsync` and the notification is sent best effort. A timeout becomes
|
||||
`"<tool> timed out after <n>s"`, and an abort propagates as `OperationCanceledException`, which the loop already
|
||||
handles.
|
||||
- If the process exits or the transport faults, the client is disposed and marked dead, and the call fails with
|
||||
`"<name> MCP server exited (<code>): <stderr tail>. It restarts on the next call; browser state was lost."`. The
|
||||
next call respawns it. There's no automatic retry of the failed call, because it may have had side effects.
|
||||
- The idle timer (reset per call) disposes the client after `idleTimeout` with no calls in flight.
|
||||
next call respawns it. There's no automatic retry of the failed call, because it may have had side effects. An exit
|
||||
is noticed either when the next call finds the SDK session completed or when a call fails and the process ends
|
||||
within 2 s. The exit code comes from the SDK's `StdioClientCompletionDetails`.
|
||||
- A JSON-RPC error from the server (e.g. invalid params) is an ordinary error result and doesn't mark the server dead.
|
||||
- The idle timer (reset per call) disposes the client after `idleTimeout` with no calls in flight. The in-flight count
|
||||
is taken under the start gate and the idle stop re-checks it under the same gate, so an idle stop never kills a
|
||||
process a call has just been handed.
|
||||
- `DisposeAsync` is idempotent: dispose the client, then kill the process tree if it's still alive. SDK 2.2 waits up to
|
||||
`ShutdownTimeout` for the process to exit with stdin still open, then kills its tree, so disposing a live server
|
||||
always takes about `ShutdownTimeout`. The extension calls it on every `SessionShutdownEvent` (quit, new,
|
||||
resume). A new session in the same chat starts from a fresh process, and the persistent profile keeps the logins.
|
||||
always takes about `ShutdownTimeout`. After it, calls throw `ObjectDisposedException`. `StopAsync` does the same
|
||||
but leaves the connection usable. The extension stops connections on `SessionShutdownEvent` for `new`, `resume` and
|
||||
`fork` (the same extension instance serves the next session) and disposes them for `quit` and `reload`. A new
|
||||
session in the same chat starts from a fresh process, and the persistent profile keeps the logins.
|
||||
- If the live server's `tools/list` differs from the cached catalogue (a package upgrade in place), a warning is
|
||||
logged. Calls go by name either way.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user