The Model Context Protocol (MCP) is a standard for connecting AI coding assistants to external tools and data sources. An MCP server exposes tools that the assistant can discover and call at runtime, which means capabilities live outside the assistant itself and can be added, removed, or swapped without touching the assistant’s code. We will build a small weather server in Python, expose it over two different transports (local stdio and remote HTTP), and wire both into opencode to see how each one behaves.
The weather data is hardcoded and uninteresting on purpose, since what matters here is the protocol mechanics: how tools get declared, how the assistant discovers them, and what happens at the transport layer when things go wrong.
mkdir mcp-demo && cd mcp-demo
python -m venv .venv && source .venv/bin/activate
pip install mcpOpencode should already be installed and working from previous labs.
Create weather_server.py. The entire server fits in one
file because the SDK handles all the protocol negotiation, and a
--transport flag at the bottom selects between stdio and
HTTP so the tool logic stays identical regardless of how the client
connects:
import argparse
from mcp.server.fastmcp import FastMCP
parser = argparse.ArgumentParser()
parser.add_argument("--transport", choices=["stdio", "sse"], default="stdio")
parser.add_argument("--port", type=int, default=8080)
args = parser.parse_args()
mcp = FastMCP("weather", host="127.0.0.1", port=args.port)
@mcp.tool()
def get_weather(city: str) -> str:
"""Return the current weather for a city."""
data = {
"london": "12C, overcast",
"tokyo": "24C, sunny",
"new york": "18C, partly cloudy",
"paris": "15C, light rain",
}
result = data.get(city.lower())
return f"Weather in {city}: {result}" if result else f"No data for {city}."
@mcp.tool()
def list_cities() -> list[str]:
"""List all cities with available weather data."""
return ["London", "Tokyo", "New York", "Paris"]
if __name__ == "__main__":
mcp.run(transport=args.transport)@mcp.tool() is the only thing needed to register a
function. The SDK infers the tool’s name, parameter types, and
description from the function signature and docstring, which is also all
the information the assistant will see when it decides whether to call
it.
Try running the server directly:
python weather_server.py --transport stdioNothing visible happens, which is expected: the server is waiting on
stdin for JSON-RPC messages because in stdio mode the host process
spawns it as a subprocess and pipes messages to it. Ctrl-C
to kill it.
To register it with opencode, create (or edit)
opencode.jsonc in the project root:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"weather-local": {
"type": "local",
"command": ["/absolute/path/to/mcp-demo/.venv/bin/python",
"/absolute/path/to/mcp-demo/weather_server.py",
"--transport", "stdio"],
"enabled": true
}
}
}
Both paths must be absolute because opencode spawns the process from
its own working directory, and the Python binary must point into the
venv where mcp is installed rather than the system Python.
Launch opencode and try:
use the weather-local tool to get the weather in Tokyo
Opencode spawns the server process, sends a get_weather
call over stdin, reads the result from stdout, and kills the process
when the session ends.
Open a second terminal and start the server in HTTP mode, where it stays alive independently of any client:
python weather_server.py --transport sse --port 8080The server should print a startup message and start listening on port
8080. The endpoint opencode will connect to is
http://localhost:8080/sse.
Add a second entry alongside the local one in
opencode.jsonc:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"weather-local": {
"type": "local",
"command": ["/absolute/path/to/mcp-demo/.venv/bin/python",
"/absolute/path/to/mcp-demo/weather_server.py",
"--transport", "stdio"],
"enabled": true
},
"weather-remote": {
"type": "remote",
"url": "http://localhost:8080/sse",
"enabled": true
}
}
}
Restart opencode (config is read at startup, not live-reloaded) and prompt:
use the weather-remote tool to list available cities
With both entries enabled, both servers are available in the same opencode session. The behaviour is identical, only the transport differs: stdio is tied to the client session, while a remote server runs independently and could just as well be on a different machine (the URL would simply point to a real host instead of localhost).
Add a third tool to the server,
get_forecast(city: str, days: int) -> str, that returns
a made-up multi-day forecast (something along the lines of “Day 1: 14C
rain, Day 2: 16C cloudy”). After restarting opencode, verify that the
assistant discovers the new tool and can call it without any changes to
opencode.jsonc, since tool discovery happens at connection
time through the protocol itself.
Then try prompting opencode with just “what’s the weather going to be like in Tokyo this week” and observe whether it figures out on its own which tool to call and what arguments to pass. The docstring and type signature are the only information guiding that decision.
Replace the hardcoded weather dictionary with a call to the open-meteo API, which we already used in Lab 1 and which requires no authentication:
https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35¤t_weather=true
The server should map city names to coordinates through a hardcoded
dictionary (5-10 cities is enough), call the API with
urllib.request or httpx, and return the actual
temperature and weather code. Test through opencode to confirm it works
end-to-end, and check what happens when the API is unreachable or the
city is not in the dictionary.
MCP servers can also expose resources, which are read-only data that the assistant pulls into its context. A tool is something the assistant calls to perform an action, whereas a resource is something it reads for background information. The decorator works similarly:
@mcp.resource("notes://summary")
def project_summary() -> str:
"""A short summary of the project."""
return "This project is a weather MCP server for demonstration purposes."Resources can also be templated with URI parameters, so
"notes://{topic}" would let the assistant request different
topics by name.
Add two or three resources to the weather server: a static one
(e.g. "weather://info" returning a description of what
cities and data the server covers) and a templated one
(e.g. "weather://history/{city}" returning some made-up
historical averages for a given city). Restart opencode and ask it a
question that requires both a resource read and a tool call, such as
“what cities do we have data for and what’s the weather in the warmest
one?” Observe how the assistant decides which to use.