MCP Station - Building an MCP Server Routing System
MCP Station was a project I built before Claude Control. It's a system for managing and routing across multiple MCP servers through a single endpoint. I started it in late September 2025 and wrapped it up by mid-October — five commits total.
Why It Was Needed
Running multiple MCP servers means managing each process separately. Clients need to know which tool lives on which server, and if a server dies, it has to be restarted manually. I built a centralized routing system to solve this.
Core Components
MCP Station is built around three core components.
import asyncio
import subprocess
from typing import Dict, List, Optional
from dataclasses import dataclass, field
@dataclass
class MCPServerInfo:
name: str
command: str
args: List[str]
process: Optional[subprocess.Popen] = None
tools: List[str] = field(default_factory=list)
status: str = "stopped"
class SessionManager:
"""세션 이름 추적 및 관리"""
def __init__(self):
self.sessions: Dict[str, dict] = {}
self.session_names: Dict[str, str] = {}
def register_session(self, session_id: str, name: str):
self.sessions[session_id] = {
"name": name,
"created_at": datetime.now(KST).isoformat(),
"server_bindings": [],
}
self.session_names[name] = session_id
def get_by_name(self, name: str) -> Optional[str]:
return self.session_names.get(name)
class ProcessManager:
"""MCP 서버 프로세스 생명주기 관리"""
def __init__(self):
self.servers: Dict[str, MCPServerInfo] = {}
async def start_server(self, info: MCPServerInfo) -> bool:
if info.name in self.servers and info.status == "running":
return True
process = subprocess.Popen(
[info.command] + info.args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
info.process = process
info.status = "running"
self.servers[info.name] = info
# 도구 목록 요청
info.tools = await self._discover_tools(info)
return True
async def stop_server(self, name: str):
if name in self.servers:
self.servers[name].process.terminate()
self.servers[name].status = "stopped"
Router Implementation
When a client makes a request by tool name, the router dispatches it to whichever MCP server provides that tool.
class Router:
"""도구 이름 → MCP 서버 라우팅"""
def __init__(self, process_manager: ProcessManager):
self.pm = process_manager
self._tool_map: Dict[str, str] = {} # tool_name -> server_name
async def build_routing_table(self):
self._tool_map.clear()
for name, server in self.pm.servers.items():
for tool in server.tools:
self._tool_map[tool] = name
async def route_request(self, tool_name: str, params: dict) -> dict:
server_name = self._tool_map.get(tool_name)
if not server_name:
raise ValueError(f"Unknown tool: {tool_name}")
server = self.pm.servers[server_name]
return await self._send_jsonrpc(server, tool_name, params)
async def _send_jsonrpc(self, server: MCPServerInfo,
method: str, params: dict) -> dict:
request = {
"jsonrpc": "2.0",
"method": f"tools/{method}",
"params": params,
"id": self._next_id(),
}
server.process.stdin.write(
json.dumps(request).encode() + b"\n"
)
server.process.stdin.flush()
response = server.process.stdout.readline().decode()
return json.loads(response)
KST Timezone Handling
All logs and timestamps are recorded in Korea Standard Time (KST).
from datetime import timezone, timedelta
KST = timezone(timedelta(hours=9))
def now_kst():
return datetime.now(KST)
Retrospective
MCP Station was a small project, but it gave me a lot of insight when I later built the MCP auto-loading feature in Claude Control. The tool-name-based routing pattern in particular turned out to be highly reusable.