maester-mcp
# Access receipt for MCP
A reference lab for enforcing identity-aware tool access in an MCP server.
This is a distilled version of a disposable-tenant lab. The lab question was practical: if an AI agent can reach enterprise security evidence, what proves the caller is allowed to use each tool? The answer here is deliberately plain:
1. verify the caller or agent route,
2. map identity evidence to local permissions,
3. expose only the matching MCP tools,
4. test the refusal cases.
This is not a production platform. It is a runnable pattern for access receipts: identity in, scoped tools out, and denial paths that do useful work.
## Architecture
```text
identity route -> token / authorization evidence -> verifier -> permissions -> MCP tool tags -> read-only data
```
The same permission ladder is used for all routes so the interesting variable is the identity plumbing, not the downstream tool contract.
Design constraints:
- the server exposes read-side evidence only,
- no arbitrary SQL tool is published,
- raw evidence is behind `maester.raw`,
- analysis tools return summary-shaped fields, not raw result payloads,
- Auditor is powerful, not magical; mutation remains denied.
## Permission model
| Role | Permissions | Typical tools | Explicit refusals |
| --- | --- | --- | --- |
| Reader | `maester.read` | `whoami`, `list_runs`, `get_run_summary` | failed-test analysis, raw detail |
| Analyst | `maester.read`, `maester.analyse` | Reader tools plus `get_failed_tests`, `get_test_history`, `compare_runs` | raw result detail |
| Auditor | `maester.read`, `maester.analyse`, `maester.raw` | Analyst tools plus `get_result_detail` | mutation, arbitrary SQL |
Negative tests are part of the design, not decoration. If Reader can analyse, Analyst can read raw evidence, or Auditor can mutate tests, the architecture has become a suggestion box.
## Routes covered
- **Entra RBAC**: app roles become local Maester permissions.
- **Copilot Studio / Agent ID**: the agent route still resolves to backend permissions.
- **Okta XAA / ID-JAG**: authorization evidence travels to the MCP server and is verified there.
## Raw-ish examples
`examples/` contains a deliberately small set of sanitised raw lab artefacts. These are the files that help a reader understand the access pattern; the rest of the lab trail was noise with better timestamps.
```text
examples/entra-rbac/
resource-app-create-body.raw.example.json # resource app manifest shape: scopes, app roles, token version
resource-app-current-summary.raw.example.json # resulting resource/service-principal settings
delegated-rbac-setup.raw.example.json # delegated client, consent and role-assignment shape
examples/agent-id/
agent-blueprint-created.raw.example.json # Agent ID / blueprint object shape
connector-oauth-client-created.raw.example.json # OAuth client used by the connector/runtime path
redirect-and-user-role-assignments.raw.example.json
copilot-mcp-connector-created.raw.example.json # Copilot Studio MCP connector shape
examples/okta-xaa/
custom-as-idjag-contract.raw.example.json # authorization-server and ID-JAG contract shape
role-matrix-passed.raw.example.json # Reader / Analyst / Auditor validation result
runtime-real-idjag-reader-smoke.raw.example.json
```
The examples are not copy-paste deployment recipes. They are shape references: what objects existed, which fields mattered, and where authorization evidence showed up.
## Run locally
```bash
python -m venv .venv
. .venv/bin/activate
pip install -e '.[dev]'
maester-ingest --source data/source --db data/maester.duckdb
python -m pytest tests -q
python scripts/duckdb/verify_demo.py
```
Start the MCP server with auth disabled:
```bash
MAESTER_MCP_AUTH_PROFILE=none maester-mcp
```
Or with synthetic local demo tokens:
```bash
MAESTER_MCP_AUTH_PROFILE=synthetic maester-mcp
# reader-token, analyst-token, auditor-token are local examples only
```
## Sanitisation
The included data is a sanitised Maester-shaped demo set. The original private seed database is not included. `data/maester.duckdb` is generated locally from `data/source/` and ignored by git.
The sanitisation pass replaces organisation names, user-like values, GUIDs, email-shaped values, local paths and policy names with deterministic demo values. Run the verification gate:
```bash
python scripts/duckdb/verify_demo.py
```
Publishing rule: configuration shape is useful; raw tenant history is not.
## What is intentionally absent
- no live tenant credentials,
- no private keys,
- no production evidence,
- no arbitrary SQL tool,
- no write-side Maester controls,
- no historical lab logs, agent transcripts, deployment state, BOX notes or tenant-specific handoffs.
The useful part is the boundary, not the plumbing souvenirs.
## Repository map
```text
src/maester_mcp/ FastMCP server, auth verifiers, read-only DuckDB access
tests/ permission, transport, ingestion and denial tests
sql/ schema, views and verification SQL for demo evidence
data/source/ sanitised Maester-shaped source JSON
data/maester.duckdb generated local demo database, ignored by git
examples/ sanitised raw-ish config and lab artefact shapes
```
## Related notes and videos
- Project note: https://iam.felixelliott.com/posts/mcp-reference-architecture/
- Intro: https://youtu.be/mSMquGtJ2Tk
- Entra RBAC: https://youtu.be/8XjDbdLgbRw
- Agent ID: https://youtu.be/vh8lkvPZx0k
- Okta XAA: https://youtu.be/Z7I1Y72CXOc
- Code walkthrough: https://youtu.be/IWNy2_jZ0VQ
TDQS
Scored across 7 tools
Each tool targets a distinct aspect of the Maester run data: identity, run listing, run summaries, failed tests, test history, run comparison, and raw detail. No overlap in purpose or ambiguity in selecting the appropriate tool.
All tools use snake_case with a consistent verb_noun pattern (e.g., list_runs, get_run_summary, compare_runs), with whoami as a minor but acceptable deviation. The naming is predictable and easy to infer.
With 7 tools, the server is well-scoped for a read-only analysis tool covering identity, runs, summaries, failures, history, comparison, and details. Each tool earns its place without excess.
The server lacks a tool to enumerate all tests or retrieve complete test results beyond failures, leaving a significant gap in understanding overall test outcomes. Compare_runs provides summary changes but not per-test detail, so agents cannot fully assess test coverage.