Your first MCP server and a local agent
- Source
- From the RAG & Agents Engineering course
- By
- Sourav Banerjee
- Last updated
- 9 Nov 2026
A local agent over MCP
Larkspur exposes a numeric tool through MCP and expects the client to validate its output. You will connect a local agent to the server, inspect its step budget and compare a returned failure string with the declared numeric result schema.
Step 1: Choose who starts each server
The first project serves two FastMCP servers over streamable HTTP: demo-server with one add tool, and weather with get_weather and get_forecast, which call the public wttr.in API. Each comes with a plain client that lists the tools and calls one. client_fastmcp.py uses FastMCP's Client on http://127.0.0.1:8000/mcp; client_mcp.py uses the SDK's streamablehttp_client and ClientSession, and prints the session id before and after initialize: None, then a 32-character hex id the server assigned during the handshake (T12.2 covers that header).
The second project hands the same servers to an agent that runs on your machine. mcp_use reads a server map in the shape Claude Desktop uses, starts or connects to each server, and gives their tools to a LangChain chat model:
Implementation excerpt from the configured application. The practice material supplies its models, data and session setup:
client = MCPClient.from_config_file("mcp-http.json") # or MCPClient.from_dict(config)
llm = ChatOllama(base_url="http://localhost:11434", model="gpt-oss:20b")
agent = MCPAgent(llm=llm, client=client, max_steps=30, use_server_manager=True)
await agent.run("What's the weather in New York and the 3-day forecast? Also, what's 12 multiplied by 342?")
The project's two config files show the two ways a host reaches a server:
| Config | Entry | Who starts the server |
|---|---|---|
mcp.json |
"command": "uv", "args": ["run", "--directory", "…/servers", "weather_server.py"] |
The agent, as a stdio subprocess |
mcp-http.json |
"url": "http://localhost:8001/mcp" (weather), :8000/mcp (math) |
You, in another terminal, before the agent runs |
Step 2: Inspect loop and memory settings
Two MCPAgent settings change what the model does, and both have defaults worth knowing in mcp-use 1.7.1:
max_stepscaps the agent loop. The default is 5; both clients raise it to 30. The advanced query needs at least three tool calls (weather, forecast, multiply), so a low cap ends the run before the answer.use_server_manager=Truestops the model from seeing the servers' tools directly. It gets five management tools instead (list_mcp_servers,connect_to_mcp_server,get_active_mcp_server,search_mcp_tools,disconnect_from_mcp_server) and must connect to a server before it can callmultiply. That keeps the prompt small when many servers are configured, at the price of extra model turns.
Step 3: Separate local inference from tool connectivity
The models are local Ollama models (qwen3:8b in agentic_client.py, gpt-oss:20b in the advanced client), so the whole loop runs without an API key. The weather tools still call wttr.in over the internet.
Return types are part of the contract
Step 1: Validate a returned result against its schema
FastMCP builds a tool's output schema from its return annotation. In fastmcp 3.4.11, add(a: int, b: int) -> int lists as:
{"inputSchema": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}}, "required": ["a", "b"], "type": "object", "additionalProperties": false},
"outputSchema": {"properties": {"result": {"type": "integer"}}, "required": ["result"], "type": "object", "x-fastmcp-wrap-result": true}}
Step 2: Compare text with structured output
A call to add with 5 and 7 returns the text 12 and the structured content {"result": 12}. The client keeps the output schema and checks every structured result against it.
Step 3: Trigger a result-schema mismatch
The math server's divide breaks its own schema. It is annotated -> float and returns the string "Error: Division by zero" when b is 0. The server sends that result; the mcp 1.30.0 client rejects it:
RuntimeError: Invalid structured content returned by tool divide: 'Error: Division by zero' is not of type 'number'
The friendly message never reaches the model. The host gets an exception from its own client library instead of a tool result it could show the model. Two fixes keep the error inside the protocol, both checked against fastmcp 3.4.11:
- Raise instead of returning.
raise ToolError("Division by zero")comes back as a result withisError: trueand the textDivision by zero. Without the check,1 / 0raisesZeroDivisionError, which also comes back withisError: true, asError calling tool 'divide': division by zero. - Annotate what you return.
-> float | strmakes the string valid, and moves the job of telling numbers from error messages to every client.
Try it yourself
Call add with five and seven and compare text 12 with structured result 12. Then inspect divide with a zero divisor: returning an error string violates its float output contract. Check the client exception before assuming a friendly error reached the model, and compare raising ToolError instead.
Practice notebook
Run the complete implementation from the practice material after checking the trace or calculation above. Use its stated environment for live model or server calls; recorded model choices and timings are observations, not guaranteed outputs.
Continue learning
Run the notebooks
The code for this lesson is in these notebooks. Colab runs one in your browser: install the packages its first cell imports and set the API keys it reads. To run every notebook as written, clone the repository and use its uv environment.
| Notebook | GitHub | Colab |
|---|---|---|
| Build your first MCP server | Open on GitHub | Run locally |
| MCP servers and local agent | Open on GitHub | Run locally |
Key Takeaways
- A host reaches a server either by command, starting it as a stdio subprocess, or by URL, where you start the server first.
- Raise
max_stepsabove the default of 5 when a query needs several tool calls.use_server_manager=Truekeeps the prompt small but costs extra model turns. - The return annotation is a contract the client enforces. Raise
ToolErrorfor failures instead of returning a string from a tool typed-> float.
Check yourself
1. What does use_server_manager=True change about what the model sees, and what does it trade?
The model no longer sees the servers' tools directly. It gets five management tools (list_mcp_servers, connect_to_mcp_server, get_active_mcp_server, search_mcp_tools, disconnect_from_mcp_server) and must connect to a server before it can call multiply. That keeps the prompt small when many servers are configured, at the price of extra model turns.
2. The advanced query asks for New York's weather, its 3-day forecast and 12 times 342. Why do both clients raise max_steps from its default of 5 to 30?
max_steps caps the agent loop. The query needs at least three tool calls (weather, forecast, multiply), and with the server manager on the model also spends turns connecting to servers, so a low cap ends the run before the answer.
3. Calling divide with b equal to 0 gives the host a RuntimeError about invalid structured content instead of a tool result. Why, and how do you fix it?
divide is annotated -> float but returns the string "Error: Division by zero", and the mcp 1.30.0 client checks structured results against the output schema, so it raises. Use raise ToolError("Division by zero"), which comes back with isError: true, or annotate -> float | str, which moves the job of telling numbers from errors to every client.