readonly-mcp-server
by Varun4b1
README.md
# readonly-mcp-server
An MCP (Model Context Protocol) server with:
- **Transport layer configuration** — `stdio` for local process spawning, or
**HTTP+SSE** for a networked/shared deployment. Selection follows
"if `stdio` else `http+sse`": any `MCP_TRANSPORT` value other than the
literal `stdio` falls back to HTTP+SSE.
- **Connection configuration (authentication)** — bearer API keys on
HTTP+SSE (`Authorization: Bearer <key>`, required on the SSE connection
*and* every message POST), and a pre-shared key on stdio so the same
auth model applies to local connections too.
- **Authorization limits (read / search only)** — the server exposes
exactly two tool categories, `read` and `search`. There are no
write/update/delete tools at all, and each API key is independently
granted a subset of `{read, search}`, enforced on every tool call.
## Project layout
```
mcp-server-py/
├── config/
│ ├── api_keys.json # key -> client_id + scopes (read/search)
│ └── documents.json # sample read-only data backing the tools
├── src/
│ ├── config.py # env-driven transport/auth/data settings
│ ├── auth.py # API key store, TokenVerifier, scope checks
│ ├── data_store.py # read-only document backend
│ ├── tools.py # list_documents / read_document / search_documents
│ └── server.py # wires transport + auth + tools, entry point
├── requirements.txt
├── .env.example
└── test_stdio_client.py / test_http_client.py # example clients
```
## Install
```bash
pip install -r requirements.txt
cp .env.example .env # then edit as needed
```
## Configuration (environment variables)
| Variable | Purpose | Default |
|---|---|---|
| `MCP_TRANSPORT` | `stdio` or anything else → HTTP+SSE | `stdio` |
| `MCP_HTTP_HOST` / `MCP_HTTP_PORT` | HTTP+SSE bind address | `0.0.0.0` / `8000` |
| `MCP_SSE_PATH` / `MCP_MESSAGE_PATH` | HTTP+SSE endpoint paths | `/sse` / `/messages/` |
| `MCP_API_KEYS_FILE` | Path to key→scope config | `config/api_keys.json` |
| `MCP_STDIO_API_KEY` | Pre-shared key required for stdio clients | *(unset — required)* |
| `MCP_REQUIRE_AUTH` | Disable HTTP auth (local dev only) | `true` |
| `MCP_ISSUER_URL` / `MCP_RESOURCE_SERVER_URL` | Metadata URLs for the HTTP auth middleware | `http://localhost:8000` |
| `MCP_DOCUMENTS_FILE` | Path to the document data file | `config/documents.json` |
Edit `config/api_keys.json` to add/remove keys. Each key lists `scopes`,
which may only contain `read` and/or `search` — anything else is rejected
at startup.
## Run — stdio transport
```bash
MCP_TRANSPORT=stdio MCP_STDIO_API_KEY=demo-read-search-key-change-me \
python3 src/server.py
```
Point an MCP client (Claude Desktop, Claude Code, etc.) at this command,
passing `MCP_STDIO_API_KEY` in the client's configured environment for the
server process, e.g. in `claude_desktop_config.json`:
```json
{
"mcpServers": {
"readonly-docs": {
"command": "python3",
"args": ["/absolute/path/to/mcp-server-py/src/server.py"],
"env": {
"MCP_TRANSPORT": "stdio",
"MCP_STDIO_API_KEY": "demo-read-search-key-change-me"
}
}
}
}
```
## Run — HTTP+SSE transport
```bash
MCP_TRANSPORT=http MCP_HTTP_PORT=8000 python3 src/server.py
```
Clients connect to `GET http://host:8000/sse` and `POST /messages/`, both
requiring `Authorization: Bearer <api-key>`. See `test_http_client.py` for
a minimal client example using the official `mcp` Python SDK.
## Tools exposed
| Tool | Required scope | Description |
|---|---|---|
| `list_documents` | `read` | List all document ids + titles |
| `read_document` | `read` | Fetch full content of one document by id |
| `search_documents` | `search` | Keyword search returning matches + snippets |
A key missing the required scope gets a clear `isError=true` tool result
(`Access denied: this operation requires the 'X' scope...`) rather than a
crash or silent failure.
## Design notes / how the auth is wired
- **HTTP+SSE**: uses the MCP Python SDK's built-in `TokenVerifier` +
`AuthSettings` support. `ApiKeyTokenVerifier` (in `auth.py`) adapts our
flat API-key file to that protocol; the SDK then wraps both the `/sse`
and `/messages` endpoints with `AuthenticationMiddleware` +
`RequireAuthMiddleware` automatically, and binds each SSE session to the
credential that opened it (a POST with a different/no credential for an
existing session is rejected).
- **stdio**: authenticated once at process startup via
`MCP_STDIO_API_KEY`; scopes are held for the lifetime of the process.
All logging goes to **stderr** — the stdio transport reserves stdout for
JSON-RPC protocol frames.
- **Scope enforcement**: `auth.require_scope()` is called at the top of
every tool function and reads the caller's granted scopes via
`get_access_token()` (HTTP) or the stdio session's validated key
(stdio), raising if the scope isn't present.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues