Skip to content

Generated MCP tool routes cannot emit CallToolResult._meta (Agent.Result metadata is decoded but never projected) #383

Description

@ScriptedAlchemy

Summary

Route mode lowers an Agent Document to a CallToolResult through documentToCallToolResult / projectMcpRenderStream (packages/rsc-runtime/src/project-mcp.ts) and registerGeneratedRoutes (packages/agent-bundle/src/mcp-server-runtime.ts). Those produce content, structuredContent (from Agent.Result value), and isError (from a represented Agent.Error), but nothing on the route contract reaches the result's _meta. Agent.Result does accept a metadata?: JsonValue prop, and decodeAgentDocument carries it into the document root (AgentResultNode.metadata), yet the MCP projection ignores it — so there is a documented-looking prop with no wire effect, and no other way for a generated tool route to set per-result _meta.

The defineOperation escape hatch could do this (Mcp.Result _meta={...}), so consumers migrating from createRscMcpServer to conventional routes lose the capability. The MCP Apps convention (ext-apps registerAppTool) stamps _meta.ui.resourceUri on every tool result as well as on the listing; the listing half works in route mode through config._meta, the result half cannot be expressed.

Observed on framework main af1c185.

Repro

// src/mcp/demo/tools/status.tsx
export const config = { _meta: { ui: { resourceUri: 'ui://demo/panel.html' } }, description: 'Status.' } satisfies ToolConfig;
export const inputSchema = z.object({}).strict();
export const resultSchema = z.object({ status: z.literal('ready') }).strict();
export default async function Status() {
  return (
    <Agent.Result metadata={{ ui: { resourceUri: 'ui://demo/panel.html' } }} value={{ status: 'ready' }}>
      <Agent.Text>ready</Agent.Text>
    </Agent.Result>
  );
}

tools/list shows _meta.ui.resourceUri on the tool (good); tools/call returns { content, structuredContent } with no _meta. The runtime primitives show where it is lost:

import { decodeAgentDocument, documentToCallToolResult } from '@agent-bundle/runtime';
import { createElement } from 'react';
const tree = createElement('agent-result', { metadata: { ui: { resourceUri: 'ui://demo/panel.html' } }, value: { status: 'ready' } }, createElement('agent-text', null, 'ready'));
const document = decodeAgentDocument(tree);
console.log(JSON.stringify({ rootMetadata: document.root.metadata, callToolResult: documentToCallToolResult(document) }));
// {"rootMetadata":{"ui":{"resourceUri":"ui://demo/panel.html"}},"callToolResult":{"content":[{"text":"ready","type":"text"}],"structuredContent":{"status":"ready"}}}

document.root.metadata is populated; documentToCallToolResult never reads it.

Expected

Either project AgentResultNode.metadata to CallToolResult._meta (the natural reading of the prop name), or document in docs/framework-mode.md that generated routes have no result _meta and what Agent.Result metadata is for.

Context

Hit while re-porting ScriptedAlchemy/movie-library (five generated servers + an MCP App) onto af1c185; the plugin now relies on listing _meta only and records the missing result _meta as a wire deviation from its previous defineOperation-based build.

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

    enhancementNew feature or requestruntimeRuntime context, state, rendering, or execution

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions