Using MCP Servers

This guide explains how to integrate Model Context Protocol (MCP) servers with python-agents to extend your agent’s capabilities with external tools and data sources.

What is MCP?

The Model Context Protocol (MCP) is a standardized protocol that allows AI applications to connect to external data sources and tools. MCP servers expose tools, resources, and prompts that your agents can use to interact with various systems, databases, APIs, and services.

By integrating MCP servers with python-agents, you can:

  • Access external data through MCP resources

  • Use specialized tools provided by MCP servers

  • Connect to multiple services simultaneously

  • Extend agent capabilities without writing custom tool functions

  • Leverage community MCP servers for common integrations

Understanding MCPClient

The MCPClient class provides the interface for connecting to MCP servers and making their tools available to your agents.

Key Features

  • Async-first design: Built on asyncio for efficient concurrent operations

  • Automatic tool discovery: Lists and registers available tools from the MCP server

  • Seamless integration: Works alongside regular Python function tools

  • Resource management: Handles connection lifecycle and cleanup

MCPClient Methods

class MCPClient

Client for connecting to and interacting with MCP servers.

__init__()

Initialize a new MCP client instance.

async connect_to_server(command: str, arguments: list[str])

Connect to an MCP server using stdio transport.

Parameters:
  • command (str) – The command to execute (e.g., “uv”, “python”, “node”)

  • arguments (list[str]) – List of arguments to pass to the command

The server is started as a subprocess, and communication happens via stdin/stdout.

async list_available_tools()

List all tools available from the connected MCP server.

Returns:

List of tool schemas in OpenAI function format

Return type:

list[dict]

async call_tool(tool_name: str, tool_args: dict[str, Any])

Call a tool on the MCP server.

Parameters:
  • tool_name (str) – Name of the tool to call

  • tool_args (dict[str, Any]) – Dictionary of arguments to pass to the tool

Returns:

Tool execution result

Return type:

Any

async cleanup()

Clean up resources and close the connection to the MCP server.

Always call this method when you’re done using the MCP client, preferably in a try/finally block.

Basic Usage

Simple MCP Integration

Here’s a minimal example of connecting to an MCP server:

import asyncio
from python_agents.client import MCPClient, LLMClient

async def main():
    # Create and connect MCP client
    mcp_client = MCPClient()
    await mcp_client.connect_to_server(
        "uv",
        ["run", "/path/to/mcp-server/main.py", "--transport", "stdio"]
    )

    # Create LLM client and register MCP server
    client = LLMClient("openai/gpt-4-turbo")
    client.add_mcp_server(mcp_client)

    # Use the client - MCP tools are now available
    try:
        response = await client.invoke("Use the MCP tools to get information")
        print(response.message.content)
    finally:
        await mcp_client.cleanup()

asyncio.run(main())

Using Cleanup with LLMClient

The LLMClient has a built-in cleanup method that handles all registered MCP servers:

async def main():
    client = LLMClient("openai/gpt-4-turbo")

    # Connect to MCP server
    mcp_client = MCPClient()
    await mcp_client.connect_to_server(
        "python",
        ["/path/to/mcp-server.py"]
    )
    client.add_mcp_server(mcp_client)

    try:
        response = await client.invoke("Your query here")
        print(response.message.content)
    finally:
        # This automatically cleans up all MCP servers
        await client.cleanup()

asyncio.run(main())

Connecting to Different MCP Servers

Python MCP Servers

For MCP servers written in Python:

mcp_client = MCPClient()
await mcp_client.connect_to_server(
    "python",
    ["/path/to/server.py", "--transport", "stdio"]
)

Using uv to Run Python Servers

If your MCP server uses uv for dependency management:

mcp_client = MCPClient()
await mcp_client.connect_to_server(
    "uv",
    [
        "run",
        "/path/to/mcp-server/main.py",
        "--transport",
        "stdio"
    ]
)

Node.js MCP Servers

For MCP servers written in JavaScript/TypeScript:

mcp_client = MCPClient()
await mcp_client.connect_to_server(
    "node",
    ["/path/to/server.js"]
)

Complete Example: Agent with MCP Server

This example demonstrates using an MCP server with a ReactAgent for multi-step tasks:

import os
import asyncio
import random

from python_agents.client import MCPClient, LLMClient
from python_agents.agents import ReactAgent
from tavily import TavilyClient
from dotenv import load_dotenv

load_dotenv()


def search_internet(query: str):
    """Search the internet for the given query.

    :param query: Query to search the internet for.
    :return: Response to your query
    """
    tavily_client = TavilyClient(api_key=os.environ.get("TAVILY_API_KEY"))
    response = tavily_client.search(query)
    return response


def get_random_number() -> int:
    """Return a random number between 1 and 100.

    :return: random number (integer)
    """
    return random.randint(0, 100)


def calculator(operation: str, a: int, b: int):
    """Perform operation on parameters a and b.

    :param operation: string representing the operation. Supported operations: '+', '-', '*', '/'.
    :param a: First operand.
    :param b: Second operand.
    :return: result of the operation.
    """
    if operation == "+":
        return a + b
    elif operation == "-":
        return a - b
    elif operation == "*":
        return a * b
    elif operation == "/":
        return a / b
    else:
        return "Wrong operation!"


async def run_agent_with_mcp():
    # Initialize LLM client
    client = LLMClient("anthropic/claude-sonnet-4.5")

    # Add regular Python function tools
    client.add_tool(calculator)
    client.add_tool(get_random_number)
    client.add_tool(search_internet)

    # Connect to MCP server
    mcp_client = MCPClient()
    await mcp_client.connect_to_server(
        "uv",
        [
            "run",
            "/home/michael/projects/iam-mcp-server/main.py",
            "--transport",
            "stdio"
        ]
    )

    # Register MCP server with the client
    client.add_mcp_server(mcp_client)

    # Create ReactAgent
    agent = ReactAgent(client)

    try:
        # Run a complex task that uses both regular tools and MCP tools
        response = await agent.run(
            "Get property information for ID 231852. If there are any rental "
            "candidates, check their financial information to see if they qualify.",
            verbose=True
        )
        print(f"\nAgent final answer: {response}")
    finally:
        # Clean up all resources
        await client.cleanup()


if __name__ == "__main__":
    asyncio.run(run_agent_with_mcp())

Note

In this example, the agent has access to both:

  • Regular Python tools (calculator, get_random_number, search_internet)

  • MCP server tools (property management tools from the IAM server)

The LLM can use any available tool to complete the task.

Working with Multiple MCP Servers

You can connect to multiple MCP servers simultaneously:

async def multi_server_example():
    client = LLMClient("openai/gpt-4-turbo")

    # Connect to first MCP server (e.g., database access)
    db_mcp = MCPClient()
    await db_mcp.connect_to_server(
        "python",
        ["/path/to/database-mcp-server.py"]
    )
    client.add_mcp_server(db_mcp)

    # Connect to second MCP server (e.g., file system access)
    fs_mcp = MCPClient()
    await fs_mcp.connect_to_server(
        "python",
        ["/path/to/filesystem-mcp-server.py"]
    )
    client.add_mcp_server(fs_mcp)

    # Connect to third MCP server (e.g., API integration)
    api_mcp = MCPClient()
    await api_mcp.connect_to_server(
        "node",
        ["/path/to/api-mcp-server.js"]
    )
    client.add_mcp_server(api_mcp)

    try:
        # Agent now has access to tools from all three MCP servers
        agent = ReactAgent(client)
        response = await agent.run(
            "Query the database, process the files, and sync with the API",
            verbose=True
        )
        print(response)
    finally:
        # Cleanup handles all MCP servers automatically
        await client.cleanup()

asyncio.run(multi_server_example())

Listing Available Tools

You can inspect what tools an MCP server provides:

async def list_tools_example():
    mcp_client = MCPClient()
    await mcp_client.connect_to_server(
        "python",
        ["/path/to/server.py"]
    )

    # Get list of available tools
    tools = await mcp_client.list_available_tools()

    print("Available MCP tools:")
    for tool in tools:
        print(f"  - {tool['function']['name']}: {tool['function']['description']}")

    await mcp_client.cleanup()

asyncio.run(list_tools_example())

Best Practices

Always Use Cleanup

ALWAYS clean up MCP clients when done to prevent resource leaks:

# Option 1: Use try/finally
try:
    response = await client.invoke("query")
finally:
    await client.cleanup()

# Option 2: If using LLMClient, cleanup handles all MCP servers
try:
    response = await agent.run("task")
finally:
    await client.cleanup()  # Cleans up all registered MCP servers

Error Handling

Handle connection errors gracefully:

async def safe_mcp_connection():
    mcp_client = MCPClient()

    try:
        await mcp_client.connect_to_server(
            "python",
            ["/path/to/server.py"]
        )

        client = LLMClient("openai/gpt-4-turbo")
        client.add_mcp_server(mcp_client)

        # Use the client
        response = await client.invoke("query")
        return response

    except FileNotFoundError:
        print("Error: MCP server script not found")
    except Exception as e:
        print(f"Error connecting to MCP server: {e}")
    finally:
        await mcp_client.cleanup()

Environment Variables

Use environment variables for server paths:

import os
from dotenv import load_dotenv

load_dotenv()

async def main():
    mcp_client = MCPClient()
    server_path = os.environ.get("MCP_SERVER_PATH")

    if not server_path:
        raise ValueError("MCP_SERVER_PATH environment variable not set")

    await mcp_client.connect_to_server(
        "python",
        [server_path, "--transport", "stdio"]
    )

    # Continue with setup...

Combining Tools and MCP Servers

Design your application to leverage both regular Python tools and MCP servers:

  • Use Python tools for: Simple logic, calculations, text processing

  • Use MCP servers for: External integrations, database access, complex APIs

  • Combine both: Let the LLM choose the right tool for each subtask

# Simple local tools
client.add_tool(calculator)
client.add_tool(format_text)

# Complex external integrations via MCP
client.add_mcp_server(database_mcp)
client.add_mcp_server(api_mcp)

# LLM chooses the appropriate tool for each step

Troubleshooting

MCP Server Won’t Start

If the MCP server fails to start:

  1. Check the command path: Ensure the command (“python”, “node”, “uv”) is in your PATH

  2. Verify script path: Make sure the server script path is absolute and correct

  3. Check permissions: Ensure the script is executable

  4. Review server logs: Check if the MCP server is logging errors

# Use absolute paths
await mcp_client.connect_to_server(
    "python",
    ["/absolute/path/to/server.py"]
)

Connection Timeout

If the connection times out:

  • Ensure the MCP server implements the stdio transport protocol correctly

  • Check that the server responds to initialization messages

  • Verify there are no startup errors in the server

Tools Not Appearing

If MCP tools aren’t available to the LLM:

  1. Verify registration: Ensure you called client.add_mcp_server(mcp_client)

  2. Check tool listing: Use await mcp_client.list_available_tools() to verify

  3. Test server independently: Run the MCP server directly to ensure it works

# Debug: List tools before using
tools = await mcp_client.list_available_tools()
print(f"Found {len(tools)} tools from MCP server")
for tool in tools:
    print(f"  - {tool['function']['name']}")

Common Patterns

Lazy MCP Connection

Connect to MCP server only when needed:

class LazyMCPClient:
    def __init__(self, command, args):
        self.command = command
        self.args = args
        self.mcp_client = None

    async def get_client(self):
        if self.mcp_client is None:
            self.mcp_client = MCPClient()
            await self.mcp_client.connect_to_server(
                self.command,
                self.args
            )
        return self.mcp_client

    async def cleanup(self):
        if self.mcp_client is not None:
            await self.mcp_client.cleanup()

Reusable MCP Configuration

Create a configuration system for MCP servers:

from dataclasses import dataclass

@dataclass
class MCPServerConfig:
    name: str
    command: str
    args: list[str]


async def setup_mcp_servers(client: LLMClient, configs: list[MCPServerConfig]):
    """Set up multiple MCP servers from configuration."""
    mcp_clients = []

    for config in configs:
        mcp_client = MCPClient()
        await mcp_client.connect_to_server(config.command, config.args)
        client.add_mcp_server(mcp_client)
        mcp_clients.append(mcp_client)
        print(f"Connected to MCP server: {config.name}")

    return mcp_clients


# Usage
async def main():
    client = LLMClient("openai/gpt-4-turbo")

    server_configs = [
        MCPServerConfig(
            name="Database",
            command="python",
            args=["/path/to/db-server.py"]
        ),
        MCPServerConfig(
            name="API",
            command="node",
            args=["/path/to/api-server.js"]
        ),
    ]

    mcp_clients = await setup_mcp_servers(client, server_configs)

    try:
        # Use the client
        response = await client.invoke("query")
    finally:
        await client.cleanup()

Next Steps

Additional Resources