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.
- receipts_mcp-0.1.1/MANIFEST.in +2 -0
- receipts_mcp-0.1.1/PKG-INFO +124 -0
- receipts_mcp-0.1.1/README.md +334 -0
- receipts_mcp-0.1.1/pyproject.toml +29 -0
- receipts_mcp-0.1.1/receipts_mcp/README.md +111 -0
- receipts_mcp-0.1.1/receipts_mcp/__init__.py +3 -0
- receipts_mcp-0.1.1/receipts_mcp/__main__.py +3 -0
- receipts_mcp-0.1.1/receipts_mcp/config.py +79 -0
- receipts_mcp-0.1.1/receipts_mcp/proxy.py +137 -0
- receipts_mcp-0.1.1/receipts_mcp/server.py +169 -0
- receipts_mcp-0.1.1/receipts_mcp/upstreams.json.example +17 -0
- receipts_mcp-0.1.1/receipts_mcp.egg-info/PKG-INFO +124 -0
- receipts_mcp-0.1.1/receipts_mcp.egg-info/SOURCES.txt +18 -0
- receipts_mcp-0.1.1/receipts_mcp.egg-info/dependency_links.txt +1 -0
- receipts_mcp-0.1.1/receipts_mcp.egg-info/entry_points.txt +2 -0
- receipts_mcp-0.1.1/receipts_mcp.egg-info/requires.txt +3 -0
- receipts_mcp-0.1.1/receipts_mcp.egg-info/top_level.txt +1 -0
- receipts_mcp-0.1.1/setup.cfg +4 -0
- receipts_mcp-0.1.1/tests/test_proxy.py +101 -0
- receipts_mcp-0.1.1/tests/test_verification.py +326 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
158
|
+
|
|
159
|
+

|
|
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
|
+

|
|
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,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
|