mcp-ops-kit
The domain-free half of this repo: what an MCP server needs to let agents act on a booking
system safely, plus the trajectory evals that gate it in CI. It was extracted from two working
domains, not designed up front. The synthetic clinic (mcp_ops_server) came first. Then a
synthetic salon (salon_ops) was built by copying the clinic's code. The kit is what the two
copies turned out to share (Part 8, see FAILURES.md).
pip install mcp-ops-kit # add [live] for the OpenAI agent used in live eval runs
The source lives in this repo, next to the two domains built on it. Releases are published to
PyPI from a kit-vX.Y.Z tag on main by trusted publishing, with attestations, after CI has
built the wheel and used it from a fresh venv (release-kit.yml).
What the kit owns, and what a domain supplies
| Kit module | Gives you | Your domain supplies |
|---|---|---|
auth |
Hashed bearer tokens in a versioned file: each names its audience and expiry and can be revoked; the file is re-read on change, so revocation needs no restart. A token CLI (issue, revoke, list, migrate), scopes and a subject binding, all checked in server code (Guard) |
An AuthConfig: scope names, the subject claim (patient_id, customer_id), its ID pattern, the default audience |
oauth |
OAuth 2.1 resource-server tokens: JWT access tokens (RFC 9068, RS256/ES256) checked against your authorization server's JWKS (file or URL), with a jti revocation file |
Your issuer and JWKS location (settings) |
audit |
One encrypted, HMAC-chained record per tool call (who, token, tool, scope, subject ID, outcome, time), never the arguments; verify and show CLIs |
Nothing |
crypto |
The data key (new-key), per-purpose subkeys, AES-256-GCM, atomic writes |
The key, from your secret store |
limits |
A per-client token-bucket rate limit, applied before argument validation | Nothing |
server |
The audit-and-rate-limit-first OpsServer, which also rebuilds every result's text from the validated output, so an agent sees only declared fields. A health tool, /mcp with bearer auth, /healthz, env settings, a fail-closed serve (no token source, audit log or data key: no start) |
Your tools (register(server, guard)), your settings fields, your env prefix |
sqlite |
One deadline per call: lock wait, busy wait and execution, with a late COMMIT rolled back. With a data key, the file on disk is encrypted (the database lives in memory; each committed write re-encrypts it atomically) | Your schema, seed and queries (subclass SqliteStore) |
errors |
StoreError and its kinds. Tools turn them into messages an agent can act on (store_errors) |
Your own rule errors, as subclasses |
evals |
The harness (fresh data and a real server per task), the trajectory grader, the replay and OpenAI agents, the report and the CI gate. Repeated runs (--repeats k) scored with pass^k; a prompt-injection suite (--suite injection): planted text in records, an injection_exposed check, and attempted vs carried-out outcomes |
An EvalDomain, golden tasks and negative controls; for injection, supports_plant and a start_task that writes TaskSpec.plant into the task's data |
smoke |
/healthz, 401 without a token, then an MCP session |
Your tool round trip, with each write read back |
Build a domain on it: the salon as the worked example
- Data and rules. Write a deterministic synthetic seed (
salon_ops/seed.py) and a store that subclassesSqliteStore(salon_ops/store.py). Enforce every rule in the store, and make the critical one a database constraint too. The salon keeps one booked visit per opening with a unique index. - Auth. One
AuthConfig(salon_ops/auth.py): three scopes, tokens bound tocustomer_idby the patternCUS-[0-9]{4}, and the audiencesalon-ops. The token CLI iskit.main(AUTH). - Tools. Thin wrappers (
salon_ops/tools.py). Each callsguard.check(scope, subject_id)and then the store, insidestore_errors(), and returns aTypedDictso it gets an output schema. - Server.
kit.build_server(name, version, AUTH, register, audit=...), withkit.build_verifier(settings, AUTH)andkit.open_audit(settings)in yourbuild_http_app, thenkit.build_http_app, andservein__main__(salon_ops/server.py,salon_ops/__main__.py). A database file goes throughrequire_key_for_file, so it is never plaintext. - Evals. An
EvalDomain(salon_ops/evals/run.py) says who the principals are, which argument names the subject (SubjectRule), the default clock, and how to get fresh data per task. Then write golden tasks and negative controls for every grader check (salon_ops/evals/golden/make_tasks.py) before you trust the gate. For a prompt-injection suite, setsupports_plant=True, write each task'sTaskSpec.plant(record ID -> text) into its fresh data instart_task, and addinjection.jsonandinjection-negative.json(the clinic's are inevals/golden/). A domain withoutsupports_plantrefuses a planted task, so an attack never runs on data it did not reach. - Smoke test. Your
exercise(session)forkit.smoke.main(salon_ops/smoke.py).
The salon example has the following parts:
- 6 tools;
- 3 rules the clinic does not have: a stylist must offer the service, a 24-hour late-cancel window, and at most 3 upcoming visits per customer;
- 18 golden tasks and 14 negative controls, gated in CI next to the clinic's.
Guarantees the kit enforces, with tests
- The kit never imports a domain.
tests/test_kit.pywalks every import inmcp_ops_kit/. - A bound token never loads unbound. Unknown keys are refused, so a clinic token bound to
a patient does not load in the salon as an unbound token with its
patient_idsilently dropped. Before the kit, the clinic's own loader ignored unknown keys in exactly this way. A token whose scopes another domain does not declare is refused too. - A token is for one server. Every token names its audience (static tokens in the file,
JWTs in
aud). A tokens file holding another audience's token does not load, and the verifier checks the audience of each token again. This closes the Part 8 limit, where an unbound token loaded in any domain declaring the same scope names (tests/test_kit.py). - Tokens expire and can be revoked on a running server. Static tokens by
revoke, JWTs byjti; both files are re-read on change. If either file becomes invalid, every token it governs is refused until it is fixed, so a revocation is never skipped (tests/test_tokens.py,tests/test_oauth.py). - Every tool call is audited, or no call runs. A record that cannot be written stops the
server from acting (the call already made still returns its real result). An edited,
deleted or reordered record fails
verifyat its line (tests/test_audit.py). - No plaintext database on disk. A server refuses a database file without a key, and a
plaintext SQLite file with one (
tests/test_encryption.py). - An agent sees only declared fields (
tests/test_minimisation.py), each pinned with a reason. - The state check sees every change. The record diff grades the booking lifecycle (booked,
cancelled, booked-then-cancelled), and reports anything else (an edited, removed or
odd-status record) as
unexpected. That fails the task unless the task expects it. - A refusal can be pinned to its error. A step pattern may carry
"error": {"$icontains": ...}, so a refusal task fails when the agent hits some other error. - A scope a tool names but the domain never declared raises an error. It is not quietly treated as "forbidden".
- Everything else is held to both domains' full suites and eval gates: the clinic's (on SQLite and on FHIR) and the salon's.
Not in the kit (yet)
- The FHIR backend is clinic-specific by nature, and stays in
mcp_ops_server. Encryption at rest for FHIR data is the FHIR server's storage, not this code. - Key rotation: there is no re-encrypt command yet. A new data key means a fresh audit log and database file (keep the old key to read the old ones).
- Token introspection (RFC 7662) and a shared rate limit across replicas.
Metadata
Release files for mcp-ops-kit 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mcp_ops_kit-0.2.0.tar.gz | 53.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_ops_kit-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 117.7 kB
Release files / mcp_ops_kit-0.2.0.tar.gz
| Download URL | mcp_ops_kit-0.2.0.tar.gz |
|---|---|
| Size | 53.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
171473893730c0adef2073ee00371631f2b1f98da4acf12aa815b6278039e1e1
|
|
BLAKE2b-256 checksum How to use checksums |
10da5974757b683ccacb7234c42aa541ed0c8488c9459d6691b83723b86eefd7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / mcp_ops_kit-0.2.0-py3-none-any.whl
| Download URL | mcp_ops_kit-0.2.0-py3-none-any.whl |
|---|---|
| Size | 63.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0146dcc6af380791ac6109a657b0c33297d785157f5fe582b6de60274e842e90
|
|
BLAKE2b-256 checksum How to use checksums |
521eb7eb3aa4025647f7d5d0b0afe2971ad790d5642c32f80ca7a524dd41b1b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for mcp-ops-kit 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for mcp_ops_kit-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.