Building Custom MCP Integrations for ChatDev: A Complete Technical Guide

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. 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 (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 (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 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 and referenced through 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:

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:

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:

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:

uv run mcp_example/mcp_server.py

Direct ToolManager API Usage

For programmatic control, instantiate ToolManager and execute tools directly:

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).

Binary content gets stored via 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.

  • 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, enabling the ChatDev frontend to render images or play audio directly from MCP tool outputs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →