MCP Tool Adapter¶
The MCPToolAdapter discovers tools from connected MCP servers and converts
them into fully-typed LangChain BaseTool instances. It builds recursive
Pydantic models from MCP JSON Schemas -- preserving nested objects, arrays,
unions, $ref/$defs, enums, defaults, and descriptions -- so LLMs can
generate correct tool calls on the first attempt.
Source: src/promptise/mcp/client/_tool_adapter.py
Quick example¶
import asyncio
from promptise import MCPClient, MCPMultiClient, MCPToolAdapter, build_agent
async def main():
multi = MCPMultiClient({
"math": MCPClient(url="http://localhost:8080/mcp"),
})
async with multi:
adapter = MCPToolAdapter(multi)
tools = await adapter.as_langchain_tools()
# Use tools with build_agent
agent = await build_agent(
model="openai:gpt-5-mini",
servers={},
extra_tools=tools,
)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "What is 7 + 3?"}]}
)
print(result["messages"][-1].content)
asyncio.run(main())
Concepts¶
How MCPToolAdapter fits in¶
MCPToolAdapter is the recommended way to convert MCP tools into LangChain
BaseTool instances. It is backed by MCPMultiClient, uses persistent
connections for the entire agent lifetime, supports Bearer token and API key
authentication, and provides recursive schema conversion.
How it works¶
- Discovery --
as_langchain_tools()callsMCPMultiClient.list_tools()to fetch tool definitions from all connected servers. - Schema conversion -- Each tool's
inputSchema(a JSON Schema dict) is recursively converted to a PydanticBaseModelusing_jsonschema_to_pydantic. - Tool wrapping -- Each tool is wrapped in a
_PromptiseMCPTool(aBaseToolsubclass) that callsMCPMultiClient.call_tool()on invocation. - Result extraction -- The MCP
CallToolResultis converted to a plain string by concatenating all text content parts.
Creating an adapter¶
from promptise import MCPClient, MCPMultiClient, MCPToolAdapter
multi = MCPMultiClient({
"hr": MCPClient(url="http://hr-server:8080/mcp", bearer_token="..."),
"docs": MCPClient(url="http://docs-server:9090/mcp", api_key="secret"),
})
async with multi:
adapter = MCPToolAdapter(multi)
tools = await adapter.as_langchain_tools()
Getting LangChain tools¶
as_langchain_tools() returns a list of BaseTool instances ready for any
LangChain or LangGraph agent:
async with multi:
adapter = MCPToolAdapter(multi)
tools = await adapter.as_langchain_tools()
for tool in tools:
print(f"{tool.name}: {tool.description}")
print(f" Schema: {tool.args_schema.model_json_schema()}")
Each tool's args_schema is a dynamically-created Pydantic model that
preserves the full structure of the MCP server's JSON Schema, including:
- Nested objects become nested Pydantic models
- Arrays of objects become
list[NestedModel] $ref/$defsreferences are resolved and inlinedanyOf/oneOfbecomeUnion[...]orOptional[...]allOfproperties are merged- Enums become
Literal["a", "b", "c"] - Descriptions and defaults are preserved as Pydantic
Fieldmetadata
Tool introspection¶
Use list_tool_info() to get lightweight metadata about discovered tools
without creating BaseTool wrappers:
async with multi:
adapter = MCPToolAdapter(multi)
infos = await adapter.list_tool_info()
for info in infos:
print(f"{info.server_guess}/{info.name}")
print(f" {info.description}")
print(f" Schema keys: {list(info.input_schema.get('properties', {}).keys())}")
This returns a list of ToolInfo dataclass instances -- useful for debugging,
logging, or building tool registries.
Callback hooks¶
Attach optional callbacks for tracing every tool invocation:
def on_before(tool_name: str, arguments: dict) -> None:
print(f"[TRACE] Calling {tool_name}")
def on_after(tool_name: str, result) -> None:
print(f"[TRACE] {tool_name} completed")
def on_error(tool_name: str, exc: Exception) -> None:
print(f"[ERROR] {tool_name} failed: {exc}")
adapter = MCPToolAdapter(
multi,
on_before=on_before,
on_after=on_after,
on_error=on_error,
)
tools = await adapter.as_langchain_tools()
Callbacks are wrapped in contextlib.suppress(Exception) so a failing callback
never breaks the actual tool call.
Result extraction¶
MCP servers return CallToolResult objects containing a list of content items
(text, images, embedded resources). The adapter extracts all text parts and
joins them with newlines:
# What the MCP server returns:
# CallToolResult(content=[
# TextContent(type="text", text="Found 3 results"),
# TextContent(type="text", text="Result 1: ..."),
# ])
# What the LangChain tool returns:
# "Found 3 results\nResult 1: ..."
If the result has isError=True, the text is still returned so the LLM can
see the error message and decide how to proceed.
Error handling¶
When a tool call fails at the transport or protocol level, the adapter raises
MCPClientError:
from promptise.mcp.client import MCPClientError
try:
result = await tool.ainvoke({"query": "test"})
except MCPClientError as exc:
print(f"Tool call failed: {exc}")
API summary¶
MCPToolAdapter¶
| Parameter | Type | Default | Description |
|---|---|---|---|
multi |
MCPMultiClient |
required | Connected multi-server client |
on_before |
OnBefore \| None |
None |
Callback fired before each tool invocation |
on_after |
OnAfter \| None |
None |
Callback fired after each tool invocation |
on_error |
OnError \| None |
None |
Callback fired on tool errors |
Methods¶
| Method | Returns | Description |
|---|---|---|
as_langchain_tools() |
list[BaseTool] |
Discover tools and return as LangChain BaseTool instances |
list_tool_info() |
list[ToolInfo] |
Return tool metadata for introspection |
Callback types¶
| Type | Signature |
|---|---|
OnBefore |
Callable[[str, dict[str, Any]], None] |
OnAfter |
Callable[[str, Any], None] |
OnError |
Callable[[str, Exception], None] |
ToolInfo¶
| Attribute | Type | Description |
|---|---|---|
server_guess |
str |
Server name that owns the tool |
name |
str |
Tool name |
description |
str |
Human-readable description |
input_schema |
dict[str, Any] |
Raw JSON Schema for tool input |
Tips and gotchas¶
Tip
MCPToolAdapter uses persistent connections via MCPMultiClient. Create
the adapter once and reuse the tools for the entire agent session -- do not
recreate the adapter per invocation.
Warning
You must call as_langchain_tools() (or list_tool_info()) inside
the async with multi: block. The underlying MCPMultiClient must be
connected for tool discovery to work.
Tip
The recursive schema converter handles complex MCP schemas that would
otherwise appear as opaque dict parameters to the LLM. If your tool
call accuracy is low, inspect the generated args_schema to verify the
schema conversion is correct:
Warning
Callback hooks (on_before, on_after, on_error) are silenced on
failure -- they never propagate exceptions. If your callback raises,
the error is swallowed and the tool call proceeds normally. Use proper
error handling inside your callbacks.
Tip
For simpler setups, build_agent handles tool adaptation internally.
Use MCPToolAdapter directly when you need fine-grained control over
tool discovery, callbacks, or when integrating with custom LangChain
pipelines.
What's next¶
- MCP Client -- the
MCPClientandMCPMultiClientthat power the adapter - Tools & Schema Helpers -- the schema conversion logic shared with the legacy loader
- Building Agents -- use adapted tools with
build_agent