Skip to content

feat(mcp): upgrade both MCP servers to the 2026-07-28 revision - #2290

Draft
Dani Akash (DaniAkash) wants to merge 7 commits into
mainfrom
feat/mcp-2026-07-28
Draft

feat(mcp): upgrade both MCP servers to the 2026-07-28 revision#2290
Dani Akash (DaniAkash) wants to merge 7 commits into
mainfrom
feat/mcp-2026-07-28

Conversation

@DaniAkash

Copy link
Copy Markdown
Contributor

What

Upgrades both BrowserOS MCP servers to the MCP 2026-07-28 revision while preserving full backward compatibility with legacy clients. Both /mcp endpoints negotiate per request: modern clients are served the stateless 2026-07-28 revision, and legacy clients keep the initialize handshake and mcp-session-id session model exactly as before.

TS agent server (apps/server)

Rust claw-server (apps/claw-server-rust)

Backward compatibility

Legacy clients are untouched: initialize, mcp-session-id sessions, and the existing tab-group behavior all continue to work; stdio serving is unchanged.

Status

Draft. Best-practices hardening (Host-header / DNS-rebinding validation, loopback bind review) and dual-era acceptance and probe tests are still to land on this branch before it is ready for review.

…-07-28 support (#2272)

* feat(server): migrate the MCP server to the v2 SDK with dual-era 2026-07-28 support

Replace @hono/mcp and the server-side @modelcontextprotocol/sdk 1.x with @modelcontextprotocol/server and @modelcontextprotocol/hono 2.0, serving both the legacy initialize handshake and the modern 2026-07-28 protocol per request via a stateless WebStandardStreamableHTTPServerTransport. v2 derives tools/list JSON schemas only from Zod v4, so the local browser and filesystem tool schemas move to the zod/v4 subpath of the installed zod 3.25; the Klavis connector schemas are bridged through fromJsonSchema until the Strata client migration. Drop the logging capability and the SetLevel handler, both removed in 2026-07-28. The Strata MCP client stays on the 1.x SDK for a follow-up change.

Refs #2168

* fix(ci): call the real tabs tool in the MCP integration test

The test invoked a non-existent tool name and passed only because the 1.x server returned a lenient error result for unknown tools. The v2 server correctly rejects unknown tools (its sibling test asserts the throw), so call the registered tabs tool with a valid action instead.
…2274)

Move the Klavis Strata MCP client and the remaining Klavis type imports from the 1.x @modelcontextprotocol/sdk to @modelcontextprotocol/client 2.0, so apps/server no longer depends on the legacy SDK at runtime. The SDK stays as a devDependency for the integration and end-to-end tests that simulate a legacy client against the v2 server. No behavior change; the connector schema bridge is unchanged and can be cleaned up in a follow-up.

Refs #2168
…n pin (#2275)

* chore(claw-server): upgrade rmcp to 3.x with a legacy protocol version pin

Bump rmcp from 2.1 to 3.x. rmcp 3.0 changes the ServerHandler call_tool return type to the MRTR-aware CallToolResponse, so wrap the existing CallToolResult with .into() in the production and reference handlers and the serving test. rmcp 3.x advertises the modern 2026-07-28 protocol by default, but the claw-server's sessionless state model for that revision is not in place yet, so override supported_protocol_versions() on the production handler to pin to legacy versions (2025-11-25 and earlier) and keep this upgrade behavior-neutral. The pin is lifted together with the handle-based session model. All crates compile, cargo test passes, clippy and fmt clean.

Refs #2168

* test(claw-server): prove the legacy protocol pin rejects the modern revision

Add a serving-level test that initializes against a legacy-pinned handler with protocolVersion 2026-07-28 and asserts the server negotiates down to a legacy revision rather than agreeing to the modern one. This exercises the same supported_protocol_versions() override the claw-server uses and confirms rmcp consults it during initialize negotiation.
#2278)

* feat(claw-server): serve the modern MCP revision with a session handle

Advertise the 2026-07-28 revision alongside the legacy revisions. rmcp serves 2026-07-28 clients statelessly, so the per-agent tab-group session, previously keyed on the mcp-session-id header that revision removes, is now carried as a server-minted handle passed as an ordinary `session` tool argument: minted as a UUID when absent, returned in structuredContent, and looked up on later calls so the agent keeps its tab group. Legacy clients and stdio keep the existing session model unchanged, and idle sweeping reaps handle sessions independent of transport close.

* test(claw-server): expect the injected session arg in the name_session golden schema

* fix(claw-server): only reuse live server-minted session handles

Resolve a modern session by reusing a handle only when it maps to a live session; any absent or unrecognized handle now mints a fresh server-generated handle instead of being minted under the caller-supplied value. A caller can no longer choose or seed a session id, so it cannot land on another agent's ownership by presenting a chosen id, and because every mint uses a fresh unique id, two concurrent calls can no longer both mint under the same id and orphan one session. Continuity flows through the returned handle.
…le (#2289)

* feat(server): give /mcp clients a server-minted session identity handle

Expose an optional session string argument on each /mcp browser tool. The server mints a UUID when the caller omits it and returns it in the result's structuredContent so an agent can thread it back on later calls to identify its own session; a supplied handle is echoed. The handle is stripped before the tool runs, so tool logic (including the one strict-schema tool) and the internal agent's shared tool schemas stay unchanged. It is gated behind an opt-in registration flag so only the /mcp surface carries it, and it is attributed in the per-tool execution metric. Identity only: no isolation, no per-session state, no change to browser behavior.

* fix(server): attribute the /mcp session handle on the per-call log

The session handle was added to the aggregated `tool_executed` metric event, whose rollup keeps only tool name, source, and success, so the handle was discarded. Move the attribution to the per-call log context instead, which is the right home for a per-session value and avoids the unbounded cardinality of a UUID in an aggregated metric. The returned handle in structuredContent is unchanged.
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

✅ Tests passed: 2423/2426

Ran 14 of 15 suites (1 not affected by this change).

Suite Passed Failed Skipped
server-agent 290/290 0 0
server-api 161/161 0 0
server-tools 254/254 0 0
server-browser 10/10 0 0
server-integration 10/10 0 0
server-lib 120/120 0 0
server-root 38/41 0 3
agent 316/316 0 0
claw-app 365/365 0 0
claw-onboard 86/86 0 0
build 44/44 0 0
release n/a n/a not affected
claw-server-rust 620/620 0 0
claw-server-rust-quality passed 0 0
claw-mcp 109/109 0 0

passed = ran successfully but emits no JUnit counts (a lint/format gate).

View workflow run

…point (#2298)

* fix(server): deliver the /mcp tool abort signal from the request context

The browser tool wrapper read the abort signal from a top-level `extra.signal`, which does not exist on the v2 SDK's tool handler context, so it was always undefined and cancellation never reached the tool over /mcp. Read it from `extra.mcpReq.signal` where the SDK actually places it. The internal AI-SDK agent path threads its own signal and is unaffected. Adds a test that a pre-aborted signal delivered via mcpReq.signal makes a waiting tool abort.

* feat(server): reject browser-originated /mcp requests via Sec-Fetch-Site

The /mcp endpoint is intentionally reachable across the LAN, so a loopback bind is not an option. Browsers always send a Sec-Fetch-Site header while native MCP clients and the internal ACP client never do, so rejecting requests that carry it filters browser-originated attacks (DNS rebinding, CSRF) without restricting the bind address. Scoped to /mcp so browser-facing routes are unaffected, and mirrors the claw-server's request hygiene.

* fix(server): serve the modern 2026-07-28 revision on the TS /mcp endpoint

The TS agent server advertised only legacy protocol versions and rejected 2026-07-28: the McpServer was built without supportedProtocolVersions (so it never registered server/discover), and the route dispatched through the bare streamable-HTTP transport, which does not run the modern request machinery.

Advertise the modern revision alongside the legacy list on the McpServer, and split the route: legacy (2025-era) requests keep the existing hand-wired transport with enableJsonResponse so the internal ACP client still gets single-JSON responses, while modern requests go through createMcpHandler, which serves server/discover and the stateless 2026-07-28 dispatch. Both eras are exercised end to end by new tests: legacy initialize negotiates 2025-11-25 as JSON, modern server/discover advertises 2026-07-28, and a modern tool call succeeds.
Reconcile the MCP 2026-07-28 epic with main after the AI SDK v7 upgrade (#2373)
landed. Conflicts resolved on their merits:

- filesystem tools: keep the v7 `Tool` return annotation from main and the
  `zod/v4` import the v2 MCP registerTool requires from the epic.
- apps/server deps: v2 MCP packages from the epic plus the v7-era
  @openrouter/ai-sdk-provider and @sentry/bun bumps from main.
- claw-server MCP service: keep both the session handle (with_session_arg) and
  the new save_skill/mark_skill_run tools, and wrap those two tools with the
  session arg so every tool advertises it, matching the runtime session flow.
- bun.lock reconciled via bun install to a frozen-lockfile fixed point.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant