modelcontextprotocol / modelcontextprotocol/ext-apps

Widget callServerTool calls hardcode bare tool names, breaking under multi-server MCP hosts

Open
#753 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
2.9k
Forks
387
Avg merge
3h 21m
Merged PRs (30d)
6

Description

Summary

Every example app that calls app.callServerTool() hardcodes a literal tool-name string (e.g. name: "get-time") instead of resolving it from getHostContext().toolInfo. This works fine when a client connects directly to a single MCP server, but breaks under any MCP host/proxy that aggregates multiple servers behind one session and qualifies tool names to avoid collisions (e.g. pdf-server-save_pdf instead of save_pdf). In that situation the tool that rendered the widget is reachable under its qualified name, but the widget's own callServerTool calls go out under the original bare name and fail to resolve — "tool not found" — even though the widget itself loaded and displayed correctly.

This is easy to miss because it never surfaces when testing a server standalone (1:1 with a client), which is presumably how these examples are normally exercised. It only shows up once a server is aggregated behind a multi-server hub/router.

Reproduction

Run any affected example (e.g. pdf-server) behind an MCP aggregator that qualifies tool names for multi-server sessions (we hit this with mcphub, which qualifies names as <server>-<tool> whenever a named route has 2+ connected servers). The entry tool (display_pdfpdf-server-display_pdf) is called correctly by the host and the widget renders. But any follow-up interaction that goes through the widget's own callServerTool — paging, saving, polling — fails, because the widget calls the literal string save_pdf/read_pdf_bytes/etc., which doesn't exist under that name in the aggregated session; only pdf-server-save_pdf etc. do.

Fix

getHostContext().toolInfo.tool.name already exposes the actual, host-rewritten name of the tool that invoked the widget. A widget can recover whatever prefix the host applied and reapply it to any tool it calls back with, e.g.:

function resolveToolName(baseName: string): string {
  const ctx = app.getHostContext();
  const invokedName = ctx?.toolInfo?.tool?.name;
  if (!invokedName) return baseName;
  // this widget's own bare entry-tool names, in case the host mounts it from more than one
  const entryTools = ["display_pdf"];
  const ownToolName = entryTools.find((t) => invokedName.endsWith(t));
  if (!ownToolName) return baseName;
  const prefix = invokedName.slice(0, invokedName.length - ownToolName.length);
  return prefix + baseName;
}

await app.callServerTool({ name: resolveToolName("save_pdf"), arguments: {...} });

falling back to the bare name on hosts that don't rewrite names at all. This is a small, self-contained change per example and doesn't require any protocol/SDK change — toolInfo is already available.

Affected examples

Checked every example under examples/ for callServerTool usage. 15 hardcode bare names:

  • basic-server-preactget-time
  • basic-server-reactget-time
  • basic-server-solidget-time
  • basic-server-svelteget-time
  • basic-server-vanillajsget-time
  • basic-server-vueget-time
  • quickstartget-time
  • integration-serverget-time
  • cohort-heatmap-serverget-cohort-data
  • customer-segmentation-serverget-customer-data
  • debug-serverdebug-log, debug-tool, debug-refresh
  • system-monitor-serverpoll-system-stats
  • wiki-explorer-serverget-first-degree-links
  • lazy-auth-serverget_secret, revoke_auth_token
  • pdf-serversubmit_viewer_state, submit_page_data, save_pdf, read_pdf_bytes, submit_save_data, poll_pdf_commands

Not affected: threejs-server plumbs callServerTool through as an unused prop (no live call site in the example). map-server, qr-server, scenario-modeler-server, shadertoy-server, sheet-music-server, transcript-server, video-resource-server, budget-allocator-server are render-once and never call back to the server, so the pattern doesn't apply.

Why this matters beyond the 15

The basic-server-* examples are the templates most third-party MCP App authors will fork from first. Fixing the pattern there — even as a small shared helper documented once — sets the right precedent going forward, on top of fixing the specific interactive examples (pdf-server, debug-server, wiki-explorer-server, lazy-auth-server, etc.) directly.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by searching the affected examples under examples/ for app.callServerTool() calls and inspect getHostContext().toolInfo.tool.name. Update the listed interactive examples so callbacks resolve host-qualified names while retaining bare-name behavior, then verify the interactions through a multi-server MCP aggregator and a direct single-server connection.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, full-stack
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.