Summary
A project can list the same tool name on two generated MCP servers (movie-library needs search, add_to_library, download, and delete both on their primary server and on the widget-serving server, because MCP-app tools/call routes to the server that served the widget). The natural way to author the second placement is a route module that carries its own literal config (the widget-server placement differs only in _meta.ui.resourceUri) and re-exports the rest from the primary route or a shared page:
// src/mcp/movie-library-library/tools/download.tsx
import type { ToolConfig } from 'agent-bundle';
import { WIDGET_RESOURCE_URI } from '../../../widget-resource.ts';
export const config = {
_meta: { ui: { resourceUri: WIDGET_RESOURCE_URI } },
annotations: { readOnlyHint: true },
description: 'Completed rTorrent payloads are published automatically by the seedbox completion timer.',
title: 'Download',
} satisfies ToolConfig;
export { default, inputSchema, resultSchema } from '../../movie-library-public/tools/download.tsx';
agent-bundle validate rejects it:
AB4810 Route module src/mcp/movie-library-library/tools/download.tsx does not satisfy the public route contract: default export is not an async function component.
Recovery: Export const inputSchema and resultSchema, plus one async default Server Component receiving { input, signal }.
The static contract check only recognizes a locally declared export default async function. A re-exported default (export { default } from) is a legal ESM default export that resolves to exactly such a component at run time, and export const inputSchema = other.inputSchema (a member expression) is already accepted, so the default export is the odd one out.
Repro
Preview agent-bundle@43abb2db0. Two generated servers, one tool placed on both:
src/mcp/a/tools/ping.tsx # full route: literal config, inputSchema, resultSchema, async default component
src/mcp/b/tools/ping.tsx # literal config + `export { default, inputSchema, resultSchema } from '../../a/tools/ping.tsx';`
agent-bundle validate --json → AB4810 on src/mcp/b/tools/ping.tsx.
Expected
Either accept export { default } from '<relative module>' when the target module's default export satisfies the contract, or document that a second placement must declare its own export default async function wrapper. movie-library worked around it with src/pages/<tool>.tsx shared page components and a thin export default async function X({ input, signal }) { return <XRoute input={input} signal={signal} />; } in each placement — fine, but the diagnostic text ("default export is not an async function component") is misleading for a re-export, since it is one.
Found while re-porting ScriptedAlchemy/movie-library onto 43abb2db0.
Summary
A project can list the same tool name on two generated MCP servers (movie-library needs
search,add_to_library,download, anddeleteboth on their primary server and on the widget-serving server, because MCP-apptools/callroutes to the server that served the widget). The natural way to author the second placement is a route module that carries its own literalconfig(the widget-server placement differs only in_meta.ui.resourceUri) and re-exports the rest from the primary route or a shared page:agent-bundle validaterejects it:The static contract check only recognizes a locally declared
export default async function. A re-exported default (export { default } from) is a legal ESM default export that resolves to exactly such a component at run time, andexport const inputSchema = other.inputSchema(a member expression) is already accepted, so the default export is the odd one out.Repro
Preview
agent-bundle@43abb2db0. Two generated servers, one tool placed on both:agent-bundle validate --json→ AB4810 onsrc/mcp/b/tools/ping.tsx.Expected
Either accept
export { default } from '<relative module>'when the target module's default export satisfies the contract, or document that a second placement must declare its ownexport default async functionwrapper. movie-library worked around it withsrc/pages/<tool>.tsxshared page components and a thinexport default async function X({ input, signal }) { return <XRoute input={input} signal={signal} />; }in each placement — fine, but the diagnostic text ("default export is not an async function component") is misleading for a re-export, since it is one.Found while re-porting ScriptedAlchemy/movie-library onto
43abb2db0.