# Building Custom MCP Integrations for ChatDev: A Complete Technical Guide

> Master ChatDev MCP integrations with this technical guide. Learn to discover, cache, and execute tools via HTTP and stdio, normalizing results for LLM consumption.

- Repository: [OpenBMB/ChatDev](https://github.com/OpenBMB/ChatDev)
- Tags: how-to-guide
- Published: 2026-04-01

---

**ChatDev integrates external tools via the Model Context Protocol (MCP) using `ToolManager` to discover, cache, and execute tools from both HTTP endpoints and local stdio subprocesses, normalizing all results into a unified `ToolSpec` format for LLM consumption.**

ChatDev treats external capabilities as first-class resources through the Model Context Protocol (MCP), enabling LLMs to invoke specialized tools during workflow execution. Building custom MCP integrations for ChatDev requires understanding how the `ToolManager` class discovers tool specifications and routes execution calls to remote HTTP servers or local subprocesses. This guide walks through the architecture, configuration patterns, and implementation details based on the OpenBMB/ChatDev source code.

## Architecture Overview

The MCP integration architecture in ChatDev centers on the `ToolManager` class located in [`runtime/node/agent/tool/tool_manager.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/agent/tool/tool_manager.py). This central dispatcher handles tool discovery, caching, and execution normalization, ensuring that regardless of whether a tool runs remotely over HTTP or locally via stdio, the LLM receives a consistent interface.

### Core Components

Several key classes collaborate to provide MCP functionality:

- **`ToolManager`** – The main orchestrator that builds `ToolSpec` objects from configuration entries, maintains caches of remote tool lists, and normalizes execution results from various transport protocols.

- **`McpRemoteConfig`** – Defined in [`entity/configs/node/tooling.py`](https://github.com/OpenBMB/ChatDev/blob/main/entity/configs/node/tooling.py) (lines 311-363), this dataclass describes HTTP-based MCP servers, including the endpoint URL, authentication headers, timeout values, and cache TTL.

- **`McpLocalConfig`** – Also in [`entity/configs/node/tooling.py`](https://github.com/OpenBMB/ChatDev/blob/main/entity/configs/node/tooling.py) (lines 425-499), this configuration manages stdio-based subprocess execution, specifying the command, arguments, working directory, environment variables, and startup timeout.

- **`_StdioClientWrapper`** – An internal helper class within [`tool_manager.py`](https://github.com/OpenBMB/ChatDev/blob/main/tool_manager.py) that spawns local MCP servers in daemon threads, maintaining persistent asyncio event loops to avoid subprocess overhead across multiple tool calls.

- **`ToolSpec`** – The unified representation (defined in [`entity/tool_spec.py`](https://github.com/OpenBMB/ChatDev/blob/main/entity/tool_spec.py) and referenced through [`utils/function_catalog.py`](https://github.com/OpenBMB/ChatDev/blob/main/utils/function_catalog.py)) that exposes `name`, `description`, `parameters`, and `metadata` to the LLM runtime, abstracting away transport-specific details.

### Remote vs. Local Execution Flows

**Remote HTTP Flow:**
1. **Configuration** – A node's `tooling` block contains a `type: mcp_remote` entry with server details.
2. **Discovery** – `ToolManager._fetch_mcp_tools_http()` creates a `fastmcp.Client` with `StreamableHttpTransport` and calls `list_tools()`. Results are cached using a key derived from `McpRemoteConfig.cache_key()` (based on URL, headers, and timeout).
3. **Spec Generation** – Each discovered tool becomes a `ToolSpec` with `metadata.source = "mcp"`, `metadata.server = <url>`, and `metadata.mode = "remote"`.
4. **Execution** – When invoked, `ToolManager._execute_mcp_remote_tool()` instantiates a new client, calls `client.call_tool(name, args)`, and normalizes the `FastMCP.CallToolResult` into Python values or `MessageBlock` objects.

**Local Stdio Flow:**
The process mirrors the remote flow but routes through `_StdioClientWrapper`. This wrapper spawns the command defined in `McpLocalConfig` once, maintains the process across the node's lifecycle, and forwards `list_tools` and `call_tool` requests via stdio transport.

## Configuration Patterns

ChatDev supports both remote HTTP and local stdio MCP configurations through YAML node definitions, allowing developers to integrate external services or sandboxed local executables.

### Remote MCP Configuration

To connect to an HTTP-based MCP server, define the configuration in your workflow node:

```yaml
tooling:
  - type: mcp_remote
    prefix: demo          # Optional prefix to avoid name clashes

    server: http://localhost:8001   # FastMCP HTTP endpoint

    headers: {}           # Optional authentication headers

    timeout: 5.0
    cache_ttl: 30         # Cache tool list for 30 seconds

    tool_sources: ["demo_tools"]    # Filter to specific tool sources

```

The `prefix` field prepends the value to tool names (e.g., `rand_num` becomes `demo_rand_num`). The `cache_ttl` parameter controls how long `ToolManager` retains the tool specification list before refreshing.

### Local Stdio Configuration

For subprocess-based tools that communicate via stdin/stdout, use the `mcp_local` type:

```yaml
tooling:
  - type: mcp_local
    prefix: demo
    command: uv
    args: ["run", "mcp_example/mcp_server.py"]
    cwd: .
    env: {}
    inherit_env: true
    startup_timeout: 10.0
    wait_for_log: "Starting simple MCP server..."
    cache_ttl: 0         # 0 disables caching, enabling hot-reload

```

The `wait_for_log` parameter allows the `ToolManager` to wait for a specific log line before considering the server ready, while `inherit_env` passes the parent environment variables to the subprocess.

## Implementation Guide

Creating a custom MCP integration requires implementing the server-side FastMCP protocol and optionally interacting directly with the `ToolManager` API.

### Creating a FastMCP Server

Create a minimal MCP server using the FastMCP library. Save this as [`mcp_example/mcp_server.py`](https://github.com/OpenBMB/ChatDev/blob/main/mcp_example/mcp_server.py):

```python
from fastmcp import FastMCP
import random

# Initialize the server with a service name

mcp = FastMCP("Demo MCP Server", debug=True)

# Register any callable as a tool

@mcp.tool
def rand_num(a: int, b: int) -> int:
    """Return a random integer between *a* and *b* (inclusive)."""
    return random.randint(a, b)

if __name__ == "__main__":
    # For remote/HTTP transport:

    # mcp.run(transport="streamable-http", host="0.0.0.0", port=8001)

    
    # For local/stdio transport (default):

    mcp.run()

```

Run the server in stdio mode:

```bash
uv run mcp_example/mcp_server.py

```

### Direct ToolManager API Usage

For programmatic control, instantiate `ToolManager` and execute tools directly:

```python
import asyncio
from runtime.node.agent.tool.tool_manager import ToolManager
from entity.configs.node.tooling import McpRemoteConfig, ToolingConfig

async def demo():
    # Build configuration programmatically

    remote_cfg = McpRemoteConfig(
        server="http://localhost:8001",
        headers={},
        timeout=5.0,
        cache_ttl=30.0,
        tool_sources=None,
        path="tooling[0]",
    )
    tooling_cfg = ToolingConfig(
        type="mcp_remote",
        prefix="demo",
        config=remote_cfg,
        path="tooling[0]",
    )

    # Discover available tools

    mgr = ToolManager()
    specs = mgr.get_tool_specs([tooling_cfg])
    print("Discovered specs:", [s.name for s in specs])

    # Execute a specific tool

    result = await mgr.execute_tool(
        tool_name="demo_rand_num",
        arguments={"a": 1, "b": 10},
        tool_config=tooling_cfg,
    )
    print("Result from MCP:", result)

asyncio.run(demo())

```

This approach returns the raw result from the MCP server, which the `ToolManager` automatically normalizes based on the response type.

## Handling Binary Content

When MCP tools return binary data such as images or audio, the `ToolManager` converts these into structured `MessageBlock` objects. The conversion logic resides in `_convert_mcp_content_to_blocks()` and `_materialize_mcp_binary_block()` (approximately lines 460-540 in [`tool_manager.py`](https://github.com/OpenBMB/ChatDev/blob/main/tool_manager.py)).

Binary content gets stored via [`utils/attachments.py`](https://github.com/OpenBMB/ChatDev/blob/main/utils/attachments.py), with references passed back to the LLM runtime as attachment blocks. This allows ChatDev workflows to handle file outputs from MCP tools seamlessly, displaying images in the UI or passing audio data to subsequent processing nodes without manual encoding handling.

## Summary

- **Architecture**: ChatDev uses `ToolManager` as the central hub for MCP integrations, supporting both HTTP remote servers and stdio local subprocesses through distinct configuration classes.

- **Configuration**: Use `McpRemoteConfig` for HTTP endpoints with caching based on URL plus headers, or `McpLocalConfig` for local executables managed by `_StdioClientWrapper`.

- **Normalization**: All MCP tools, regardless of transport, normalize into `ToolSpec` objects with unified metadata including `source`, `server`, and `mode` fields.

- **Binary Support**: The runtime automatically converts `ImageContent` and `AudioContent` from MCP responses into `MessageBlock` attachments via [`utils/attachments.py`](https://github.com/OpenBMB/ChatDev/blob/main/utils/attachments.py).

- **Filtering**: The optional `tool_sources` parameter in configurations allows selective exposure of MCP tool subsets without server-side modifications.

## Frequently Asked Questions

### What is the difference between remote and local MCP configurations in ChatDev?

**Remote configurations** (`mcp_remote`) connect to HTTP endpoints using `fastmcp.Client` with `StreamableHttpTransport`, suitable for services hosted on separate infrastructure or cloud APIs. **Local configurations** (`mcp_local`) spawn subprocesses via `_StdioClientWrapper`, running a dedicated asyncio loop in a background thread to maintain persistent stdio communication without process restart overhead.

### How does ChatDev cache MCP tool specifications?

The `ToolManager` generates cache keys using `McpRemoteConfig.cache_key()`, which hashes the server URL, headers, and timeout values. This ensures that authentication or endpoint changes invalidate the cache automatically. The `cache_ttl` parameter controls expiration duration, with `0` disabling caching entirely for development scenarios requiring hot-reloading.

### Can I filter which MCP tools are exposed to the LLM?

Yes. Both `McpRemoteConfig` and `McpLocalConfig` support an optional `tool_sources` list that filters discovered tools by their metadata source field. This allows developers to expose only specific tool categories (e.g., excluding internal utilities) without modifying the MCP server implementation.

### How are binary files handled when returned by MCP tools?

Binary content returned by MCP tools undergoes conversion in `ToolManager._convert_mcp_content_to_blocks()` and `_materialize_mcp_binary_block()`. These methods transform `ImageContent` or `AudioContent` into `MessageBlock` objects containing attachment references stored in [`utils/attachments.py`](https://github.com/OpenBMB/ChatDev/blob/main/utils/attachments.py), enabling the ChatDev frontend to render images or play audio directly from MCP tool outputs.