receipts-mcp 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ include receipts_mcp/upstreams.json.example
2
+ include receipts_mcp/README.md
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: receipts-mcp
3
+ Version: 0.1.1
4
+ Summary: MCP proxy that records and cryptographically signs AI agent tool calls
5
+ Author-email: Shreyansh <shreyanshkanojia104@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Shreyansh-Kanojiaa/receipts
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: mcp>=1.0.0
11
+ Requires-Dist: httpx>=0.27.0
12
+ Requires-Dist: pydantic-settings>=2.2.0
13
+
14
+ # Receipts MCP Proxy
15
+
16
+ `receipts_mcp/` is a stdio MCP server that sits between an MCP client and one or more upstream MCP servers.
17
+
18
+ For each tool call:
19
+
20
+ 1. The client calls the Receipts proxy.
21
+ 2. The proxy forwards the call to the real upstream MCP server.
22
+ 3. The real output is captured.
23
+ 4. The proxy POSTs that result to `POST /tools/record` on the Receipts backend.
24
+ 5. The real result is returned to the client even if receipting fails.
25
+
26
+ Upstream tools are namespaced as `<server>__<tool>` so collisions do not clobber each other.
27
+
28
+ If no upstream config file exists, the proxy falls back to the built-in demo tools:
29
+
30
+ - `write_file`
31
+ - `http_fetch`
32
+ - `db_query`
33
+
34
+ Those demo tools are forwarded to the backend's `/tools/call` endpoint.
35
+
36
+ ## Setup
37
+
38
+ ```bash
39
+ pip install receipts-mcp
40
+ cp upstreams.json.example upstreams.json
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ `receipts_mcp/config.py` reads environment variables and an upstreams JSON file.
46
+
47
+ Environment variables:
48
+
49
+ | Variable | Default | Purpose |
50
+ |----------|---------|---------|
51
+ | `RECEIPTS_URL` | `http://localhost:8000` | Receipts backend URL |
52
+ | `RECEIPTS_API_KEY` | empty | Proxy-role API key used for `/tools/record` |
53
+ | `UPSTREAMS_PATH` | `receipts_mcp/upstreams.json` | Path to upstream config |
54
+ | `TOOL_TIMEOUT_SECONDS` | `60` | Max time for an upstream tool call |
55
+ | `RECORD_TIMEOUT_SECONDS` | `5` | Max time for backend receipting |
56
+
57
+ The upstreams file supports:
58
+
59
+ - `stdio`
60
+ - `sse`
61
+ - `streamable_http`
62
+
63
+ `${ENV_VAR}` references inside `env` and `headers` are expanded from the process environment.
64
+
65
+ Example:
66
+
67
+ ```json
68
+ {
69
+ "include_demo_tools": false,
70
+ "upstreams": {
71
+ "github": {
72
+ "transport": "stdio",
73
+ "command": "/path/to/github-mcp",
74
+ "args": [],
75
+ "env": {
76
+ "GITHUB_TOKEN": "${GITHUB_TOKEN}"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Claude Code / Cursor config
84
+
85
+ Add this to `~/.claude/claude_mcp_config.json` or the equivalent Cursor MCP config:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "receipts": {
91
+ "command": "/home/shreyansh/receipts/.venv/bin/python3",
92
+ "args": ["-m", "receipts_mcp.server"],
93
+ "cwd": "/home/shreyansh/receipts",
94
+ "env": {
95
+ "RECEIPTS_URL": "http://localhost:8000",
96
+ "RECEIPTS_API_KEY": "<proxy-key>",
97
+ "UPSTREAMS_PATH": "receipts_mcp/upstreams.json",
98
+ "PYTHONPATH": "/home/shreyansh/receipts"
99
+ }
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ Restart the MCP client after changing the config.
106
+
107
+ ## Runtime behavior
108
+
109
+ - One proxy process gets one `mcp-<hex>` session ID.
110
+ - All tool calls from that process share that session so they can be reconciled together.
111
+ - If the backend is unreachable, the tool result still returns and the receipt is skipped.
112
+ - If the backend rejects the API key, the proxy logs it as a configuration error.
113
+
114
+ ## Run
115
+
116
+ ```bash
117
+ python3 -m receipts_mcp
118
+ ```
119
+
120
+ or
121
+
122
+ ```bash
123
+ python3 -m receipts_mcp.server
124
+ ```
@@ -0,0 +1,334 @@
1
+ # Receipts
2
+
3
+ Receipts is a verification layer between an AI agent and the tools it uses.
4
+
5
+ Flow:
6
+
7
+ `Agent -> Receipts MCP proxy -> upstream MCP server(s) -> Receipts backend -> dashboard`
8
+
9
+ The proxy forwards real tool calls to real upstream MCP servers, captures the real output, and sends that result to the backend to be signed as a receipt. The backend stores receipts, verifies signatures, reconciles agent claims against stored output, and tracks session state. The React dashboard shows the ledger, sessions, reconciliation flow, alert rules, help, and settings in real time.
10
+
11
+ The repo also includes a built-in demo path so a fresh checkout works without any external MCP server. In demo mode the backend executes three mock tools, and the proxy can fall back to those same tools when no upstreams are configured.
12
+
13
+ ![Live Ledger](docs/live-ledger-2.png)
14
+
15
+ ## What is in the repo
16
+
17
+ - `backend/` — FastAPI API, SQLite persistence, auth, signing, verification, alert delivery (webhook/email/Slack), structured logging, rate limiting, and demo tool execution
18
+ - `receipts_mcp/` — stdio MCP proxy that fronts upstream MCP servers and records receipts
19
+ - `frontend/` — React + Vite dashboard (single SPA, sidebar navigation, dark theme)
20
+ - `tests/` — backend verification, session lifecycle, auth, and MCP proxy integration tests
21
+ - `demo_agent.py` — CLI demo client that exercises all three verdict modes
22
+ - `test_mcp.py` — smoke test against a live backend
23
+
24
+ ## Stack
25
+
26
+ - Backend: Python 3.12, FastAPI, SQLite, Pydantic v2, slowapi (rate limiting)
27
+ - Frontend: React 19, Vite 8, Tailwind 3 (CSS utilities only — the dashboard is inline-styled)
28
+ - MCP proxy: `mcp` Python SDK, `httpx`
29
+ - Signing: HMAC-SHA256 over canonical receipt fields (`json.dumps(sort_keys=True)`)
30
+ - Auth: bearer API keys with `viewer`, `proxy`, and `admin` roles (SHA-256 hashed in DB)
31
+ - Logging: structured JSON logs via stdlib logging (configurable level and format)
32
+
33
+ ## Quick start
34
+
35
+ ```bash
36
+ # 1. Install Python dependencies from the repo root
37
+ python -m venv .venv
38
+ source .venv/bin/activate
39
+ pip install -r requirements.txt
40
+
41
+ # Or install just the MCP proxy from PyPI
42
+ pip install receipts-mcp
43
+
44
+ # 2. Start the backend in development mode
45
+ cd backend
46
+ RECEIPT_SECRET=dev-secret \
47
+ API_KEYS="dashboard:viewer:devviewer,proxy:proxy:devproxy" \
48
+ python3 -m uvicorn main:app --reload
49
+
50
+ # 3. Start the frontend in another terminal
51
+ cd frontend
52
+ npm install
53
+ echo "VITE_RECEIPTS_VIEWER_KEY=devproxy" > .env.local
54
+ npm run dev
55
+ ```
56
+
57
+ - Backend: http://localhost:8000
58
+ - API docs: http://localhost:8000/docs
59
+ - Frontend: http://localhost:5173
60
+
61
+ `VITE_RECEIPTS_VIEWER_KEY` should point at a `proxy` role key during local development because the dashboard can run reconciliation, which uses write endpoints.
62
+
63
+ ## Configuration
64
+
65
+ The main environment variables live in [`.env.example`](.env.example).
66
+
67
+ Important ones:
68
+
69
+ | Variable | Purpose |
70
+ |---|---|
71
+ | `ENVIRONMENT` | `development` or `production` |
72
+ | `RECEIPT_SECRET` | HMAC signing key, required in production (≥16 chars) |
73
+ | `DATABASE_URL` | SQLite URL today (`sqlite:///./receipts.db`), Postgres-ready form |
74
+ | `API_KEYS` | comma-separated `label:role:rawkey` bootstrap entries |
75
+ | `ENABLE_DEMO_TOOLS` | enables the mock `/tools/call` path |
76
+ | `CORS_ORIGINS` | allowed frontend origin(s), `*` for dev |
77
+ | `LOG_LEVEL` | logging level (`INFO`, `DEBUG`, etc.) |
78
+ | `LOG_JSON` | `true` for structured JSON log output |
79
+ | `RATE_LIMIT` | global per-IP rate limit (e.g. `120/minute`) |
80
+ | `RECEIPTS_URL` | backend URL for the MCP proxy |
81
+ | `RECEIPTS_API_KEY` | proxy-role key for the MCP proxy |
82
+ | `UPSTREAMS_PATH` | upstream MCP server config for the proxy |
83
+ | `VITE_BACKEND_URL` | frontend build-time backend URL |
84
+ | `VITE_RECEIPTS_VIEWER_KEY` | frontend dev-time API key |
85
+ | `RECEIPTS_PROXY_KEY` | nginx-injected key for the production frontend container |
86
+
87
+ ## Backend
88
+
89
+ ### Modules
90
+
91
+ | File | Purpose |
92
+ |---|---|
93
+ | `main.py` | FastAPI routes, lifespan startup, timeout loop, alerts, demo run |
94
+ | `database.py` | SQLite schema, CRUD, session state, alert rules, API keys |
95
+ | `signer.py` | canonical hashing, HMAC signing, receipt assembly |
96
+ | `verifier.py` | claim verification and session verdict derivation |
97
+ | `auto_verify.py` | signature-only verification for session close / inactivity |
98
+ | `alerts.py` | webhook, email (SMTP/STARTTLS), and Slack alert delivery |
99
+ | `auth.py` | bearer / X-API-Key auth, role hierarchy, bootstrap key seeding |
100
+ | `settings.py` | pydantic-settings env-based config with production safety checks |
101
+ | `tools.py` | built-in demo tools (`write_file`, `http_fetch`, `db_query`) |
102
+ | `models.py` | Pydantic v2 request / response schemas |
103
+ | `logging_config.py` | structured JSON logging with context fields |
104
+ | `Dockerfile` | production container with healthcheck |
105
+
106
+ ### Endpoints
107
+
108
+ | Endpoint | Auth | Description |
109
+ |---|---|---|
110
+ | `POST /tools/record` | proxy | sign and store an already-executed tool call |
111
+ | `POST /tools/call` | proxy | demo-only mock tool execution (gated by `ENABLE_DEMO_TOOLS`) |
112
+ | `POST /verify` | proxy | compare claimed outputs against stored receipts |
113
+ | `GET /sessions` | viewer | list sessions |
114
+ | `GET /sessions/{id}` | viewer | session detail |
115
+ | `POST /sessions/{id}/close` | proxy | close a session and schedule auto-verify |
116
+ | `POST /sessions/{id}/verify-claim` | proxy | full-claim reconciliation with verdict persistence |
117
+ | `GET /receipts/all` | viewer | all receipts (paginated by limit) |
118
+ | `GET /receipts/{session_id}` | viewer | receipts for a session |
119
+ | `GET /stats` | viewer | aggregate receipt and session counts |
120
+ | `GET /alerts` | viewer | list alert rules |
121
+ | `POST /alerts` | proxy | create an alert rule |
122
+ | `GET /alerts/{id}` | viewer | get an alert rule |
123
+ | `PATCH /alerts/{id}` | proxy | update an alert rule |
124
+ | `DELETE /alerts/{id}` | proxy | delete an alert rule |
125
+ | `POST /alerts/{id}/test` | proxy | send a test alert |
126
+ | `POST /demo/run` | proxy | run a built-in demo scenario |
127
+ | `GET /healthz` | none | liveness probe |
128
+ | `GET /readyz` | none | readiness probe (checks DB) |
129
+
130
+ ### Verification
131
+
132
+ Verification has two scopes:
133
+
134
+ - **`signature_only`** — automatic on session close or 30-second inactivity sweep. Checks receipt HMAC integrity only. Can detect `TAMPERED` but cannot tell whether the agent lied about the tool output.
135
+ - **`full_claim`** — manual reconciliation or `/demo/run`. Compares agent claims against stored receipts and can return `VERIFIED`, `CONTRADICTED`, `UNVERIFIED`, or `TAMPERED`.
136
+
137
+ A `full_claim` verdict is never overwritten by a later `signature_only` sweep. The `verify-claim` endpoint has a guard: if a full_claim verdict already exists, it returns the stored verdict instead of re-running (which would use stored receipts as the claim source and always collapse to VERIFIED). Pass `?force=true` to override this guard.
138
+
139
+ Sessions are auto-closed by a background timeout loop (configurable via `INACTIVITY_TIMEOUT_SECONDS`). Closed sessions are then auto-verified, and the dashboard labels signature-only verdicts as such.
140
+
141
+ ### Auth
142
+
143
+ Three roles with a hierarchy: `viewer` < `proxy` < `admin`. A key satisfies any requirement at or below its level.
144
+
145
+ - `viewer` — read-only dashboard access (stats, receipts, sessions, alert listing)
146
+ - `proxy` — record receipts, run verification, manage alerts
147
+ - `admin` — everything
148
+
149
+ Keys are presented as `Authorization: Bearer <key>` or `X-API-Key: <key>`. Only SHA-256 hashes are stored in the `api_keys` table; raw keys live only in the operator's env/secret store.
150
+
151
+ Bootstrap keys from the `API_KEYS` env var are seeded on first startup when the table is empty.
152
+
153
+ ### Alerts
154
+
155
+ Alert rules fire on verdict events. Each rule specifies a trigger (`CONTRADICTED`, `TAMPERED`, `UNVERIFIED`, or `ANY`) and a delivery channel:
156
+
157
+ ![Slack alert channel receiving CONTRADICTED verdicts in real time](docs/slack-alerts.png)
158
+
159
+ ![Email alert for a CONTRADICTED session — includes session ID, tool, receipt ID, and HMAC signature](docs/email-alert.png)
160
+
161
+ - **Webhook** — POST a JSON payload to a URL
162
+ - **Email** — SMTP/STARTTLS delivery (Gmail App Passwords, Alertmanager, etc.)
163
+ - **Slack** — POST to an Incoming Webhook URL with Block Kit formatting
164
+
165
+ ### Rate limiting
166
+
167
+ Global per-client-IP rate limiting via slowapi (default: `120/minute`, configurable via `RATE_LIMIT`).
168
+
169
+ ## Dashboard
170
+
171
+ The frontend is a single dashboard SPA with a fixed sidebar. It has these views:
172
+
173
+ | View | Description |
174
+ |---|---|
175
+ | **Live Ledger** | Real-time receipt stream with stats, filtering (verdict/time/search), pagination, and new-row highlighting |
176
+ | **Sessions** | Session registry with status, scope, duration, receipt count, and verdict |
177
+ | **Reconciliation** | Full-claim verification interface — select a session, run validation, view per-receipt cards with field-level match status |
178
+ | **Alerts** | CRUD for alert rules — multi-step creation wizard, enable/disable toggle, test delivery, delete |
179
+ | **Help** | Setup guides for Claude Code, Cursor, Slack, Gmail, Alertmanager, and custom webhooks |
180
+ | **Settings** | System config display and raw hash toggle |
181
+
182
+ The Live Ledger polls `/stats`, `/receipts/all`, and `/sessions` every 3 seconds. Reconciliation can be launched from the ledger or sessions view, and the dashboard can export a JSON audit report.
183
+
184
+ Design: dark theme with JetBrains Mono + Inter fonts, monochrome surfaces, color-coded status indicators (green=verified, amber=contradicted, red=tampered/unverified, blue=open/active), no emojis. JSON code blocks in Help are syntax-highlighted (keys blue, string values green, numbers amber).
185
+
186
+ ## Demo agent
187
+
188
+ ```bash
189
+ python3 demo_agent.py --mode normal
190
+ python3 demo_agent.py --mode lying
191
+ python3 demo_agent.py --mode replit
192
+ ```
193
+
194
+ - `normal` — claims match receipts, so the run is `VERIFIED`
195
+ - `lying` — claims are fabricated, so the run is `UNVERIFIED`
196
+ - `replit` — the claim does not match what actually ran, so the run is `CONTRADICTED`
197
+
198
+ The demo agent uses `RECEIPTS_URL` (default `http://localhost:8000`) and `RECEIPTS_API_KEY` (default `devproxy`).
199
+
200
+ ## MCP proxy
201
+
202
+ ```bash
203
+ pip install receipts-mcp
204
+ ```
205
+
206
+ `receipts_mcp/` is a stdio MCP server that aggregates upstream MCP servers, namespaces tools as `<server>__<tool>`, forwards calls to the real upstream, and posts the real output to `/tools/record`.
207
+
208
+ If `receipts_mcp/upstreams.json` does not exist, the proxy falls back to the built-in demo tools:
209
+
210
+ - `write_file`
211
+ - `http_fetch`
212
+ - `db_query`
213
+
214
+ Supported upstream transports: `stdio`, `sse`, `streamable_http`.
215
+
216
+ `${ENV_VAR}` references inside upstream configs are expanded from the process environment at runtime.
217
+
218
+ Each proxy process uses one `mcp-<hex>` session ID.
219
+
220
+ The real result is ALWAYS returned to the agent, even if receipting fails — a backend outage must not break the agent's tools. A 401 (misconfigured key) is logged loudly.
221
+
222
+ ### Example upstream config
223
+
224
+ See [`receipts_mcp/upstreams.json.example`](receipts_mcp/upstreams.json.example):
225
+
226
+ ```json
227
+ {
228
+ "upstreams": {
229
+ "filesystem": {
230
+ "transport": "stdio",
231
+ "command": "npx",
232
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
233
+ "env": {}
234
+ },
235
+ "github": {
236
+ "transport": "streamable_http",
237
+ "url": "https://mcp.internal.example.com/github",
238
+ "headers": { "Authorization": "Bearer ${GITHUB_MCP_TOKEN}" }
239
+ }
240
+ },
241
+ "include_demo_tools": false
242
+ }
243
+ ```
244
+
245
+ ### Claude Code / Cursor config
246
+
247
+ ![Receipts MCP server connected in Cursor — write_file, http_fetch, db_query tools visible](docs/cursor-mcp.png)
248
+
249
+ ```json
250
+ {
251
+ "mcpServers": {
252
+ "receipts": {
253
+ "command": "/home/shreyansh/receipts/.venv/bin/python3",
254
+ "args": ["-m", "receipts_mcp.server"],
255
+ "cwd": "/home/shreyansh/receipts",
256
+ "env": {
257
+ "RECEIPTS_URL": "http://localhost:8000",
258
+ "RECEIPTS_API_KEY": "<proxy-key>",
259
+ "UPSTREAMS_PATH": "receipts_mcp/upstreams.json",
260
+ "PYTHONPATH": "/home/shreyansh/receipts"
261
+ }
262
+ }
263
+ }
264
+ }
265
+ ```
266
+
267
+ ### Proxy settings
268
+
269
+ | Variable | Default | Description |
270
+ |---|---|---|
271
+ | `RECEIPTS_URL` | `http://localhost:8000` | Backend base URL |
272
+ | `RECEIPTS_API_KEY` | none | Proxy-role key for `/tools/record` |
273
+ | `UPSTREAMS_PATH` | `receipts_mcp/upstreams.json` | Upstream config file |
274
+ | `RECORD_TIMEOUT_SECONDS` | `5.0` | Backend POST timeout |
275
+ | `TOOL_TIMEOUT_SECONDS` | `60.0` | Upstream tool-call timeout |
276
+
277
+ ## Tests
278
+
279
+ ```bash
280
+ python -m pytest
281
+ ```
282
+
283
+ The test suite covers:
284
+
285
+ - **`tests/test_verification.py`** — verification logic, session lifecycle, auto-verify (VERIFIED/TAMPERED/empty), session timeout detection, explicit close endpoint, `/tools/record` signing, and auth enforcement (401/403/200 per role)
286
+ - **`tests/test_proxy.py`** — real upstream MCP server via `tests/fixtures/mock_upstream.py`, proxy forwarding and receipting, namespacing, error handling
287
+ - **`test_mcp.py`** — smoke test against a live backend (receipting path, session visibility, auth enforcement)
288
+
289
+ Tests use isolated temporary SQLite databases via `tmp_path` fixtures.
290
+
291
+ ## Docker
292
+
293
+ The repo includes `docker-compose.yml` for a production-style single-tenant deployment:
294
+
295
+ - **backend** on port `8000` — Python 3.12, uvicorn, SQLite in a Docker volume, healthcheck on `/healthz`
296
+ - **frontend** on port `8080` — Node 20 build stage, nginx 1.27 serving stage with API reverse proxy; nginx injects the `PROXY_KEY` header so the JS bundle never contains credentials
297
+
298
+ ```bash
299
+ # Copy and fill in secrets
300
+ cp .env.example .env
301
+ # Edit .env: set RECEIPT_SECRET, API_KEYS, RECEIPTS_PROXY_KEY
302
+
303
+ docker compose up --build
304
+ ```
305
+
306
+ SQLite data is persisted in the `receipts-data` Docker volume.
307
+
308
+ ## Architecture
309
+
310
+ ```
311
+ ┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐
312
+ │ AI Agent │────▶│ Receipts MCP │────▶│ Upstream MCP │
313
+ │ (Claude, │ │ Proxy (stdio) │ │ Server(s) │
314
+ │ Cursor) │ │ │ │ (real tools) │
315
+ └─────────────┘ └──────┬───────────┘ └──────────────────┘
316
+ │ POST /tools/record
317
+ ┌──────▼───────────┐
318
+ │ Receipts │
319
+ │ Backend │
320
+ │ (FastAPI) │
321
+ │ ┌────────────┐ │
322
+ │ │ SQLite │ │
323
+ │ │ receipts │ │
324
+ │ │ sessions │ │
325
+ │ │ api_keys │ │
326
+ │ │ alerts │ │
327
+ │ └────────────┘ │
328
+ └──────┬───────────┘
329
+ │ /stats, /receipts, /sessions
330
+ ┌──────▼───────────┐
331
+ │ Dashboard │
332
+ │ (React SPA) │
333
+ └──────────────────┘
334
+ ```
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "receipts-mcp"
7
+ version = "0.1.1"
8
+ description = "MCP proxy that records and cryptographically signs AI agent tool calls"
9
+ readme = "receipts_mcp/README.md"
10
+ requires-python = ">=3.12"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Shreyansh", email = "shreyanshkanojia104@gmail.com" }]
13
+ dependencies = [
14
+ "mcp>=1.0.0",
15
+ "httpx>=0.27.0",
16
+ "pydantic-settings>=2.2.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ receipts-mcp = "receipts_mcp.server:main"
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/Shreyansh-Kanojiaa/receipts"
24
+
25
+ [tool.setuptools.packages.find]
26
+ include = ["receipts_mcp*"]
27
+
28
+ [tool.setuptools.package-data]
29
+ receipts_mcp = ["upstreams.json.example"]
@@ -0,0 +1,111 @@
1
+ # Receipts MCP Proxy
2
+
3
+ `receipts_mcp/` is a stdio MCP server that sits between an MCP client and one or more upstream MCP servers.
4
+
5
+ For each tool call:
6
+
7
+ 1. The client calls the Receipts proxy.
8
+ 2. The proxy forwards the call to the real upstream MCP server.
9
+ 3. The real output is captured.
10
+ 4. The proxy POSTs that result to `POST /tools/record` on the Receipts backend.
11
+ 5. The real result is returned to the client even if receipting fails.
12
+
13
+ Upstream tools are namespaced as `<server>__<tool>` so collisions do not clobber each other.
14
+
15
+ If no upstream config file exists, the proxy falls back to the built-in demo tools:
16
+
17
+ - `write_file`
18
+ - `http_fetch`
19
+ - `db_query`
20
+
21
+ Those demo tools are forwarded to the backend's `/tools/call` endpoint.
22
+
23
+ ## Setup
24
+
25
+ ```bash
26
+ pip install receipts-mcp
27
+ cp upstreams.json.example upstreams.json
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ `receipts_mcp/config.py` reads environment variables and an upstreams JSON file.
33
+
34
+ Environment variables:
35
+
36
+ | Variable | Default | Purpose |
37
+ |----------|---------|---------|
38
+ | `RECEIPTS_URL` | `http://localhost:8000` | Receipts backend URL |
39
+ | `RECEIPTS_API_KEY` | empty | Proxy-role API key used for `/tools/record` |
40
+ | `UPSTREAMS_PATH` | `receipts_mcp/upstreams.json` | Path to upstream config |
41
+ | `TOOL_TIMEOUT_SECONDS` | `60` | Max time for an upstream tool call |
42
+ | `RECORD_TIMEOUT_SECONDS` | `5` | Max time for backend receipting |
43
+
44
+ The upstreams file supports:
45
+
46
+ - `stdio`
47
+ - `sse`
48
+ - `streamable_http`
49
+
50
+ `${ENV_VAR}` references inside `env` and `headers` are expanded from the process environment.
51
+
52
+ Example:
53
+
54
+ ```json
55
+ {
56
+ "include_demo_tools": false,
57
+ "upstreams": {
58
+ "github": {
59
+ "transport": "stdio",
60
+ "command": "/path/to/github-mcp",
61
+ "args": [],
62
+ "env": {
63
+ "GITHUB_TOKEN": "${GITHUB_TOKEN}"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Claude Code / Cursor config
71
+
72
+ Add this to `~/.claude/claude_mcp_config.json` or the equivalent Cursor MCP config:
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "receipts": {
78
+ "command": "/home/shreyansh/receipts/.venv/bin/python3",
79
+ "args": ["-m", "receipts_mcp.server"],
80
+ "cwd": "/home/shreyansh/receipts",
81
+ "env": {
82
+ "RECEIPTS_URL": "http://localhost:8000",
83
+ "RECEIPTS_API_KEY": "<proxy-key>",
84
+ "UPSTREAMS_PATH": "receipts_mcp/upstreams.json",
85
+ "PYTHONPATH": "/home/shreyansh/receipts"
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ Restart the MCP client after changing the config.
93
+
94
+ ## Runtime behavior
95
+
96
+ - One proxy process gets one `mcp-<hex>` session ID.
97
+ - All tool calls from that process share that session so they can be reconciled together.
98
+ - If the backend is unreachable, the tool result still returns and the receipt is skipped.
99
+ - If the backend rejects the API key, the proxy logs it as a configuration error.
100
+
101
+ ## Run
102
+
103
+ ```bash
104
+ python3 -m receipts_mcp
105
+ ```
106
+
107
+ or
108
+
109
+ ```bash
110
+ python3 -m receipts_mcp.server
111
+ ```
@@ -0,0 +1,3 @@
1
+ """Receipts MCP proxy — cryptographic receipting for AI agent tool calls."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,3 @@
1
+ from receipts_mcp.server import main
2
+
3
+ main()
@@ -0,0 +1,79 @@
1
+ """Configuration for the Receipts MCP proxy.
2
+
3
+ Two inputs:
4
+ 1. Environment (via pydantic-settings): where the Receipts backend is and the API
5
+ key to authenticate to it.
6
+ 2. An upstreams file (JSON): the company's real MCP servers to front. Format mirrors
7
+ the familiar ``mcpServers`` convention. ``${ENV_VAR}`` references inside ``env`` and
8
+ ``headers`` are expanded from the process environment so upstream secrets are never
9
+ committed to the file.
10
+ """
11
+ import json
12
+ import os
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from pydantic_settings import BaseSettings, SettingsConfigDict
18
+
19
+ from mcp.client.stdio import StdioServerParameters
20
+ from mcp.client.session_group import SseServerParameters, StreamableHttpParameters
21
+
22
+ _ENV_REF = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
23
+
24
+
25
+ class ProxySettings(BaseSettings):
26
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore", case_sensitive=False)
27
+
28
+ receipts_url: str = "http://localhost:8000"
29
+ receipts_api_key: str | None = None # proxy-role key for /tools/record
30
+ upstreams_path: str = "receipts_mcp/upstreams.json"
31
+ record_timeout_seconds: float = 5.0 # backend POST timeout
32
+ tool_timeout_seconds: float = 60.0 # upstream tool-call timeout
33
+
34
+
35
+ def _expand(value: Any) -> Any:
36
+ """Recursively expand ${ENV_VAR} references in strings."""
37
+ if isinstance(value, str):
38
+ return _ENV_REF.sub(lambda m: os.environ.get(m.group(1), ""), value)
39
+ if isinstance(value, dict):
40
+ return {k: _expand(v) for k, v in value.items()}
41
+ if isinstance(value, list):
42
+ return [_expand(v) for v in value]
43
+ return value
44
+
45
+
46
+ def load_upstreams(path: str) -> tuple[dict[str, Any], bool]:
47
+ """Load and validate the upstreams file.
48
+
49
+ Returns ``(server_params_by_key, include_demo_tools)`` where each value is an
50
+ SDK params object ready for ``ClientSessionGroup.connect_to_server``.
51
+ """
52
+ p = Path(path)
53
+ if not p.exists():
54
+ return {}, False
55
+ data = json.loads(p.read_text())
56
+ include_demo = bool(data.get("include_demo_tools", False))
57
+
58
+ result: dict[str, Any] = {}
59
+ for key, spec in (data.get("upstreams") or {}).items():
60
+ spec = _expand(spec)
61
+ transport = (spec.get("transport") or "stdio").lower()
62
+ if transport == "stdio":
63
+ result[key] = StdioServerParameters(
64
+ command=spec["command"],
65
+ args=spec.get("args", []),
66
+ env={**os.environ, **spec.get("env", {})} if spec.get("env") else None,
67
+ cwd=spec.get("cwd"),
68
+ )
69
+ elif transport == "sse":
70
+ result[key] = SseServerParameters(
71
+ url=spec["url"], headers=spec.get("headers"),
72
+ )
73
+ elif transport in ("streamable_http", "http"):
74
+ result[key] = StreamableHttpParameters(
75
+ url=spec["url"], headers=spec.get("headers"),
76
+ )
77
+ else:
78
+ raise ValueError(f"upstream '{key}': unknown transport '{transport}'")
79
+ return result, include_demo