A demo Model Context Protocol (MCP) server built on FastMCP — 15 tools, 5 resources, 3 prompts, SQLite-backed notes, per-call telemetry, and a Streamlit UI.
Project description
Sample Local MCP Server
A minimal Model Context Protocol server built with
FastMCP. It exposes a handful of demo tools (math,
random number, server time, text analysis, echo) over SSE on http://localhost:8000,
so you can wire it into an MCP-aware client such as VS Code Copilot Chat, Claude
Desktop, or any custom agent.
Project structure
sample-mcp-server-script/
├── server/ # The MCP server
│ ├── __init__.py
│ ├── __main__.py # enables `python -m server`
│ ├── app.py # FastMCP app + tools + resources + prompts
│ └── notes_db.py # SQLite-backed notes store
├── clients/ # Ways to talk to the server
│ ├── cli.py # CLI: list / call / smoke
│ └── streamlit_app.py # Streamlit UI
├── tests/ # pytest suite
│ ├── test_notes_db.py
│ └── test_server.py
├── docs/ # Misc notes
│ ├── instructions.txt
│ └── testing_server.txt
├── data/ # Local SQLite DB + logs (gitignored)
├── Dockerfile
├── .dockerignore
├── .gitignore
├── README.md
└── requirements.txt
Tools exposed
| Tool | Description |
|---|---|
calculate |
Evaluate a safe math expression, e.g. 2 + 2 * 10. |
get_server_time |
Return the current server time (UTC). |
random_number |
Random int between min_val and max_val. |
analyze_text |
Word / character / sentence stats for a text blob. |
echo |
Echoes a message back — handy for connectivity testing. |
hash_text |
MD5/SHA1/SHA256/SHA512 hash of a string. |
base64_encode |
Base64-encode a string (standard or URL-safe alphabet). |
base64_decode |
Decode a base64 string back to UTF-8 text. |
uuid_generate |
Generate one or more UUIDs (v1 or v4). |
password_generate |
Cryptographically strong random password with configurable charset. |
json_format |
Pretty-print and validate a JSON string. |
regex_match |
Find all matches of a regex pattern in text (first 50, with groups). |
convert_units |
Convert length / weight / temperature between common units. |
fetch_url |
HTTP GET/HEAD an http(s) URL and return status, headers, truncated body. |
weather |
Current weather for a lat/lon via the free Open-Meteo API (no key). |
note_add |
Create a new note (title + body) in the local SQLite store. |
note_list |
List recent notes, newest first. Optional substring search. |
note_get |
Fetch a single note by id. |
note_update |
Update a note's title and/or body. |
note_delete |
Delete a note by id. |
Resources exposed
| URI | Description |
|---|---|
resource://server/info |
Server metadata: name, version, uptime, notes count. |
resource://notes/all |
All notes (max 500), newest first. |
resource://notes/{note_id} |
A single note by id, e.g. resource://notes/3. |
resource://telemetry/recent |
Last 100 tool calls (tool, ts, duration_ms, status). |
resource://telemetry/summary |
Aggregates: 60-min KPIs + 24-h per-tool counts. |
Prompts exposed
| Prompt | Description |
|---|---|
summarize_text |
Summarize a text blob in a chosen style. |
code_review |
Structured code review (bugs / security / improvements). |
explain_error |
Plain-language explanation of an error + likely fixes. |
Prerequisites
- Python 3.10+ (3.11 recommended)
pipandvenvavailable
Setup
From the project root:
# 1. Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate
# 2. Install dependencies
pip install --upgrade pip
pip install -r requirements.txt
Run the server
# With the venv activated:
python -m server
You should see FastMCP start an SSE server on port 8000. The SSE endpoint is:
http://localhost:8000/sse
Leave this terminal running while you use the server from a client.
Quick-start cheat sheet
Three terminals, all from the project root after source .venv/bin/activate.
Terminal 1 — start the server
python -m server
# or with debug logging to a file
python -m server --log-level DEBUG --log-file data/mcp.log
Terminal 2 — Streamlit UI
streamlit run clients/streamlit_app.py
# opens http://localhost:8501
Terminal 3 — CLI client
python -m clients.cli list # discover tools
python -m clients.cli smoke # run the built-in smoke suite
python -m clients.cli call echo --args '{"message":"hi"}'
CLI flags
python -m server --help
# Examples
python -m server --transport stdio
python -m server --transport sse --host 127.0.0.1 --port 9000
python -m server --log-level DEBUG --log-file data/mcp.log
All flags can also be set via environment variables: MCP_TRANSPORT, MCP_HOST,
MCP_PORT, MCP_LOG_LEVEL, MCP_LOG_FILE, MCP_NOTES_DB.
Verify it's running
In another terminal:
curl -N http://localhost:8000/sse
You should get a streaming response (press Ctrl+C to stop). If the connection opens
and stays open, the server is healthy.
Test from the terminal
Two convenient clients are included.
1. CLI harness — clients/cli.py
source .venv/bin/activate
# List every tool the server exposes
python -m clients.cli list
# Call any tool (arguments are a JSON object)
python -m clients.cli call echo --args '{"message": "hello"}'
python -m clients.cli call password_generate --args '{"length": 32, "use_symbols": true}'
python -m clients.cli call weather --args '{"latitude": 28.6139, "longitude": 77.2090}'
# Run a small predefined smoke suite that exercises ~9 tools
python -m clients.cli smoke
# Point at a non-default URL
python -m clients.cli --url http://localhost:9000/sse list
2. Streamlit UI — clients/streamlit_app.py
A zero-config web UI that lists tools, auto-renders a form from each tool's input schema, and shows results + call history.
source .venv/bin/activate
streamlit run clients/streamlit_app.py
Streamlit opens at http://localhost:8501. In the sidebar, click
Connect / Refresh to load tools from http://localhost:8000/sse (editable),
pick a tool from the dropdown, fill in the form, hit Call tool.
3. Raw curl flow (advanced)
If you want to test the JSON-RPC wire protocol directly, see
docs/instructions.txt for the manual SSE +
/messages/?session_id=… flow.
Use it from VS Code Copilot Chat
A ready-to-use config is included at .vscode/mcp.json:
{
"servers": {
"local-sample-mcp": {
"type": "sse",
"url": "http://localhost:8000/sse"
}
}
}
Steps:
- Make sure the server is running (
python -m server). - Open this folder in VS Code.
- Open Copilot Chat and switch to Agent mode.
- Click the tools picker — the
local-sample-mcpserver and its tools should appear. - Ask something like: "Use the calculate tool to compute 12 * (3 + 4)" or "Call analyze_text on this paragraph."
Use it from Claude Desktop
Claude Desktop currently launches MCP servers via stdio, so the easiest path is to use
mcp-proxy to bridge stdio → SSE. Add an
entry like this to your Claude Desktop config:
{
"mcpServers": {
"local-sample-mcp": {
"command": "mcp-proxy",
"args": ["http://localhost:8000/sse"]
}
}
}
Then start the Python server in a terminal as shown above.
Switching to stdio transport (optional)
If you prefer running over stdio (no port needed):
python -m server --transport stdio
And update your client config to launch the script directly, e.g.:
{
"servers": {
"local-sample-mcp": {
"type": "stdio",
"command": "python",
"args": ["-m", "server"],
"cwd": "/absolute/path/to/sample-mcp-server-script"
}
}
}
Telemetry
Every tool call is recorded to a local SQLite file (data/telemetry.db by
default) via the @traced decorator in server/telemetry.py.
Per call we store: timestamp, tool name, duration in ms, and status (ok
or error) — and the exception message when something raises. We deliberately
do not record arguments or return values to keep secrets, tokens, hashed
inputs, password outputs, etc. out of the telemetry DB.
Tool-level failures returned as {"error": "..."} count as ok (they are a
normal Python return); only uncaught exceptions are recorded as error.
Toggle via env vars:
| Variable | Default | Meaning |
|---|---|---|
MCP_TELEMETRY |
1 |
Set to 0 to disable recording. |
MCP_TELEMETRY_DB |
data/telemetry.db |
Path to the telemetry SQLite file. |
The Streamlit UI has a 📊 Telemetry tab that pulls
resource://telemetry/summary and resource://telemetry/recent and renders:
KPI cards (calls, error rate, avg + p50 / p95 / p99 latency), a Plotly
bar chart of calls per tool (last 24 h), slowest-calls and recent-errors
tables side-by-side, a latency scatter over time, and a filterable
recent-calls table. An auto-refresh (5 s) checkbox keeps it live, and the
Tool caller tab shows a per-tool "called N times, avg X ms" badge under the
description.
Guardrails
The server applies a small set of defensive checks so a careless (or malicious) agent can't trivially hang the process, escape the network sandbox, or fill up the database. They live in server/safety.py and are wired into the relevant tools.
| Tool | Guardrail | Why |
|---|---|---|
calculate |
AST-based evaluator instead of eval(). Only + - * / // % **, parens, unary +/-, numeric literals. Exponent capped at 100; expression capped at 200 chars. |
eval() is dangerous even with a charset filter — 9**9**9**9 would peg a CPU core forever. |
fetch_url |
Resolves the hostname and rejects loopback, private (10/8, 172.16/12, 192.168/16), link-local (incl. 169.254/16 cloud metadata), multicast, and reserved IPs. Streams the response and stops after max_bytes. |
Blocks SSRF and prevents downloading huge files just to truncate them. |
regex_match |
Pattern capped at 1 KB, text at 1 MB, execution wrapped in a 2-second timeout via a worker thread. | Stops catastrophic backtracking (ReDoS) from blocking the server. |
note_add / note_update |
Title required, ≤200 chars; body ≤100 KB; both validated at the DB layer. | Prevents one bad call from bloating the SQLite file. |
What's not included (yet, by design — this is a local demo):
- No per-tool rate limiting
- No authentication on the SSE/HTTP transports
- No output-size cap on tool return values
- No prompt-injection sanitization on fetched HTML
If you ever expose this beyond localhost, add at least auth and rate limits
before doing so.
Run with Docker
# Build the image
docker build -t sample-mcp-server .
# Run with a persistent notes DB volume
docker run --rm -p 8000:8000 -v "$PWD/data:/data" sample-mcp-server
# Override transport/host/port/log-level via env or CLI args
docker run --rm -p 9000:9000 -e MCP_PORT=9000 sample-mcp-server
docker run --rm sample-mcp-server --transport stdio
The image exposes port 8000 and stores the notes DB at /data/notes.db (mount a
volume there to persist it across container restarts).
Run the tests
source .venv/bin/activate
pytest -q
tests/test_notes_db.py covers the SQLite store and tests/test_server.py covers
every pure tool, resource, and prompt by importing the server module directly.
Troubleshooting
-
ModuleNotFoundError: No module named 'fastmcp'— activate the venv and rerunpip install -r requirements.txt. -
Port 8000 already in use — change the
port=8000argument inserver/app.py, and update theurlin.vscode/mcp.jsonto match. -
VS Code doesn't see the server — restart the MCP server from the Copilot Chat tools picker, or reload the VS Code window.
-
calculatereturns an error — only0-9,+,-,*,/,(,),., and spaces are accepted (intentional safety restriction).
Demo Video
https://github.com/user-attachments/assets/3dacf24b-3717-4b82-a036-e5ff4131bd09
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastmcp_sample_server-0.4.0.tar.gz.
File metadata
- Download URL: fastmcp_sample_server-0.4.0.tar.gz
- Upload date:
- Size: 34.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad667b62737830a2512adfdf3fb9756debae1e56091028ab97dd5ec8727f504f
|
|
| MD5 |
4528fa3807ceaf2506e4332f2075cdd7
|
|
| BLAKE2b-256 |
22c1a49b96b43cd69347b7965c9bf671cccae09632477f53a32440d89da4b506
|
File details
Details for the file fastmcp_sample_server-0.4.0-py3-none-any.whl.
File metadata
- Download URL: fastmcp_sample_server-0.4.0-py3-none-any.whl
- Upload date:
- Size: 31.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ae353ca45c6e8fa7bafb50b780eaa00b77a238aa8e477dbd26f24e9f860933c
|
|
| MD5 |
5b429635d39d8db4673f9812ed439cf2
|
|
| BLAKE2b-256 |
7ebc00611fe03fde9d12f785d1c2991f2f208d822c51c917ec43752654c6aa86
|