Use MCP tools in agents

In shortConnect 10xGraph agents to MCP servers and mix local Python tools with remote MCP tools in a single graph.

  • 7 min read
  • 14 sections
  • Updated
  • v0.10.0
  • Markdown

10xGraph integrates with Model Context Protocol (MCP) servers, allowing your agents to call tools exposed by remote MCP servers alongside local Python functions. The Model Context Protocol is an open standard for LLM tool integration, maintained by Anthropic, letting you compose tools from multiple sources seamlessly.

This guide covers connecting an MCP server, invoking its tools from a graph, testing with mock clients, and mixing MCP tools with local tools. After completing it, your agent will be able to execute both local and remote tools in the same workflow.

Prerequisites

Install the MCP extra to add fastmcp and mcp libraries:

Terminal
pip install "10xgraph[mcp,openai]"

The samples use gpt-4o and need OPENAI_API_KEY. The final example uses Google and needs pip install "10xgraph[mcp,google-genai]".

Step 1: Create an MCP client

The fastmcp.Client class connects to MCP servers and fetches their available tools. It supports three transport modes: a config dict (for multiple servers or HTTP), a local subprocess (stdio), or a remote URL. Choose the one that matches your server.

Using a config dict for multiple servers or HTTP

If your MCP server is accessed over HTTP or you need to configure multiple servers at once, use a dict with the server config:

Python
from fastmcp import Client

config = {
    "mcpServers": {
        "github": {
            "url": "https://api.githubcopilot.com/mcp/",
            "headers": {"Authorization": "Bearer YOUR_GITHUB_TOKEN"},
            "transport": "streamable-http",
        },
        "filesystem": {
            "command": "python",
            "args": ["mcp_filesystem_server.py"],
        },
    }
}

client = Client(config)

Using a stdio subprocess server

To run an MCP server as a subprocess on your machine, pass the path of the server script and fastmcp starts it over stdio:

Python
from fastmcp import Client

# Starts the server and communicates over stdio
client = Client("my_mcp_server.py")

Using a remote HTTP server

For an MCP server listening on HTTP, pass the URL directly:

Python
from fastmcp import Client

client = Client("https://my-mcp-server.example.com/mcp")

Step 2: Wire the MCP client into ToolNode

Create a ToolNode and pass the MCP client. The ToolNode queries the client for available tools each time the Agent prepares its tool list before calling the LLM. It pings the server first and offers no MCP tools if the ping fails.

Python
from tenxgraph.core.graph import ToolNode

tool_node = ToolNode(
    tools=[],       # no local tools, or add them here
    client=client,
)

The first argument tools is a list of local Python functions (or empty if you have none). The client is the MCP client created in the previous step.

Step 3: Build and wire the graph

The graph structure is identical to one using local tools. Create an Agent that uses the ToolNode, add it to the graph alongside a routing function, and compile:

Python
from tenxgraph.core.graph import StateGraph, Agent, ToolNode
from tenxgraph.core.state import AgentState, Message
from tenxgraph.utils import END

tool_node = ToolNode(tools=[], client=client)

agent = Agent(
    model="gpt-4o",
    system_prompt=[{"role": "system", "content": "You are a helpful assistant."}],
    tool_node=tool_node,
)

def should_use_tools(state: AgentState) -> str:
    """Route to tools if the agent requested them."""
    last = state.context[-1] if state.context else None
    if last and last.role == "assistant" and getattr(last, "tools_calls", None):
        return "tools"
    return END

graph = StateGraph()
graph.add_node("agent", agent)
graph.add_node("tools", tool_node)
graph.add_conditional_edges("agent", should_use_tools, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")
graph.set_entry_point("agent")

app = graph.compile()

Step 4: Invoke the agent

Call the compiled graph with a message. The agent will see the MCP tools available and call them when appropriate:

Python
result = app.invoke(
    {"messages": [Message.text_message("List the latest commits in owner/repo-name.")]},
    config={"thread_id": "mcp-demo-1"},
)
print(result["messages"][-1].content)

Verify that the agent used the MCP tools by checking the message history for ToolCallBlock and ToolResultBlock entries.

Mixing local and remote tools

You can register both local Python functions and MCP tools in the same ToolNode. The runtime automatically routes each tool call to the correct backend (local or remote):

Python
from tenxgraph.prebuilt.tools import safe_calculator

# Local function
def my_custom_tool(query: str) -> str:
    return f"Custom result: {query}"

# Both local and MCP tools available
tool_node = ToolNode(
    tools=[safe_calculator, my_custom_tool],
    client=client,
)

When the LLM requests a tool, ToolNode runs it on the MCP client if the name is one of the MCP tools it listed, otherwise it runs the local function. Avoid giving a local function and an MCP tool the same name: the MCP tool wins.

Forwarding user context to MCP

Many MCP servers need to know who is calling them (for access control, logging, or per-user state). Set pass_user_info_to_mcp=True on ToolNode to send the user dict from the execution config to the MCP server. It is added to the tool call arguments under the key user. If config has no user dict but has a user_id, the server receives {"user_id": ...} instead.

Python
tool_node = ToolNode(
    tools=[],
    client=client,
    pass_user_info_to_mcp=True,
)

The MCP tool receives it as an argument, so declare a user parameter on the server tool:

Python
# Code on the MCP server (fastmcp)
from fastmcp import FastMCP

mcp = FastMCP("secure")

@mcp.tool()
def secure_action(query: str, user: dict | None = None) -> str:
    user = user or {}
    if "admin" not in user.get("roles", []):
        raise PermissionError(f"User {user.get('id')} does not have permission")
    return f"Action completed by {user.get('id')}"

Pass the user dict in the invoke config. When requests go through the 10xGraph API server with authentication, the server sets config["user"] for you. When you call the graph directly, pass it yourself:

Python
result = app.invoke(
    {"messages": [Message.text_message("Do something.")]},
    config={
        "thread_id": "mcp-auth-1",
        "user": {"id": "user-123", "name": "Alice", "roles": ["admin"]},
    },
)

Filtering MCP tools by tag

MCP servers can tag their tools with metadata. If the server supports it, you can filter which tools are offered to the Agent using tools_tags on the Agent. MCP tools are matched against the server’s fastmcp tags:

Python
agent = Agent(
    model="gpt-4o",
    tool_node=tool_node,
    tools_tags={"read"},   # only expose tools tagged "read"
)

The Agent will only see MCP tools that share at least one of the specified tags. Local tools registered with @tool(tags=[...]) are filtered the same way; untagged local tools are always offered.

Testing MCP tools without a server

To test your graph with MCP tools without running an actual MCP server, use MockMCPClient from tenxgraph.qa.testing. It simulates tool execution and tracks calls for assertions:

Python
import pytest
from tenxgraph.qa.testing import MockMCPClient
from tenxgraph.core.graph import ToolNode, StateGraph, Agent
from tenxgraph.core.state import Message
from tenxgraph.utils import END

# Set up mock client
mock_client = MockMCPClient()
mock_client.add_tool(
    name="search",
    description="Search for information",
    parameters={"query": {"type": "string"}},
    handler=lambda query: f"Results for: {query}",
)

# Create ToolNode with mock client
tool_node = ToolNode(tools=[], client=mock_client)

# Build graph (same as before)
agent = Agent(
    model="gpt-4o",
    system_prompt=[{"role": "system", "content": "You are helpful."}],
    tool_node=tool_node,
)

def should_use_tools(state) -> str:
    last = state.context[-1] if state.context else None
    if last and last.role == "assistant" and getattr(last, "tools_calls", None):
        return "tools"
    return END

graph = StateGraph()
graph.add_node("agent", agent)
graph.add_node("tools", tool_node)
graph.add_conditional_edges("agent", should_use_tools, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")
graph.set_entry_point("agent")

app = graph.compile()

# Test the graph
result = app.invoke(
    {"messages": [Message.text_message("Search for AI trends.")]},
    config={"thread_id": "test-1"},
)

# Verify the tool was called
mock_client.assert_called("search")
assert mock_client.call_count("search") == 1

# Check the arguments
call = mock_client.get_last_call("search")
assert "AI trends" in call["arguments"]["query"]

MockMCPClient provides helper methods for testing:

  • add_tool(name, description, parameters, handler): Register a mock tool
  • was_called(name): Check if a tool was invoked
  • call_count(name): Get the number of times a tool was called
  • get_calls(name): Get all calls to a tool
  • get_last_call(name): Get the most recent call with its arguments
  • assert_called(name): Assert a tool was called (raises if not)
  • assert_called_with(name, **args): Assert a tool was called with specific arguments
  • reset(): Clear call history but keep tool registrations
  • clear(): Remove all tools and call history

Using ReactAgent with MCP

ReactAgent is a prebuilt agent that includes tool calling built-in. Pass the MCP client directly:

Python
from tenxgraph.prebuilt.agent import ReactAgent

agent = ReactAgent(
    model="gpt-4o",
    tools=[],
    client=client,
    pass_user_info_to_mcp=True,
)
app = agent.compile()

Invoke it the same way as the manual graph:

Python
result = app.invoke(
    {"messages": [Message.text_message("Search for the latest news.")]},
    config={"thread_id": "react-mcp-1"},
)

Complete example: GitHub MCP integration

This example builds a complete agent that uses the GitHub MCP server to read repository information:

Python
import os
from fastmcp import Client
from tenxgraph.core.graph import StateGraph, Agent, ToolNode
from tenxgraph.core.state import AgentState, Message
from tenxgraph.storage.checkpointer import InMemoryCheckpointer
from tenxgraph.utils import END

# Configure the GitHub MCP server
mcp_config = {
    "mcpServers": {
        "github": {
            "url": "https://api.githubcopilot.com/mcp/",
            "headers": {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"},
            "transport": "streamable-http",
        },
    }
}

client = Client(mcp_config)
tool_node = ToolNode(tools=[], client=client)

agent = Agent(
    model="gemini-2.0-flash",
    provider="google",
    system_prompt=[{"role": "system", "content": "You are a helpful GitHub assistant."}],
    tool_node=tool_node,
    trim_context=True,
)

def should_use_tools(state: AgentState) -> str:
    last = state.context[-1] if state.context else None
    if last and last.role == "assistant" and getattr(last, "tools_calls", None):
        return "tools"
    return END

graph = StateGraph()
graph.add_node("agent", agent)
graph.add_node("tools", tool_node)
graph.add_conditional_edges("agent", should_use_tools, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")
graph.set_entry_point("agent")

app = graph.compile(checkpointer=InMemoryCheckpointer())

result = app.invoke(
    {"messages": [Message.text_message("List the latest commits in the 10xgraph repo.")]},
    config={"thread_id": "github-1", "recursion_limit": 10},
)
print(result["messages"][-1].content)

What you learned

  • Install pip install "10xgraph[mcp]" to enable MCP support.
  • Create an MCP client with fastmcp.Client(config_dict), Client(stdio_command), or Client(http_url).
  • Pass the client to ToolNode(tools=[], client=client) to offer MCP tools to your agent.
  • Mix local and MCP tools in the same ToolNode; the runtime routes each call appropriately.
  • Forward user context to MCP servers with pass_user_info_to_mcp=True.
  • Filter MCP tools by tag with tools_tags on the Agent.
  • Test MCP tools without a real server using MockMCPClient.

Next steps

Frequently asked questions

Can I use MCP tools without a local Python function?
Yes. Pass an empty list to ToolNode(tools=[]) and provide the MCP client. The graph will offer only the MCP server's tools.
Do MCP tools run in parallel like local tools?
Yes. The LLM can request multiple MCP tools in one turn, and ToolNode executes them concurrently.
How do I test MCP tools without a real server?
Use MockMCPClient from tenxgraph.qa.testing. It simulates an MCP client and tracks tool calls for assertions.
Last updated for v0.10.0Edit this page on GitHubReport an issue