Objective
Break it: an empty or missing argument
Ask Claude something ambiguous enough that it calls get_weather with an empty string, or call read_workspace_file for a file that doesn't exist. Watch what comes back.
An unhandled exception here is worse than it looks
The SDK catches an exception your tool raises and turns it into an error result automatically -- your server process doesn't crash from one bad tool call. But for an unexpected exception the message the model receives is just Error executing tool get_weather: it knows the call failed, and has no idea why or what to try next. The fix isn't "prevent all errors" -- it's "decide what the model should see when one happens."
Validate before you act, not after you fail
@mcp.tool()
def get_weather(city: str) -> str:
"""Get current weather for a city."""
if not city or not city.strip():
raise ToolError("Please provide a city name")
# ... rest of the implementation
This one-line guard turns "the call failed for some reason" into "the tool tells the model exactly what was wrong, and the model can ask the user to clarify." The pattern generalizes: check for the obviously-invalid cases at the top of every tool, before any I/O happens, and raise ToolError with a message that names the problem and the fix. ToolError comes from mcp.server.mcpserver.exceptions.
Structured errors: the isError pattern
Every tool result carries an isError flag. Any exception that escapes your tool sets it -- but only ToolError carries your own message through. So catch the failure modes you expect and re-raise them as ToolError with a clear message; let truly unexpected bugs surface during development.
Do not "handle" an error by returning an error string. A returned string is a successful result (isError: false), so the host and the model treat "Error: city not found" as the weather.
Your notify tool's webhook call times out because the user's Discord webhook URL is temporarily unreachable. Your code doesn't catch this -- httpx raises TimeoutException, which propagates out of the tool function. What does the model see?
The SDK's automatic exception handling keeps the server alive, which is the good news -- an unhandled exception in one tool doesn't take down the other four. The bad news is that the model learns only that notify failed, not why, so it cannot tell the user "the notification couldn't be sent -- want me to retry?". This is exactly why Lesson 5's lab has you wrap every tool's I/O in a try/except that raises ToolError with a clear message.
What you're building
All 5 tools get an input-validation guard at the top and a try/except around their actual I/O (SQLite query, file read, HTTP call, webhook POST), each raising ToolError with a clear, specific message instead of relying on the SDK's default "Error executing tool" — proven by deliberately breaking all 5 with bad input and confirming each failure comes back with isError: true and a message the model can act on.