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.

MethodWhen 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:

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:

  1. Receive a request
  2. Call another server’s tool
  3. Ask the client’s LLM to process results
  4. Return the final answer

Server-side agent loops become possible without managing API keys in each server.

Debugging composition

ProblemCheck
Tool not foundclaude mcp list shows all servers?
Wrong server respondingTool names unique across servers?
Server not startingRun server command manually to see errors
TimeoutServer process running? Check logs

Logs live at ~/.claude/mcp/[server-name]/logs/.

When to compose vs. build

SituationApproach
Tools exist as separate serversCompose via config
Building tightly coupled featuresSingle server with FastMCP mount
Team owns different capabilitiesSeparate servers, compose at runtime
Need to share server with othersSingle-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

Topics: mcp architecture ai-agents