Skip to content

Merge MCP tool annotations and meta into LangChain metadata #284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion langchain_mcp_adapters/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,18 @@ async def call_tool(
call_tool_result = await session.call_tool(tool.name, arguments)
return _convert_call_tool_result(call_tool_result)

meta = tool.meta if hasattr(tool, "meta") else None
base = tool.annotations.model_dump() if tool.annotations is not None else {}
meta = {"_meta": meta} if meta is not None else {}
metadata = {**base, **meta} or None

return StructuredTool(
name=tool.name,
description=tool.description or "",
args_schema=tool.inputSchema,
coroutine=call_tool,
response_format="content_and_artifact",
metadata=tool.annotations.model_dump() if tool.annotations else None,
metadata=metadata,
)


Expand Down
69 changes: 69 additions & 0 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ImageContent,
TextContent,
TextResourceContents,
ToolAnnotations,
)
from mcp.types import Tool as MCPTool
from pydantic import BaseModel
Expand Down Expand Up @@ -454,3 +455,71 @@ def custom_httpx_client_factory(
# Expected to fail since server doesn't have SSE endpoint,
# but the important thing is that httpx_client_factory was passed correctly
pass


@pytest.mark.asyncio
async def test_convert_mcp_tool_metadata_variants():
"""Verify metadata merging rules in convert_mcp_tool_to_langchain_tool."""
tool_input_schema = {
"properties": {},
"required": [],
"title": "EmptySchema",
"type": "object",
}

session = AsyncMock()
session.call_tool.return_value = CallToolResult(
content=[TextContent(type="text", text="ok")], isError=False
)

mcp_tool_none = MCPTool(
name="t_none",
description="",
inputSchema=tool_input_schema,
)
lc_tool_none = convert_mcp_tool_to_langchain_tool(session, mcp_tool_none)
assert lc_tool_none.metadata is None

mcp_tool_ann = MCPTool(
name="t_ann",
description="",
inputSchema=tool_input_schema,
annotations=ToolAnnotations(
title="Title", readOnlyHint=True, idempotentHint=False
),
)
lc_tool_ann = convert_mcp_tool_to_langchain_tool(session, mcp_tool_ann)
assert lc_tool_ann.metadata == {
"title": "Title",
"readOnlyHint": True,
"idempotentHint": False,
"destructiveHint": None,
"openWorldHint": None,
}

mcp_tool_meta = MCPTool(
name="t_meta",
description="",
inputSchema=tool_input_schema,
meta={"source": "unit-test", "version": 1},
)
lc_tool_meta = convert_mcp_tool_to_langchain_tool(session, mcp_tool_meta)
assert lc_tool_meta.metadata == {"_meta": {"source": "unit-test", "version": 1}}

mcp_tool_both = MCPTool(
name="t_both",
description="",
inputSchema=tool_input_schema,
annotations=ToolAnnotations(title="Both"),
meta={"flag": True},
)

lc_tool_both = convert_mcp_tool_to_langchain_tool(session, mcp_tool_both)
assert lc_tool_both.metadata == {
"title": "Both",
"readOnlyHint": None,
"idempotentHint": None,
"destructiveHint": None,
"openWorldHint": None,
"_meta": {"flag": True},
}