Skip to content

spec: Add tool registration for Apps, to be called by Host (WebMCP-style!)#72

Open
ochafik wants to merge 42 commits intomainfrom
feat/app-tool-registration
Open

spec: Add tool registration for Apps, to be called by Host (WebMCP-style!)#72
ochafik wants to merge 42 commits intomainfrom
feat/app-tool-registration

Conversation

@ochafik
Copy link
Copy Markdown
Contributor

@ochafik ochafik commented Dec 3, 2025

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 can query structured state
app.registerTool("get_board_state", {
  outputSchema: z.object({
    board: z.array(z.enum(['X', 'O', null])),
    currentPlayer: z.enum(['X', 'O']),
    winner: z.enum(['X', 'O', 'draw', null]).nullable()
  })
}, async () => ({
  structuredContent: { board, currentPlayer, winner: checkWinner(board) }
}));

// Agent can execute actions
app.registerTool("make_move", {
  inputSchema: z.object({ position: z.number().int().min(0).max(8) })
}, async ({ position }) => {
  board[position] = currentPlayer;
  currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
  return { structuredContent: { board, winner: checkWinner(board) } };
});

Agent interaction:

// Discover → Query → Act
const { tools } = await bridge.sendListTools({});
const state = await bridge.sendCallTool({ name: "get_board_state" });
await bridge.sendCallTool({ name: "make_move", arguments: { position: 4 } });

Changes

App side (app.ts)

  • registerTool() - Register tools with Standard Schema validation (Zod, ArkType, Valibot, …)
  • oncalltool / onlisttools - Handle tool requests
  • sendToolListChanged() - Notify on tool updates
  • Tools support enable(), disable(), update(), remove()

Host side (app-bridge.ts)

  • sendCallTool() - Call app tools
  • sendListTools() - List app tools
  • Fix: Use correct ListToolsResultSchema

Capabilities (types.ts)

  • Apps: tools: { listChanged?: boolean }
  • Hosts: serverTools: { listChanged?: boolean } (existing)

Tests: ✓ 27 passing, 100% coverage

Design

Reuses standard MCP messages: tools/call, tools/list, notifications/tools/list_changed

Similar 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.registerTool accepts any Standard Schema implementation (Zod, ArkType, Valibot, …) — aligned with typescript-sdk#1689. zod is now an optional peer; there is no static zod import in src/app.ts.

Compat: zod v3.25.x lacks ~standard.jsonSchema, so serialization falls back to a lazy z.toJSONSchema when vendor==='zod'. The full ^3.25.0 || ^4.0.0 peer range is preserved.

Long-running app tools

App tools/call may be task-augmented per core MCP Tasks. Hosts proxy tasks/* to the app like they proxy tools/call; apps SHOULD delegate long-lived work to the server so the task survives iframe teardown.

Spec clarifications

  • Tool registration framed as pull, complementing the existing push ui/update-model-context (feat: add ui/update-model-context #125 landed after this PR was opened).
  • Host has no guaranteed DOM access into apps (cross-origin frameDomains); the tool surface is the only reliable introspection path.
  • Tool structure now lists title/outputSchema/_meta and links to the core MCP Tool type. SDK no longer emits an empty outputSchema when 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

Comment thread src/app.ts Fixed
Comment thread src/app-bridge.test.ts Fixed
@ochafik ochafik changed the title feat: Add tool registration and bidirectional tool support feat: Make Apps bidirectional with tool registration support Dec 3, 2025
@ochafik ochafik changed the title feat: Make Apps bidirectional with tool registration support feat: Add tool registration for Apps Dec 3, 2025
@ochafik ochafik marked this pull request as draft December 3, 2025 00:18
@ochafik ochafik changed the title feat: Add tool registration for Apps feat: Add tool registration for Apps (WebMCP-style!) Dec 3, 2025
@ochafik ochafik changed the title feat: Add tool registration for Apps (WebMCP-style!) spec: Add tool registration for Apps, to be called by Host (WebMCP-style!) Dec 3, 2025
@ochafik ochafik marked this pull request as ready for review December 3, 2025 00:53
Comment thread src/app-bridge.test.ts Fixed
Comment thread src/app-bridge.test.ts Fixed
Comment thread src/app.ts Fixed
@ochafik ochafik force-pushed the feat/app-tool-registration branch from 1dba38b to 55ad06a Compare January 9, 2026 16:55
Comment thread src/app-bridge.test.ts Fixed
Comment thread src/app-bridge.test.ts Fixed
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Jan 9, 2026

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@72

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@72

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@72

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@72

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@72

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@72

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@72

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@72

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@72

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@72

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@72

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@72

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@72

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@72

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@72

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@72

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@72

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@72

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@72

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@72

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@72

commit: 5f6dec0

@connor4312
Copy link
Copy Markdown

connor4312 commented Jan 9, 2026

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

ochafik and others added 16 commits January 17, 2026 02:58
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.'
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

📖 Docs Preview Deployed

Preview (stable) https://pr-72.mcp-ext-apps-docs-preview.pages.dev
This commit https://5d3598f9.mcp-ext-apps-docs-preview.pages.dev
Commit dcfc95c

Includes drafts and future-dated posts. All pages served with noindex, nofollow — search engines will not crawl this preview.

Comment thread src/app-bridge.test.ts Fixed
Comment thread src/app-bridge.test.ts Fixed
ochafik added 2 commits March 26, 2026 14:08
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.
Comment thread src/app.ts Outdated
@connor4312
Copy link
Copy Markdown

For native mobile sdks we'll definitely need to accept death of scrolled-out apps as inevitable.
Widget state persistence / restoration can be one way to handle this gracefully, but we'll ideally need new protocol support for this (currently the workaround is to use localStorage indexed by hostInfo.toolInfo.id)

@ 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 app.persist(AbortSignal) -- could workshop it 😄

Comment thread specification/draft/apps.mdx
Comment thread specification/draft/apps.mdx Outdated
Comment thread specification/draft/apps.mdx Outdated
Comment thread specification/draft/apps.mdx Outdated
Comment thread specification/draft/apps.mdx
ochafik added 14 commits April 9, 2026 12:36
…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.
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.
@ochafik ochafik requested review from antonpk1 and idosal April 9, 2026 16:00
antonpk1
antonpk1 previously approved these changes Apr 14, 2026
Copy link
Copy Markdown
Contributor

@antonpk1 antonpk1 left a comment

Choose a reason for hiding this comment

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

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
ochafik added 2 commits April 14, 2026 15:47
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
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.

6 participants