Practical Examples
Three progressive cases: a local notes resource, wrapping a public HTTP API as a tool, and attaching the same server to Cursor and Claude Code. The only extra dependency is official mcp. Case 2 uses the Python standard library—no separate HTTP client.
Case 1: Local notes resource
Goal: let the host read Markdown under notes/ by URI, without handing your home directory to a filesystem server.
notes/inbox.md:
# Inbox
- Review MCP transports: stdio vs Streamable HTTP
- Write instructions for the workshop server
notes_server.py:
from pathlib import Path
from mcp.server import MCPServer
ROOT = Path(__file__).resolve().parent / "notes"
mcp = MCPServer("notes", instructions="Read markdown notes under ./notes only.")
def _safe(name: str) -> Path:
path = (ROOT / name).resolve()
if not str(path).startswith(str(ROOT.resolve())) or path.suffix != ".md":
raise ValueError("only markdown files under notes/ are allowed")
return path
@mcp.resource("notes://inbox")
def inbox() -> str:
"""The default inbox note."""
path = ROOT / "inbox.md"
return path.read_text(encoding="utf-8") if path.is_file() else "(empty inbox)"
@mcp.resource("notes://file/{name}")
def read_note(name: str) -> str:
"""Read notes/{name}.md (name without extension)."""
return _safe(f"{name}.md").read_text(encoding="utf-8")
@mcp.prompt()
def weekly_review() -> str:
"""Start a weekly review using the inbox note."""
return "Read notes://inbox and list three next actions."
if __name__ == "__main__":
mcp.run()
Check: mcp dev .\notes_server.py, read notes://inbox, then the template notes://file/{name} (name=inbox). The path guard rejects names such as ../secrets.
Case 2: Wrap a public HTTP API
Goal: fetch the current temperature from Open-Meteo (no API key) to show “a tool is a constrained HTTP call.”
weather_server.py:
import json
import urllib.parse
import urllib.request
from mcp.server import MCPServer
mcp = MCPServer("weather", instructions="Public forecast via Open-Meteo. No API key.")
@mcp.tool()
def current_temperature(latitude: float, longitude: float) -> str:
"""Return current temperature (°C) for a WGS84 coordinate pair."""
query = urllib.parse.urlencode(
{
"latitude": latitude,
"longitude": longitude,
"current": "temperature_2m",
}
)
url = f"https://api.open-meteo.com/v1/forecast?{query}"
with urllib.request.urlopen(url, timeout=10) as resp:
payload = json.loads(resp.read().decode("utf-8"))
current = payload.get("current", {})
value = current.get("temperature_2m")
return f"{value} °C" if value is not None else json.dumps(payload, ensure_ascii=False)
if __name__ == "__main__":
mcp.run()
Near Shanghai try latitude=31.23, longitude=121.47.
Check: call the tool in Inspector, then ask a host to use current_temperature for that pair. Do not concatenate arbitrary user URLs into urlopen—the host is fixed here. For an authenticated internal API, read the token from the environment and return business fields only.
Case 3: One server, two hosts
Merge cases 1 and 2 into workshop.py (or keep the file from Python Server). One codebase, two configs.
Cursor — .cursor/mcp.json:
{
"mcpServers": {
"workshop": {
"command": "C:\\Users\\you\\mcp-demo\\.venv\\Scripts\\python.exe",
"args": ["C:\\Users\\you\\mcp-demo\\workshop.py"]
}
}
}
Claude Code — .mcp.json at the project root, or:
claude mcp add --scope user workshop -- C:\Users\you\mcp-demo\.venv\Scripts\python.exe C:\Users\you\mcp-demo\workshop.py
On Unix, point command at .venv/bin/python.
Check:
- After restart, both hosts list the same tools and resources
- Ask Cursor about a line in the inbox; ask Claude Code for the same resource
- Call
current_temperature on both sides; the numbers should match (same public API)
That is the USB-C metaphor in practice: the protocol stays still; the host only launches the command and approves tools.
Anti-patterns
Next