A Go-based hub that aggregates multiple MCP (Model Context Protocol) servers and exposes their tools through a unified HTTP API.
- Standard MCP Protocol: Full support for MCP 2024-11-05 specification
- Multiple Transport Types:
- Stdio: For local MCP servers (Node.js, Python, etc.)
- HTTP/SSE: For remote MCP servers
- Configuration-Based: JSON configuration compatible with Cursor/VSCode format
- Docker-Ready: Easy deployment in containers with volume mounts
- Tool Aggregation: Combine tools from multiple MCP servers in one place
- HTTP API: RESTful endpoints for tool discovery and execution
cd cmd/mcp-hub
go build -o ../../mcp-hubCreate a config.json file (see config.example.json for a template):
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
}
}
}# Using default config.json
./mcp-hub
# Or specify a config file
./mcp-hub --config /path/to/config.jsonNotes:
- The HTTP listen address can be overridden with the
MCP_HUB_PORTorPORTenvironment variable. If the value contains a colon it is treated as a full address (e.g.0.0.0.0:8080), otherwise it is treated as a port and is prefixed with a colon. - The binary accepts a
--configflag (default:config.json).
List all available tools:
curl http://localhost:8080/mcp/toolsExecute a tool:
curl -X POST http://localhost:8080/mcp/execute \
-H 'Content-Type: application/json' \
-d '{
"plugin_id": "filesystem",
"tool_name": "read_file",
"arguments": {"path": "/tmp/test.txt"}
}'List connected servers:
curl http://localhost:8080/mcp/serversThe configuration file uses the standard mcpServers format compatible with Cursor, VSCode, and Claude Desktop.
For MCP servers that run as local processes:
{
"mcpServers": {
"server-name": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-package"],
"env": {
"API_KEY": "${YOUR_API_KEY}"
},
"timeout": 30
}
}
}Fields:
command: Executable to run (required)args: Command line arguments (optional)env: Environment variables (optional, supports${VAR}expansion)timeout: Request timeout in seconds (optional, default: 30)disabled: Set totrueto disable a server (optional)
For MCP servers accessible via HTTP:
{
"mcpServers": {
"remote-server": {
"type": "http",
"url": "http://localhost:3000/mcp",
"headers": {
"Authorization": "Bearer ${API_TOKEN}"
},
"timeout": 45
}
}
}Fields:
type: Set to"http"for HTTP transport (or auto-detected fromurl)url: HTTP endpoint URL (required)headers: HTTP headers to include (optional, supports${VAR}expansion)timeout: Request timeout in seconds (optional, default: 30)
Environment variables in the configuration are expanded using ${VAR_NAME} syntax. For example:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
}
}
}Then run:
GITHUB_TOKEN=your_token_here ./mcp-hubThe repository provides two Docker image variants, optimized for different use cases:
- Size: ~47MB
- Includes: Alpine Linux + Docker CLI + mcp-hub binary
- Use when: You need Docker transport for running MCP servers in containers, or mixed transports
- Best for: Production deployments with container-based MCP servers
- Size: ~800MB
- Includes: Node.js 20, Python 3, uv, curl, git, and mcp-hub binary
- Use when: You want to run local stdio-based MCP servers (Node.js, Python) in the same container
- Best for: Self-contained deployments where the hub and all MCP servers run together
To build a specific image locally:
# Standard image (with Docker CLI)
docker build -t mcp-hub:latest .
# Local MCP servers image (with Node.js, Python, uv)
docker build -f Dockerfile.local -t mcp-hub:local .# Create a config directory
mkdir -p config
# Put your config.json in the config directory
cp config.json config/
# Run the container
docker run -d \
-p 8080:8080 \
-v $(pwd)/config:/config:ro \
-e GITHUB_TOKEN=${GITHUB_TOKEN} \
mcp-hubversion: '3.8'
services:
mcp-hub:
build: .
ports:
- "8080:8080"
volumes:
- ./config:/config:ro
- ./plugins:/plugins:ro # Optional: mount custom plugins
environment:
- GITHUB_TOKEN=${GITHUB_TOKEN}
- BRAVE_API_KEY=${BRAVE_API_KEY}List all available tools from all connected MCP servers.
Response:
[
{
"id": "read_file",
"name": "read_file",
"description": "Read contents of a file",
"plugin_id": "filesystem"
}
]Execute a tool on a specific MCP server.
Request:
{
"plugin_id": "filesystem",
"tool_name": "read_file",
"arguments": {
"path": "/tmp/test.txt"
}
}Response: MCP tool call result (format depends on the tool)
List all connected MCP servers.
Response:
{
"servers": ["filesystem", "github", "brave-search"]
}Server-Sent Events stream of tool registry updates (for real-time tool discovery).
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/documents"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
},
"brave-search": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
"env": {
"BRAVE_API_KEY": "${BRAVE_API_KEY}"
}
}
}
}{
"mcpServers": {
"custom-tools": {
"command": "python3",
"args": ["/app/plugins/custom_mcp_server.py"]
}
}
}{
"mcpServers": {
"local-filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
},
"remote-api": {
"type": "http",
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer ${API_TOKEN}"
}
}
}
}.
├── cmd/
│ └── mcp-hub/
│ └── main.go # Entry point
├── internal/
│ ├── config/
│ │ └── config.go # Configuration parsing
│ ├── mcp/
│ │ └── protocol.go # MCP protocol structures
│ ├── plugin/
│ │ └── manager.go # Server management
│ ├── registry/
│ │ └── registry.go # Tool registry
│ ├── server/
│ │ └── server.go # HTTP server
│ └── transport/
│ ├── transport.go # Transport interface
│ ├── stdio.go # Stdio transport
│ └── http.go # HTTP transport
├── config.example.json # Example configuration
└── README.md
Implement the Transport interface in internal/transport/:
type Transport interface {
Start(ctx context.Context) error
SendRequest(ctx context.Context, req interface{}) (json.RawMessage, error)
SendNotification(ctx context.Context, notification interface{}) error
Close() error
IsConnected() bool
}Check logs for specific error messages. Common issues:
- Missing
npxorpython3in PATH - Invalid MCP server package names
- Missing environment variables
- Incorrect file paths in configuration
- Verify the MCP server is properly initialized (check
/mcp/servers) - Ensure tool arguments match the expected schema
- Check server logs (stderr output is visible in hub logs)
Increase the timeout value in server configuration:
{
"mcpServers": {
"slow-server": {
"command": "...",
"timeout": 120
}
}
}MIT
For MCP servers running in Docker containers:
{
"mcpServers": {
"containerized-server": {
"type": "docker",
"image": "my-mcp-server:latest",
"args": ["--option", "value"],
"env": {
"API_KEY": "${YOUR_API_KEY}"
},
"volumes": {
"/host/path": "/container/path",
"${HOME}/data": "/data"
},
"network": "mcp-network",
"timeout": 60
}
}
}Fields:
type: Set to"docker"for Docker transport (or auto-detected fromimage)image: Docker image name (required)args: Command arguments to pass to container entrypoint (optional)env: Environment variables (optional, supports${VAR}expansion)volumes: Volume mounts ashost:containermappings (optional, supports${VAR}expansion)network: Docker network to connect to (optional)timeout: Request timeout in seconds (optional, default: 30)
Benefits of Docker Transport:
- No need to install Node.js, Python, or other runtimes on the hub host
- Isolated dependencies per MCP server
- Easy version management with Docker tags
- Consistent environment across deployments
You can containerize any MCP server to avoid installing its dependencies on the hub host.
Dockerfile:
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy MCP server code
COPY mcp_server.py .
# Run as non-root user
RUN useradd -m -u 1000 mcpuser && chown -R mcpuser:mcpuser /app
USER mcpuser
# The server should read from stdin and write to stdout
ENTRYPOINT ["python", "-u", "mcp_server.py"]Build and use:
# Build the image
docker build -t my-mcp-server:latest .
# Add to config.json
{
"mcpServers": {
"my-server": {
"image": "my-mcp-server:latest"
}
}
}Dockerfile:
FROM node:20-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --production
# Copy server code
COPY . .
# Run as non-root user
RUN addgroup -g 1000 mcpuser && \
adduser -D -u 1000 -G mcpuser mcpuser && \
chown -R mcpuser:mcpuser /app
USER mcpuser
ENTRYPOINT ["node", "server.js"]- Use
-iflag: The hub runs containers withdocker run -ifor interactive stdin/stdout - Unbuffered output: Ensure your server outputs are unbuffered (use
python -uorflush()) - Stdin/Stdout only: The MCP protocol uses stdin for input and stdout for output
- Stderr for logs: Use stderr for logging (visible in hub logs)
- Cleanup: Containers are run with
--rmfor automatic cleanup
Create a registry of Docker images for common MCP servers:
# Example: Build an echo server image
cd examples/plugins
cat > Dockerfile << 'DOCKERFILE'
FROM python:3.11-slim
COPY mcp_echo.py /app/server.py
WORKDIR /app
RUN chmod +x server.py
ENTRYPOINT ["python", "-u", "server.py"]
DOCKERFILE
docker build -t mcp-echo:latest .Then use it:
{
"mcpServers": {
"echo": {
"image": "mcp-echo:latest"
}
}
}| Transport | Use Case | Pros | Cons |
|---|---|---|---|
| stdio | Local MCP servers with direct access | Fast, low overhead | Requires runtime (Node.js/Python) installed |
| Docker | Isolated, reproducible MCP servers | No runtime dependencies, easy versioning | Slightly higher overhead, requires Docker |
| HTTP | Remote/cloud-hosted MCP servers | Scalable, can be load-balanced | Network latency, requires server infrastructure |
Choose Docker transport when:
- ✅ You want to avoid installing Node.js, Python, or other runtimes on your hub host
- ✅ You need consistent, reproducible environments across deployments
- ✅ You want easy version management with Docker tags
- ✅ You're running the hub in a containerized environment (Kubernetes, Docker Compose)
- ✅ You need to isolate server dependencies
- ✅ You want to use pre-built MCP server images from a registry
Choose stdio transport when:
- ✅ You're developing locally and want faster iteration
- ✅ Runtime dependencies are already installed
- ✅ You need the absolute lowest latency
Choose HTTP transport when:
- ✅ MCP servers are hosted remotely
- ✅ You need to scale servers independently
- ✅ You want to use managed MCP server services
Here's a complete example running multiple MCP servers in Docker:
# 1. Build your custom MCP server image
docker build -t my-mcp-server:v1.0 ./my-server
# 2. Create Docker network for inter-container communication
docker network create mcp-network
# 3. Configure servers
cat > config.json << 'JSON'
{
"mcpServers": {
"echo": {
"image": "mcp-echo:latest"
},
"custom-tools": {
"image": "my-mcp-server:v1.0",
"env": {
"API_KEY": "${MY_API_KEY}"
},
"volumes": {
"/host/data": "/data"
}
}
}
}
JSON
# 4. Run the hub
MY_API_KEY=secret123 ./mcp-hub --config config.jsonThis setup gives you:
- ✨ No runtime dependencies on the hub host
- ✨ Isolated environments for each MCP server
- ✨ Easy updates by changing Docker image tags
- ✨ Reproducible deployments
The mcp-hub itself can run in a Docker container. See DOCKER_DEPLOYMENT.md for complete deployment guide.
# Build the image
docker build -t mcp-hub:latest .
# Run with your config
docker run -d \
--name mcp-hub \
-p 8080:8080 \
-v $(pwd)/config.json:/app/config.json:ro \
-v /var/run/docker.sock:/var/run/docker.sock \
-e GITHUB_TOKEN=${GITHUB_TOKEN} \
mcp-hub:latestdocker-compose up -dImage Sizes:
- Standard (with Docker CLI): ~47MB
- Minimal (stdio/HTTP only): ~6MB
Environment variables are automatically passed through and expanded in your configuration.
The MCP Hub automatically watches the configuration file for changes and updates the registry accordingly. When you modify config.json, the hub will:
- Add new servers: Automatically start any newly added MCP servers
- Remove servers: Stop servers that are removed from config or disabled
- Reload servers: Restart servers whose configuration has changed
- Update registry: Keep the tool registry in sync with active servers
The watcher uses fsnotify to monitor the config file for write events. When changes are detected:
- The new config is loaded and validated
- Changes are compared with the previous configuration
- Appropriate actions are taken (start/stop/reload servers)
- The registry is automatically updated
- Changes are logged for visibility
To avoid processing rapid successive changes (e.g., when editors write multiple times), the watcher includes a 500ms debounce delay. This ensures the config is only reloaded once after you finish editing.
# Start mcp-hub
./mcp-hub --config=config.json
# In another terminal, edit config.json
vim config.json
# The hub will automatically detect changes and log:
# "config file changed, reloading..."
# "adding server: new-server"
# "loaded MCP server: new-server (stdio transport)"If the new config contains errors:
- Invalid JSON: Changes are rejected, hub continues with previous config
- Missing required fields: Changes are rejected with validation error
- Server startup failures: Logged as warnings, other servers continue running