Here’s a complete README.md you can drop next to your two files.
This repo contains a minimal, robust Model Context Protocol (MCP) setup:
-
mcp_fs_server.py— an MCP server (Streamable HTTP) that safely exposes:fs_list_here()– zero-arg directory listing (can’t be mis-called)fs_read()– forgiving file reader (accepts string or object)fs_summary()– one-shot list + README preview (best for tiny models)
-
host_ollama_fs.py— an MCP host using PydanticAI + Ollama (local LLM).- Works across older/newer
pydantic-aiAPIs. - Strong prompt to nudge tool use; resilient output printing.
- Optional event-loop handler to silence a known AnyIO shutdown warning.
- Works across older/newer
-
Python 3.10+ (3.11 recommended)
-
Ollama installed and running locally
- Pull at least one instruct model (e.g.
llama3.2:1bto start; 3B–7B works better)
- Pull at least one instruct model (e.g.
-
A folder you’re happy to expose read-only to the model (the sandbox)
⚠️ The server sandboxes access underMCP_FS_BASE. Paths outside that base are rejected.
your-folder/
├── mcp_fs_server.py
├── host_ollama_fs.py
└── README.md <-- this file
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activatepip install mcp pydantic uvicorn pydantic-ai openai httpx anyio sniffioIt’s fine if some packages are already present; this command ensures the pieces you need are installed.
Pick a real directory with at least one file (e.g., create a README.md):
export MCP_FS_BASE=/home/<you>/safe-demo
mkdir -p "$MCP_FS_BASE"
printf "Hello from README in %s\n" "$MCP_FS_BASE" > "$MCP_FS_BASE/README.md"On Windows PowerShell:
$env:MCP_FS_BASE="C:\Users\<you>\safe-demo"
python mcp_fs_server.pyYou should see something like:
[SERVER] BASE_DIR = /home/<you>/safe-demo
Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Keep it running.
The Streamable HTTP transport uses a session. Open an SSE stream and post JSON-RPC with the same MCP-Session-ID.
Terminal A – open SSE (creates/binds session):
SID=$(
curl -sS -D - -o /dev/null \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":"0","method":"ping","params":{}}' \
http://127.0.0.1:8000/mcp \
| awk -F': ' '/^mcp-session-id:/ {print $2}' | tr -d '\r'
)
echo "SID=$SID"
curl -svN \
-H 'Accept: text/event-stream' \
-H "MCP-Session-ID: $SID" \
http://127.0.0.1:8000/mcpTerminal B – send JSON-RPC commands (same session id):
# initialize (include clientInfo)
curl -sS \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "MCP-Session-ID: $SID" \
-d '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{
"protocolVersion":"2024-11-05",
"capabilities":{},
"clientInfo":{"name":"curl","version":"1.0"}
}}' \
http://127.0.0.1:8000/mcp
# list tools
curl -sS \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "MCP-Session-ID: $SID" \
-d '{"jsonrpc":"2.0","id":"2","method":"tools/list","params":{}}' \
http://127.0.0.1:8000/mcp
# call tools (examples)
curl -sS \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "MCP-Session-ID: $SID" \
-d '{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"fs_list_here","arguments":{}}}' \
http://127.0.0.1:8000/mcp
curl -sS \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "MCP-Session-ID: $SID" \
-d '{"jsonrpc":"2.0","id":"4","method":"tools/call","params":{"name":"fs_read","arguments":"README.md"}}' \
http://127.0.0.1:8000/mcpIf you prefer cookies instead of headers, open the SSE with
-c mcp.jar -b mcp.jarand reuse that cookie jar on POSTs.
In another terminal:
ollama serve
# pull a model if needed:
# ollama pull llama3.2:1b
# (tool-following improves with larger models, e.g., llama3.2:3b / qwen2.5:3b)Open a new terminal (venv active) and run:
python host_ollama_fs.pyExpected behavior:
-
The server logs will show lines like:
[SERVER] fs_summary on /home/<you>/safe-demo → N files; README = README.mdor
[SERVER] fs_list_here called → N items [SERVER] fs_read args: README.md -
The host prints a short human summary (no code).
If the 1B model ignores tools, switch the prompt in host_ollama_fs.py to the one-shot fs_summary (already the default), or try a slightly larger local model.
- Ollama error. Run
ollama listto see installed tags and updateOLLAMA_MODELinhost_ollama_fs.pyto match exactly. - If you just pulled, restart Ollama:
pkill ollama; ollama serve.
- Small 1B models often do this. The host prompt already forbids code; still, prefer
fs_summary(single call). - Try a 3B–7B model for better tool compliance.
-
The call failed before entering the tool (name/args validation).
-
The provided tools avoid this:
fs_list_here()has no argsfs_read()accepts a string or object
-
Make sure your prompt refers to the exact tool names reported by
tools/list.
- Use the two-terminal sequence. Send
MCP-Session-ID: $SIDon both the SSE GET and all POSTs. - Or use cookies with the same jar for SSE and POSTs.
- You posted
tools/listbeforeinitializecompleted for the same session. Wait for the SSEmessageof theinitializeresponse, then calltools/list.
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
-
This is a known async teardown quirk when exceptions bubble out during transport cleanup.
-
It does not mean your tools failed (check server logs).
-
Fixes:
- Upgrade:
pip install -U anyio mcp pydantic-ai httpx sniffio - Keep the loop-level exception handler included in
host_ollama_fs.py(swallows only that known message).
- Upgrade:
- Keep
MCP_FS_BASEpointed to a folder with at least one file (e.g., README.md) to demofs_read. - For best results with tool use, try a slightly larger model:
llama3.2:3b,qwen2.5:3b, etc. - You can always validate the server independently via the curl sequence in §7.
# venv
python -m venv venv
source venv/bin/activate
# install deps
pip install mcp pydantic uvicorn pydantic-ai openai httpx anyio sniffio
# set base dir for server
export MCP_FS_BASE=/home/<you>/safe-demo
# run server
python mcp_fs_server.py
# run ollama
ollama serve
ollama list
# ollama pull llama3.2:3b # (optional, better tool following)
# run host
python host_ollama_fs.py-
fs_list_here()→ no args Returns:{ "ok": true, "base": "...", "path": ".", "items": [ { "name": "file.txt", "kind": "file", "size": 123, "mtime": 172... }, ... ] } -
fs_read(args)→argscan be string (e.g.,"README.md") or object:{ "path": "README.md" } // or { "dir": ".", "name": "README.md" }Returns:
{ "ok": true, "path": "README.md", "encoding": "utf-8", "content": "..." } -
fs_summary({ "path": "." })→ list + README preview in one call.
That’s it! You now have a minimal but sturdy MCP server + host you can run locally and iterate on.