mcp-glp-audit-proxy 0.1.0__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.
Files changed (63) hide show
  1. mcp_glp_audit_proxy-0.1.0/LICENSE +21 -0
  2. mcp_glp_audit_proxy-0.1.0/PKG-INFO +201 -0
  3. mcp_glp_audit_proxy-0.1.0/README.md +165 -0
  4. mcp_glp_audit_proxy-0.1.0/pyproject.toml +82 -0
  5. mcp_glp_audit_proxy-0.1.0/setup.cfg +4 -0
  6. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/__init__.py +10 -0
  7. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/__main__.py +19 -0
  8. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/anchor.py +149 -0
  9. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/approvals.py +72 -0
  10. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/canonical.py +44 -0
  11. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/chain.py +101 -0
  12. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/cli.py +84 -0
  13. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/clockutil.py +46 -0
  14. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/config.py +233 -0
  15. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/errors.py +27 -0
  16. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/features.py +113 -0
  17. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/framing.py +86 -0
  18. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/interfaces.py +34 -0
  19. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/plugins.py +19 -0
  20. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/policy.py +88 -0
  21. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/proxy.py +1693 -0
  22. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/records.py +88 -0
  23. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/reporters/__init__.py +6 -0
  24. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/reporters/json_export.py +88 -0
  25. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/reporters/text_export.py +33 -0
  26. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/sdkcheck.py +40 -0
  27. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/server.py +146 -0
  28. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/signers/__init__.py +5 -0
  29. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/signers/unsigned.py +32 -0
  30. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/storage/__init__.py +5 -0
  31. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/storage/jsonl.py +265 -0
  32. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/tlsutil.py +24 -0
  33. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/upstream.py +345 -0
  34. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/verification/__init__.py +15 -0
  35. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/verification/__main__.py +13 -0
  36. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/verification/oq.py +270 -0
  37. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy/verification/rtm.py +88 -0
  38. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy.egg-info/PKG-INFO +201 -0
  39. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy.egg-info/SOURCES.txt +61 -0
  40. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy.egg-info/dependency_links.txt +1 -0
  41. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy.egg-info/entry_points.txt +14 -0
  42. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy.egg-info/requires.txt +13 -0
  43. mcp_glp_audit_proxy-0.1.0/src/mcp_glp_audit_proxy.egg-info/top_level.txt +1 -0
  44. mcp_glp_audit_proxy-0.1.0/tests/test_anchor.py +244 -0
  45. mcp_glp_audit_proxy-0.1.0/tests/test_approval.py +156 -0
  46. mcp_glp_audit_proxy-0.1.0/tests/test_chain.py +99 -0
  47. mcp_glp_audit_proxy-0.1.0/tests/test_docs.py +82 -0
  48. mcp_glp_audit_proxy-0.1.0/tests/test_export.py +97 -0
  49. mcp_glp_audit_proxy-0.1.0/tests/test_fail_closed.py +106 -0
  50. mcp_glp_audit_proxy-0.1.0/tests/test_faults.py +255 -0
  51. mcp_glp_audit_proxy-0.1.0/tests/test_features.py +72 -0
  52. mcp_glp_audit_proxy-0.1.0/tests/test_framing.py +108 -0
  53. mcp_glp_audit_proxy-0.1.0/tests/test_limits.py +79 -0
  54. mcp_glp_audit_proxy-0.1.0/tests/test_oq_runner.py +94 -0
  55. mcp_glp_audit_proxy-0.1.0/tests/test_policy.py +102 -0
  56. mcp_glp_audit_proxy-0.1.0/tests/test_properties.py +66 -0
  57. mcp_glp_audit_proxy-0.1.0/tests/test_protocol.py +254 -0
  58. mcp_glp_audit_proxy-0.1.0/tests/test_reconciliation_sop.py +66 -0
  59. mcp_glp_audit_proxy-0.1.0/tests/test_rtm.py +48 -0
  60. mcp_glp_audit_proxy-0.1.0/tests/test_sample_report.py +27 -0
  61. mcp_glp_audit_proxy-0.1.0/tests/test_server_loop.py +279 -0
  62. mcp_glp_audit_proxy-0.1.0/tests/test_stdio_e2e.py +395 -0
  63. mcp_glp_audit_proxy-0.1.0/tests/test_tamper.py +85 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antonio Castellon / Castellon.CH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-glp-audit-proxy
3
+ Version: 0.1.0
4
+ Summary: Local MCP proxy that writes a tamper-evident audit trail. Designed to support GLP traceability.
5
+ Author-email: Antonio Castellon <antonio@castellon.ch>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/antonio-castellon/mcp-glp-audit-proxy
8
+ Project-URL: Issues, https://github.com/antonio-castellon/mcp-glp-audit-proxy/issues
9
+ Project-URL: Documentation, https://github.com/antonio-castellon/mcp-glp-audit-proxy/blob/main/docs/STANDARDS.md
10
+ Keywords: mcp,glp,audit,proxy,part-11,alcoa
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Security
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: mcp<3,>=2.2.0
24
+ Requires-Dist: rfc8785==0.1.4
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.3; extra == "dev"
27
+ Requires-Dist: jsonschema>=4.21; extra == "dev"
28
+ Requires-Dist: hypothesis>=6.110; extra == "dev"
29
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
30
+ Requires-Dist: PyYAML>=6.0; extra == "dev"
31
+ Requires-Dist: pip-audit>=2.7; extra == "dev"
32
+ Requires-Dist: bandit>=1.7; extra == "dev"
33
+ Requires-Dist: ruff>=0.6; extra == "dev"
34
+ Requires-Dist: defusedxml>=0.7; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ mcp-name: io.github.antonio-castellon/mcp-glp-audit-proxy
38
+
39
+ # mcp-glp-audit-proxy
40
+
41
+ Local MCP proxy that sits between an MCP host and your MCP servers and writes a tamper-evident audit trail. Community edition, v0.1.0.
42
+
43
+ It is designed to support GLP, ALCOA+, 21 CFR Part 11 and EU GMP Annex 11 traceability. Data stays on the customer's infrastructure. This package is the open core. Signed checkpoints, WORM storage, Part 11 electronic signatures, PDF/CSV reports, the IQ/OQ pack and SSO/OAuth are Pro features and are not in this repository.
44
+
45
+ ```
46
+ host (Claude, Cursor, Grok, self-hosted LLM)
47
+ | stdio JSON-RPC
48
+ v
49
+ mcp-glp-audit-proxy
50
+ policy (allow / deny, reason, human approval)
51
+ audit (append-only JSONL, RFC 8785, SHA-256 chain)
52
+ | stdio or HTTPS (TLS 1.3)
53
+ v
54
+ upstream MCP servers (Excel, ELN, LIMS, databases, instruments)
55
+ ```
56
+
57
+ ## Install
58
+
59
+ Requires Python 3.11 or newer.
60
+
61
+ ```bash
62
+ pip install mcp-glp-audit-proxy
63
+ ```
64
+
65
+ From a checkout:
66
+
67
+ ```bash
68
+ pip install -e ".[dev]"
69
+ ```
70
+
71
+ The `mcp` dependency is pinned to `>=2.2.0,<3`. That floor is outside GHSA-84m7-p3x7-pcfv (affected ranges are `mcp >=1.8.0,<1.30.0` and `>=2.0.0,<2.2.0`). The process refuses to start on an older SDK.
72
+
73
+ ## Run
74
+
75
+ ```bash
76
+ export GLP_PRINCIPAL_ID=operator-1
77
+ export GLP_PRINCIPAL_NAME="Ada Analyst"
78
+ export GLP_PRINCIPAL_ROLE=operator
79
+ mcp-glp-audit-proxy --config config.json
80
+ ```
81
+
82
+ Stdio identity comes from the environment. There is no HTTP listener in 0.1.0. A Cursor snippet is in `mcp.json`. A full config is in `config.example.json`.
83
+
84
+ Verify a chain and write a searchable JSON export (the export is skipped when the chain does not verify):
85
+
86
+ ```bash
87
+ mcp-glp-audit-proxy-verify data/audit.jsonl --json-export exports/audit.json
88
+ mcp-glp-audit-proxy-verify data/audit.jsonl --tool excel__write_cell --status ok
89
+ mcp-glp-audit-proxy-verify data/audit.jsonl --print-head
90
+ ```
91
+
92
+ `--print-head` prints `seq` and `entry_hash`. Copy that line into a ticket, an email or a LIMS record. The sidecar `data/audit.jsonl.head` makes a cut of whole trailing lines fail verification. Someone who can edit both the log and the sidecar can move them together; the external copy is the control. Signed checkpoints and WORM storage are Pro.
93
+
94
+ ## What is recorded
95
+
96
+ Every mediated call, including denials and failures, is one JSON object on an append-only JSONL file. Each line is RFC 8785 canonical JSON. `entry_hash` is the SHA-256 of that object with `entry_hash` removed. `prev_hash` is the previous entry (64 zero digits for the first). `hash_algo` and `canonicalization` are stored on the entry. The open schema is `schema/audit-record.schema.json`. Cross-language vectors are in `schema/jcs-test-vectors.json`.
97
+
98
+ Fields include who (`principal` id, name, role), which tool, inputs, outputs, UTC timestamp plus `utc_offset`, protocol version, optional session id, record class (`gxp_data`, `access`, `technical`, `config`), reason for writes, and the previous hash.
99
+
100
+ The GxP trail is that file. The Python `logging` logger `mcp_glp_audit_proxy.technical` writes method, status and record class to stderr. It is not the record. Arguments are not written there.
101
+
102
+ ## Policy
103
+
104
+ - Allow list and deny list use shell-style globs on the exposed tool name (`excel__write_cell`). Deny wins. An empty allow list denies every upstream tool.
105
+ - Roles are `operator`, `approver`, `qa` and `admin`. Only `operator` may call upstream tools. `qa` is read-only on the trail (`glp_verify_chain`, `glp_search_audit`, `glp_export_audit`). `approver` may record an approval and read the trail. `admin` may change policy and read the trail, and cannot call upstream tools.
106
+ - Auditing cannot be switched off. `glp_update_policy` with `audit_enabled: false` is denied and logged.
107
+ - Write tools (names matching `write_tools`) require a reason by default. The reason is `_meta["io.github.antonio-castellon/reason"]` or the argument `_glp_reason` (stripped before the upstream call).
108
+ - Optional human approval holds the write until a different person in the `approver` role records meaning `approved`. `reviewed` is stored and does not authorise the write. `rejected` rejects it. An approval is single-use, bound to the SHA-256 of the canonical `{tool, arguments}`, and expires (default 3600 seconds). The record label is `approval`. It is not an electronic signature.
109
+ - Fail closed is the default. The store is checked before a call is forwarded. If the append after a successful upstream call fails, the upstream payload is not returned to the host. Denied and failed calls are still logged. If the upstream may already have applied a write, follow `docs/RECONCILIATION_SOP.md`.
110
+
111
+ ## Protocol
112
+
113
+ The proxy speaks both handshake-era MCP revisions (`2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`, with `initialize`) and spec `2026-07-28` (`server/discover`, no initialize, no session). A session id is recorded only when that revision has one. Upstream protocol `auto` tries `server/discover` and falls back to `initialize` at `2025-11-25`.
114
+
115
+ Exposed tool names are `server__tool`. Resource URIs are prefixed `server:`. Credential-bearing `_meta` keys are stripped, their names are logged, and their values are neither stored nor forwarded. Upstream stdio receives a safe environment allow-list from the MCP SDK, plus the env block in the config.
116
+
117
+ ## Regulatory coverage
118
+
119
+ mcp-glp-audit-proxy is designed to support the audit-trail requirements below. Meeting them still depends on your validated use, SOPs and reviews. The clause-by-clause matrix is `docs/COVERAGE.md`. The norms and the verification log are `docs/STANDARDS.md`.
120
+
121
+ | Framework | What the proxy helps with | Edition |
122
+ |---|---|---|
123
+ | OECD GLP No. 17 (3.4) / Swiss GLPV Annex 2 | Always-on, append-only audit trail; who/what/when/why; admin-only settings; QA read-only role; 10-year Swiss retention profile | Community core; WORM retention and PDF reports in Pro |
124
+ | FDA 21 CFR Part 11 | Computer-generated, time-stamped, hash-chained audit trail (11.10(e)); tamper detection; complete copies; authority checks. Part 11 approval e-signatures (11.50, 11.70, 11.200) are Pro only: the Community `approval` is not an electronic signature | Community core; e-signatures (Pro only), reports, signing, SSO in Pro |
125
+ | EU GMP Annex 11 (2011; 2025 draft) | Audit trail of changes with reason; role, time zone; locked audit trail; searchable complete export; access log | Community core; review tooling and inactivity logout in Pro |
126
+ | MHRA / PIC/S PI 041-1 / WHO TRS 1033 | ALCOA+ data integrity, segregation of duties, metadata with each record, true-copy exports | Community core; signed manifests and validation pack in Pro |
127
+ | ISPE GAMP 5 / FDA CSA | Supplier documentation, proposed Cat. 4 configured components, machine-readable test evidence | Pro (validation pack) |
128
+ | EU AI Act Art. 12, 19, 26(6) | Automatic event logging, minimum 6-month retention | Community |
129
+ | Crypto and time (RFC 8785, SHA-256, Ed25519/ECDSA, RFC 3161, eIDAS/ZertES, TLS 1.3) | Canonical hashing and encrypted transport | Hash chain and TLS in Community; signing and timestamps in Pro |
130
+ | MCP authorization, OWASP LLM/Agentic | No token passthrough, tool allowlist/denylist, approval before write tools, OAuth 2.1 | Allowlist and no-passthrough in Community; OAuth/SSO in Pro |
131
+ | ISO/IEC 42001, NIST AI RMF | Event-log evidence for AI governance and monitoring | Community (supporting evidence only) |
132
+
133
+ Default retention is the Swiss GLP profile: 10 years after study completion, with a floor of 6 months. The `minimum` profile may use a shorter year count but cannot go under 6 months. A legal-hold flag refuses disposal. The JSONL backend has no delete. Retention-period WORM enforcement is a Pro backend.
134
+
135
+ ## Community and Pro
136
+
137
+ This repository is the Community edition. Pro is available as a licensed / paid service - contact [antonio@castellon.ch](mailto:antonio@castellon.ch).
138
+
139
+ Pro adds SQL and S3 WORM backends, Ed25519 and ECDSA signing, RFC 3161 timestamps, Part 11 e-signatures, PDF and CSV auditor reports, an IQ/OQ validation pack, and SSO. It also includes the GLP Lab Data Pack for [mcp-advanced-excel](https://github.com/antonio-castellon/mcp-advanced-excel), unlocked by the same offline licence key. The free mcp-advanced-excel server reads and writes lab spreadsheets. The Data Pack adds what a GLP lab needs on top: provenance hashes that tie every result back to its source file, instrument templates for common lab exports, advanced LOD/LOQ calculations and molar unit conversions, batch processing of many files in one run, and Ed25519 signed output.
140
+
141
+ | Capability | Community (this repo) | Pro (licensed service) |
142
+ |---|---|---|
143
+ | Proxy core, both protocol eras | yes | plugs in, does not fork |
144
+ | Append-only JSONL hash chain | yes | |
145
+ | PostgreSQL / MSSQL append-only | | yes |
146
+ | S3 Object Lock | | yes |
147
+ | Verify CLI and searchable JSON export | yes | |
148
+ | Plain-text verify listing | yes | |
149
+ | PDF / CSV auditor reports | | yes |
150
+ | Audit-trail review tooling (flags, reviewer sign-off) | search, filter, export | yes |
151
+ | DE / FR / EN report templates | | later |
152
+ | Old/new value capture for write tools | planned | change highlighting in reports |
153
+ | Tool allowlist / denylist | yes | |
154
+ | Local roles (operator, approver, qa, admin) | yes | |
155
+ | Human approval before writes | yes (label: approval) | |
156
+ | Part 11 electronic signatures | | yes |
157
+ | Ed25519 / ECDSA signed checkpoints | unsigned checkpoints only | yes |
158
+ | RFC 3161 timestamps | | yes |
159
+ | IQ/OQ validation pack | | yes |
160
+ | SSO / OAuth | | yes |
161
+ | Syslog (RFC 5424) / SIEM export | | yes |
162
+ | Key management (KMS/HSM), field-level encryption and redaction | | yes |
163
+ | FIPS 140-3 mode | | on request |
164
+ | Merkle log with inclusion / consistency proofs | | later |
165
+ | OpenTelemetry spans (technical telemetry, not the audit record) | planned | |
166
+ | GLP Lab Data Pack for mcp-advanced-excel (provenance hashes, instrument templates, advanced LOD/LOQ and molar units, batch processing, Ed25519 signed output) | | yes |
167
+ | TLS 1.3 to remote upstreams, no token passthrough | yes | |
168
+ | Offline licence file, no phone-home | yes | reads the same file |
169
+
170
+ Plugin entry points, the `AuditStore` / `Signer` / `Reporter` contracts, and the licence file are described in `docs/PLUGINS.md`.
171
+
172
+ ## Local tools
173
+
174
+ | Tool | Who | What |
175
+ |---|---|---|
176
+ | `glp_verify_chain` | qa, approver, admin | Check chain integrity. Still returns the report if the chain is broken. |
177
+ | `glp_search_audit` | qa, approver, admin | Filter entries. The audit of the search stores the filter and the count, not a second copy of every row. |
178
+ | `glp_export_audit` | qa, approver, admin | JSON electronic export with a hash manifest. A filtered export is marked `complete: false`. A broken chain is not exported. |
179
+ | `glp_approve` | approver | Record meaning `approved`, `reviewed` or `rejected`. |
180
+ | `glp_update_policy` | admin | Append a config event. Policy is replayed from the log. |
181
+
182
+ ## Safety
183
+
184
+ Read `SAFETY.md`. Short version: fail closed, no token passthrough, secrets are stripped from `_meta`, stdio children do not inherit the parent environment, an approval is not an electronic signature, and the JSONL file is tamper-evident, not a WORM store.
185
+
186
+ ## Development
187
+
188
+ ```bash
189
+ pip install -e ".[dev]"
190
+ python -m pytest -q
191
+ ```
192
+
193
+ CI runs the same suite on Python 3.11, 3.12 and 3.13 (`.github/workflows/ci.yml`).
194
+
195
+ ## MCP registry
196
+
197
+ `server.json` names this server `io.github.antonio-castellon/mcp-glp-audit-proxy` for the official MCP Registry. The marker on the first line of this file is the registry check.
198
+
199
+ ## Licence
200
+
201
+ MIT. Copyright (c) 2026 Antonio Castellon / Castellon.CH. See `LICENSE`.
@@ -0,0 +1,165 @@
1
+ mcp-name: io.github.antonio-castellon/mcp-glp-audit-proxy
2
+
3
+ # mcp-glp-audit-proxy
4
+
5
+ Local MCP proxy that sits between an MCP host and your MCP servers and writes a tamper-evident audit trail. Community edition, v0.1.0.
6
+
7
+ It is designed to support GLP, ALCOA+, 21 CFR Part 11 and EU GMP Annex 11 traceability. Data stays on the customer's infrastructure. This package is the open core. Signed checkpoints, WORM storage, Part 11 electronic signatures, PDF/CSV reports, the IQ/OQ pack and SSO/OAuth are Pro features and are not in this repository.
8
+
9
+ ```
10
+ host (Claude, Cursor, Grok, self-hosted LLM)
11
+ | stdio JSON-RPC
12
+ v
13
+ mcp-glp-audit-proxy
14
+ policy (allow / deny, reason, human approval)
15
+ audit (append-only JSONL, RFC 8785, SHA-256 chain)
16
+ | stdio or HTTPS (TLS 1.3)
17
+ v
18
+ upstream MCP servers (Excel, ELN, LIMS, databases, instruments)
19
+ ```
20
+
21
+ ## Install
22
+
23
+ Requires Python 3.11 or newer.
24
+
25
+ ```bash
26
+ pip install mcp-glp-audit-proxy
27
+ ```
28
+
29
+ From a checkout:
30
+
31
+ ```bash
32
+ pip install -e ".[dev]"
33
+ ```
34
+
35
+ The `mcp` dependency is pinned to `>=2.2.0,<3`. That floor is outside GHSA-84m7-p3x7-pcfv (affected ranges are `mcp >=1.8.0,<1.30.0` and `>=2.0.0,<2.2.0`). The process refuses to start on an older SDK.
36
+
37
+ ## Run
38
+
39
+ ```bash
40
+ export GLP_PRINCIPAL_ID=operator-1
41
+ export GLP_PRINCIPAL_NAME="Ada Analyst"
42
+ export GLP_PRINCIPAL_ROLE=operator
43
+ mcp-glp-audit-proxy --config config.json
44
+ ```
45
+
46
+ Stdio identity comes from the environment. There is no HTTP listener in 0.1.0. A Cursor snippet is in `mcp.json`. A full config is in `config.example.json`.
47
+
48
+ Verify a chain and write a searchable JSON export (the export is skipped when the chain does not verify):
49
+
50
+ ```bash
51
+ mcp-glp-audit-proxy-verify data/audit.jsonl --json-export exports/audit.json
52
+ mcp-glp-audit-proxy-verify data/audit.jsonl --tool excel__write_cell --status ok
53
+ mcp-glp-audit-proxy-verify data/audit.jsonl --print-head
54
+ ```
55
+
56
+ `--print-head` prints `seq` and `entry_hash`. Copy that line into a ticket, an email or a LIMS record. The sidecar `data/audit.jsonl.head` makes a cut of whole trailing lines fail verification. Someone who can edit both the log and the sidecar can move them together; the external copy is the control. Signed checkpoints and WORM storage are Pro.
57
+
58
+ ## What is recorded
59
+
60
+ Every mediated call, including denials and failures, is one JSON object on an append-only JSONL file. Each line is RFC 8785 canonical JSON. `entry_hash` is the SHA-256 of that object with `entry_hash` removed. `prev_hash` is the previous entry (64 zero digits for the first). `hash_algo` and `canonicalization` are stored on the entry. The open schema is `schema/audit-record.schema.json`. Cross-language vectors are in `schema/jcs-test-vectors.json`.
61
+
62
+ Fields include who (`principal` id, name, role), which tool, inputs, outputs, UTC timestamp plus `utc_offset`, protocol version, optional session id, record class (`gxp_data`, `access`, `technical`, `config`), reason for writes, and the previous hash.
63
+
64
+ The GxP trail is that file. The Python `logging` logger `mcp_glp_audit_proxy.technical` writes method, status and record class to stderr. It is not the record. Arguments are not written there.
65
+
66
+ ## Policy
67
+
68
+ - Allow list and deny list use shell-style globs on the exposed tool name (`excel__write_cell`). Deny wins. An empty allow list denies every upstream tool.
69
+ - Roles are `operator`, `approver`, `qa` and `admin`. Only `operator` may call upstream tools. `qa` is read-only on the trail (`glp_verify_chain`, `glp_search_audit`, `glp_export_audit`). `approver` may record an approval and read the trail. `admin` may change policy and read the trail, and cannot call upstream tools.
70
+ - Auditing cannot be switched off. `glp_update_policy` with `audit_enabled: false` is denied and logged.
71
+ - Write tools (names matching `write_tools`) require a reason by default. The reason is `_meta["io.github.antonio-castellon/reason"]` or the argument `_glp_reason` (stripped before the upstream call).
72
+ - Optional human approval holds the write until a different person in the `approver` role records meaning `approved`. `reviewed` is stored and does not authorise the write. `rejected` rejects it. An approval is single-use, bound to the SHA-256 of the canonical `{tool, arguments}`, and expires (default 3600 seconds). The record label is `approval`. It is not an electronic signature.
73
+ - Fail closed is the default. The store is checked before a call is forwarded. If the append after a successful upstream call fails, the upstream payload is not returned to the host. Denied and failed calls are still logged. If the upstream may already have applied a write, follow `docs/RECONCILIATION_SOP.md`.
74
+
75
+ ## Protocol
76
+
77
+ The proxy speaks both handshake-era MCP revisions (`2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`, with `initialize`) and spec `2026-07-28` (`server/discover`, no initialize, no session). A session id is recorded only when that revision has one. Upstream protocol `auto` tries `server/discover` and falls back to `initialize` at `2025-11-25`.
78
+
79
+ Exposed tool names are `server__tool`. Resource URIs are prefixed `server:`. Credential-bearing `_meta` keys are stripped, their names are logged, and their values are neither stored nor forwarded. Upstream stdio receives a safe environment allow-list from the MCP SDK, plus the env block in the config.
80
+
81
+ ## Regulatory coverage
82
+
83
+ mcp-glp-audit-proxy is designed to support the audit-trail requirements below. Meeting them still depends on your validated use, SOPs and reviews. The clause-by-clause matrix is `docs/COVERAGE.md`. The norms and the verification log are `docs/STANDARDS.md`.
84
+
85
+ | Framework | What the proxy helps with | Edition |
86
+ |---|---|---|
87
+ | OECD GLP No. 17 (3.4) / Swiss GLPV Annex 2 | Always-on, append-only audit trail; who/what/when/why; admin-only settings; QA read-only role; 10-year Swiss retention profile | Community core; WORM retention and PDF reports in Pro |
88
+ | FDA 21 CFR Part 11 | Computer-generated, time-stamped, hash-chained audit trail (11.10(e)); tamper detection; complete copies; authority checks. Part 11 approval e-signatures (11.50, 11.70, 11.200) are Pro only: the Community `approval` is not an electronic signature | Community core; e-signatures (Pro only), reports, signing, SSO in Pro |
89
+ | EU GMP Annex 11 (2011; 2025 draft) | Audit trail of changes with reason; role, time zone; locked audit trail; searchable complete export; access log | Community core; review tooling and inactivity logout in Pro |
90
+ | MHRA / PIC/S PI 041-1 / WHO TRS 1033 | ALCOA+ data integrity, segregation of duties, metadata with each record, true-copy exports | Community core; signed manifests and validation pack in Pro |
91
+ | ISPE GAMP 5 / FDA CSA | Supplier documentation, proposed Cat. 4 configured components, machine-readable test evidence | Pro (validation pack) |
92
+ | EU AI Act Art. 12, 19, 26(6) | Automatic event logging, minimum 6-month retention | Community |
93
+ | Crypto and time (RFC 8785, SHA-256, Ed25519/ECDSA, RFC 3161, eIDAS/ZertES, TLS 1.3) | Canonical hashing and encrypted transport | Hash chain and TLS in Community; signing and timestamps in Pro |
94
+ | MCP authorization, OWASP LLM/Agentic | No token passthrough, tool allowlist/denylist, approval before write tools, OAuth 2.1 | Allowlist and no-passthrough in Community; OAuth/SSO in Pro |
95
+ | ISO/IEC 42001, NIST AI RMF | Event-log evidence for AI governance and monitoring | Community (supporting evidence only) |
96
+
97
+ Default retention is the Swiss GLP profile: 10 years after study completion, with a floor of 6 months. The `minimum` profile may use a shorter year count but cannot go under 6 months. A legal-hold flag refuses disposal. The JSONL backend has no delete. Retention-period WORM enforcement is a Pro backend.
98
+
99
+ ## Community and Pro
100
+
101
+ This repository is the Community edition. Pro is available as a licensed / paid service - contact [antonio@castellon.ch](mailto:antonio@castellon.ch).
102
+
103
+ Pro adds SQL and S3 WORM backends, Ed25519 and ECDSA signing, RFC 3161 timestamps, Part 11 e-signatures, PDF and CSV auditor reports, an IQ/OQ validation pack, and SSO. It also includes the GLP Lab Data Pack for [mcp-advanced-excel](https://github.com/antonio-castellon/mcp-advanced-excel), unlocked by the same offline licence key. The free mcp-advanced-excel server reads and writes lab spreadsheets. The Data Pack adds what a GLP lab needs on top: provenance hashes that tie every result back to its source file, instrument templates for common lab exports, advanced LOD/LOQ calculations and molar unit conversions, batch processing of many files in one run, and Ed25519 signed output.
104
+
105
+ | Capability | Community (this repo) | Pro (licensed service) |
106
+ |---|---|---|
107
+ | Proxy core, both protocol eras | yes | plugs in, does not fork |
108
+ | Append-only JSONL hash chain | yes | |
109
+ | PostgreSQL / MSSQL append-only | | yes |
110
+ | S3 Object Lock | | yes |
111
+ | Verify CLI and searchable JSON export | yes | |
112
+ | Plain-text verify listing | yes | |
113
+ | PDF / CSV auditor reports | | yes |
114
+ | Audit-trail review tooling (flags, reviewer sign-off) | search, filter, export | yes |
115
+ | DE / FR / EN report templates | | later |
116
+ | Old/new value capture for write tools | planned | change highlighting in reports |
117
+ | Tool allowlist / denylist | yes | |
118
+ | Local roles (operator, approver, qa, admin) | yes | |
119
+ | Human approval before writes | yes (label: approval) | |
120
+ | Part 11 electronic signatures | | yes |
121
+ | Ed25519 / ECDSA signed checkpoints | unsigned checkpoints only | yes |
122
+ | RFC 3161 timestamps | | yes |
123
+ | IQ/OQ validation pack | | yes |
124
+ | SSO / OAuth | | yes |
125
+ | Syslog (RFC 5424) / SIEM export | | yes |
126
+ | Key management (KMS/HSM), field-level encryption and redaction | | yes |
127
+ | FIPS 140-3 mode | | on request |
128
+ | Merkle log with inclusion / consistency proofs | | later |
129
+ | OpenTelemetry spans (technical telemetry, not the audit record) | planned | |
130
+ | GLP Lab Data Pack for mcp-advanced-excel (provenance hashes, instrument templates, advanced LOD/LOQ and molar units, batch processing, Ed25519 signed output) | | yes |
131
+ | TLS 1.3 to remote upstreams, no token passthrough | yes | |
132
+ | Offline licence file, no phone-home | yes | reads the same file |
133
+
134
+ Plugin entry points, the `AuditStore` / `Signer` / `Reporter` contracts, and the licence file are described in `docs/PLUGINS.md`.
135
+
136
+ ## Local tools
137
+
138
+ | Tool | Who | What |
139
+ |---|---|---|
140
+ | `glp_verify_chain` | qa, approver, admin | Check chain integrity. Still returns the report if the chain is broken. |
141
+ | `glp_search_audit` | qa, approver, admin | Filter entries. The audit of the search stores the filter and the count, not a second copy of every row. |
142
+ | `glp_export_audit` | qa, approver, admin | JSON electronic export with a hash manifest. A filtered export is marked `complete: false`. A broken chain is not exported. |
143
+ | `glp_approve` | approver | Record meaning `approved`, `reviewed` or `rejected`. |
144
+ | `glp_update_policy` | admin | Append a config event. Policy is replayed from the log. |
145
+
146
+ ## Safety
147
+
148
+ Read `SAFETY.md`. Short version: fail closed, no token passthrough, secrets are stripped from `_meta`, stdio children do not inherit the parent environment, an approval is not an electronic signature, and the JSONL file is tamper-evident, not a WORM store.
149
+
150
+ ## Development
151
+
152
+ ```bash
153
+ pip install -e ".[dev]"
154
+ python -m pytest -q
155
+ ```
156
+
157
+ CI runs the same suite on Python 3.11, 3.12 and 3.13 (`.github/workflows/ci.yml`).
158
+
159
+ ## MCP registry
160
+
161
+ `server.json` names this server `io.github.antonio-castellon/mcp-glp-audit-proxy` for the official MCP Registry. The marker on the first line of this file is the registry check.
162
+
163
+ ## Licence
164
+
165
+ MIT. Copyright (c) 2026 Antonio Castellon / Castellon.CH. See `LICENSE`.
@@ -0,0 +1,82 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mcp-glp-audit-proxy"
7
+ version = "0.1.0"
8
+ description = "Local MCP proxy that writes a tamper-evident audit trail. Designed to support GLP traceability."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ { name = "Antonio Castellon", email = "antonio@castellon.ch" },
14
+ ]
15
+ keywords = ["mcp", "glp", "audit", "proxy", "part-11", "alcoa"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Security",
26
+ ]
27
+ dependencies = [
28
+ # 2.2.0 is the first 2.x release outside GHSA-84m7-p3x7-pcfv
29
+ # (affected: mcp >=1.8.0,<1.30.0 and >=2.0.0,<2.2.0).
30
+ "mcp>=2.2.0,<3",
31
+ # Pinned: audit hashes must be reproducible. See schema/jcs-test-vectors.json.
32
+ "rfc8785==0.1.4",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=8.3",
38
+ "jsonschema>=4.21",
39
+ "hypothesis>=6.110",
40
+ "pytest-cov>=5.0",
41
+ "PyYAML>=6.0",
42
+ "pip-audit>=2.7",
43
+ "bandit>=1.7",
44
+ "ruff>=0.6",
45
+ "defusedxml>=0.7",
46
+ ]
47
+
48
+ [project.scripts]
49
+ mcp-glp-audit-proxy = "mcp_glp_audit_proxy.__main__:main"
50
+ mcp-glp-audit-proxy-verify = "mcp_glp_audit_proxy.cli:main"
51
+ mcp-glp-audit-proxy-oq = "mcp_glp_audit_proxy.verification.oq:main"
52
+
53
+ [project.urls]
54
+ Homepage = "https://github.com/antonio-castellon/mcp-glp-audit-proxy"
55
+ Issues = "https://github.com/antonio-castellon/mcp-glp-audit-proxy/issues"
56
+ Documentation = "https://github.com/antonio-castellon/mcp-glp-audit-proxy/blob/main/docs/STANDARDS.md"
57
+
58
+ # The licensed Pro edition registers additional names in these
59
+ # groups. This repo ships only the Community implementations.
60
+ [project.entry-points."mcp_glp_audit_proxy.storage"]
61
+ jsonl = "mcp_glp_audit_proxy.storage.jsonl:JsonlAuditStore"
62
+
63
+ [project.entry-points."mcp_glp_audit_proxy.signers"]
64
+ unsigned = "mcp_glp_audit_proxy.signers.unsigned:UnsignedSigner"
65
+
66
+ [project.entry-points."mcp_glp_audit_proxy.reporters"]
67
+ json = "mcp_glp_audit_proxy.reporters.json_export:JsonReporter"
68
+ text = "mcp_glp_audit_proxy.reporters.text_export:TextReporter"
69
+
70
+ [tool.setuptools.packages.find]
71
+ where = ["src"]
72
+
73
+ [tool.pytest.ini_options]
74
+ testpaths = ["tests"]
75
+ pythonpath = ["src", "tests"]
76
+
77
+ [tool.coverage.run]
78
+ source = ["mcp_glp_audit_proxy"]
79
+ patch = ["subprocess"]
80
+
81
+ [tool.coverage.report]
82
+ show_missing = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """Community edition of the GLP audit MCP proxy.
2
+
3
+ This package is designed to support audit-trail traceability. It does not
4
+ certify a customer's system. Pro features are a separately licensed
5
+ edition and plug in through entry points.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __version__ = "0.1.0"
@@ -0,0 +1,19 @@
1
+ """python -m mcp_glp_audit_proxy - start the stdio MCP proxy."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def main() -> None:
7
+ import sys
8
+
9
+ if len(sys.argv) > 1 and sys.argv[1] == "oq-report":
10
+ from .verification.oq import main as report
11
+
12
+ raise SystemExit(report(sys.argv[2:]))
13
+ from .server import main as run
14
+
15
+ run()
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -0,0 +1,149 @@
1
+ """Sidecar head for the JSONL log.
2
+
3
+ ``<log>.head`` stores the latest ``seq`` and ``entry_hash`` after each fsynced
4
+ line. It is written to a temp file in the same directory, fsynced, then
5
+ renamed into place.
6
+
7
+ A crash between the log fsync and this rename may leave the head one record
8
+ behind. That lag is accepted when the extra record itself verifies. The next
9
+ append, and startup, advance the head. Any other disagreement (missing head,
10
+ shorter log, hash mismatch, lag of more than one) is a store error.
11
+
12
+ Someone who can edit both files can move them together. A copy of the head
13
+ kept outside this machine (ticket, email, LIMS) is the local control. Signed
14
+ checkpoints and WORM storage are Pro.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import errno
20
+ import json
21
+ import os
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+
25
+ from .canonical import canonicalize
26
+ from .chain import VerifyReport, verify_file
27
+ from .errors import AuditStoreError
28
+
29
+
30
+ @dataclass
31
+ class Head:
32
+ seq: int
33
+ entry_hash: str
34
+
35
+
36
+ @dataclass
37
+ class AnchorStatus:
38
+ ok: bool
39
+ errors: list[str]
40
+ lag: int
41
+
42
+
43
+ def head_path(log: Path) -> Path:
44
+ return log.with_name(log.name + ".head")
45
+
46
+
47
+ def read_head(log: Path) -> Head | None:
48
+ path = head_path(log)
49
+ if not path.exists():
50
+ return None
51
+ try:
52
+ raw = path.read_text(encoding="utf-8")
53
+ parsed = json.loads(raw)
54
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
55
+ raise AuditStoreError(f"audit head unreadable: {exc}") from exc
56
+ if not isinstance(parsed, dict):
57
+ raise AuditStoreError("audit head is not an object")
58
+ seq = parsed.get("seq")
59
+ entry_hash = parsed.get("entry_hash")
60
+ if isinstance(seq, bool) or not isinstance(seq, int) or seq < 1:
61
+ raise AuditStoreError("audit head seq is invalid")
62
+ if not isinstance(entry_hash, str) or len(entry_hash) != 64:
63
+ raise AuditStoreError("audit head entry_hash is invalid")
64
+ return Head(seq=seq, entry_hash=entry_hash)
65
+
66
+
67
+ def write_head(log: Path, seq: int, entry_hash: str) -> None:
68
+ """Atomic replace. Raises OSError if the head cannot be made durable."""
69
+ target = head_path(log)
70
+ temporary = target.with_name(target.name + ".tmp")
71
+ payload = canonicalize({"entry_hash": entry_hash, "seq": seq}) + b"\n"
72
+ target.parent.mkdir(parents=True, exist_ok=True)
73
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
74
+ try:
75
+ view = payload
76
+ while view:
77
+ written = os.write(fd, view)
78
+ if written <= 0:
79
+ raise OSError(errno.EIO, "short write on audit head")
80
+ view = view[written:]
81
+ os.fsync(fd)
82
+ except OSError:
83
+ try:
84
+ os.close(fd)
85
+ except OSError:
86
+ pass
87
+ try:
88
+ os.unlink(temporary)
89
+ except OSError:
90
+ pass
91
+ raise
92
+ os.close(fd)
93
+ os.replace(temporary, target)
94
+ directory = os.open(target.parent, os.O_RDONLY)
95
+ try:
96
+ os.fsync(directory)
97
+ finally:
98
+ os.close(directory)
99
+
100
+
101
+ def assess(report: VerifyReport, head: Head | None) -> AnchorStatus:
102
+ """Compare a verified log with its sidecar. The log must already be intact."""
103
+ if not report.ok:
104
+ return AnchorStatus(False, list(report.errors), 0)
105
+ entries = report.entries
106
+ if not entries:
107
+ if head is None:
108
+ return AnchorStatus(True, [], 0)
109
+ return AnchorStatus(False, ["audit head present but log is empty"], 0)
110
+ if head is None:
111
+ return AnchorStatus(False, ["audit head missing for a non-empty log"], 0)
112
+ last = entries[-1]
113
+ if head.seq == last.get("seq") and head.entry_hash == last.get("entry_hash"):
114
+ return AnchorStatus(True, [], 0)
115
+ if len(entries) >= 2:
116
+ previous = entries[-2]
117
+ if head.seq == previous.get("seq") and head.entry_hash == previous.get("entry_hash"):
118
+ return AnchorStatus(True, [], 1)
119
+ return AnchorStatus(
120
+ False,
121
+ [f"audit head disagrees with log: head seq={head.seq} log seq={last.get('seq')}"],
122
+ 0,
123
+ )
124
+
125
+
126
+ def verify_anchored(path: Path) -> VerifyReport:
127
+ """Chain verify plus the sidecar. A one-record lag stays ok."""
128
+ report = verify_file(path)
129
+ if not report.ok:
130
+ return report
131
+ try:
132
+ head = read_head(path)
133
+ except AuditStoreError as exc:
134
+ return _failed(report, [str(exc)])
135
+ status = assess(report, head)
136
+ if not status.ok:
137
+ return _failed(report, status.errors)
138
+ report.anchor_lag = status.lag
139
+ return report
140
+
141
+
142
+ def _failed(report: VerifyReport, errors: list[str]) -> VerifyReport:
143
+ return VerifyReport(
144
+ ok=False,
145
+ entries=report.entries,
146
+ errors=errors,
147
+ head_hash=report.head_hash,
148
+ anchor_lag=0,
149
+ )