MCP Tools in a Python AI Agent with 10xGraph
In shortConnect a 10xGraph ReactAgent to local and remote MCP servers over streamable HTTP, use several servers at once, and send auth headers in Python.
- 6 min read
- 9 sections
- Updated
- v1.0.0rc4
- Markdown
MCP (Model Context Protocol) servers expose tools over a network, so an agent can use them without you writing Python tool functions. This example runs a small FastMCP weather server on your machine and points a 10xGraph ReactAgent at two copies of it. The agent uses Google gemini-2.5-flash. Two more pages cover hosted servers that need a token: GitHub MCP and Xquik MCP.
What you will build
A ReactAgent whose tools come from MCP servers instead of Python functions. local/ runs a FastMCP weather server and shows an agent that uses two of them at once. The GitHub and Xquik examples connect to hosted servers that need a token.
All of them share one pattern: describe the servers in an mcpServers dict, wrap it in a fastmcp Client, and pass the client to ReactAgent.
Key concepts
fastmcp.Client(config): the MCP client. The config lists servers by name withurl,transportand optionalheaders.ReactAgent(client=...): builds aToolNodearound the client and wires the model and tool loop.ToolNode.all_tools(): lists the tool schemas the client discovered.pass_user_info_to_mcp: forwards the caller identity to the server as request metadata (meta).- Tool name prefixes: with several servers, each tool is named
<server>_<tool>.
Run it
git clone https://github.com/10xGraph/10xGraph.git
cd 10xGraph/agentflow
pip install "10xgraph[mcp,google-genai]" python-dotenv
export GEMINI_API_KEY=...Start the weather server in one terminal, then the check script in another:
python examples/mcp/local/server.py
python examples/mcp/local/raw_client.py # check the server, no LLMFor the two-server agent:
python examples/mcp/local/server.py --name us --port 8000
python examples/mcp/local/server.py --name eu --port 8001
python examples/mcp/local/agent.pyHow the code works
local/server.py
The server builds a FastMCP instance and registers tools with a decorator.
@mcp.tool(
description="Get the current weather for a location",
tags={"weather", "information"},
)
def get_weather(location: str, ctx: Context) -> dict:
# ToolNode sends the caller as request metadata under the key "user".
meta = ctx.request_context.meta
user = (meta.model_extra or {}).get("user") if meta else None
print(f"[{name}] weather for {location}, caller: {user}")
return {"location": location, "temperature": "22C", "description": f"Sunny ({name})"}The ctx: Context parameter is injected by FastMCP and is not part of the schema the model sees. With pass_user_info_to_mcp=True, the ToolNode sends the user dict as request metadata (meta) under the key user. The tool reads it from ctx.request_context.meta, an object that keeps extra fields in model_extra. The model never sees or sets it.
build_server(args.name).run(transport="streamable-http", host="127.0.0.1", port=args.port)--name and --port let you start several copies. Each one has its own port, which the agent needs.
local/raw_client.py
This script talks to the server with no LLM. Run it first when a server misbehaves.
async with Client(config) as client:
for tool in await client.list_tools():
tags = (tool.meta or {}).get("_fastmcp", {}).get("tags", [])
print(f"tool: {tool.name} tags={tags}")
print(" schema:", tool.inputSchema)
result = await client.call_tool("get_weather", {"location": "New York"})
print("data:", result.data)list_tools() shows what the model would see. call_tool() runs one tool directly. The config also carries an example Authorization header, which remote servers usually want.
local/agent.py
The agent config lists two servers under different keys and URLs.
mcp_config = {
"mcpServers": {
"us": {"url": "http://127.0.0.1:8000/mcp", "transport": "streamable-http"},
"eu": {"url": "http://127.0.0.1:8001/mcp", "transport": "streamable-http"},
}
}With more than one server, fastmcp prefixes every tool with its key. The model sees us_get_weather and eu_get_weather. Each server needs its own URL, or you only duplicate tools.
agent = ReactAgent(
model="gemini-2.5-flash",
system_prompt=[
{"role": "system", "content": "Answer weather questions with the available tools."}
],
client=Client(mcp_config),
# Sends config["user"] (or {"user_id": ...}) with every MCP call as request metadata (meta["user"]).
pass_user_info_to_mcp=True,
)client=Client(mcp_config) replaces the tools=[...] list. ReactAgent wraps the client in a ToolNode and discovers the tools when it first runs.
result = await app.ainvoke(
{"messages": [Message.text_message("Compare the weather in New York and Berlin.")]},
config={"thread_id": "mcp-local", "user_id": "alice", "recursion_limit": 10},
)The user_id in the run config is what reaches the server in the request metadata (meta["user"]). recursion_limit caps the tool loop at 10 steps.
Remote servers with authentication
For a hosted server, put the credential in headers. The GitHub and Xquik pages show two real cases.
Full code
local/server.py
"""A small FastMCP weather server over streamable HTTP.
Run it in its own terminal first:
python examples/mcp/local/server.py # port 8000
python examples/mcp/local/server.py --port 8001 --name eu # a second server
"""
import argparse
from fastmcp import Context, FastMCP
def build_server(name: str) -> FastMCP:
mcp = FastMCP(f"{name} weather server")
# With pass_user_info_to_mcp=True (see agent.py) the ToolNode sends the caller as request
# metadata. A `Context` parameter is injected by FastMCP and never shows in the tool
# schema the LLM sees, so the model cannot invent or change the caller.
@mcp.tool(
description="Get the current weather for a location",
tags={"weather", "information"},
)
def get_weather(location: str, ctx: Context) -> dict:
# ToolNode sends the caller as request metadata under the key "user".
meta = ctx.request_context.meta
user = (meta.model_extra or {}).get("user") if meta else None
print(f"[{name}] weather for {location}, caller: {user}")
return {"location": location, "temperature": "22C", "description": f"Sunny ({name})"}
@mcp.tool(description="Get a 3-day forecast for a location")
def get_forecast(location: str) -> list[dict]:
return [{"day": d, "temperature": f"{20 + d}C", "description": "Clear"} for d in range(3)]
return mcp
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--name", default="us")
parser.add_argument("--port", type=int, default=8000)
args = parser.parse_args()
build_server(args.name).run(transport="streamable-http", host="127.0.0.1", port=args.port)local/raw_client.py
"""Talk to an MCP server directly: list its tools and call one, with no LLM involved.
Useful to check a server before giving it to an agent.
Start `python examples/mcp/local/server.py` first, then run this file.
"""
import asyncio
from dotenv import load_dotenv
from fastmcp import Client
load_dotenv()
config = {
"mcpServers": {
"weather": {
"url": "http://127.0.0.1:8000/mcp",
"transport": "streamable-http",
# Remote servers usually want a header like this one.
"headers": {"Authorization": "Bearer TEST_WEATHER_API_KEY"},
},
},
}
async def main() -> None:
async with Client(config) as client:
for tool in await client.list_tools():
tags = (tool.meta or {}).get("_fastmcp", {}).get("tags", [])
print(f"tool: {tool.name} tags={tags}")
print(" schema:", tool.inputSchema)
result = await client.call_tool("get_weather", {"location": "New York"})
print("data:", result.data)
if __name__ == "__main__":
asyncio.run(main())local/agent.py
"""A ReactAgent that uses tools from two local MCP servers.
Start two servers first (separate terminals):
python examples/mcp/local/server.py --name us --port 8000
python examples/mcp/local/server.py --name eu --port 8001
Then: GEMINI_API_KEY=... python examples/mcp/local/agent.py
"""
import asyncio
from dotenv import load_dotenv
from fastmcp import Client
from tenxgraph.core.state import Message
from tenxgraph.prebuilt.agent import ReactAgent
load_dotenv()
# Each server needs its own URL. With more than one server, fastmcp prefixes every tool
# with the server key, so the agent sees `us_get_weather` and `eu_get_weather`
# (a single-server config keeps the plain name).
mcp_config = {
"mcpServers": {
"us": {"url": "http://127.0.0.1:8000/mcp", "transport": "streamable-http"},
"eu": {"url": "http://127.0.0.1:8001/mcp", "transport": "streamable-http"},
}
}
agent = ReactAgent(
model="gemini-2.5-flash",
system_prompt=[
{"role": "system", "content": "Answer weather questions with the available tools."}
],
client=Client(mcp_config),
# Sends config["user"] (or {"user_id": ...}) with every MCP call as request metadata (meta["user"]).
pass_user_info_to_mcp=True,
)
async def main() -> None:
app = agent.compile()
result = await app.ainvoke(
{"messages": [Message.text_message("Compare the weather in New York and Berlin.")]},
config={"thread_id": "mcp-local", "user_id": "alice", "recursion_limit": 10},
)
print("assistant:", result["messages"][-1].text())
await app.aclose()
if __name__ == "__main__":
asyncio.run(main())Source on GitHub: examples/mcp
Expected output
Illustrative, from agent.py:
[us] weather for New York, caller: {'user_id': 'alice'}
[eu] weather for Berlin, caller: {'user_id': 'alice'}
assistant: New York is 22C and sunny. Berlin is 22C and sunny.Using a different model
The model is a string in the code. Change gemini-2.5-flash to gpt-5-mini (pip install "10xgraph[mcp,openai]") or claude-sonnet-5-5 (pip install "10xgraph[mcp,anthropic]") and set the matching API key. The MCP setup does not change.
Next steps
- GitHub MCP: a hosted server with a bearer token.
- Xquik MCP: a hosted server with an API key header.
- Tools for tools written as Python functions.
- Providers for the model options.
- Dependency injection for passing your own services to tools.
Frequently asked questions
- How do I give a 10xGraph agent tools from an MCP server?
- Build a fastmcp Client from an mcpServers config and pass it as ReactAgent(client=...). The agent discovers the server's tools at run time, so you do not write tool functions.
- What happens to tool names when I connect more than one MCP server?
- fastmcp prefixes each tool with its server key, for example us_get_weather and eu_get_weather. A config with a single server keeps the original tool name.
- How do I authenticate to a remote MCP server?
- Add a headers entry to that server in the mcpServers config, such as an Authorization bearer token read from an environment variable.
- How does the MCP server know which user made the call?
- Set pass_user_info_to_mcp=True on ReactAgent or ToolNode. The ToolNode adds config["user"], or a user_id dict when only user_id is set, as request metadata (`meta`) on each MCP tool call, not as a tool argument.