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 buildsToolSpecobjects from configuration entries, maintains caches of remote tool lists, and normalizes execution results from various transport protocols. -
McpRemoteConfig– Defined inentity/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 inentity/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 withintool_manager.pythat 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 inentity/tool_spec.pyand referenced throughutils/function_catalog.py) that exposesname,description,parameters, andmetadatato the LLM runtime, abstracting away transport-specific details.
Remote vs. Local Execution Flows
Remote HTTP Flow:
- Configuration – A node's
toolingblock contains atype: mcp_remoteentry with server details. - Discovery –
ToolManager._fetch_mcp_tools_http()creates afastmcp.ClientwithStreamableHttpTransportand callslist_tools(). Results are cached using a key derived fromMcpRemoteConfig.cache_key()(based on URL, headers, and timeout). - Spec Generation – Each discovered tool becomes a
ToolSpecwithmetadata.source = "mcp",metadata.server = <url>, andmetadata.mode = "remote". - Execution – When invoked,
ToolManager._execute_mcp_remote_tool()instantiates a new client, callsclient.call_tool(name, args), and normalizes theFastMCP.CallToolResultinto Python values orMessageBlockobjects.
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
ToolManageras the central hub for MCP integrations, supporting both HTTP remote servers and stdio local subprocesses through distinct configuration classes. -
Configuration: Use
McpRemoteConfigfor HTTP endpoints with caching based on URL plus headers, orMcpLocalConfigfor local executables managed by_StdioClientWrapper. -
Normalization: All MCP tools, regardless of transport, normalize into
ToolSpecobjects with unified metadata includingsource,server, andmodefields. -
Binary Support: The runtime automatically converts
ImageContentandAudioContentfrom MCP responses intoMessageBlockattachments viautils/attachments.py. -
Filtering: The optional
tool_sourcesparameter 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →