Skip to content

Commit ea054f9

Browse files
committed
Improve code quality and fix tests
- Improve readability in func_metadata.py by using helper functions and clear if-statements - Restore complex_inputs.py example to original state from main branch - Fix test_88_random_error.py by adding list_tools handler - Refactor test_lowlevel_tool_output.py to test server behavior directly without clients - Fix README formatting for structured output examples - Add type assertions to fix pyright errors in tests This completes the code improvements requested in the PR review.
1 parent 0511acf commit ea054f9

File tree

7 files changed

+183
-517
lines changed

7 files changed

+183
-517
lines changed

README.md

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ async def fetch_weather(city: str) -> str:
255255
Tools can return structured data with automatic validation using the `structured_output=True` parameter. This ensures your tools return well-typed, validated data that clients can easily process:
256256

257257
```python
258+
from mcp.server.fastmcp import FastMCP
258259
from pydantic import BaseModel, Field
259260
from typing import TypedDict
260261

@@ -273,10 +274,7 @@ class WeatherData(BaseModel):
273274
def get_weather(city: str) -> WeatherData:
274275
"""Get structured weather data"""
275276
return WeatherData(
276-
temperature=22.5,
277-
humidity=65.0,
278-
condition="partly cloudy",
279-
wind_speed=12.3
277+
temperature=22.5, humidity=65.0, condition="partly cloudy", wind_speed=12.3
280278
)
281279

282280

@@ -290,22 +288,14 @@ class LocationInfo(TypedDict):
290288
@mcp.tool(structured_output=True)
291289
def get_location(address: str) -> LocationInfo:
292290
"""Get location coordinates"""
293-
return LocationInfo(
294-
latitude=51.5074,
295-
longitude=-0.1278,
296-
name="London, UK"
297-
)
291+
return LocationInfo(latitude=51.5074, longitude=-0.1278, name="London, UK")
298292

299293

300294
# Using dict[str, Any] for flexible schemas
301295
@mcp.tool(structured_output=True)
302296
def get_statistics(data_type: str) -> dict[str, float]:
303297
"""Get various statistics"""
304-
return {
305-
"mean": 42.5,
306-
"median": 40.0,
307-
"std_dev": 5.2
308-
}
298+
return {"mean": 42.5, "median": 40.0, "std_dev": 5.2}
309299

310300

311301
# Lists and other types are wrapped automatically

examples/fastmcp/complex_inputs.py

Lines changed: 1 addition & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
"""
22
FastMCP Complex inputs Example
33
4-
Demonstrates validation via pydantic with complex models,
5-
and structured output for returning validated data.
4+
Demonstrates validation via pydantic with complex models.
65
"""
76

8-
from datetime import datetime
97
from typing import Annotated
108

119
from pydantic import BaseModel, Field
@@ -18,12 +16,8 @@
1816
class ShrimpTank(BaseModel):
1917
class Shrimp(BaseModel):
2018
name: Annotated[str, Field(max_length=10)]
21-
color: str = "red"
22-
age_days: int = 0
2319

2420
shrimp: list[Shrimp]
25-
temperature: float = Field(default=24.0, ge=20.0, le=28.0)
26-
ph_level: float = Field(default=7.0, ge=6.5, le=8.0)
2721

2822

2923
@mcp.tool()
@@ -34,140 +28,3 @@ def name_shrimp(
3428
) -> list[str]:
3529
"""List all shrimp names in the tank"""
3630
return [shrimp.name for shrimp in tank.shrimp] + extra_names
37-
38-
39-
# Structured output example - returns a validated tank analysis
40-
class TankAnalysis(BaseModel):
41-
"""Analysis of shrimp tank conditions"""
42-
total_shrimp: int
43-
temperature_status: str # "optimal", "too_cold", "too_hot"
44-
ph_status: str # "optimal", "too_acidic", "too_basic"
45-
shrimp_by_color: dict[str, int]
46-
oldest_shrimp: str | None
47-
recommendations: list[str]
48-
49-
50-
@mcp.tool(structured_output=True)
51-
def analyze_tank(tank: ShrimpTank) -> TankAnalysis:
52-
"""Analyze tank conditions and provide recommendations"""
53-
# Temperature analysis
54-
if tank.temperature < 22:
55-
temp_status = "too_cold"
56-
elif tank.temperature > 26:
57-
temp_status = "too_hot"
58-
else:
59-
temp_status = "optimal"
60-
61-
# pH analysis
62-
if tank.ph_level < 6.8:
63-
ph_status = "too_acidic"
64-
elif tank.ph_level > 7.5:
65-
ph_status = "too_basic"
66-
else:
67-
ph_status = "optimal"
68-
69-
# Count shrimp by color
70-
color_counts: dict[str, int] = {}
71-
for shrimp in tank.shrimp:
72-
color_counts[shrimp.color] = color_counts.get(shrimp.color, 0) + 1
73-
74-
# Find oldest shrimp
75-
oldest = None
76-
if tank.shrimp:
77-
oldest_shrimp_obj = max(tank.shrimp, key=lambda s: s.age_days)
78-
oldest = oldest_shrimp_obj.name
79-
80-
# Generate recommendations
81-
recommendations = []
82-
if temp_status == "too_cold":
83-
recommendations.append("Increase water temperature to 22-26°C")
84-
elif temp_status == "too_hot":
85-
recommendations.append("Decrease water temperature to 22-26°C")
86-
87-
if ph_status == "too_acidic":
88-
recommendations.append("Add crushed coral or baking soda to raise pH")
89-
elif ph_status == "too_basic":
90-
recommendations.append("Add Indian almond leaves or driftwood to lower pH")
91-
92-
if len(tank.shrimp) > 20:
93-
recommendations.append("Consider dividing colony to prevent overcrowding")
94-
95-
if not recommendations:
96-
recommendations.append("Tank conditions are optimal!")
97-
98-
return TankAnalysis(
99-
total_shrimp=len(tank.shrimp),
100-
temperature_status=temp_status,
101-
ph_status=ph_status,
102-
shrimp_by_color=color_counts,
103-
oldest_shrimp=oldest,
104-
recommendations=recommendations
105-
)
106-
107-
108-
# Another structured output example - breeding recommendations
109-
@mcp.tool(structured_output=True)
110-
def get_breeding_pairs(tank: ShrimpTank) -> dict[str, list[str]]:
111-
"""Suggest breeding pairs by color
112-
113-
Returns a dictionary mapping colors to lists of shrimp names
114-
that could be bred together.
115-
"""
116-
pairs_by_color: dict[str, list[str]] = {}
117-
118-
for shrimp in tank.shrimp:
119-
if shrimp.age_days >= 60: # Mature enough to breed
120-
if shrimp.color not in pairs_by_color:
121-
pairs_by_color[shrimp.color] = []
122-
pairs_by_color[shrimp.color].append(shrimp.name)
123-
124-
# Only return colors with at least 2 shrimp
125-
return {
126-
color: names
127-
for color, names in pairs_by_color.items()
128-
if len(names) >= 2
129-
}
130-
131-
132-
if __name__ == "__main__":
133-
# For testing the tools
134-
import asyncio
135-
136-
async def test():
137-
# Create a test tank
138-
tank = ShrimpTank(
139-
shrimp=[
140-
ShrimpTank.Shrimp(name="Rex", color="red", age_days=90),
141-
ShrimpTank.Shrimp(name="Blue", color="blue", age_days=45),
142-
ShrimpTank.Shrimp(name="Crimson", color="red", age_days=120),
143-
ShrimpTank.Shrimp(name="Azure", color="blue", age_days=80),
144-
ShrimpTank.Shrimp(name="Jade", color="green", age_days=30),
145-
ShrimpTank.Shrimp(name="Ruby", color="red", age_days=75),
146-
],
147-
temperature=23.5,
148-
ph_level=7.2
149-
)
150-
151-
# Test name_shrimp (non-structured output)
152-
names = name_shrimp(tank, ["Bonus1", "Bonus2"])
153-
print("Shrimp names:", names)
154-
155-
# Test analyze_tank (structured output)
156-
print("\nTank Analysis:")
157-
analysis = analyze_tank(tank)
158-
print(analysis.model_dump_json(indent=2))
159-
160-
# Test get_breeding_pairs (structured output returning dict)
161-
print("\nBreeding Pairs:")
162-
pairs = get_breeding_pairs(tank)
163-
print(f"Mature shrimp by color: {pairs}")
164-
165-
# Show the tools that would be exposed
166-
print("\nAvailable tools:")
167-
tools = await mcp.list_tools()
168-
for tool in tools:
169-
print(f"- {tool.name}: {tool.description}")
170-
if tool.outputSchema:
171-
print(f" Output schema: {tool.outputSchema.get('title', tool.outputSchema.get('type', 'structured'))}")
172-
173-
asyncio.run(test())

examples/fastmcp/weather_structured.py

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# Example 1: Using a Pydantic model for structured output
2121
class WeatherData(BaseModel):
2222
"""Structured weather data response"""
23+
2324
temperature: float = Field(description="Temperature in Celsius")
2425
humidity: float = Field(description="Humidity percentage (0-100)")
2526
condition: str = Field(description="Weather condition (sunny, cloudy, rainy, etc.)")
@@ -32,18 +33,13 @@ class WeatherData(BaseModel):
3233
def get_weather(city: str) -> WeatherData:
3334
"""Get current weather for a city with full structured data"""
3435
# In a real implementation, this would fetch from a weather API
35-
return WeatherData(
36-
temperature=22.5,
37-
humidity=65.0,
38-
condition="partly cloudy",
39-
wind_speed=12.3,
40-
location=city
41-
)
36+
return WeatherData(temperature=22.5, humidity=65.0, condition="partly cloudy", wind_speed=12.3, location=city)
4237

4338

4439
# Example 2: Using TypedDict for a simpler structure
4540
class WeatherSummary(TypedDict):
4641
"""Simple weather summary"""
42+
4743
city: str
4844
temp_c: float
4945
description: str
@@ -52,27 +48,19 @@ class WeatherSummary(TypedDict):
5248
@mcp.tool(structured_output=True)
5349
def get_weather_summary(city: str) -> WeatherSummary:
5450
"""Get a brief weather summary for a city"""
55-
return WeatherSummary(
56-
city=city,
57-
temp_c=22.5,
58-
description="Partly cloudy with light breeze"
59-
)
51+
return WeatherSummary(city=city, temp_c=22.5, description="Partly cloudy with light breeze")
6052

6153

6254
# Example 3: Using dict[str, Any] for flexible schemas
6355
@mcp.tool(structured_output=True)
6456
def get_weather_metrics(cities: list[str]) -> dict[str, dict[str, float]]:
6557
"""Get weather metrics for multiple cities
66-
58+
6759
Returns a dictionary mapping city names to their metrics
6860
"""
6961
# Returns nested dictionaries with weather metrics
7062
return {
71-
city: {
72-
"temperature": 20.0 + i * 2,
73-
"humidity": 60.0 + i * 5,
74-
"pressure": 1013.0 + i * 0.5
75-
}
63+
city: {"temperature": 20.0 + i * 2, "humidity": 60.0 + i * 5, "pressure": 1013.0 + i * 0.5}
7664
for i, city in enumerate(cities)
7765
}
7866

@@ -81,6 +69,7 @@ def get_weather_metrics(cities: list[str]) -> dict[str, dict[str, float]]:
8169
@dataclass
8270
class WeatherAlert:
8371
"""Weather alert information"""
72+
8473
severity: str # "low", "medium", "high"
8574
title: str
8675
description: str
@@ -99,15 +88,15 @@ def get_weather_alerts(region: str) -> list[WeatherAlert]:
9988
title="Heat Wave Warning",
10089
description="Temperatures expected to exceed 40°C",
10190
affected_areas=["Los Angeles", "San Diego", "Riverside"],
102-
valid_until=datetime(2024, 7, 15, 18, 0)
91+
valid_until=datetime(2024, 7, 15, 18, 0),
10392
),
10493
WeatherAlert(
10594
severity="medium",
10695
title="Air Quality Advisory",
10796
description="Poor air quality due to wildfire smoke",
10897
affected_areas=["San Francisco Bay Area"],
109-
valid_until=datetime(2024, 7, 14, 12, 0)
110-
)
98+
valid_until=datetime(2024, 7, 14, 12, 0),
99+
),
111100
]
112101
return []
113102

@@ -116,26 +105,28 @@ def get_weather_alerts(region: str) -> list[WeatherAlert]:
116105
@mcp.tool(structured_output=True)
117106
def get_temperature(city: str, unit: str = "celsius") -> float:
118107
"""Get just the temperature for a city
119-
108+
120109
When returning primitives with structured_output=True,
121110
the result is wrapped in {"result": value}
122111
"""
123112
base_temp = 22.5
124113
if unit.lower() == "fahrenheit":
125-
return base_temp * 9/5 + 32
114+
return base_temp * 9 / 5 + 32
126115
return base_temp
127116

128117

129118
# Example 6: Weather statistics with nested models
130119
class DailyStats(BaseModel):
131120
"""Statistics for a single day"""
121+
132122
high: float
133123
low: float
134124
mean: float
135-
136-
125+
126+
137127
class WeatherStats(BaseModel):
138128
"""Weather statistics over a period"""
129+
139130
location: str
140131
period_days: int
141132
temperature: DailyStats
@@ -151,32 +142,32 @@ def get_weather_stats(city: str, days: int = 7) -> WeatherStats:
151142
period_days=days,
152143
temperature=DailyStats(high=28.5, low=15.2, mean=21.8),
153144
humidity=DailyStats(high=85.0, low=45.0, mean=65.0),
154-
precipitation_mm=12.4
145+
precipitation_mm=12.4,
155146
)
156147

157148

158149
if __name__ == "__main__":
159150
# For testing individual tools
160151
import asyncio
161-
152+
162153
async def test():
163154
# Test the tools
164155
weather = get_weather("London")
165156
print(f"Weather in London: {weather.model_dump_json(indent=2)}")
166-
157+
167158
summary = get_weather_summary("Paris")
168159
print(f"\nWeather summary for Paris: {summary}")
169-
160+
170161
metrics = get_weather_metrics(["Tokyo", "Sydney", "Mumbai"])
171162
print(f"\nWeather metrics: {metrics}")
172-
163+
173164
alerts = get_weather_alerts("California")
174165
print(f"\nWeather alerts: {len(alerts)} alerts found")
175-
166+
176167
temp = get_temperature("Berlin", "fahrenheit")
177168
print(f"\nTemperature in Berlin: {temp}°F")
178-
169+
179170
stats = get_weather_stats("Seattle", 30)
180171
print(f"\nWeather stats for Seattle: {stats.model_dump_json(indent=2)}")
181-
182-
asyncio.run(test())
172+
173+
asyncio.run(test())

src/mcp/client/session.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
396396
)
397397

398398
# Cache tool output schemas for future validation
399+
# Note: don't clear the cache, as we may be using a cursor
399400
for tool in result.tools:
400401
self._tool_output_schemas[tool.name] = tool.outputSchema
401402

0 commit comments

Comments
 (0)