Skip to content

createRscMcpServer drops tool listing title and _meta (breaks MCP Apps widget binding via _meta.ui.resourceUri) #43

Description

@ScriptedAlchemy

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).

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 request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions