Skip to content

feat(harness): add MCP client - #79

Open
sanchitrk wants to merge 5 commits into
canaryfrom
claude/sleepy-gates-ce83d0
Open

sanchitrk wants to merge 5 commits into
canaryfrom
claude/sleepy-gates-ce83d0

Conversation

@sanchitrk

@sanchitrk sanchitrk commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds the low-level MCP client the MCP service will build on: one scoped connection to one server, over stdio or Streamable HTTP, through the official SDK (@modelcontextprotocol/client 2.3.1).

  • src/mcp/client.ts: McpClient.connect(...) returns a Connection with tools(), resources(cursor?), resourceTemplates(cursor?), readResource(uri), callTool(...) and onToolsChanged / onResourcesChanged / onSessionExpired / onClose callbacks.

    • Timeouts: 30 s to start, 60 s per request, restarted by progress.
    • Retries: HTTP connect and list/read retry transient failures; tool calls never retry.
    • Errors: HTTP session expiry becomes SessionExpiredError, auth failures become NeedsAuthError.
    • Closing the scope ends the HTTP session and never fires onClose.
  • src/mcp/stdio.ts: stdio transport. Servers run on the host in their own process group.

    • A crash is reported with the exit code and the stderr tail.
    • Closing ends stdin, waits 2 s, then SIGTERM and SIGKILL to the group.
    • An exit hook stops leftover servers if the host exits without closing them.
  • src/mcp/content.ts: tool results as model text and image content. src/mcp/error.ts: tagged errors.

  • SDK validation: list pages and tool results (against a tool's outputSchema) are validated by the SDK.

  • Process.terminate: the process-group stop sequence moves out of the host shell into sandbox/utils/process.ts, shared by the host shell and the stdio transport.

  • Server logging: onLog receives the server's notifications/message entries, and src/mcp/log.ts (McpLog) appends them one line per entry, rotating the file past 5 MB.

  • Readable validation errors: the SDK's multi-line issue list becomes one line, Invalid result for <method>: <path>: <message>; the raw list is logged at debug level.

  • Tracing: entry points use Effect.fn (McpClient.connect, McpStdio.make, McpLog.make), and each request gets a span named McpClient.<method>.

No user-facing surface yet: config, the MCP service, scoping and tool wiring come in follow-up PRs stacked on this one.

Test plan

  • test/mcp.client.test.ts: 29 scenarios against real stdio and Streamable HTTP fixture servers.
    • Process behaviour: crash mid-call, process-group cleanup, a server that ignores SIGTERM.
    • Protocol: message framing and the 16 MiB limit, progress keeping a call alive, cancellation.
    • HTTP: transient retry, connection refused, session expiry, 401 with and without an auth provider.
    • Data: roots, resources and paging, output-schema validation, malformed list pages, server logging and log rotation.
    • Error messages: a missing command or working directory.
    • Ends with __artifacts__/mcp.client.json, stable across runs.
  • Sandbox tests still pass after the Process.terminate extraction
  • vp check and vp run typecheck
  • Node SEA bundle with the SDK (checked once the follow-up PRs land)

🤖 Generated with Claude Code

sanchitrk and others added 5 commits October 14, 2026 19:59
Connect to one MCP server over stdio or Streamable HTTP through the
official SDK. Stdio servers run on the host in their own process group
and are stopped with the group; HTTP connects retry transient failures.
List pages and tool results are validated by the SDK.

The process-group stop sequence moves out of the host shell into a
shared Process.terminate helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connections pass the server's notifications/message entries to an
onLog handler, and McpLog appends them to a file with one line per
entry, rotating it past 5 MB.

A result that fails the SDK's schema is reported on one line as
`Invalid result for <method>: <path>: <message>` instead of the
validator's multi-line issue list, which is still logged at debug level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Name the MCP entry points with Effect.fn and give each request its own
span. A server without resource templates is now recovered from inside
the Effect rather than on the SDK promise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Count stderr as an open pipe, so a descendant holding only stderr is
  still killed with the server's process group.
- Bound the whole connect attempt by the startup timeout; the SDK only
  times the initialize request, not the initialized notification.
- End the HTTP session a failed connect attempt was given, through a
  fresh transport, since the SDK aborts the failed transport's signal.

Tests now cover these, retries of reads but not tool calls, session
termination and 400 classification, and register their own teardown
for spawned processes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The process spawner kills the group when the connection's scope closes,
which hid a missing kill. Assert instead that a grandchild holding only
stderr is gone once the connection reports the server's exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant