How can we sovlve a integration problem with a centralized approach
The Problem That Started It All
If you've worked with multiple Model Context Protocol (MCP) servers, you know the pain: each server has its own quirks, different endpoint structures, varying response formats, and unique payload requirements.
Imagine you're building an AI application that needs to connect to:
- A GitHub MCP server (for repository operations)
- A SQL MCP server (for database queries)
- A Jupyter MCP server (for code execution)
- Your custom business logic MCP
There are four different integrations to maintain, monitor, and debug. It may sound simple, and in fact it is, but scale this to more than 10 servers/clients and you'll have a maintenance nightmare.
I know, the math is simple but brutal: M clients × N servers = M×N integration points.
Enter MCP one: From M×N to M+N
Instead of every client integrating with every MCP server directly, what if we had a single hub that normalizes everything?
Before (M×N):
Client A ──┬── MCP Server 1
├── MCP Server 2
└── MCP Server 3
Client B ──┬── MCP Server 1
├── MCP Server 2
└── MCP Server 3
After (M+N):
Client A ──┐
├── MCP Hub ──┬── MCP Server 1
Client B ──┘ ├── MCP Server 2
└── MCP Server 3
This is exactly what MCP one does, it acts as a unified gateway that speaks the same language to all your clients while handling the complexity of different MCP implementations behind the scenes.
Key Architecture Decisions
1. Configuration-Driven Approach
Rather than hardcoding server specifics, everything is defined in YAML:
servers:
- name: github_mcp
url: http://localhost:3001
enabled: true
timeout: 30
endpoints:
health: /health
tools: /api/tools
call: /api/execute
response_map:
tools_key: "available_tools" # GitHub MCP wraps tools in this key
tool_name_field: "name"
tool_desc_field: "description"
payload_map:
tool_field: "tool_name" # GitHub MCP expects this field name
args_field: "parameters" # Instead of "arguments"
- name: sql_mcp
url: http://localhost:3002
enabled: true
endpoints:
health: /status
tools: /tools
call: /execute
response_map:
tools_key: "" # SQL MCP returns tools directly
tool_name_field: "name"
tool_desc_field: "desc"
payload_map:
tool_field: "tool"
args_field: "args"
This approach means adding a new MCP server is just a few lines of config, no code changes required.
2. Flexible Mapping System
Different MCP servers structure their responses differently. Some return tools directly as an array, others wrap them in an object. Some use description
, others use desc
.
The mapping system handles this elegantly:
# In registry.py
resp_map = server_info.config.response_map
tools_key = resp_map.get("tools_key", "tools")
name_field = resp_map.get("tool_name_field", "name")
desc_field = resp_map.get("tool_desc_field", "description")
# Handle direct array vs wrapped response
if tools_key:
raw_tools = raw.get(tools_key, [])
else:
raw_tools = raw # Already an array
# Extract fields dynamically
for tool in raw_tools:
t_name = tool.get(name_field)
t_desc = tool.get(desc_field, "")
3. Async-First Design
Built with FastAPI and httpx, everything is async from the ground up:
async def execute_tool(self, request: ToolCallRequest) -> ToolCallResponse:
# Find the tool
tool = await self.registry.get_tool(request.tool)
# Get server info
server_info = await self.registry.get_server_info(tool.server_name)
# Execute asynchronously
response = await self._call_mcp_tool(
server_info.config,
tool.name,
request.arguments
)
return response
This ensures the hub can handle multiple concurrent requests without blocking.
4. Health Monitoring & Auto-Recovery
The hub continuously monitors all registered servers:
async def _background_refresh_loop(self, interval: int) -> None:
while not self._shutdown:
try:
await self.refresh_all_servers()
await asyncio.sleep(interval)
except Exception as e:
logger.error("background_refresh_error", error=str(e))
await asyncio.sleep(5) # Retry in 5 seconds
Servers that go offline are automatically detected and marked as unavailable. When they come back online, they're automatically re-integrated.
The API That Just Works
From the client perspective, MCP one provides a clean, consistent API regardless of the underlying server complexity:
List All Available Tools
GET /tools
Response:
{
"tools": [
{
"name": "query_database",
"description": "Execute SQL queries",
"full_name": "sql_mcp.query_database",
"server_name": "sql_mcp"
},
{
"name": "create_pr",
"description": "Create a GitHub pull request",
"full_name": "github_mcp.create_pr",
"server_name": "github_mcp"
}
],
"total_count": 2
}
Execute Any Tool
POST /call
{
"tool": "sql_mcp.query_database",
"arguments": {
"query": "SELECT * FROM users LIMIT 10"
}
}
Response:
{
"success": true,
"result": {
"rows": [...],
"count": 10
},
"server_name": "sql_mcp",
"execution_time_ms": 45.2
}
Monitor System Health
GET /status
{
"version": "0.1.0",
"uptime_seconds": 3600,
"servers_count": 3,
"servers_online": 2,
"tools_count": 15,
"last_refresh": "2024-01-15T10:30:00Z"
}
Real-World Benefits
For Developers
- Single integration point instead of N different APIs
- Consistent error handling across all MCP servers
- Built-in monitoring and health checks
- Easy configuration, no code changes to add new servers
For Operations
- Centralized logging with structured logs via structlog
- Performance metrics with execution time tracking
- Health monitoring with automatic failure detection
- Graceful degradation when servers go offline
For Architecture
- Reduced coupling between clients and MCP servers
- Easier testing with a single API surface
- Better scalability with async request handling
- Configuration as code approach
Implementation Highlights
Clean Separation of Concerns
# Registry: Manages server lifecycle and tool discovery
class MCPRegistry:
async def register_server(self, config: MCPServerConfig) -> bool
async def refresh_all_servers(self) -> None
async def list_tools(self, server_name: Optional[str] = None) -> List[ToolSchema]
# Router: Handles tool execution
class MCPRouter:
async def execute_tool(self, request: ToolCallRequest) -> ToolCallResponse
# Main: FastAPI application with dependency injection
app = FastAPI(lifespan=lifespan)
Robust Error Handling
async def execute_tool(self, request: ToolCallRequest) -> ToolCallResponse:
start_time = time.time()
try:
tool = await self.registry.get_tool(request.tool)
if not tool:
return ToolCallResponse(
success=False,
error="tool_not_found",
execution_time_ms=(time.time() - start_time) * 1000
)
# ... execution logic
except Exception as e:
logger.error("tool_execution_failed", tool_name=request.tool, error=str(e))
return ToolCallResponse(
success=False,
error="execution_failed",
execution_time_ms=(time.time() - start_time) * 1000
)
Type Safety with Pydantic
All data structures are properly typed:
class ToolCallRequest(BaseModel):
tool: str = Field(..., description="Full tool name (server.tool)")
arguments: Dict[str, Any] = Field(default_factory=dict)
@validator('tool')
def validate_tool_name(cls, v):
if '.' not in v:
raise ValueError('Tool name must be in format: server.tool')
return v
Getting Started
- Clone and install:
git clone https://github.com/your-username/mcp-one.git
cd mcp-one
pip install -r requirements.txt
Configure your servers in
src/config.yaml
Run the hub:
cd src
uvicorn app.main:app --reload
-
Start using the unified API at
http://localhost:8000
What's Next?
This is an MVP that solves the core problem, but there's more to build (feel free to contribute 😁) :
- Authentication & authorization for production deployments
- Rate limiting to prevent abuse
- Metrics & monitoring integration (Prometheus, etc.)
- Load balancing for high-availability MCP servers
- WebSocket support for real-time tool streaming
- Plugin system for custom transformations
Why This Matters
As AI applications become more complex, they need to integrate with more external services. The MCP standard is growing, but without proper tooling, we'll face the same integration hell we've seen before.
MCP one represents a different approach: instead of fighting complexity, we embrace it through abstraction. By providing a single, consistent interface to multiple MCP servers, we reduce maintenance burden while enabling more sophisticated AI applications.
The goal isn't to replace MCP servers - it's to make them easier to use together.
Top comments (0)