Skip to content

Commit 3b514f0

Browse files
authored
merge MCP tool annotations and meta into LangChain metadata (#284)
Add support for merging MCP tool annotations and spec `meta` field into LangChain metadata. Meta information is stored under a `_meta` key to avoid conflicts while preserving annotation behavior. **Key changes:** - Merge `tool.annotations` and `tool.meta` into LangChain metadata - Store meta under `_meta` to prevent conflicts - Add tests covering all metadata merge scenarios
1 parent 8eac1a6 commit 3b514f0

File tree

2 files changed

+75
-1
lines changed

2 files changed

+75
-1
lines changed

langchain_mcp_adapters/tools.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,18 @@ async def call_tool(
136136
call_tool_result = await session.call_tool(tool.name, arguments)
137137
return _convert_call_tool_result(call_tool_result)
138138

139+
meta = tool.meta if hasattr(tool, "meta") else None
140+
base = tool.annotations.model_dump() if tool.annotations is not None else {}
141+
meta = {"_meta": meta} if meta is not None else {}
142+
metadata = {**base, **meta} or None
143+
139144
return StructuredTool(
140145
name=tool.name,
141146
description=tool.description or "",
142147
args_schema=tool.inputSchema,
143148
coroutine=call_tool,
144149
response_format="content_and_artifact",
145-
metadata=tool.annotations.model_dump() if tool.annotations else None,
150+
metadata=metadata,
146151
)
147152

148153

tests/test_tools.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ImageContent,
1212
TextContent,
1313
TextResourceContents,
14+
ToolAnnotations,
1415
)
1516
from mcp.types import Tool as MCPTool
1617
from pydantic import BaseModel
@@ -468,3 +469,71 @@ def custom_httpx_client_factory(
468469
# Expected to fail since server doesn't have SSE endpoint,
469470
# but the important thing is that httpx_client_factory was passed correctly
470471
pass
472+
473+
474+
@pytest.mark.asyncio
475+
async def test_convert_mcp_tool_metadata_variants():
476+
"""Verify metadata merging rules in convert_mcp_tool_to_langchain_tool."""
477+
tool_input_schema = {
478+
"properties": {},
479+
"required": [],
480+
"title": "EmptySchema",
481+
"type": "object",
482+
}
483+
484+
session = AsyncMock()
485+
session.call_tool.return_value = CallToolResult(
486+
content=[TextContent(type="text", text="ok")], isError=False
487+
)
488+
489+
mcp_tool_none = MCPTool(
490+
name="t_none",
491+
description="",
492+
inputSchema=tool_input_schema,
493+
)
494+
lc_tool_none = convert_mcp_tool_to_langchain_tool(session, mcp_tool_none)
495+
assert lc_tool_none.metadata is None
496+
497+
mcp_tool_ann = MCPTool(
498+
name="t_ann",
499+
description="",
500+
inputSchema=tool_input_schema,
501+
annotations=ToolAnnotations(
502+
title="Title", readOnlyHint=True, idempotentHint=False
503+
),
504+
)
505+
lc_tool_ann = convert_mcp_tool_to_langchain_tool(session, mcp_tool_ann)
506+
assert lc_tool_ann.metadata == {
507+
"title": "Title",
508+
"readOnlyHint": True,
509+
"idempotentHint": False,
510+
"destructiveHint": None,
511+
"openWorldHint": None,
512+
}
513+
514+
mcp_tool_meta = MCPTool(
515+
name="t_meta",
516+
description="",
517+
inputSchema=tool_input_schema,
518+
meta={"source": "unit-test", "version": 1},
519+
)
520+
lc_tool_meta = convert_mcp_tool_to_langchain_tool(session, mcp_tool_meta)
521+
assert lc_tool_meta.metadata == {"_meta": {"source": "unit-test", "version": 1}}
522+
523+
mcp_tool_both = MCPTool(
524+
name="t_both",
525+
description="",
526+
inputSchema=tool_input_schema,
527+
annotations=ToolAnnotations(title="Both"),
528+
meta={"flag": True},
529+
)
530+
531+
lc_tool_both = convert_mcp_tool_to_langchain_tool(session, mcp_tool_both)
532+
assert lc_tool_both.metadata == {
533+
"title": "Both",
534+
"readOnlyHint": None,
535+
"idempotentHint": None,
536+
"destructiveHint": None,
537+
"openWorldHint": None,
538+
"_meta": {"flag": True},
539+
}

0 commit comments

Comments
 (0)