spec: Add tool registration for Apps, to be called by Host (WebMCP-style!)#72
spec: Add tool registration for Apps, to be called by Host (WebMCP-style!)#72
Conversation
1dba38b to
55ad06a
Compare
@modelcontextprotocol/ext-apps
@modelcontextprotocol/server-basic-preact
@modelcontextprotocol/server-basic-react
@modelcontextprotocol/server-basic-solid
@modelcontextprotocol/server-basic-svelte
@modelcontextprotocol/server-basic-vanillajs
@modelcontextprotocol/server-basic-vue
@modelcontextprotocol/server-budget-allocator
@modelcontextprotocol/server-cohort-heatmap
@modelcontextprotocol/server-customer-segmentation
@modelcontextprotocol/server-debug
@modelcontextprotocol/server-map
@modelcontextprotocol/server-pdf
@modelcontextprotocol/server-scenario-modeler
@modelcontextprotocol/server-shadertoy
@modelcontextprotocol/server-sheet-music
@modelcontextprotocol/server-system-monitor
@modelcontextprotocol/server-threejs
@modelcontextprotocol/server-transcript
@modelcontextprotocol/server-video-resource
@modelcontextprotocol/server-wiki-explorer
commit: |
|
How should it would if I'm in a chat and have multiple MCP apps currently visible, and they register the same tool? Does the host send tool calls to most recent one? (If so is there a way to signal to the older app that it can no longer receive tool calls?) I think I'm also not clear on the lifetime of apps. Currently in the spec there is no specific bounds on the lifetime of the MCP apps. For VS Code, we implement virtualization and so an app is not loaded if the user has it scrolled out of the viewport. But now we need to actively keep app apps alive for some amount of time (forever?) in case a model decides to later call them. For long chat sessions this could get quite expensive. I think #125 is a simpler approach that should cover the bases here without introducing lifetime concerns |
This PR adds comprehensive tool support for MCP Apps, enabling apps to register their own tools and handle tool calls from the host. - Add `registerTool()` method for registering tools with input/output schemas - Add `oncalltool` setter for handling tool call requests from host - Add `onlisttools` setter for handling tool list requests from host - Add `sendToolListChanged()` for notifying host of tool updates - Registered tools support enable/disable/update/remove operations - Add `sendCallTool()` method for calling tools on the app - Add `sendListTools()` method for listing available app tools - Fix: Use correct ListToolsResultSchema (was ListToolsRequestSchema) - Add comprehensive tests for tool registration lifecycle - Add tests for input/output schema validation - Add tests for bidirectional tool call communication - Add tests for tool list change notifications - All 27 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Implement automatic `oncalltool` and `onlisttools` handlers that are initialized when apps register tools. This removes the need for manual handler setup and ensures tools work seamlessly out of the box. - Add automatic `oncalltool` handler that routes calls to registered tools - Add automatic `onlisttools` handler that returns full Tool objects with JSON schemas - Convert Zod schemas to MCP-compliant JSON Schema using `zod-to-json-schema` - Add 27 comprehensive tests covering automatic handlers and tool lifecycle - Test coverage includes error handling, schema validation, and multi-app isolation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Always return inputSchema as object (never undefined) - Keep filter for enabled tools only in list - Update test to match behavior (only enabled tools in list) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Avoid double-verb naming pattern for consistency with existing API.
- Add McpUiScreenshotRequest/Result and McpUiClickRequest/Result types - Add onscreenshot and onclick handlers to App class - Add screenshot() and click() methods to AppBridge class - Generate updated Zod schemas
…t-view - Uncomment and fix the navigate-to tool for animated navigation - Add get-current-view tool to query camera position and bounding box - Add flyToBoundingBox function for smooth camera animation - Add setLabel function for displaying location labels
- get-document-info: Get title, current page, total pages, zoom level - go-to-page: Navigate to a specific page - get-page-text: Extract text from a page - search-text: Search for text across the document - set-zoom: Adjust zoom level
…dertoy, wiki-explorer, and threejs budget-allocator: - get-allocations: Get current budget allocations - set-allocation: Set allocation for a category - set-total-budget: Adjust total budget - set-company-stage: Change stage for benchmarks - get-benchmark-comparison: Compare against benchmarks shadertoy: - set-shader-source: Update shader source code - get-shader-info: Get shader source and compilation status - Sends errors via updateModelContext wiki-explorer: - search-article: Search for Wikipedia articles - get-current-article: Get current article info - highlight-node: Highlight a graph node - get-visible-nodes: List visible nodes threejs: - set-scene-source: Update the Three.js scene source code - get-scene-info: Get current scene state and any errors - Sends syntax errors to model via updateModelContext
Wiki Explorer: - Add expand-node tool - the critical missing tool for graph exploration - Claude can now programmatically expand nodes to discover linked articles Server descriptions updated to mention widget tools: - map-server: navigate-to, get-current-view - pdf-server: go-to-page, get-page-text, search-text, set-zoom, get-document-info - budget-allocator: get-allocations, set-allocation, set-total-budget, etc. - shadertoy: set-shader-source, get-shader-info - wiki-explorer: expand-node, search-article, highlight-node, etc. All descriptions now mention 'Use list_widget_tools to discover available actions.'
📖 Docs Preview Deployed
Includes drafts and future-dated posts. All pages served with |
When registerTool is called before connect() on an App created without
explicit tools capability, setRequestHandler's capability assertion
would throw, breaking app initialization at module load.
Auto-register { tools: { listChanged: true } } on first registerTool
call (pre-connect only), mirroring McpServer.registerTool behavior.
Fixes pdf-annotations e2e failures where the PDF canvas never rendered
because registerTool threw at module scope.
Converts the 5 placeholder app-registered tools into 12 tools that map directly to the server's interact commands (navigate, search, find, search_navigate, zoom, add_annotations, update_annotations, remove_annotations, highlight_text, fill_form, get_text, get_screenshot) plus the existing get-document-info. Implementation stays DRY by dispatching through the existing processCommands() handler — each tool callback constructs a PdfCommand and runs it via a small runCommand() wrapper. For get_text and get_screenshot, the page-data collection is extracted from handleGetPages into a shared collectPageData() helper so results can be returned directly instead of round-tripping through the server. Tool names and zod schemas mirror the interact command parameter shapes from server.ts so the model sees the same surface whether it goes through interact or app-tools. The server-side interact tool is unchanged and remains available for hosts without app-tool support.
@ the MCP summit, there is a talk from an engineer who mentions using MCP Apps as a way to deliver state updates (triggering async sendMessage or updateModelContext). I think we could definitely use some API for an app to request to be pinned or not virtualized because it's actively providing context. I feel like I would not want this to be implicit for whether the model provides tools or not, because it feels a bit heavy to need to register or unregister all my tools to signal this. Something like |
…ation # Conflicts: # examples/pdf-server/src/mcp-app.ts # src/app.examples.ts # src/app.ts
- Add outputSchema to Tool struct (matches SDK + core MCP) - Reword "black box" bullets: drop stale screenshots reference, clarify no DOM access - Clarify push vs pull contrast with explicit setWidgetState + ui/update-model-context refs - Add Task-reference future-work bullet for tools outliving render lifecycle
…s pull complementing update-model-context push
App.registerTool and registerAppTool now accept any schema implementing the Standard Schema spec (Zod ≥3.25, ArkType, Valibot, …) instead of zod-only. Uses ~standard.validate / ~standard.jsonSchema directly, so app.ts no longer imports zod at runtime — zod is now an optional peer. Mirrors the StandardSchemaWithJSON type from modelcontextprotocol/typescript-sdk#1689 so bumping to SDK v2 is a drop-in import swap. Adds @standard-schema/spec (types-only) and a hand-rolled non-zod test.
App tools/call MAY be task-augmented per the core MCP Tasks utility (spec 2025-11-25). Adds a Long-Running App Tools subsection: hosts proxy tasks/* while the app is mounted; apps SHOULD delegate long-lived work to the server so the task survives iframe teardown. Removes the stale 'future extension' bullet — Tasks is shipped.
… registerAppTool runtime caveat
AppToolCallback (introduced with the Standard Schema migration) references the local RequestHandlerExtra alias in its parameter type. The alias is a private Parameters<> derivation and isn't part of the public API surface, so add it to intentionallyNotExported alongside AppOptions/MethodSchema.
zod v3.25.x implements ~standard.validate but not ~standard.jsonSchema, so constraining to StandardSchemaWithJSON broke the ^3.25.0 peer range. Widen registerTool to StandardSchemaV1 and fall back to a lazy zod/v4 import for serialization when ~standard.jsonSchema is absent and vendor==='zod'. Non-zod schemas without jsonSchema get a clear error.
antonpk1
left a comment
There was a problem hiding this comment.
big fan of this feature!
- Handler reads registeredTool.{input,output}Schema so update() takes effect
- Call cb(extra) when no inputSchema, matching AppToolCallback<undefined>
- Throw on duplicate name; guard list_changed pre-connect; notify on register
- Drop misleading zod optional peer (still required by generated/schema.ts)
- examples: fix pdf search/find stale match count; threejs set-scene-source re-render
- spec: Zod → Standard Schema; clarify tools availability via tools/list after init
AppToolCallback gains an Out param: when outputSchema is provided, structuredContent is required and typed via StandardSchemaV1.InferOutput, with an `isError: true` escape hatch. Without outputSchema, return type is unchanged (CallToolResult). Uses intersection rather than Omit since CallToolResult's index signature swallows Omit's known keys. Also drops the `as any` casts on z.object(...) in tests — zod 4 satisfies StandardSchemaV1 directly.
App.registerTool:
- Skip outputSchema validation when result.isError (matches AppToolResult type)
- Pin callback arity to original config.inputSchema; only the validation schema is mutable via update()
- Default missing tools/call arguments to {}
- remove() is a no-op if the handle is stale (re-registered or already removed)
- Gate list_changed notifications on the listChanged capability
Examples:
- threejs: propagate executeThreeCode failures to onSceneError/onSceneRendering; catch updateModelContext rejection
- wiki-explorer: search-article preserves graph on no-match
- budget-allocator: fix category/stage values in tool descriptions
Summary
This PR enables Apps to register their own tools that agents can call, making apps introspectable and accessible to the model.
Apps expose semantic interfaces (state queries, operations) via standard MCP tools. The agent discovers capabilities via
tools/list, queries state, and drives interactions. This is pull-based and complements the existing push channel (ui/update-model-context): push lets the app proactively update the model's context, pull lets the agent query and command the app on demand. Apps may use either or both.Example:
Agent interaction:
Changes
App side (
app.ts)registerTool()- Register tools with Standard Schema validation (Zod, ArkType, Valibot, …)oncalltool/onlisttools- Handle tool requestssendToolListChanged()- Notify on tool updatesenable(),disable(),update(),remove()Host side (
app-bridge.ts)sendCallTool()- Call app toolssendListTools()- List app toolsListToolsResultSchemaCapabilities (
types.ts)tools: { listChanged?: boolean }serverTools: { listChanged?: boolean }(existing)Tests: ✓ 27 passing, 100% coverage
Design
Reuses standard MCP messages:
tools/call,tools/list,notifications/tools/list_changedSimilar to WebMCP but without turning the App (embedded page) into an MCP server - apps register tools within the App/Host architecture.
Lifecycle: App tools exist only while app loaded (ephemeral, sandboxed)
Separation: Server tools (persistent, trusted) vs App tools (ephemeral, sandboxed)
Review feedback addressed
Schema validation — Standard Schema
App.registerToolaccepts any Standard Schema implementation (Zod, ArkType, Valibot, …) — aligned with typescript-sdk#1689.zodis now an optional peer; there is no static zod import insrc/app.ts.Compat: zod v3.25.x lacks
~standard.jsonSchema, so serialization falls back to a lazyz.toJSONSchemawhenvendor==='zod'. The full^3.25.0 || ^4.0.0peer range is preserved.Long-running app tools
App
tools/callmay be task-augmented per core MCP Tasks. Hosts proxytasks/*to the app like they proxytools/call; apps SHOULD delegate long-lived work to the server so the task survives iframe teardown.Spec clarifications
ui/update-model-context(feat: add ui/update-model-context #125 landed after this PR was opened).frameDomains); the tool surface is the only reliable introspection path.Toolstructure now liststitle/outputSchema/_metaand links to the core MCPTooltype. SDK no longer emits an emptyoutputSchemawhen none was declared.Breaking Changes
None. Purely additive.
Related
Implements the gist of #35 (WebMCP-style tool registration) while preserving the App/Host architecture.
🤖 Generated with Claude Code