Implementing an MCP (Model Context Protocol) Auto-Loading System
One of the core features of Claude Control is automatic MCP server loading. Drop MCP server code into the project's mcp/ folder, and Claude Control automatically detects and connects to it at session startup. This post documents the design and implementation of that system.
What is MCP?
MCP (Model Context Protocol) is a protocol proposed by Anthropic that provides a standard for AI models to access external tools and data sources. It communicates over JSON-RPC and exposes tools, resources, and prompts.
Auto-Discovery System
The system scans the mcp/ folder to automatically detect MCP server configurations.
import os
import json
import glob
from pathlib import Path
from typing import List
@dataclass
class MCPServerConfig:
name: str
command: str
args: List[str]
env: dict = field(default_factory=dict)
class MCPAutoLoader:
def __init__(self, base_dir: str):
self.base_dir = Path(base_dir)
self.mcp_dir = self.base_dir / "mcp"
self.servers: Dict[str, MCPServerConfig] = {}
def discover_servers(self) -> List[MCPServerConfig]:
configs = []
if not self.mcp_dir.exists():
return configs
# mcp_config.json 파일 탐색
for config_file in self.mcp_dir.glob("**/mcp_config.json"):
with open(config_file) as f:
config_data = json.load(f)
server = MCPServerConfig(
name=config_data["name"],
command=config_data.get("command", "python"),
args=config_data.get("args", [str(config_file.parent / "server.py")]),
env=config_data.get("env", {}),
)
configs.append(server)
# 단독 Python MCP 서버 탐색
for py_file in self.mcp_dir.glob("*/server.py"):
name = py_file.parent.name
if name not in [c.name for c in configs]:
configs.append(MCPServerConfig(
name=name,
command="python",
args=[str(py_file)],
))
return configs
MCP Server Process Management
Discovered MCP servers are launched as subprocesses, and a JSON-RPC communication channel is established with each one.
import subprocess
import asyncio
class MCPProcessManager:
def __init__(self):
self.processes: Dict[str, subprocess.Popen] = {}
async def start_server(self, config: MCPServerConfig) -> bool:
try:
env = os.environ.copy()
env.update(config.env)
process = subprocess.Popen(
[config.command] + config.args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)
self.processes[config.name] = process
# 초기화 핸드셰이크
init_msg = {"jsonrpc": "2.0", "method": "initialize", "id": 1}
process.stdin.write(json.dumps(init_msg).encode() + b"\n")
process.stdin.flush()
response = process.stdout.readline().decode()
result = json.loads(response)
return "result" in result
except Exception as e:
print(f"Failed to start MCP server {config.name}: {e}")
return False
async def auto_load_all(self, loader: MCPAutoLoader):
configs = loader.discover_servers()
for config in configs:
success = await self.start_server(config)
status = "✅" if success else "❌"
print(f" {status} MCP Server: {config.name}")
Loading Custom Tools from the tools/ Folder
In addition to MCP servers, Claude Control also loads custom tools from the tools/ folder. It automatically detects any function decorated with @tool in Python files there and registers them with Claude.
import importlib.util
def load_custom_tools(tools_dir: str) -> List[dict]:
tools = []
for py_file in Path(tools_dir).glob("*.py"):
spec = importlib.util.spec_from_file_location(py_file.stem, py_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
for attr_name in dir(module):
attr = getattr(module, attr_name)
if hasattr(attr, "_is_tool"):
tools.append(attr._tool_schema)
return tools
Retrospective
MCP auto-loading has significantly improved the extensibility of Claude Control. Adding a new MCP server now only requires dropping in a config file — the connection is handled automatically, which has sped up development considerably. It also makes it much easier to share MCP servers across the team.