Skip to content

Route contract (AB4810) rejects a re-exported default component for a second placement of the same tool #446

Description

@ScriptedAlchemy

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions