Skip to content

Commit 642a915

Browse files
committed
docs: add guide for organizing larger FastMCP servers with versioned tools
- Add docs/server_organization.md with recommended patterns - Create examples/snippets/servers/server_layout/ demonstrating organization - Show name-based tool versioning (tool_v1, tool_v2) - Wire new docs page into mkdocs.yml navigation - No changes to core library behavior
1 parent 5983a65 commit 642a915

File tree

7 files changed

+433
-1
lines changed

7 files changed

+433
-1
lines changed

docs/server_organization.md

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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.

examples/snippets/servers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def run_server():
2222
print("Usage: server <server-name> [transport]")
2323
print("Available servers: basic_tool, basic_resource, basic_prompt, tool_progress,")
2424
print(" sampling, elicitation, completion, notifications,")
25-
print(" fastmcp_quickstart, structured_output, images")
25+
print(" fastmcp_quickstart, structured_output, images,")
26+
print(" server_layout")
2627
print("Available transports: stdio (default), sse, streamable-http")
2728
sys.exit(1)
2829

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Example demonstrating recommended layout for larger FastMCP servers.
3+
4+
This example shows how to organize tools into separate modules
5+
and implement versioned tools using name-based versioning.
6+
"""
7+
8+
from .server import mcp
9+
10+
__all__ = ["mcp"]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
Example FastMCP server demonstrating recommended layout for larger servers.
3+
4+
This server shows how to:
5+
- Organize tools into separate modules
6+
- Implement versioned tools using name-based versioning
7+
- Structure a maintainable FastMCP server
8+
9+
Run from the repository root:
10+
uv run examples/snippets/servers/server_layout/server.py
11+
"""
12+
13+
from mcp.server.fastmcp import FastMCP
14+
15+
# Import tool implementations from the tools package
16+
from servers.server_layout.tools import get_info
17+
18+
# Create the FastMCP server instance
19+
mcp = FastMCP("ServerLayoutDemo", json_response=True)
20+
21+
22+
# Register version 1 of the get_info tool
23+
# The function name determines the tool name exposed to clients
24+
@mcp.tool()
25+
def get_info_v1(topic: str) -> str:
26+
"""Get basic information about a topic (v1).
27+
28+
Version 1 provides simple string output with basic information.
29+
30+
Args:
31+
topic: The topic to get information about
32+
33+
Returns:
34+
A simple string with basic information
35+
"""
36+
return get_info.get_info_v1(topic)
37+
38+
39+
# Register version 2 of the get_info tool
40+
# Breaking changes from v1: different return type and new parameter
41+
@mcp.tool()
42+
def get_info_v2(topic: str, include_metadata: bool = False) -> dict[str, str]:
43+
"""Get information about a topic with optional metadata (v2).
44+
45+
Version 2 introduces breaking changes:
46+
- Returns structured dict instead of string (breaking change)
47+
- Adds include_metadata parameter for richer output
48+
49+
Args:
50+
topic: The topic to get information about
51+
include_metadata: Whether to include additional metadata
52+
53+
Returns:
54+
A dictionary with structured information
55+
"""
56+
return get_info.get_info_v2(topic, include_metadata)
57+
58+
59+
# Run the server
60+
if __name__ == "__main__":
61+
mcp.run(transport="streamable-http")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tools package for the server layout example."""

0 commit comments

Comments
 (0)