|
| 1 | +# Organizing Larger FastMCP Servers |
| 2 | + |
| 3 | +As your MCP server grows beyond the initial quickstart examples, you may find that organizing all tools, resources, and prompts in a single file becomes unwieldy. This guide presents a recommended pattern for structuring larger FastMCP servers and managing tool versions. |
| 4 | + |
| 5 | +## When to Use This Pattern |
| 6 | + |
| 7 | +Consider this organizational approach when: |
| 8 | + |
| 9 | +- Your server exposes more than 5-10 tools |
| 10 | +- You need to maintain multiple versions of tools |
| 11 | +- Multiple developers are working on the codebase |
| 12 | +- You want to separate concerns and improve code maintainability |
| 13 | + |
| 14 | +For simple servers with just a few tools, the single-file quickstart pattern is perfectly fine. |
| 15 | + |
| 16 | +## Recommended Project Layout |
| 17 | + |
| 18 | +Here's the recommended structure for organizing a larger FastMCP server: |
| 19 | + |
| 20 | +```text |
| 21 | +my_fastmcp_server/ |
| 22 | + server.py # FastMCP wiring and server startup |
| 23 | + tools/ |
| 24 | + __init__.py |
| 25 | + get_info.py # get_info_v1, get_info_v2, ... |
| 26 | + other_tool.py # other_tool_v1, ... |
| 27 | + resources/ # (optional) if you have many resources |
| 28 | + __init__.py |
| 29 | + ... |
| 30 | + prompts/ # (optional) if you have many prompts |
| 31 | + __init__.py |
| 32 | + ... |
| 33 | +``` |
| 34 | + |
| 35 | +### Benefits of This Structure |
| 36 | + |
| 37 | +**Per-tool modules** help with: |
| 38 | + |
| 39 | +- **Code organization**: Each conceptual tool lives in its own file |
| 40 | +- **Team collaboration**: Reduces merge conflicts when multiple developers work on different tools |
| 41 | +- **Testing**: Makes unit testing individual tools easier |
| 42 | +- **Documentation**: Tool implementations are self-contained and easier to document |
| 43 | + |
| 44 | +**Multi-version functions in the same file** enable: |
| 45 | + |
| 46 | +- **Easy comparison**: See all versions of a tool side-by-side |
| 47 | +- **Reduced duplication**: Share helper functions between versions |
| 48 | +- **Clear diffs**: Review changes between versions more easily |
| 49 | +- **Maintenance**: Update shared logic across versions in one place |
| 50 | + |
| 51 | +## Tool Versioning Pattern |
| 52 | + |
| 53 | +FastMCP servers can expose multiple versions of a tool simultaneously using **name-based versioning**. This pattern works with the current SDK without requiring protocol-level versioning support. |
| 54 | + |
| 55 | +### Version Naming Convention |
| 56 | + |
| 57 | +Include the major version number in the tool name: |
| 58 | + |
| 59 | +- `get_info_v1` - Version 1 of the tool |
| 60 | +- `get_info_v2` - Version 2 of the tool |
| 61 | +- `get_info_v3` - Version 3 of the tool |
| 62 | + |
| 63 | +### When to Create a New Version |
| 64 | + |
| 65 | +Create a new major version when making **breaking changes**: |
| 66 | + |
| 67 | +- **Changed mandatory parameters**: Adding required parameters, removing parameters, or changing parameter types |
| 68 | +- **Changed semantics**: Altering the tool's behavior in ways that would surprise existing clients |
| 69 | +- **Changed output format**: Non-backward-compatible changes to the response structure |
| 70 | +- **Changed side effects**: Modifications that would break existing client workflows |
| 71 | + |
| 72 | +For **non-breaking changes** (bug fixes, performance improvements, additional optional parameters with defaults), keep the same version number. |
| 73 | + |
| 74 | +## Complete Example |
| 75 | + |
| 76 | +### Server Entrypoint (`server.py`) |
| 77 | + |
| 78 | +<!-- snippet-source examples/snippets/servers/server_layout/server.py --> |
| 79 | +```python |
| 80 | +""" |
| 81 | +Example FastMCP server demonstrating recommended layout for larger servers. |
| 82 | +
|
| 83 | +This server shows how to: |
| 84 | +- Organize tools into separate modules |
| 85 | +- Implement versioned tools using name-based versioning |
| 86 | +- Structure a maintainable FastMCP server |
| 87 | +
|
| 88 | +Run from the repository root: |
| 89 | + uv run examples/snippets/servers/server_layout/server.py |
| 90 | +""" |
| 91 | + |
| 92 | +from mcp.server.fastmcp import FastMCP |
| 93 | + |
| 94 | +# Import tool implementations from the tools package |
| 95 | +from servers.server_layout.tools import get_info |
| 96 | + |
| 97 | +# Create the FastMCP server instance |
| 98 | +mcp = FastMCP("ServerLayoutDemo", json_response=True) |
| 99 | + |
| 100 | + |
| 101 | +# Register version 1 of the get_info tool |
| 102 | +# The function name determines the tool name exposed to clients |
| 103 | +@mcp.tool() |
| 104 | +def get_info_v1(topic: str) -> str: |
| 105 | + """Get basic information about a topic (v1). |
| 106 | +
|
| 107 | + Version 1 provides simple string output with basic information. |
| 108 | +
|
| 109 | + Args: |
| 110 | + topic: The topic to get information about |
| 111 | +
|
| 112 | + Returns: |
| 113 | + A simple string with basic information |
| 114 | + """ |
| 115 | + return get_info.get_info_v1(topic) |
| 116 | + |
| 117 | + |
| 118 | +# Register version 2 of the get_info tool |
| 119 | +# Breaking changes from v1: different return type and new parameter |
| 120 | +@mcp.tool() |
| 121 | +def get_info_v2(topic: str, include_metadata: bool = False) -> dict[str, str]: |
| 122 | + """Get information about a topic with optional metadata (v2). |
| 123 | +
|
| 124 | + Version 2 introduces breaking changes: |
| 125 | + - Returns structured dict instead of string (breaking change) |
| 126 | + - Adds include_metadata parameter for richer output |
| 127 | +
|
| 128 | + Args: |
| 129 | + topic: The topic to get information about |
| 130 | + include_metadata: Whether to include additional metadata |
| 131 | +
|
| 132 | + Returns: |
| 133 | + A dictionary with structured information |
| 134 | + """ |
| 135 | + return get_info.get_info_v2(topic, include_metadata) |
| 136 | + |
| 137 | + |
| 138 | +# Run the server |
| 139 | +if __name__ == "__main__": |
| 140 | + mcp.run(transport="streamable-http") |
| 141 | +``` |
| 142 | + |
| 143 | +_Full example: [examples/snippets/servers/server_layout/server.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/server_layout/server.py)_ |
| 144 | +<!-- /snippet-source --> |
| 145 | + |
| 146 | +### Tool Implementation (`tools/get_info.py`) |
| 147 | + |
| 148 | +<!-- snippet-source examples/snippets/servers/server_layout/tools/get_info.py --> |
| 149 | +```python |
| 150 | +""" |
| 151 | +Example tool module showing versioned tool implementations. |
| 152 | +
|
| 153 | +This module demonstrates the recommended pattern for managing |
| 154 | +multiple versions of a tool in a single file. |
| 155 | +""" |
| 156 | + |
| 157 | + |
| 158 | +def get_info_v1(topic: str) -> str: |
| 159 | + """Get basic information about a topic (v1). |
| 160 | +
|
| 161 | + Version 1 provides simple string output with basic information. |
| 162 | +
|
| 163 | + Args: |
| 164 | + topic: The topic to get information about |
| 165 | +
|
| 166 | + Returns: |
| 167 | + A simple string with basic information |
| 168 | + """ |
| 169 | + return f"Information about {topic}: This is version 1 with basic details." |
| 170 | + |
| 171 | + |
| 172 | +def get_info_v2(topic: str, include_metadata: bool = False) -> dict[str, str]: |
| 173 | + """Get information about a topic with optional metadata (v2). |
| 174 | +
|
| 175 | + Version 2 introduces breaking changes: |
| 176 | + - Returns structured dict instead of string (breaking change) |
| 177 | + - Adds include_metadata parameter for richer output |
| 178 | +
|
| 179 | + Args: |
| 180 | + topic: The topic to get information about |
| 181 | + include_metadata: Whether to include additional metadata |
| 182 | +
|
| 183 | + Returns: |
| 184 | + A dictionary with structured information |
| 185 | + """ |
| 186 | + result = { |
| 187 | + "topic": topic, |
| 188 | + "description": f"This is version 2 with enhanced details about {topic}.", |
| 189 | + "version": "2", |
| 190 | + } |
| 191 | + |
| 192 | + if include_metadata: |
| 193 | + result["metadata"] = { |
| 194 | + "source": "server_layout_example", |
| 195 | + "confidence": "high", |
| 196 | + } |
| 197 | + |
| 198 | + return result |
| 199 | +``` |
| 200 | + |
| 201 | +_Full example: [examples/snippets/servers/server_layout/tools/get_info.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/server_layout/tools/get_info.py)_ |
| 202 | +<!-- /snippet-source --> |
| 203 | + |
| 204 | +## Running the Example |
| 205 | + |
| 206 | +To run the complete example server: |
| 207 | + |
| 208 | +```bash |
| 209 | +# From the repository root |
| 210 | +uv run examples/snippets/servers/server_layout/server.py |
| 211 | +``` |
| 212 | + |
| 213 | +The server will start on `http://localhost:8000/mcp` and expose both `get_info_v1` and `get_info_v2` tools. |
| 214 | + |
| 215 | +You can test it with the MCP Inspector: |
| 216 | + |
| 217 | +```bash |
| 218 | +npx -y @modelcontextprotocol/inspector |
| 219 | +``` |
| 220 | + |
| 221 | +Then connect to `http://localhost:8000/mcp` in the inspector UI. |
| 222 | + |
| 223 | +## Client Considerations |
| 224 | + |
| 225 | +When connecting to servers that expose multiple tool versions: |
| 226 | + |
| 227 | +### Using Tool Whitelists |
| 228 | + |
| 229 | +Clients should use a **whitelist** to explicitly control which tools they interact with: |
| 230 | + |
| 231 | +```python |
| 232 | +# Client configuration (conceptual) |
| 233 | +allowed_tools = [ |
| 234 | + "get_info_v1", # Use only v1 for now |
| 235 | + "other_tool_v2" |
| 236 | +] |
| 237 | + |
| 238 | +# Filter available tools based on whitelist |
| 239 | +available_tools = await session.list_tools() |
| 240 | +usable_tools = [ |
| 241 | + tool for tool in available_tools.tools |
| 242 | + if tool.name in allowed_tools |
| 243 | +] |
| 244 | +``` |
| 245 | + |
| 246 | +### Version Selection Strategy |
| 247 | + |
| 248 | +Clients can adopt different strategies: |
| 249 | + |
| 250 | +- **Conservative**: Pin to a specific version (e.g., always use `v1`) |
| 251 | +- **Latest stable**: Use the highest version known to be stable |
| 252 | +- **Fallback chain**: Try `v2`, fall back to `v1` if unavailable |
| 253 | +- **Per-operation**: Use different versions for different use cases |
| 254 | + |
| 255 | +## Advanced Patterns |
| 256 | + |
| 257 | +### Sharing Logic Between Versions |
| 258 | + |
| 259 | +When multiple versions share common logic: |
| 260 | + |
| 261 | +```python |
| 262 | +def _fetch_data(topic: str) -> dict: |
| 263 | + """Internal helper shared by multiple versions.""" |
| 264 | + # Common data fetching logic |
| 265 | + return {"raw_data": f"Data for {topic}"} |
| 266 | + |
| 267 | + |
| 268 | +def get_info_v1(topic: str) -> str: |
| 269 | + """Version 1: simple output.""" |
| 270 | + data = _fetch_data(topic) |
| 271 | + return f"Info: {data['raw_data']}" |
| 272 | + |
| 273 | + |
| 274 | +def get_info_v2(topic: str) -> dict: |
| 275 | + """Version 2: structured output.""" |
| 276 | + data = _fetch_data(topic) |
| 277 | + return {"topic": topic, "data": data["raw_data"]} |
| 278 | +``` |
| 279 | + |
| 280 | +### Deprecating Old Versions |
| 281 | + |
| 282 | +Use docstrings to communicate deprecation: |
| 283 | + |
| 284 | +```python |
| 285 | +def get_info_v1(topic: str) -> str: |
| 286 | + """Get basic information about a topic (v1). |
| 287 | +
|
| 288 | + .. deprecated:: |
| 289 | + Use get_info_v2 for richer structured output. |
| 290 | + This version will be removed in a future release. |
| 291 | + """ |
| 292 | + # Implementation... |
| 293 | +``` |
| 294 | + |
| 295 | +Server operators can remove old versions in new releases once clients have migrated. |
| 296 | + |
| 297 | +## Future: Protocol-Level Versioning |
| 298 | + |
| 299 | +This guide documents a pattern that works with the **current SDK** (main branch). The MCP protocol may introduce native tool versioning in the future, which would allow version metadata at the protocol level. When that becomes available, you'll be able to enhance this pattern with additional version fields while maintaining backward compatibility with name-based versioning. |
| 300 | + |
| 301 | +## Summary |
| 302 | + |
| 303 | +- **Single entrypoint** (`server.py`) for server wiring |
| 304 | +- **Per-tool modules** (`tools/get_info.py`) for organization |
| 305 | +- **Name-based versioning** (`get_info_v1`, `get_info_v2`) for managing breaking changes |
| 306 | +- **Client whitelists** for explicit version control |
| 307 | +- **Side-by-side versions** in the same module for easy comparison and maintenance |
| 308 | + |
| 309 | +This pattern scales well as your server grows and helps maintain stability for clients as your tool APIs evolve. |
0 commit comments