MCP Server Composition
Table of content
MCP server composition is connecting multiple specialized servers to your AI agent simultaneously. Your agent talks to your calendar, database, and file system through the same protocol. David Soria Parra, who co-created MCP at Anthropic, designed the protocol specifically to solve this integration problem.
Why compose servers
Single-purpose servers stay maintainable. A calendar server handles scheduling. A database server handles queries. A file server handles documents. Composition lets you combine them without merging codebases.
[Claude Code]
↓
[MCP Client]
├── [Calendar Server] → Google Calendar
├── [Database Server] → PostgreSQL
├── [File Server] → Local filesystem
└── [Search Server] → Exa, web search
One request can span multiple servers. “Find my meeting notes from last Tuesday and add a follow-up task” hits calendar, files, and tasks.
Configuration
Claude Code connects to multiple servers through ~/.claude/mcp.json:
{
"servers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/docs"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_xxx"
}
},
"calendar": {
"command": "python",
"args": ["/path/to/calendar_server.py"]
},
"memory": {
"command": "npx",
"args": ["-y", "episodic-memory"]
}
}
}
Each server runs as a separate process. Claude Code manages the connections.
FastMCP composition patterns
FastMCP provides two methods for combining servers in Python:
Import (static copy)
Copy tools from one server into another at startup:
from mcp.server.fastmcp import FastMCP
main = FastMCP("main")
weather = FastMCP("weather")
calendar = FastMCP("calendar")
@weather.tool()
def get_forecast(city: str) -> str:
"""Get weather forecast."""
return f"Sunny in {city}"
@calendar.tool()
def next_meeting() -> str:
"""Get next calendar event."""
return "Team standup at 10am"
# Copy tools with prefix
main.import_server(weather, prefix="weather")
main.import_server(calendar, prefix="cal")
# Tools available as: weather_get_forecast, cal_next_meeting
Import works when you control all servers and want a single deployment.
Mount (dynamic delegation)
Route requests to sub-servers at runtime:
from mcp.server.fastmcp import FastMCP
main = FastMCP("main")
weather = FastMCP("weather")
database = FastMCP("database")
@weather.tool()
def forecast(city: str) -> str:
return f"Weather for {city}"
@database.tool()
def query(sql: str) -> str:
return f"Results for: {sql}"
# Live link - requests delegated to subservers
main.mount("weather", weather)
main.mount("db", database)
Mount keeps servers isolated. Changes to subservers reflect immediately. Use this for modular architectures where teams maintain separate servers.
| Method | When to use |
|---|---|
import_server() | Single deployment, want all tools in one process |
mount() | Microservices, separate teams, runtime flexibility |
Practical compositions
Developer workflow
{
"servers": {
"github": { "command": "..." },
"filesystem": { "command": "..." },
"memory": { "command": "..." },
"shell": { "command": "..." }
}
}
Handles: code review, file operations, cross-session context, command execution.
Research workflow
{
"servers": {
"exa": { "command": "..." },
"web-fetch": { "command": "..." },
"memory": { "command": "..." },
"obsidian": { "command": "..." }
}
}
Handles: web search, URL scraping, remembering findings, note storage.
Productivity workflow
{
"servers": {
"google-calendar": { "command": "..." },
"things": { "command": "..." },
"slack": { "command": "..." },
"memory": { "command": "..." }
}
}
Handles: scheduling, task management, team communication, context retention.
Name conflicts
When multiple servers expose similar tools, prefixing prevents collisions:
# Without prefix
main.import_server(server_a) # has "search" tool
main.import_server(server_b) # also has "search" - overwrites!
# With prefix
main.import_server(server_a, prefix="files") # files_search
main.import_server(server_b, prefix="web") # web_search
With mounted servers in Claude Code, tools from each server already namespace by server name in the interface.
November 2025 spec: Tasks
The 2025-11-25 MCP specification added Tasks for long-running operations. A tool call returns a task handle instead of blocking:
@mcp.tool()
async def analyze_dataset(path: str) -> Task:
"""Start async analysis, return task handle."""
task = create_task()
# Processing happens in background
background_analyze(path, task.id)
return task
Clients poll for completion. This matters for composed systems where one request triggers work across multiple servers, some fast, some slow.
Tasks enable:
- Data processing jobs that run for minutes
- Multi-agent coordination where sub-agents work concurrently
- Test suites that stream results as they complete
- Research tools that spawn parallel searches
Sampling with tools
The November 2025 spec also added Sampling with Tools. Servers can request LLM completions from the client, with tool definitions included.
A composed server can:
- Receive a request
- Call another server’s tool
- Ask the client’s LLM to process results
- Return the final answer
Server-side agent loops become possible without managing API keys in each server.
Debugging composition
| Problem | Check |
|---|---|
| Tool not found | claude mcp list shows all servers? |
| Wrong server responding | Tool names unique across servers? |
| Server not starting | Run server command manually to see errors |
| Timeout | Server process running? Check logs |
Logs live at ~/.claude/mcp/[server-name]/logs/.
When to compose vs. build
| Situation | Approach |
|---|---|
| Tools exist as separate servers | Compose via config |
| Building tightly coupled features | Single server with FastMCP mount |
| Team owns different capabilities | Separate servers, compose at runtime |
| Need to share server with others | Single-purpose server, let users compose |
The MCP Registry lists close to 2,000 servers. Check there before building from scratch.
Next: Building Your First MCP Server
Get updates
New guides, workflows, and AI patterns. No spam.
Thank you! You're on the list.