Skip to content

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

Description

@jcollas

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions