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)
- 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";
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
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