Skip to main content
Glama
Varun4b1

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.