Port from MoonshotAI/kimi-code#2596/#2600: surface MCP tool-result _meta to the model, minus protocol-reserved keys

MCP tool results carry a server _meta mapping (exposed as .meta by the
Python SDK) alongside structuredContent. Servers return namespaced
machine-readable contracts there (validated payloads, browser-handoff
URLs); Hermes previously dropped the field entirely, so that data was
invisible to the agent.

Now _meta is included in the JSON tool output, after filtering
protocol-reserved keys per the MCP spec's key-name rules: a prefix is
reserved when a modelcontextprotocol or mcp label is followed by at
least one more label (modelcontextprotocol.io/..., tools.mcp.com/...).
Vendor namespaces with a trailing reserved word (com.example.mcp/...)
and unprefixed keys pass through. Non-serializable metadata drops the
extras rather than failing the call.
This commit is contained in:
Teknium
2026-08-06 19:14:54 -07:00
parent 8bbda8ff33
commit c031fec365
2 changed files with 173 additions and 7 deletions
+109 -1
View File
@@ -25,10 +25,12 @@ class _FakeCallToolResult:
MCP SDK Pydantic model (``mcp.types.CallToolResult``).
"""
def __init__(self, content, is_error=False, structuredContent=None):
def __init__(self, content, is_error=False, structuredContent=None, meta=None):
self.content = content
self.isError = is_error
self.structuredContent = structuredContent
# Real SDK exposes the wire ``_meta`` field as ``.meta`` (Pydantic alias).
self.meta = meta
def _fake_run_on_mcp_loop(coro_or_factory, timeout=30):
@@ -107,3 +109,109 @@ class TestStructuredContentPreservation:
raw = handler({})
data = json.loads(raw)
assert data["result"] == payload
class TestMetaPassthrough:
"""Server ``_meta`` is surfaced, minus protocol-reserved keys.
Ported from MoonshotAI/kimi-code#2596/#2600.
"""
def test_vendor_meta_passes_through(self, _patch_mcp_server):
session = _patch_mcp_server
session.call_tool = AsyncMock(
return_value=_FakeCallToolResult(
content=[_FakeContentBlock("done")],
meta={"com.example/handoff": {"url": "https://x"}},
)
)
handler = mcp_tool._make_tool_handler("test-server", "my-tool", 30.0)
data = json.loads(handler({}))
assert data["result"] == "done"
assert data["_meta"] == {"com.example/handoff": {"url": "https://x"}}
def test_reserved_meta_keys_dropped(self, _patch_mcp_server):
session = _patch_mcp_server
session.call_tool = AsyncMock(
return_value=_FakeCallToolResult(
content=[_FakeContentBlock("done")],
meta={
"modelcontextprotocol.io/progress": 1,
"tools.mcp.com/trace": "x",
"com.example.mcp/vendor": "keep", # trailing mcp label = vendor ns
"unprefixed": "keep",
},
)
)
handler = mcp_tool._make_tool_handler("test-server", "my-tool", 30.0)
data = json.loads(handler({}))
assert data["_meta"] == {
"com.example.mcp/vendor": "keep",
"unprefixed": "keep",
}
def test_all_reserved_meta_omits_field(self, _patch_mcp_server):
session = _patch_mcp_server
session.call_tool = AsyncMock(
return_value=_FakeCallToolResult(
content=[_FakeContentBlock("done")],
meta={"mcp.io/internal": True},
)
)
handler = mcp_tool._make_tool_handler("test-server", "my-tool", 30.0)
data = json.loads(handler({}))
assert data == {"result": "done"}
def test_meta_with_structured_content(self, _patch_mcp_server):
session = _patch_mcp_server
session.call_tool = AsyncMock(
return_value=_FakeCallToolResult(
content=[_FakeContentBlock("txt")],
structuredContent={"ok": True},
meta={"com.example/k": "v"},
)
)
handler = mcp_tool._make_tool_handler("test-server", "my-tool", 30.0)
data = json.loads(handler({}))
assert data == {
"result": "txt",
"structuredContent": {"ok": True},
"_meta": {"com.example/k": "v"},
}
def test_non_serializable_meta_dropped(self, _patch_mcp_server):
session = _patch_mcp_server
session.call_tool = AsyncMock(
return_value=_FakeCallToolResult(
content=[_FakeContentBlock("done")],
meta={"com.example/obj": object()},
)
)
handler = mcp_tool._make_tool_handler("test-server", "my-tool", 30.0)
data = json.loads(handler({}))
assert data == {"result": "done"}
def test_non_dict_meta_ignored(self, _patch_mcp_server):
session = _patch_mcp_server
session.call_tool = AsyncMock(
return_value=_FakeCallToolResult(
content=[_FakeContentBlock("done")],
meta="not-a-dict",
)
)
handler = mcp_tool._make_tool_handler("test-server", "my-tool", 30.0)
data = json.loads(handler({}))
assert data == {"result": "done"}
class TestReservedMetaKeyPredicate:
def test_reserved_prefixes(self):
assert mcp_tool._is_reserved_mcp_meta_key("modelcontextprotocol.io/x")
assert mcp_tool._is_reserved_mcp_meta_key("mcp.dev/x")
assert mcp_tool._is_reserved_mcp_meta_key("tools.mcp.com/x")
def test_vendor_and_unprefixed_not_reserved(self):
assert not mcp_tool._is_reserved_mcp_meta_key("com.example.mcp/x") # trailing label
assert not mcp_tool._is_reserved_mcp_meta_key("com.example/x")
assert not mcp_tool._is_reserved_mcp_meta_key("plain-key")
assert not mcp_tool._is_reserved_mcp_meta_key("/leading-slash")
+64 -6
View File
@@ -884,6 +884,39 @@ def _wrap_command_with_watchdog(command: str, args: list) -> tuple[str, list]:
# ---------------------------------------------------------------------------
def _is_reserved_mcp_meta_key(key: str) -> bool:
"""Return True if an MCP ``_meta`` key uses a protocol-reserved prefix.
Per the MCP spec's key-name rules, a prefix is reserved when a
``modelcontextprotocol`` or ``mcp`` label is followed by at least one
more label (``modelcontextprotocol.io/...``, ``tools.mcp.com/...``).
A trailing reserved word (``com.example.mcp/...``) is a legitimate
vendor namespace and passes through. Ported from
MoonshotAI/kimi-code#2600.
"""
slash = key.find("/")
if slash <= 0:
return False
labels = key[:slash].split(".")
return any(
label in ("modelcontextprotocol", "mcp") and i < len(labels) - 1
for i, label in enumerate(labels)
)
def _strip_reserved_meta_keys(meta) -> "Optional[Dict[str, Any]]":
"""Drop protocol-reserved keys from a tool result's ``_meta`` mapping.
Returns the filtered dict, or ``None`` when there is nothing
model-facing left (or the input wasn't a mapping).
"""
if not isinstance(meta, dict):
return None
out = {k: v for k, v in meta.items()
if isinstance(k, str) and not _is_reserved_mcp_meta_key(k)}
return out or None
def _mcp_image_extension_for_mime_type(mime_type: str) -> str:
"""Return a reasonable file extension for an MCP image MIME type."""
import mimetypes
@@ -5509,14 +5542,39 @@ def _make_tool_handler(server_name: str, tool_name: str, tool_timeout: float):
# MCP spec: content is model-oriented (text), structuredContent
# is machine-oriented (JSON metadata). For an AI agent, content
# is the primary payload; structuredContent supplements it.
#
# Server-level `_meta` is also surfaced (ported from
# MoonshotAI/kimi-code#2596): servers return namespaced metadata
# there (validated contracts, browser-handoff payloads, ...) that
# was previously invisible to the agent. Protocol-reserved keys
# are dropped first (kimi-code#2600) — per the MCP spec's key-name
# rules a prefix is reserved when a `modelcontextprotocol` or
# `mcp` label is followed by at least one more label (e.g.
# `modelcontextprotocol.io/...`, `tools.mcp.com/...`); those carry
# host/protocol plumbing, not model-facing data. Unprefixed and
# vendor-namespaced keys (`com.example.mcp/...`) pass through —
# their semantics belong to the server.
structured = getattr(result, "structuredContent", None)
if structured is not None:
meta = _strip_reserved_meta_keys(getattr(result, "meta", None))
if structured is not None or meta is not None:
payload: Dict[str, Any] = {}
if text_result:
return json.dumps({
"result": text_result,
"structuredContent": structured,
}, ensure_ascii=False)
return json.dumps({"result": structured}, ensure_ascii=False)
payload["result"] = text_result
if structured is not None:
if text_result:
payload["structuredContent"] = structured
else:
payload["result"] = structured
if meta is not None:
payload["_meta"] = meta
if "result" not in payload:
payload["result"] = text_result
try:
return json.dumps(payload, ensure_ascii=False)
except (TypeError, ValueError):
# Non-serializable metadata: drop the extras rather than
# failing the whole tool call.
return json.dumps({"result": text_result}, ensure_ascii=False)
return json.dumps({"result": text_result}, ensure_ascii=False)
def _call_once():