Skip to content

Proposal: Standard MCP-to-WebMCP tool format converter #84

Description

@hemanth

Context

This proposal complements #83 (API for connecting to remote MCP servers). Even if WebMCP doesn't natively support remote MCP connections, there should be a standardized way to convert MCP tool definitions to WebMCP format.

Problem

MCP and WebMCP tool schemas are similar but not identical. Currently, developers bridging MCP servers to WebMCP must manually handle:

  1. Schema mapping: MCP inputSchema → WebMCP inputSchema (mostly compatible, but edge cases exist)
  2. Response format: MCP content arrays → WebMCP content format
  3. Error handling: MCP JSON-RPC errors → WebMCP error responses
  4. Metadata preservation: Tool descriptions, annotations, etc.

Current Manual Conversion

// MCP tool from tools/list response
const mcpTool = {
  name: "search_proposals",
  description: "Search TC39 proposals by keyword",
  inputSchema: {
    type: "object",
    properties: {
      query: { type: "string", description: "Search term" },
      stage: { type: "number", description: "Filter by stage" }
    },
    required: ["query"]
  }
};

// Manual conversion to WebMCP format
const webMCPTool = {
  name: mcpTool.name,
  description: mcpTool.description || '',
  inputSchema: mcpTool.inputSchema || { type: 'object', properties: {} },
  execute: async (args) => {
    // Developer must implement the bridge logic
    const response = await fetch(mcpServerUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        jsonrpc: '2.0',
        id: Date.now(),
        method: 'tools/call',
        params: { name: mcpTool.name, arguments: args }
      })
    });
    
    const result = await response.json();
    
    // Convert MCP response format to WebMCP
    if (result.error) {
      return { content: [{ type: 'text', text: `Error: ${result.error.message}` }] };
    }
    
    // Handle MCP content array format
    if (result.result?.content) {
      return result.result;
    }
    
    // Wrap raw results
    return {
      content: [{
        type: 'text',
        text: typeof result.result === 'string' 
          ? result.result 
          : JSON.stringify(result.result, null, 2)
      }]
    };
  }
};

navigator.modelContext.provideContext({ tools: [webMCPTool] });

Proposed Solution

A utility function or class in the WebMCP spec that standardizes this conversion:

// Option A: Static utility
const webMCPTools = WebMCPToolConverter.fromMCP(mcpTools, {
  executor: (toolName, args) => callRemoteMCP(toolName, args)
});

// Option B: Built into modelContext
navigator.modelContext.registerMCPTools(mcpTools, {
  endpoint: 'https://example.com/mcp',
  headers: { 'Authorization': 'Bearer ...' }
});

// Option C: Conversion helper only (no execution)
const schema = WebMCPToolConverter.convertSchema(mcpInputSchema);

Benefits

  1. Interoperability: Easy bridging between MCP ecosystem and WebMCP
  2. Consistency: Standard error handling and response formatting
  3. Reduced boilerplate: Developers don't reinvent the conversion logic
  4. Forward compatibility: Single place to update when formats evolve

Considerations

  • Should this be normative spec or informative guidance?
  • Should it handle streaming responses from MCP servers?
  • How to handle MCP-specific features not in WebMCP (resources, prompts, sampling)?

This would lower the barrier for developers wanting to expose existing MCP tools to browser AI agents.

Activity

  1. manticorevault commented on Oct 7, 2026

    @manticorevault

    Adding the point of view based on a converter that's already in use.

    I work on snyk.io. We are wiring a read-only content-search tool up as a server-side MCP tool and registering it in document.modelContext through mcp-handler's experimental_webMcp bridge. Not in production yet. The bridge is close to what is presented in Option B: the server serves a script from GET /mcp?webmcp-script that runs initialize and tools/list, keeps an allowlisted subset, calls registerTool() for each, and forwards execute to tools/call. Source: src/webmcp/script-handler.ts.

    Against the four things the proposal says developers currently do by hand:

    1. Schema mapping was a non-issue. name and inputSchema pass through unchanged. The one mismatch is description, required on ModelContextTool and optional in MCP, so the bridge falls back to title and then name.

    2. Response format is where the sketch in this issue and the bridge seem to disagree. The sketch returns result.result when content is present. The bridge returns the whole CallToolResult from execute, and WebMCP JSON-serializes whatever that resolves to, so the agent sees MCP's envelope (content, structuredContent, isError) instead of something more WebMCP-shaped. Our tool fills both content and structuredContent, so in this way the agent receives both. I don't think either choice is obviously right (which is the useful part).

    3. Errors are the one part a converter can't paper over. An isError: true result resolves normally and the text reaches the agent. A JSON-RPC or HTTP error rejects execute, and the spec completes that as a failure while only optionally logging the reason, so the agent learns the call failed and not why. The sketch avoids that by returning the error as content. That split seems worth writing down, and it depends on No structured way to signal a tool's refusal, distinct from success or a schema-validation error #282 and Define specific errors for tool execution failures #323, from what I could see.

    4. Metadata: only readOnlyHint is copied. idempotentHint and openWorldHint have nowhere to go. destructiveHint is dropped instead of mapped onto consequentialHint, so a destructive MCP tool arrives looking neither read-only nor consequential. outputSchema is dropped because ModelContextTool has no field for it yet (based on Introduce outputSchema (was: Should output also have a schema?) #9).

    On the questions at the end: I'd keep a converter as informative guidance. Streaming isn't handled, so the bridge reads one finite SSE response and ignores progress notifications. Resources, prompts and sampling are ignored, tools only. The part I'd want written down anyway is the error mapping, forcing destructiveHint to collapse to consequentialHint: false.

    One thing about Option B as sketched, registerMCPTools(tools, { endpoint, headers }): it registers every tool and sends the caller's headers. The bridge won't start without an allowlist. We allowlist one read-only tool and set credentials: "omit", because anything in the page can invoke a registered tool as the visitor. That feels like part of the advice even though it isn't part of the format.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions