Post

Chapter 6 - Model Context Protocol (MCP): The Universal Adapter for Tools

This article is part of my book Tinib00k: OpenAI Agents SDK, which explores the OpenAI Agents SDK, its primitives, and patterns for building agentic AI systems. All of the chapters can be found here and the code is available on Github. The book is available for free online and as a PDF eBook. If you find this content valuable, please consider supporting my work by purchasing the eBook (or download it for free!) or sharing it with others. For any issues around the book, contact me on LinkedIn

In the preceding chapters, we’ve explored how to empower agents by giving them tools, primarily through the @function_tool decorator. This is incredibly powerful for exposing your own Python code, but what about the vast ecosystem of external tools and data sources? How can we connect our agents to them without writing custom wrapper code for every single one?

The answer lies in the Model Context Protocol (MCP). MCP is an open standard designed to be the “USB-C port for AI.” Just as USB-C provides a universal way to connect peripherals to a device, MCP provides a standardized way for LLMs to connect to and interact with external tools and data sources.

The Agents SDK has first-class support for MCP, allowing you to seamlessly integrate any MCP-compliant server as a set of tools for your agents. This chapter will delve into the types of MCP servers, how to connect to them, and how the SDK handles the protocol details for you automatically.

MCP in the Agents SDK

Integrating an MCP server into your agent is remarkably simple. The Agent class accepts an mcp_servers parameter, which takes a list of server instances.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#
# Basic MCP integration
#
from agents import Agent
from agents.mcp import MCPServerStdio # Example server type

# Assume mcp_server is an instantiated and connected server object
mcp_server = MCPServerStdio(...)

data_agent = Agent(
    name="Data Agent",
    instructions="Use your tools to answer questions about the provided data.",
    model="litellm/gemini/gemini-1.5-flash-latest",
    mcp_servers=[mcp_server] # Simply pass the server instance
)

When you run an agent configured this way, the Runner automatically performs two key actions:

  1. Tool Discovery: Before calling the LLM, it communicates with each MCP server to get a list of available tools (list_tools).

  2. Tool Execution: If the LLM decides to use one of the discovered tools, the SDK communicates with the appropriate server to execute it (call_tool) and relays the result back to the LLM.

This means you can plug in complex, third-party tool servers without writing any of the protocol-handling logic yourself.

Connecting to MCP Servers

The MCP specification defines several transport mechanisms, and the SDK provides a client class for each. The main difference between them is how you connect: locally via a subprocess or remotely via a URL.

Local Servers with MCPServerStdio

This is the most common type for tools that need to run locally, such as those interacting with your machine’s filesystem or a local git repository. The server runs as a subprocess, and the SDK communicates with it over its standard input/output (stdin/stdout).

You configure it with the command needed to start the server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from agents.mcp import MCPServerStdio

# This configuration will run the official MCP filesystem server
# using npx, pointing it to a specific directory.
filesystem_server = MCPServerStdio(
    params={
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-filesystem", "./sample_files"],
    }
)

# The server must be connected, ideally using an async context manager
# async with filesystem_server as server:
#     # ... run agent ...

Remote Servers with MCPServerSse and MCPServerStreamableHttp

These server types are for connecting to tools hosted on a remote server. You connect via a URL.

  • MCPServerSse: Uses HTTP with Server-Sent Events (SSE), a common web standard for streaming.
  • MCPServerStreamableHttp: Uses a newer, MCP-specific HTTP transport.

The configuration is similar for both, primarily requiring a URL.

1
2
3
4
5
6
7
8
9
from agents.mcp import MCPServerSse

# Configuration for a hypothetical remote weather tool server
remote_weather_server = MCPServerSse(
    params={
        "url": "https://api.weather-tools.com/mcp/sse",
        "headers": {"x-api-key": "your-remote-tool-api-key"}
    }
)

A Complete Example: The Filesystem Server

Let’s walk through a complete, runnable example using the official MCP filesystem server to give our agent the ability to read local files.

Prerequisite: npx

This example uses npx, which is part of the Node.js ecosystem, to run the MCP server. If you don’t have it, please install Node.js.

First, let’s set up a directory with some sample files for our agent to read.

sample_files/favorite_cities.txt:

1
2
3
4
- In the summer, I love visiting London.
- In the winter, Tokyo is great.
- In the spring, San Francisco.
- In the fall, New York is the best.

Now, let’s write the Python code to orchestrate this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import asyncio
import os
import shutil
from agents import Agent, Runner, trace, ModelSettings
from agents.mcp import MCPServerStdio

from tinib00k.utils import DEFAULT_LLM, load_and_check_keys
load_and_check_keys()

async def main():
    if not shutil.which("npx"):
        raise RuntimeError("npx is not installed. Please install it.")

    # 1. Define the path to our sample files
    project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
    samples_dir_relative_path = os.path.relpath(
        os.path.join(os.path.dirname(__file__), "sample_files"),
        project_root
    )

    # Dynamically list the files to include in the prompt
    available_files = [f for f in os.listdir(samples_dir_relative_path) if os.path.isfile(os.path.join(samples_dir_relative_path, f))]
    file_list_str = ", ".join(f"`{file}`" for file in available_files)

    # 2. Instantiate and connect to the MCP server
    # We explicitly tell the agent what files it can access.
    PROMPT_WITH_CONTEXT = f"""
    You are a file assistant. Use your tools to answer questions.

    IMPORTANT: The file server's root is the main project directory. To access
    the necessary files, you MUST use the following full relative path prefix:
    `{samples_dir_relative_path}/`

    For example, to read the cities file, you must call the tool with the path:
    `read_file(path="{samples_dir_relative_path}/favorite_cities.txt")`

    The available files are: {file_list_str}.
    """

    # We will run the script from the project root, so '.' is the correct argument.
    async with MCPServerStdio(
        name="Local Filesystem Server",
        params={
            "command": "npx",
            "args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
            "cwd": project_root # Explicitly set the CWD for the server process
        },
    ) as fs_server:
        file_agent = Agent(
            name="File Assistant",
            instructions=PROMPT_WITH_CONTEXT,
            mcp_servers=[fs_server],
            model=DEFAULT_LLM,
            # We force tool use here for a predictable demonstration
            model_settings=ModelSettings(tool_choice="required")
        )

        with trace("MCP Filesystem Example - Corrected"):
            # Now the agent has the full context to make a correct tool call
            result_read = await Runner.run(file_agent, "When do I like to visit Tokyo?")
            print(f"Agent's response: {result_read.final_output}")

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

# Expected Output:
#
# Agent's response (read): Based on the provided text, you might like to visit Tokyo in the winter.

In this run, the file_agent’s LLM is presented with tools like list_directory and read_file, provided by the MCP server. It correctly deduces which tools to call to answer the user’s questions.

When developing a new tool or MCP integration, it can be useful to force the agent to use a tool to ensure it’s working correctly. Setting ModelSettings(tool_choice="required") on your agent will compel the LLM to select a tool on its first turn, which can make debugging the tool-calling mechanism much faster.

Behind the Scenes: How the SDK Integrates MCP

The seamless integration is handled by the MCPUtil class. It performs a crucial translation step: converting MCP’s tool definition format into the FunctionTool format that the Runner and LLM understand.

*The SDK's process for converting MCP tools into usable FunctionTools.*

For every tool advertised by the MCP server, MCPUtil creates a corresponding FunctionTool in memory. The on_invoke_tool property of this FunctionTool is set to a special handler, invoke_mcp_tool, which knows how to format the request and send it to the correct MCPServer instance. This elegant abstraction means the rest of the SDK—the Runner, the LLM, the tracing system—treats MCP tools just like any other FunctionTool.

The MCP specification is more lenient about its tool schemas than the “strict mode” that some models (like OpenAI’s) prefer for reliable JSON generation. The Agents SDK can attempt to automatically convert these schemas for you. The Agent class has an mcp_config parameter where you can set {"convert_schemas_to_strict": True}. This will apply the same ensure_strict_json_schema logic we discussed in Chapter 3 to schemas coming from your MCP servers, increasing the reliability of tool calls with models that support strict JSON output.

Performance Considerations: Caching

Calling list_tools() on every agent run can introduce latency, especially for remote servers. If you know that an MCP server’s toolset is stable and won’t change during your application’s lifecycle, you can enable caching.

Pass cache_tools_list=True when instantiating the server.

1
2
3
4
server = MCPServerSse(
    params={"url": "..."},
    cache_tools_list=True
)

With caching enabled, the SDK will only call list_tools() on the first run and will use the cached list for all subsequent runs with that server instance. If you need to manually refresh the cache (for example, if you know the remote server has been updated), you can call server.invalidate_tools_cache().

Tracing MCP Interactions

The built-in tracing system has native support for MCP. When an MCP tool is used, the trace will contain:

  1. An mcp_tools span, showing the call to list_tools and the names of the tools that were returned.
  2. The function_span for the tool call will include special mcp_data in its metadata, identifying which server fulfilled the request.

This gives you a clear and debuggable view of how your agent is interacting with its external MCP-based tools, helping you diagnose routing issues or performance bottlenecks.

Chapter Summary

In this chapter, we unlocked a vast ecosystem of external capabilities by exploring the Model Context Protocol. We learned that MCP provides a universal standard for tools, and the Agents SDK makes integrating them trivial via the mcp_servers parameter. We covered the different server types for local and remote tools, walked through a complete filesystem example, and peeked behind the scenes to see how MCPUtil bridges the gap between the MCP world and the SDK’s native FunctionTool.

You are now able to augment your agents with a potentially limitless supply of third-party tools, from git and filesystem operations to specialized data APIs, without having to write custom wrapper code for each one. This is a key step in building truly powerful, real-world applications.

In the next chapter, we will explore the other primary method for multi-agent coordination: Handoffs. While agents-as-tools is about delegation and return, handoffs are about transferring control, which unlocks a different set of powerful workflow patterns.

This post is licensed under CC BY 4.0 by the author.