Skip to main content
Glama
EduardoDknight

Secure MCP Server Template

README.md
# Secure MCP Server Template

A minimal, **correct, and security-first** [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server in Python. It exposes three example tools to any MCP client (Claude Desktop, Claude Code, etc.) — each one shipped with the security guard it actually needs.

Most "MCP server" tutorials show you how to expose a tool. They skip the part that matters once a model has real hands on your database, network, and disk. This template is built around that gap: **how to give an LLM capabilities without opening a hole.**

```
LLM client ──MCP──> this server ──> [ PostgreSQL | HTTP | filesystem ]
                         │
                         └── every tool passes through guards.py (tested)
```

## What is MCP, in one paragraph

MCP is an open protocol that lets an AI client call external "tools" (functions) and read "resources" through a standard interface. You run a small server that declares tools; the client (Claude Desktop, Claude Code, …) discovers them and the model calls them during a conversation. It's the clean way to give a model access to *your* data and actions instead of pasting everything into the prompt.

## Tools

| Tool | What it does | Guard |
|---|---|---|
| `db_query(sql)` | Run a **read-only** SQL query against `DATABASE_URL`, returns rows as JSON | SELECT/WITH only **and** a real `READ ONLY` Postgres session |
| `http_get(url)` | HTTP GET, returns the body as text | http/https only + host **allowlist, deny-by-default** (anti-SSRF) |
| `read_file(path)` | Read a text file under `MCP_FILES_ROOT` | Path resolved inside the root, **blocks `../` traversal & absolute paths** |

## Security design (the point of this template)

- **`db_query` can't write.** Two layers: a static check (`is_safe_select` — rejects writes, DDL, and stacked `;` statements) *and* the connection is opened `READ ONLY`, so even a clever bypass can't mutate data.
- **`http_get` can't be turned into an SSRF.** Only `http(s)`, and the host must be in `MCP_URL_ALLOWLIST`. Empty allowlist = nothing allowed (deny-by-default), so a misconfigured server isn't an open proxy.
- **`read_file` can't escape its root.** Paths are resolved and checked to live inside `MCP_FILES_ROOT`; `../`, absolute paths, and symlink escapes raise.

All three guards live in [`guards.py`](guards.py) (zero dependencies) and are covered by [`tests/test_guards.py`](tests/test_guards.py), so they run without even installing MCP.

## Quickstart

```bash
git clone <this repo> && cd mcp-server-template
python -m venv .venv && . .venv/Scripts/activate   # (Linux/mac: . .venv/bin/activate)
pip install -e .            # add: pip install -e ".[db]" for PostgreSQL
cp .env.example .env        # edit with your values
python server.py            # runs over stdio
```

### Use it from Claude Desktop

Add to your `claude_desktop_config.json` (see [`claude_desktop_config.example.json`](claude_desktop_config.example.json)):

```json
{
  "mcpServers": {
    "atelier-template": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"],
      "env": { "MCP_URL_ALLOWLIST": "api.github.com", "MCP_FILES_ROOT": "/safe/dir" }
    }
  }
}
```

Restart Claude Desktop; the three tools appear. (For Claude Code: `claude mcp add atelier-template -- python /abs/path/server.py`.)

## Add your own tool (the 5-line version)

```python
@mcp.tool()
def word_count(text: str) -> str:
    """Count words in a string."""   # <- this docstring is what the model reads
    return str(len(text.split()))
```

That's the whole loop: decorate a function, write a clear docstring (the model uses it to decide when/how to call), return a string. If the tool touches data/network/disk, **add a guard** — that's the habit this template is trying to teach.

## Layout

```
server.py     # the MCP server + 3 example tools
guards.py     # pure, dependency-free security helpers (tested)
tests/        # pytest for the guards (run without mcp installed)
.env.example  # configuration
```

## Tests

```bash
pip install pytest && pytest -q
```

## License

MIT © 2026 Eduardo Pérez Ignacio