Skip to content

feat(experimental): Add WebMCP Adapter#1222

Open
Muhammad-Bin-Ali wants to merge 13 commits intomainfrom
feat/webmcp-adapter-clean
Open

feat(experimental): Add WebMCP Adapter#1222
Muhammad-Bin-Ali wants to merge 13 commits intomainfrom
feat/webmcp-adapter-clean

Conversation

@Muhammad-Bin-Ali
Copy link
Copy Markdown
Contributor

@Muhammad-Bin-Ali Muhammad-Bin-Ali commented Mar 27, 2026

Add experimental WebMCP adapter

Bridges MCP server tools to Chrome's native navigator.modelContext API so browser-native AI agents can discover and call tools registered on a Cloudflare McpAgent — no extra infrastructure needed.

import { registerWebMcp } from "agents/experimental/webmcp";

const handle = await registerWebMcp({ url: "/mcp" });
// Chrome's AI can now call your MCP tools

What's included

  • agents/experimental/webmcp — New public export. Connects to an MCP server via the SDK's StreamableHTTPClientTransport, discovers tools, and registers them with navigator.modelContext.registerTool().
  • examples/webmcp/ — Demo app with an McpAgent server and React client showing tool discovery, execution, and live refresh.
  • 28 tests running in Chromium via Playwright — covers tool execution, watch mode (SSE re-sync), error handling, edge cases, and known-bug regression tests.

Design decisions

  • Uses the official @modelcontextprotocol/sdk Client + StreamableHTTPClientTransport instead of a hand-rolled HTTP client. This gives us proper SSE parsing, reconnection with backoff, protocol version negotiation, session termination, and pagination for free.
  • Supports headers (static) and getHeaders (per-request dynamic) for authenticated MCP endpoints.
  • Gracefully no-ops when navigator.modelContext is unavailable (non-Chrome browsers).

Closes #1216


Open with Devin

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 27, 2026

🦋 Changeset detected

Latest commit: 72a0526

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Muhammad-Bin-Ali Muhammad-Bin-Ali self-assigned this Mar 27, 2026
@Muhammad-Bin-Ali Muhammad-Bin-Ali force-pushed the feat/webmcp-adapter-clean branch 3 times, most recently from f8423d2 to ff51a38 Compare March 31, 2026 20:36
@Muhammad-Bin-Ali Muhammad-Bin-Ali marked this pull request as ready for review March 31, 2026 20:37
@Muhammad-Bin-Ali Muhammad-Bin-Ali changed the title Feat/webmcp adapter clean feat(experimental): Add WebMCP Adapter Mar 31, 2026
@Muhammad-Bin-Ali
Copy link
Copy Markdown
Contributor Author

/bonk review this PR

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

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

Review: feat(experimental): Add WebMCP Adapter

Overall this is a well-structured addition. The adapter design is clean, the test coverage is solid, the example follows repo conventions, and the experimental warning is appropriately prominent. Below are the issues I found, organized by severity.


Issues to address

1. onChanged callback signature mismatch (bug)

In webmcp.ts:118-121, the listChanged.tools.onChanged callback is typed as () => void:

onChanged: () => {
  this._onToolsChanged?.();
}

But ListChangedCallback<Tool> from @modelcontextprotocol/sdk expects (error: Error | null, items: Tool[] | null) => void. This works at runtime (JS ignores extra args), but:

  • It silently swallows errors the SDK passes to the callback
  • It discards the items array, forcing a redundant listTools() round-trip on every change notification

Suggested fix:

onChanged: (error: Error | null, _items: Tool[] | null) => {
  if (error) {
    console.warn("[webmcp-adapter] listChanged error:", error);
    return;
  }
  this._onToolsChanged?.();
}

You could also use the items parameter to skip the extra listTools() call when the SDK already provides the updated list.

2. Missing dependencies in examples/webmcp/package.json (consistency)

server.ts imports @modelcontextprotocol/sdk and zod, but neither is declared in the example's package.json. Every other MCP example in the repo (mcp, mcp-worker, mcp-elicitation, x402-mcp, etc.) explicitly declares both. This works today due to hoisting but is fragile and inconsistent.

Add to examples/webmcp/package.json:

"@modelcontextprotocol/sdk": "1.28.0",
"zod": "^4.3.6"

3. tools getter returns the internal mutable array by reference (footgun)

At webmcp.ts:358-360:

get tools() {
  return registeredTools as readonly string[];
}

The as readonly string[] cast doesn't prevent mutation — callers can still do (handle.tools as string[]).push("evil"). The no-op handle at line 263 correctly returns [] as readonly string[] (a fresh empty array each time), creating an inconsistency: one code path is safe, the other leaks internals.

Fix with return [...registeredTools] or return Object.freeze([...registeredTools]).

4. Test key stability issue in client.tsx

At client.tsx:392, log entries use key={log.timestamp}. If two logs arrive in the same millisecond (entirely possible in the onSync + onError paths), React will see duplicate keys. Use a counter or crypto.randomUUID() instead.


Minor / nit-level observations

5. _client field used as InstanceType<typeof Client> but typed implicitly — The McpHttpClient class stores _client and _transport as private fields with inferred types. Since this is a browser-only module that gets bundled, this is fine, but adding explicit type annotations would improve readability for contributors.

6. Image content handling is lossy — In webmcp.ts:309, image data is prefixed with data:image;base64, (no specific MIME type) even though mimeType is available on the content item. The mimeType field from the MCP response is discarded. This means all images are treated identically regardless of format. Consider data:${c.mimeType ?? 'image/png'};base64,${c.data}.

7. eslint-disable comment in client.tsx:165 — The repo uses oxlint, not eslint. The // eslint-disable-next-line react-hooks/exhaustive-deps comment is dead. Replace with the oxlint equivalent or just add initWebMcp to the dependency array (it's already wrapped in useCallback with stable deps).

8. PR title says "28 tests" — I count 20 it() blocks in the test file, not 28. Minor but worth correcting in the PR description.


What looks good

  • Clean separation between McpHttpClient (transport) and registerWebMcp (registration logic)
  • Proper pagination support in listTools() with cursor handling
  • Graceful no-op when navigator.modelContext is unavailable
  • Good test coverage: no-op path, headers, dynamic headers, merge precedence, tool execution, watch mode, error handling, edge cases
  • Example follows all repo conventions (Kumo UI, dark mode toggle, connection indicator, explainer card, PoweredByCloudflare footer, correct wrangler.jsonc/vite.config.ts/tsconfig.json structure)
  • Changeset, build entry, package.json export, and AGENTS.md documentation all properly updated

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Mar 31, 2026

Review posted on PR #1222. Here's a summary of the findings:

4 actionable issues:

  1. onChanged callback signature mismatch — The listChanged.tools.onChanged handler ignores the error and items parameters from the SDK, silently swallowing errors and forcing redundant listTools() calls.
  2. Missing dependencies in exampleexamples/webmcp/package.json is missing @modelcontextprotocol/sdk and zod, which every other MCP example declares explicitly.
  3. tools getter leaks internal array — The as readonly string[] cast doesn't prevent runtime mutation; callers get a reference to the actual internal array.
  4. Duplicate React keys — Log entries use timestamp as key, which will collide when two logs arrive in the same millisecond.

4 minor observations on image MIME handling, dead eslint-disable comment, implicit typing on private fields, and incorrect test count in the PR description.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 31, 2026

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1222

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1222

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1222

hono-agents

npm i https://pkg.pr.new/hono-agents@1222

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1222

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1222

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1222

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1222

commit: 4423bcd

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@Muhammad-Bin-Ali Muhammad-Bin-Ali force-pushed the feat/webmcp-adapter-clean branch from 4423bcd to 8852476 Compare April 1, 2026 14:37
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@Muhammad-Bin-Ali Muhammad-Bin-Ali force-pushed the feat/webmcp-adapter-clean branch from a678799 to 07d938a Compare April 7, 2026 17:18
devin-ai-integration[bot]

This comment was marked as resolved.

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.

Adding WebMCP-compatible Support for Tools

2 participants