Skip to main content
Glama
tarun-km

hydris-diagnostic-mcp

by tarun-km
README.md
# hydris-mcp-integration

Hydris Pulse Bridge track, Work Item 1: the Diagnostic MCP server skeleton
that will live at `lite/diagnostic_mcp/` inside the Lite monorepo. It
discharges **BRIDGE-2** (per-call factory authorization), **BRIDGE-5**
(per-call audit logging) and **NFR-4** (security), and structurally
enforces **BRIDGE-1** (read-only toolset).

See [`docs/scope.md`](docs/scope.md) for exactly what is and isn't built
here, and [`docs/decisions.md`](docs/decisions.md) for the open-item
decisions (revocation latency, audit-sink-failure behaviour, the
non-leaking error shape, the toolset version scheme) this skeleton took a
position on, pending Nematullah's ratification.

## Architecture

- **Two identities, kept separate** (`diagnostic_mcp/auth/`): a
  service-principal JWT (read-only scopes, verified independently) and a
  per-call user-context JWT. The service token never widens access.
- **One pipeline for every call** (`diagnostic_mcp/registry/pipeline.py`):
  authenticate both identities, validate arguments, check the user's live
  factory grant (no caching), dispatch, audit-log the outcome - success,
  rejected, or error - every time.
- **Registration-time read-only gate** (`diagnostic_mcp/registry/tool_registry.py`):
  a tool without `classification="read_only"` or without a required
  `factory_id` argument is rejected when it's registered, not when it's
  called.
- **FastMCP for schema advertisement, one override for dispatch**
  (`diagnostic_mcp/server.py`): `tools/list` comes from FastMCP as usual;
  `tools/call` is intercepted once, at the low-level server, so every tool
  - present or future - goes through the same pipeline with no per-tool
  boilerplate.
- **One demonstration tool**, `get_plant_overview`
  (`diagnostic_mcp/tools/plant_overview.py`), backed by an in-memory
  stub of Lite's read-service layer, seeded with the exact fixtures the
  source docs name (`u-ops-alpha` granted `fx-mbr-01`; `u-ops-beta`
  granted `fx-beta-01`).

## Quickstart

```bash
python -m venv .venv
.venv/Scripts/pip install -e ".[dev]"      # .venv/bin/pip on macOS/Linux
cp .env.example .env                        # then edit the two secrets
```

Run the server:

```bash
set -a; source .env; set +a                 # or export the vars another way
python -m diagnostic_mcp.server
```

It listens on `streamable-http` at `http://127.0.0.1:8765/mcp` by default.

## Verify it end-to-end

```bash
pytest -v            # includes a real streamable-HTTP client/server round trip
ruff check .
mypy diagnostic_mcp tests
python scripts/check_no_write_imports.py
```

To call the running server manually with the official MCP client, mint a
service token and a user-context token against the same secrets the
server is running with:

```python
from diagnostic_mcp.auth.service_principal import issue_service_token
from diagnostic_mcp.auth.user_context import issue_user_context_token

service_token = issue_service_token("<LITE_MCP_SERVICE_TOKEN_SECRET>")
user_token = issue_user_context_token("<LITE_MCP_USER_CONTEXT_SECRET>", "u-ops-alpha", "org-alpha")
print(f"Authorization: Bearer {service_token}")
print(f"X-Pulse-User-Context: {user_token}")
```

Then call `get_plant_overview` with `factory_id=fx-mbr-01` (granted -
succeeds) or `factory_id=fx-beta-01` (not granted - denied, and both
attempts land in `audit.jsonl`).

## Work Item 1 definition of done

| Item | Status |
|---|---|
| MCP server registered in-process, streamable-HTTP reachable | Done |
| Tool registry with read-only classification, rejecting non-conforming registration at startup | Done |
| Service principal issued with read-only scopes, scope inventory written down against the 15-tool minimum set | Done - `diagnostic_mcp/auth/service_principal.py::MINIMUM_READ_SCOPES` |
| Per-call user context header parsed, grants resolved, call rejected before the service token is used | Done |
| Non-leaking authorization error shape agreed and implemented | Done - see O3 in `docs/decisions.md` |
| Revocation latency chosen and documented | Done - see O1 in `docs/decisions.md` |
| Audit sink writing all six mandated fields, on success, failure and rejection | Done |
| Pulse session id propagated and recorded, for TC-BRIDGE-5.3 parity | Done - `X-Pulse-Session-Id` header |
| Audit-sink-failure behaviour decided with Nematullah and implemented | Proposed, pending ratification - see O2 |
| No-write CI check live in the Lite repository | Stand-in only - see `docs/scope.md` |
| Secrets resolving from the managed store, TLS enforced on the listener | Secrets: done (env-var seam). TLS: infra concern, see `docs/scope.md` |