Package: @agent-bundle/rsc-runtime (defineOperation, createRscMcpServer), observed at pkg.pr.new preview 0.0.0-preview-4e546516 (main @ 4e546516).
What we tried
MCP Apps hosts associate a tool with its widget through listing-level _meta: { ui: { resourceUri } } (the ext-apps registerAppTool convention); human-readable titles ride on the same listing. While porting a 42-tool plugin (movie-library) whose widget-bound tools all carry that metadata, there was nowhere to put either:
import { defineOperation } from '@agent-bundle/rsc-runtime/plugin';
const operation = defineOperation({
id: 'search',
inputSchema,
resultSchema,
execute,
render,
mcp: {
name: 'search',
server: 'movie-library',
description: 'Unified search across indexers',
readOnly: true,
// ✗ RscMcpDefinition has no slot for either of these:
// title: 'Search',
// _meta: { ui: { resourceUri: 'ui://movie-library/widget.html' } },
},
});
RscMcpDefinition is name / description / server / readOnly / destructive / idempotent / openWorld only, and defineOperation rebuilds the frozen mcp object key-by-key, so even a cast can't smuggle extras through. On the serving side, createRscMcpServer registers each tool with annotations / description / inputSchema only — while the underlying @modelcontextprotocol/server@2 registerTool accepts both title and _meta in its config.
Why it matters
Without listing-level _meta.ui.resourceUri the host never binds a tool to its widget, so widget-bound tools silently degrade to text-only. Any plugin that combines MCP Apps with the RSC operation pipeline currently cannot use createRscMcpServer at all. The lost title is milder but visible in every host that renders tool listings.
Current workaround
A custom server factory that mirrors createRscMcpServer, plus a module-level side table keyed by operation id and populated as operations are defined:
export const toolListingExtras = new Map<
string,
{ title?: string; _meta?: Readonly<Record<string, unknown>> }
>();
// … in the custom factory, per operation:
const extras = toolListingExtras.get(operation.id);
server.registerTool(
operation.mcp.name,
{
annotations: { /* same mapping createRscMcpServer applies */ },
description: operation.mcp.description,
inputSchema: operation.inputSchema,
...(extras?._meta === undefined ? {} : { _meta: extras._meta }),
...(extras?.title === undefined ? {} : { title: extras.title }),
},
async (input, context) => {
const result = await operation.execute(input, { signal: context.mcpReq.signal });
return lowerMcpResult(operation.render(result));
},
);
(Result-level _meta is fine — <Mcp.Result _meta={…}> already handles per-result metadata. This issue is only about the tool listing.)
Suggested fix direction
Add optional title?: string and _meta?: Readonly<Record<string, unknown>> to RscMcpDefinition, preserve them in defineOperation, and forward them from createRscMcpServer into registerTool. _meta presumably deserves the same JSON-shape validation the result lowering applies.
Relevant code: packages/rsc-runtime/src/operation.ts (RscMcpDefinition, defineOperation), packages/rsc-runtime/src/mcp-server.ts (createRscMcpServer).
Related: #42, #44 — sibling framework gaps surfaced by the same consumer port (movie-library on preview 4e546516).
Package:
@agent-bundle/rsc-runtime(defineOperation,createRscMcpServer), observed at pkg.pr.new preview0.0.0-preview-4e546516(main @4e546516).What we tried
MCP Apps hosts associate a tool with its widget through listing-level
_meta: { ui: { resourceUri } }(the ext-appsregisterAppToolconvention); human-readabletitles ride on the same listing. While porting a 42-tool plugin (movie-library) whose widget-bound tools all carry that metadata, there was nowhere to put either:RscMcpDefinitionisname/description/server/readOnly/destructive/idempotent/openWorldonly, anddefineOperationrebuilds the frozenmcpobject key-by-key, so even a cast can't smuggle extras through. On the serving side,createRscMcpServerregisters each tool withannotations/description/inputSchemaonly — while the underlying@modelcontextprotocol/server@2registerToolaccepts bothtitleand_metain its config.Why it matters
Without listing-level
_meta.ui.resourceUrithe host never binds a tool to its widget, so widget-bound tools silently degrade to text-only. Any plugin that combines MCP Apps with the RSC operation pipeline currently cannot usecreateRscMcpServerat all. The losttitleis milder but visible in every host that renders tool listings.Current workaround
A custom server factory that mirrors
createRscMcpServer, plus a module-level side table keyed by operation id and populated as operations are defined:(Result-level
_metais fine —<Mcp.Result _meta={…}>already handles per-result metadata. This issue is only about the tool listing.)Suggested fix direction
Add optional
title?: stringand_meta?: Readonly<Record<string, unknown>>toRscMcpDefinition, preserve them indefineOperation, and forward them fromcreateRscMcpServerintoregisterTool._metapresumably deserves the same JSON-shape validation the result lowering applies.Relevant code:
packages/rsc-runtime/src/operation.ts(RscMcpDefinition,defineOperation),packages/rsc-runtime/src/mcp-server.ts(createRscMcpServer).Related: #42, #44 — sibling framework gaps surfaced by the same consumer port (movie-library on preview
4e546516).