Skip to content

plugin-mcp drops MCP tool _meta when decoding tools/list #1683

Description

@mapleeit

Summary

@executor-js/plugin-mcp silently drops each tool's MCP _meta object while decoding tools/list. Hosts that embed the plugin as their MCP client never see that field, even though the MCP spec reserves it as opaque metadata on Tool.

Observed on @executor-js/plugin-mcp@1.5.40; main still has the same ListedTool schema.

Spec

MCP Tool includes _meta as a reserved, implementation-defined map (Tools). Servers use it for host-only routing / policy hints that must not be interpreted by the model and must not be stuffed into the closed annotations set (title, readOnlyHint, destructiveHint, …).

Examples we see on the wire:

{
  "name": "time_get_current_time",
  "description": "Get the current time",
  "inputSchema": { "type": "object" },
  "_meta": {
    "serverName": "time",
    "shortDescription": "Current time",
    "defer_loading": false
  }
}

Where it is stripped

packages/plugins/mcp/src/sdk/manifest.ts decodes each list entry with a closed Effect Schema.Struct. Excess keys are ignored:

const ListedTool = Schema.Struct({
  name: Schema.String,
  description: Schema.optional(Schema.NullOr(Schema.String)),
  inputSchema: Schema.optional(Schema.Unknown),
  parameters: Schema.optional(Schema.Unknown),
  outputSchema: Schema.optional(Schema.Unknown),
  annotations: Schema.optional(McpToolAnnotations),
  // `_meta` is not declared, so decode drops it
});

extractManifestFromListToolsResult then copies only the declared fields onto McpToolManifestEntry, so _meta is gone before toToolDef.

Pagination is already opaque (ListToolsPage.tools is Schema.Array(Schema.Unknown)); the loss happens on the per-entry decode after pages are merged.

Why hosts cannot recover it

mcpPlugin() has no hook that exposes the raw tools/list result. connections.refresh() returns already-constructed Tool values. Wrapping the exported extractManifestFromListToolsResult does not help, because the plugin's internal callers import the local function.

Executor Tool / ToolDef also has no _meta field. The plugin already persists the real MCP tool name in the existing annotations.mcp stamp (toToolDef in packages/plugins/mcp/src/sdk/plugin.ts), which is the natural bag to carry opaque _meta as well.

Note: McpStampSchema / readStamp currently decode annotations.mcp as { toolName, upstream? } only. If stamp round-trips through that schema, _meta would be stripped a second time unless the stamp schema is extended too.

Suggested fix (small)

  1. Add _meta: Schema.optional(Schema.Unknown) to ListedTool and McpToolManifestEntry.
  2. Copy _meta in extractManifestFromListToolsResult.
  3. Stamp it onto annotations.mcp._meta in toToolDef (Executor Tool has no _meta field).
  4. Add _meta to McpStampSchema if stamps are re-decoded.

Happy to send a PR if this direction looks right.

Repro

import { extractManifestFromListToolsResult } from "@executor-js/plugin-mcp/core";

const meta = { serverName: "time", shortDescription: "Current time" };

const manifest = extractManifestFromListToolsResult({
  tools: [{
    name: "time_get_current_time",
    description: "Get the current time",
    inputSchema: { type: "object" },
    _meta: meta,
  }],
});

// expected: manifest.tools[0]._meta === meta
// actual:   `_meta` is missing

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions