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:
constListedTool=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)
- Add
_meta: Schema.optional(Schema.Unknown) to ListedTool and McpToolManifestEntry. - Copy
_meta in extractManifestFromListToolsResult. - Stamp it onto
annotations.mcp._meta in toToolDef (Executor Tool has no _meta field). - 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";constmeta={serverName: "time",shortDescription: "Current time"};constmanifest=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
Summary
@executor-js/plugin-mcpsilently drops each tool's MCP_metaobject while decodingtools/list. Hosts that embed the plugin as their MCP client never see that field, even though the MCP spec reserves it as opaque metadata onTool.Observed on
@executor-js/plugin-mcp@1.5.40;mainstill has the sameListedToolschema.Spec
MCP
Toolincludes_metaas 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 closedannotationsset (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.tsdecodes each list entry with a closed EffectSchema.Struct. Excess keys are ignored:extractManifestFromListToolsResultthen copies only the declared fields ontoMcpToolManifestEntry, so_metais gone beforetoToolDef.Pagination is already opaque (
ListToolsPage.toolsisSchema.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 rawtools/listresult.connections.refresh()returns already-constructedToolvalues. Wrapping the exportedextractManifestFromListToolsResultdoes not help, because the plugin's internal callers import the local function.Executor
Tool/ToolDefalso has no_metafield. The plugin already persists the real MCP tool name in the existingannotations.mcpstamp (toToolDefinpackages/plugins/mcp/src/sdk/plugin.ts), which is the natural bag to carry opaque_metaas well.Note:
McpStampSchema/readStampcurrently decodeannotations.mcpas{ toolName, upstream? }only. If stamp round-trips through that schema,_metawould be stripped a second time unless the stamp schema is extended too.Suggested fix (small)
_meta: Schema.optional(Schema.Unknown)toListedToolandMcpToolManifestEntry._metainextractManifestFromListToolsResult.annotations.mcp._metaintoToolDef(ExecutorToolhas no_metafield)._metatoMcpStampSchemaif stamps are re-decoded.Happy to send a PR if this direction looks right.
Repro