In the evolving landscape of AI tools, combining OpenAI’s agent capabilities with Anthropic’s Message Control Protocol (MCP) opens up powerful new possibilities. This integration enables developers to create sophisticated applications that leverage both platforms’ strengths. In this article, I’ll walk through how to build this integration using Python, explaining each component in detail.

The Core Integration: Creating an Agent with MCP Tools

Let’s start by examining the main code that creates an OpenAI Agent SDK connected to a WhastApp MCP sever:

async def run_agent() -> None:
    async with mcp_tools(
        command="npx",
        args=[
            "wweb-mcp",
            "-m", "mcp", 
            "-s", "local",
            "-c", "api",
            "-t", "command",
            "--api-base-url", "http://localhost:3001/api",
            "--api-key", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        ],
    ) as whatsapp_tools:
        agent = Agent(
            name="whatsapp_agent",
            tools=[*whatsapp_tools],
            model="gpt-4o-mini",
        )

        result = await Runner.run(agent, "Can you find the phone number of Isabela?")
        print(result.final_output)

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

This code snippet demonstrates the fundamental integration between OpenAI’s Agent SDK and an MCP server. Let’s break down what’s happening:

  1. We create an asynchronous function run_agent() that orchestrates the integration.
  2. The mcp_tools context manager establishes a connection to the MCP server (in this case, a WhatsApp integration).
  3. Inside the context, we create an OpenAI Agent with a specific name and model, and provide it with the tools obtained from the MCP server.
  4. We use the OpenAI Runner to execute the agent with a simple query.
  5. Finally, we print the result and wrap everything in an asyncio execution.

The magic happens within the mcp_tools function, which converts the MCP server's capabilities into tools that the OpenAI agent can use. Let's examine this critical component in detail.

Understanding the mcp_tools Function

The mcp_tools function serves as the bridge between the OpenAI Agents SDK and Anthropic's MCP server. Here's a detailed breakdown of each part:

Establishing the Connection

The function first sets up the server parameters, which define how to start and communicate with the MCP server.

server_params = StdioServerParameters(
    command=command,
    args=args,
    env=env,
)

It then creates a client connection using stdio_client, which provides read and write streams for communication. Using these streams, it establishes a client session that will handle all communication with the MCP server:

async with stdio_client(server_params) as (read, write):
  async with ClientSession(read, write) as session:
    # Initialize the connection
    await session.initialize()

Discovering and Converting Tools

After initializing the connection, mcp_tools requests a list of all available tools from the MCP server:

# List available tools
tools = (await session.list_tools()).tools

This is where the function’s core purpose comes into play. For each tool provided by the MCP server, it creates a corresponding FunctionTool that the OpenAI agent can use:

yield [
    FunctionTool(
        name=tool.name,
        description=tool.description,
        params_json_schema=tool.inputSchema,
        on_invoke_tool=create_invoke_tool(tool),
    )
    for tool in tools
]

Each FunctionTool includes:

  • The original tool’s name
  • The tool’s description
  • A JSON schema defining the tool’s input parameters
  • A function that will be called when the OpenAI agent invokes this tool

The Tool Invocation Handler

The create_invoke_tool function creates a handler that is called whenever the OpenAI SDK agent wants to use a specific MCP tool:

def create_invoke_tool(tool: Tool) -> Callable[[RunContextWrapper[Any], str], Awaitable[str]]:
    async def on_invoke_tool(context: RunContextWrapper[Any], args: str) -> str:
        if confirmation_fn:
            if not await confirmation_fn(tool.name, args):
                return "Action not allowed by user"
        result = await session.call_tool(tool.name, json.loads(args))
        if result.isError:
            raise Exception(result.content[0].text)
        return result.content[0].text
    return on_invoke_tool

This handler performs several important tasks:

  1. If a confirmation function was provided, it calls that function to ask for permission to execute the tool.
  2. It calls the MCP tool with the provided arguments.
  3. It handles any errors that occur during tool execution.
  4. It returns the tool’s output as a string, which is passed back to the OpenAI agent.

Practical Application: Building a WhatsApp Assistant

In our example, we’ve created a WhatsApp assistant that can search for contact information. When the agent receives the query “Can you find the phone number of Isabela?”, it will:

  1. Analyze the request to understand what’s being asked
  2. Identify which MCP tool can handle contact searches
  3. Invoke that tool with the name “Isabela” as a parameter
  4. Process the result and return it to the user

This creates a seamless experience where users can interact with the agent using natural language, and the agent can leverage the capabilities of both the OpenAI model and the MCP-connected service.

Here is the complete solution:

import asyncio
import json
from contextlib import asynccontextmanager
from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List

from mcp import ClientSession, StdioServerParameters, Tool, stdio_client

from agents import Agent, FunctionTool, RunContextWrapper, Runner


@asynccontextmanager
async def mcp_tools(
    command: str,
    args: List[str],
    env: Dict[str, str] | None = None,
    confirmation_fn: Callable[[str, str], Awaitable[bool]] | None = None,
) -> AsyncGenerator[List[FunctionTool], None]:
    server_params = StdioServerParameters(
        command=command,
        args=args,
        env=env,
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # Initialize the connection
            await session.initialize()

            # List available tools
            tools = (await session.list_tools()).tools

            def create_invoke_tool(tool: Tool) -> Callable[[RunContextWrapper[Any], str], Awaitable[str]]:
                async def on_invoke_tool(context: RunContextWrapper[Any], args: str) -> str:
                    if confirmation_fn:
                        if not await confirmation_fn(tool.name, args):
                            return "Action not allowed by user"

                    result = await session.call_tool(tool.name, json.loads(args))
                    if result.isError:
                        raise Exception(result.content[0].text)  # type: ignore

                    return result.content[0].text  # type: ignore

                return on_invoke_tool

            yield [
                FunctionTool(
                    name=tool.name,
                    description=tool.description,
                    params_json_schema=tool.inputSchema,
                    on_invoke_tool=create_invoke_tool(tool),
                )
                for tool in tools
            ]

async def confirm_action(tool_name: str, args: str) -> bool:
    print(f"The agent wants to use {tool_name} with arguments {args}")
    response = input("Allow? (y/n): ")
    return response.lower() == 'y'


async def run_agent() -> None:
    async with mcp_tools(
        command="wweb-mcp",
        args=[
            "-m", "mcp", 
            "-s", "local",
            "-c", "api",
            "-t", "command",
            "--api-base-url", "http://localhost:3001/api",
            "--api-key", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        ],
        confirmation_fn=confirm_action,
    ) as whatsapp_tools:
        agent = Agent(
            name="whatsapp_agent",
            tools=[*whatsapp_tools],
            model="gpt-4o-mini",
        )

        result = await Runner.run(agent, "Can you find the phone number of Isabela?")
        print(result.final_output)


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

Extending the Integration

This basic integration can be extended in several ways:

Adding User Confirmation

You could implement a confirmation function that asks the user before executing potentially sensitive actions:

async def confirm_action(tool_name: str, args: str) -> bool:
    print(f"The agent wants to use {tool_name} with arguments {args}")
    response = input("Allow? (y/n): ")
    return response.lower() == 'y'

async def run_agent() -> None:
    async with mcp_tools(
        command="wweb-mcp",
        args=[...],
        confirmation_fn=confirm_action,
    ) as whatsapp_tools:
        # Rest of the code remains the same

Supporting Multiple MCP Services

You could connect to multiple MCP services and combine their tools:

async def run_multi_service_agent() -> None:
    async with mcp_tools(command="wweb-mcp", args=[...]) as whatsapp_tools:
        async with mcp_tools(command="email-mcp", args=[...]) as email_tools:
            agent = Agent(
                name="communication_agent",
                tools=[*whatsapp_tools, *email_tools],
                model="gpt-4o",
            )
            # Rest of the code

Conclusion

The integration between OpenAI’s Agent SDK and Anthropic’s MCP opens up powerful possibilities for creating AI assistants that can interact with a wide range of services. By understanding the core components of this integration — particularly the mcp_tools function that bridges the gap between these platforms—developers can build sophisticated applications that leverage the best of both worlds.

This approach allows AI agents to access external tools and services while maintaining a simple, unified interface for users. As both OpenAI and Anthropic continue to evolve their platforms, we can expect even more powerful integration possibilities in the future.