SEP-2624: Interceptors for the Model Context Protocol - #2624
Conversation
7da4402 to
0fca7d1
Compare
adaefca to
52679f2
Compare
|
Adding a concrete persistent-memory-server use case from Memoria, a local MCP memory server for coding agents. The interceptor/use-case shape that seems important for memory tools is that one MCP response can contain two very different output classes:
For this kind of server, tool-level annotations are not enough by themselves. The risky sequence is often cross-call, for example: |
Interceptor SEP has been moved from modelcontextprotocol/modelcontextprotocol#1763 to modelcontextprotocol/modelcontextprotocol#2624 . This PR updates the SEP references over the codebase and import revised SEP Signed-off-by: Ukjae Jeong <jeongukjae@gmail.com>
…gelog link, stdio flag) (#492) - README: note the 1.5 fail-closed non-loopback bind (`--unsafe-no-auth` / auth block) so the `--http` quickstart doesn't fail as written (#485). - release.yml: point the release-notes Documentation link at the canonical mcp-hangar.io, not the stale github.io domain (#486). - CHANGELOG: render the upstream MCP `modelcontextprotocol/modelcontextprotocol#2624` reference as code so it stops auto-linking to this repo's issues (#487). - Dockerfile.test-provider: add `-i` to the stdio run example (container exits on EOF without it) (#482). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kurt Degiorgio <kdegiorgio@bloomberg.net>
Separate interceptor capability (server-declared type/hooks/configSchema) from execution policy (invoker-declared). Chain entries gain an optional InterceptorOverrides (failOpen, priorityHint, mode, timeoutMs, and narrowing-only hooks) that takes precedence over server-declared defaults, enabling per-deployment control (e.g. audit->active promotion, SLO-based failOpen/timeouts, hook narrowing) without modifying interceptor servers. Also switch capability negotiation to the io.modelcontextprotocol/interceptors extensions namespace and bump the example protocolVersion to 2025-06-18. Signed-off-by: Kurt Degiorgio <kdegiorgio@bloomberg.net>
* feat(cli): add `auth bootstrap-admin` command (durable initial admin) (#463)
Exposes `mcp-hangar auth bootstrap-admin --config PATH --principal PRINCIPAL`
(#451): the escape hatch for a deployment with API-key auth enabled and
anonymous access off, where no admin yet exists to create the first one
through the protected API.
The command reuses the server's own `bootstrap_auth()` storage composition
(never an in-memory store) and performs the single atomic claim from #456.
Fails closed: auth must be enabled, non-anonymous, on a durable backend
(sqlite/postgresql -- memory/event_sourcing refused); a second run exits
nonzero without mutating storage. No credential is printed -- the grant is
a global admin *role* for an existing external principal that authenticates
via its own identity.
While testing this end-to-end (#452), found and fixed a durability bug in
the SQLite role store: `initialize()` seeded built-in roles with
`INSERT OR REPLACE`, which DELETEs the conflicting row, and
`role_assignments.role_name` has `ON DELETE CASCADE`. So every store
re-initialization (every process start / `bootstrap_auth`) cascade-wiped
all assignments to built-in roles -- the bootstrapped admin lost its admin
role on the next restart. The seed and `add_role` now upsert in place via
`ON CONFLICT(name) DO UPDATE`, matching the PostgreSQL store.
Tests (#452): CLI success/refusal/no-credential/no-mutation coverage, plus
a store-level regression that the admin assignment survives re-init.
Full suite 4452 passed; mypy + ruff clean.
Closes #451
Closes #452
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(core): fix CONTRIBUTING dev setup to build from the repo root (#465)
The Quick Start told contributors to `cd packages/core` and run
`pip install -e ".[dev]"` + `pytest`, but that path no longer works after the
monorepo -> multi-repo split. `packages/core/` has no `mcp_hangar` source --
its pyproject declares `[tool.hatch.build.targets.wheel] packages =
["mcp_hangar"]`, but that package directory does not exist there -- so the
editable install fails and pytest finds no suite.
The Python core lives at the repo root: `src/mcp_hangar/` (root pyproject,
`packages = ["src/mcp_hangar"]`, v1.4.0), `tests/` (`testpaths = ["tests"]`,
236 test files), and working `make setup` / `make test` targets. This matches
the authoritative guide in mcp-hangar/docs, which uses the root flow.
Drop the `cd packages/core` / `cd ../..` dance so the documented commands run
from the repo root, and correct the structure table (Core is at
`src/mcp_hangar/`, not `packages/core/`).
How tested: confirmed the root wheel target `packages = ["src/mcp_hangar"]`
resolves to an existing `src/mcp_hangar/__init__.py`, while the old
`packages/core` target `["mcp_hangar"]` points at a directory that does not
exist (editable install cannot find the package). `make setup`/`make test`
and root `pytest` (testpaths=["tests"]) are the working targets.
Closes #464
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(core): use mcp-hangar namespace in the kubernetes discovery example (#468)
Rename mcp-system -> mcp-hangar in examples/kubernetes/discovery-source.yaml to
match the product namespace and the operator default
(mcp-hangar/mcp-hangar-operator#28). Example manifest only.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci(ci): upgrade ambient setuptools/wheel before pip-audit (#469)
The Dependency Audit job failed on setuptools 79.0.1 (PYSEC-2026-3447, fixed in
83.0.0) -- the setuptools that ships with the runner's Python. The project builds
with hatchling and does not depend on setuptools, so the finding is purely the
ambient build tool. Upgrade pip/setuptools/wheel before running pip-audit so the
audited environment is current. No project dependency or runtime change.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tests): implement the T2 live auth harness (Keycloak/OIDC) (#470)
Fill in the tier-2 live-verification stubs. Add conftest fixtures that bring up
the example Keycloak (examples/auth-keycloak/) and front it with
`mcp-hangar serve --http` in OIDC front-door mode, reusing the exported realm
(issuer, mcp-hangar client, group->role mapping). All fixtures skip cleanly when
Docker/Keycloak/the binary are absent, so this stays opt-in and CI-safe.
Implements the four T2 claims black-box over HTTP: unauthenticated front-door
call is denied (401), a valid OIDC token authenticates, a well-formed token from
an untrusted issuer is rejected, and the PRM endpoint advertises the trusted
issuer (RFC 9728). Runs via the live-verify workflow (dispatch + nightly).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(security): read the Authorization header case-insensitively in JWTAuthenticator (#472)
* fix(security): read the Authorization header case-insensitively in JWTAuthenticator
OIDC/JWT bearer auth was fully broken over the HTTP surface. The auth middleware
normalizes header names to lowercase (ASGI headers already are), but
JWTAuthenticator.supports()/authenticate() looked up "Authorization"
(capitalized), so supports() returned False, the middleware routed no
authenticator, and every valid token was rejected as auth_method:none.
ApiKeyAuthenticator already handled both cases; JWT now does too via a shared
case-insensitive helper. Add a unit regression.
Caught by the T2 live auth harness (its valid-token case now passes).
Fixes #471.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(core): changelog entry for the JWT header-casing fix (#471)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(release): release 1.5.0 (#288)
Co-authored-by: mcp-hangar-release-bot[bot] <283430731+mcp-hangar-release-bot[bot]@users.noreply.github.com>
* docs(repo): note auth/RBAC and the admin bootstrap in the README capabilities (#473)
1.5.0 makes auth first-class (OIDC front-door now works over serve --http;
mcp-hangar auth bootstrap-admin grants the initial admin). Add an Auth & RBAC
bullet to What you get so the overview reflects it.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(repo): add basic client scope to keycloak example realm so tokens carry sub (#476)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(infra): remove the divergent duplicate install script (#478)
* docs(docs): point ADR governance at the real ADR location (#479)
ADR_AGENTS.md still claimed to govern a docs/adr/ directory in this repo,
but the ADRs moved to the mcp-hangar/docs repo's top-level adr/ as part
of the ADR-009 multi-repo split. Every location reference and relative
ADR link in the file was stale or dead. Repoint them at the real repo
and paths without changing any governance rule.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci(ci): use the shared reusable PR validators (#480)
Replace the full local copies of pr-title, pr-body, and branch-name
with thin callers into mcp-hangar/.github (reusable), matching the
helm-charts pilot. Removes scripts/check_pr_body.sh since its logic
is now inlined in the reusable pr-body workflow.
Refs mcp-hangar/.github#1
* chore(core): retire the Hangar Cloud connector, /agent/policy endpoint, and agent RBAC role (#490)
* chore(server): retire the Hangar Cloud connector, /agent/policy endpoint, and agent RBAC role
Removes the dead Hangar Cloud connector coupling from the core Python
package:
- Delete src/mcp_hangar/cloud/ (CloudConfig, CloudClient, CloudConnector,
buffer) and its tests.
- Delete the /agent/policy REST endpoint (server/api/agent_policy.py) and
its route mount, plus its tests.
- Remove the --cloud-key/--cloud-url CLI flags and CLIConfig fields.
- Remove the cloud connector wiring from lifecycle.run_server()
(_resolve_cloud_config, start/stop/event-bus subscription).
- Remove the "agent" RBAC role (AGENT_ROLE / ROLE_AGENT) from
domain/value_objects/security.py, auth/roles.py, and the
domain/security re-export stubs. PERMISSION_POLICY_WRITE is kept
(shared with the admin role).
- Drop the now-obsolete K1 security regression test that targeted
/agent/policy, and the agent-role RBAC test.
- Reword stale hangar-agent/hangar-cloud references in tools.py's
docstring, AGENTS.md's directory tree, and UPGRADE.md.
PolicyPushRejected is intentionally KEPT in domain/events.py,
infrastructure/persistence/event_serializer.py, and the event
serialization fuzz test: it's a durable, versioned domain event that
must remain decodable for event-store replay, even though its only
producer (the removed agent_policy.py) is gone. ToolAccessResolver,
ToolAccessPolicy, and all tenant/front-door/OIDC functionality are
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(core): drop stale /agent/policy comment in the batch executor
Follow-up to the connector removal: the approval-gate comment still pointed at
the removed POST /api/agent/policy endpoint. The policy is config-driven.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(core): add CHANGELOG entry for the agent/cloud retirement
Satisfies the changelog gate (scripts/check_changelog.sh) with a real
[Unreleased] Removed entry rather than skip-changelog, since this removes
user-facing surface (CLI flags, an endpoint, an RBAC role).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs(repo): fix four review findings (auth note, release domain, changelog link, stdio flag) (#492)
- README: note the 1.5 fail-closed non-loopback bind (`--unsafe-no-auth` / auth block) so the `--http` quickstart doesn't fail as written (#485).
- release.yml: point the release-notes Documentation link at the canonical mcp-hangar.io, not the stale github.io domain (#486).
- CHANGELOG: render the upstream MCP `modelcontextprotocol/modelcontextprotocol#2624` reference as code so it stops auto-linking to this repo's issues (#487).
- Dockerfile.test-provider: add `-i` to the stdio run example (container exits on EOF without it) (#482).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(core): resolve discovery/config review findings (#481, #483, #484) (#493)
- http/sse container discovery now prefers the published host-port
binding (NetworkSettings.Ports) over the internal bridge-network IP,
falling back to the bridge IP only when no host binding exists. The
documented deployment runs Hangar on the host, which cannot reach a
container's internal bridge IP but can reach its published port.
- Container/stdio-mode discovery is unaffected (still gateway-launched
via its own image, no network reachability needed).
- A discovery-only config.yaml (discovery.enabled: true, no top-level
mcp_servers) now loads by defaulting mcp_servers to {} instead of
raising; configs with neither a static mcp_servers section nor a
configured discovery source still fail loudly.
- The expected/transient "container has no IP" discovery skip now
logs at debug instead of warning.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci(core): run the core test suite on dependency changes (uv.lock, packages) (#494)
ci-core.yml was path-filtered to src/**, tests/**, and the root pyproject.toml,
so dependency-only PRs never ran the test suite: dependabot's `mcp` SDK bumps
touch packages/core/pyproject.toml + uv.lock, neither of which matched, so #491
(bump the MCP protocol SDK 1.25.0 -> 1.27.2) went all-green without a single
test running. A protocol-SDK bump is exactly the change that must run tests.
Add `packages/**` and `uv.lock` to the push/pull_request path filters.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(deps): bump mcp from 1.25.0 to 1.27.2 in /packages/core (#491)
---
updated-dependencies:
- dependency-name: mcp
dependency-version: 1.27.2
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mapyr <pyrka.marcin@gmail.com>
* ci(core): remove the vestigial no-op enterprise-boundary check (#495)
The `enterprise-boundary` job stopped checking anything when the enterprise
tier was merged into `src/mcp_hangar/` and license gating was removed (#196) —
it now only echoes "Enterprise boundary check removed". It remained a required
status check on branch protection, so every PR waited on a job that validates
nothing. Drop the job and its `required-check` dependency; `required-check`
still runs (needs detect-changes) as the branch-protection aggregate.
NOTE: `enterprise-boundary` must also be dropped from the branch-protection
required-status-checks contexts, or PRs will hang waiting for a check that no
longer runs.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(deps): bump mcp from 1.27.2 to 1.28.1 in /packages/core (#496)
Bumps [mcp](https://github.com/modelcontextprotocol/python-sdk) from 1.27.2 to 1.28.1.
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.27.2...v1.28.1)
---
updated-dependencies:
- dependency-name: mcp
dependency-version: 1.28.1
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mapyr <pyrka.marcin@gmail.com>
* fix(security): require mcp>=1.28.1 (CVE-2026-59950) (#497)
The dependabot bumps (#491, #496) moved the uv.lock to mcp 1.28.1, but the
published dependency constraint stayed `mcp>=1.0.0` in both pyproject.toml
files. The lock only pins the dev/CI environment — a user installing mcp-hangar
from PyPI resolves `mcp` themselves and could still get a vulnerable
`mcp < 1.28.1`. Raising the floor to `>=1.28.1` makes the published package
actually require the patched SDK, so cutting a release ships a real fix rather
than a hollow one.
CVE-2026-59950 (HIGH, CVSS 7.6): MCP Python SDK WebSocket server transport
accepts handshakes without Host/Origin validation (CWE-346/1385), enabling
cross-origin / DNS-rebinding connections. Fixed in mcp 1.28.1.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(release): release 1.5.1 (#477)
Co-authored-by: mcp-hangar-release-bot[bot] <283430731+mcp-hangar-release-bot[bot]@users.noreply.github.com>
* chore(core): sync the vestigial packages/core version to 1.5.1 (#499)
packages/core/pyproject.toml still declared version 1.0.2 while the actual
package (built from the root pyproject) is 1.5.1 — release-please only tracks
the root manifest, so this file drifted far behind. Sync it so the number
isn't misleading. (The real fix is removing this vestigial manifest, tracked
in #466.)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore: remove the vestigial packages/core tree (monorepo split leftover) (#500)
packages/core/ is a leftover from the pre-split monorepo layout. It cannot
build (its pyproject targets `packages = ["mcp_hangar"]` but there is no
mcp_hangar/ source dir under it — the code lives in src/mcp_hangar/, which the
root pyproject builds and publishes). Nothing imports it, no CI/Makefile runs
it, and its duplicate pyproject/uv.lock only confused Dependabot (the mcp CVE
bump landed in this non-building manifest). Root Dependabot security scanning is
unaffected — it auto-discovers the real root manifest.
Also removes the now-dead scripts/verify-migration.sh (verified a long-finished
migration; would fail today) and the stale packages/* CODEOWNERS entries
(packages/core, and packages/operator + packages/helm-charts which moved to
their own repos) plus the packages/core/dist gitignore line. The `* @mapyr`
default already owns everything, so those specific entries were redundant too.
Closes #466.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci(repo): add dependabot version-update config (#502)
Core had no dependabot.yml — only Dependabot security updates ran. Add weekly
version-updates for the uv (Python) and github-actions ecosystems, with
minor+patch grouped into one PR (majors stay individual for scrutiny),
conventional-commit titles (chore(deps): / ci(deps):) so the reusable pr-title
validator passes, and a bounded PR count. Track 1 of the change-management epic
(#501). ci-core already runs the suite on these (uv.lock/pyproject in its path
filter, #494).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci(ci): run project-add on pull_request_target so Dependabot PRs get the project token (#517)
GitHub withholds Actions secrets from workflows triggered by Dependabot on the
pull_request event, so `secrets.PROJECT_AUTOMATION_TOKEN` was empty and
add-to-project failed ("github-token not supplied") on every Dependabot PR.
pull_request_target runs in the base context with secrets available; it is safe
here because the workflow only adds the PR to the board by node id and never
checks out or runs PR code.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci(deps): bump docker/build-push-action from 6 to 7 (#504)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)
---
updated-dependencies:
- dependency-name: docker/build-push-action
dependency-version: '7'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* ci(deps): bump DavidAnson/markdownlint-cli2-action from 19 to 24 (#506)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 19 to 24.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v19...v24)
---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
dependency-version: '24'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* ci(deps): bump docker/login-action from 3 to 4 (#505)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)
---
updated-dependencies:
- dependency-name: docker/login-action
dependency-version: '4'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* ci(deps): bump aquasecurity/trivy-action (#503)
Bumps the github-actions group with 1 update in the / directory: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action).
Updates `aquasecurity/trivy-action` from 0.35.0 to 0.36.0
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/v0.35.0...v0.36.0)
---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
dependency-version: 0.36.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* ci(deps): bump dependabot/fetch-metadata from 2 to 3 (#507)
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2 to 3.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v2...v3)
---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
dependency-version: '3'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* ci(ci): stop cancel-in-progress orphaning required validator checks (#518)
* ci(ci): keep required validator checks from being cancelled
* ci(ci): keep required validator checks from being cancelled
* ci(ci): keep required validator checks from being cancelled
* docs(repo): reposition README as MCP policy enforcement plane (#519)
Tagline and Why paragraph move from 'control plane' to 'policy
enforcement plane'; tagline now names what the code actually enforces
(deterministic admission/egress policy, attributable audit, SIEM export)
plus MIT/self-hosted/no-SaaS.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(deps): bump the python-minor-patch group with 14 updates (#508)
Bumps the python-minor-patch group with 14 updates:
| Package | From | To |
| --- | --- | --- |
| [pydantic](https://github.com/pydantic/pydantic) | `2.12.5` | `2.13.4` |
| [prometheus-client](https://github.com/prometheus/client_python) | `0.24.1` | `0.25.0` |
| [docker](https://github.com/docker/docker-py) | `7.1.0` | `7.2.0` |
| [typer](https://github.com/fastapi/typer) | `0.24.1` | `0.27.0` |
| [python-multipart](https://github.com/Kludex/python-multipart) | `0.0.31` | `0.0.32` |
| [pytest](https://github.com/pytest-dev/pytest) | `9.0.3` | `9.1.1` |
| [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) | `1.3.0` | `1.4.0` |
| [ruff](https://github.com/astral-sh/ruff) | `0.14.10` | `0.15.22` |
| [hypothesis](https://github.com/HypothesisWorks/hypothesis) | `6.151.9` | `6.156.6` |
| [langfuse](https://github.com/langfuse/langfuse) | `4.0.1` | `4.14.0` |
| [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python) | `1.40.0` | `1.44.0` |
| [opentelemetry-sdk](https://github.com/open-telemetry/opentelemetry-python) | `1.40.0` | `1.44.0` |
| [opentelemetry-exporter-otlp](https://github.com/open-telemetry/opentelemetry-python) | `1.40.0` | `1.44.0` |
| [vulture](https://github.com/jendrikseipp/vulture) | `2.15` | `2.16` |
Updates `pydantic` from 2.12.5 to 2.13.4
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.12.5...v2.13.4)
Updates `prometheus-client` from 0.24.1 to 0.25.0
- [Release notes](https://github.com/prometheus/client_python/releases)
- [Commits](https://github.com/prometheus/client_python/compare/v0.24.1...v0.25.0)
Updates `docker` from 7.1.0 to 7.2.0
- [Release notes](https://github.com/docker/docker-py/releases)
- [Commits](https://github.com/docker/docker-py/compare/7.1.0...7.2.0)
Updates `typer` from 0.24.1 to 0.27.0
- [Release notes](https://github.com/fastapi/typer/releases)
- [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md)
- [Commits](https://github.com/fastapi/typer/compare/0.24.1...0.27.0)
Updates `python-multipart` from 0.0.31 to 0.0.32
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.31...0.0.32)
Updates `pytest` from 9.0.3 to 9.1.1
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.3...9.1.1)
Updates `pytest-asyncio` from 1.3.0 to 1.4.0
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v1.3.0...v1.4.0)
Updates `ruff` from 0.14.10 to 0.15.22
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.10...0.15.22)
Updates `hypothesis` from 6.151.9 to 6.156.6
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.151.9...v6.156.6)
Updates `langfuse` from 4.0.1 to 4.14.0
- [Release notes](https://github.com/langfuse/langfuse/releases)
- [Commits](https://github.com/langfuse/langfuse/commits)
Updates `opentelemetry-api` from 1.40.0 to 1.44.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.40.0...v1.44.0)
Updates `opentelemetry-sdk` from 1.40.0 to 1.44.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.40.0...v1.44.0)
Updates `opentelemetry-exporter-otlp` from 1.40.0 to 1.44.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.40.0...v1.44.0)
Updates `vulture` from 2.15 to 2.16
- [Release notes](https://github.com/jendrikseipp/vulture/releases)
- [Changelog](https://github.com/jendrikseipp/vulture/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jendrikseipp/vulture/compare/v2.15...v2.16)
---
updated-dependencies:
- dependency-name: pydantic
dependency-version: 2.13.4
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: prometheus-client
dependency-version: 0.25.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: docker
dependency-version: 7.2.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: typer
dependency-version: 0.27.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: python-multipart
dependency-version: 0.0.32
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: python-minor-patch
- dependency-name: pytest
dependency-version: 9.1.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: pytest-asyncio
dependency-version: 1.4.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: ruff
dependency-version: 0.15.22
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: hypothesis
dependency-version: 6.156.6
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: langfuse
dependency-version: 4.14.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: opentelemetry-api
dependency-version: 1.44.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: opentelemetry-sdk
dependency-version: 1.44.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: opentelemetry-exporter-otlp
dependency-version: 1.44.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-minor-patch
- dependency-name: vulture
dependency-version: '2.16'
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: python-minor-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mapyr <pyrka.marcin@gmail.com>
* chore(deps): bump structlog from 25.5.0 to 26.1.0 (#512)
Bumps [structlog](https://github.com/hynek/structlog) from 25.5.0 to 26.1.0.
- [Release notes](https://github.com/hynek/structlog/releases)
- [Changelog](https://github.com/hynek/structlog/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/structlog/compare/25.5.0...26.1.0)
---
updated-dependencies:
- dependency-name: structlog
dependency-version: 26.1.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mapyr <pyrka.marcin@gmail.com>
* chore(deps): bump protobuf from 6.33.6 to 7.35.1 (#513)
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 6.33.6 to 7.35.1.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)
---
updated-dependencies:
- dependency-name: protobuf
dependency-version: 7.35.1
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump cryptography from 48.0.1 to 49.0.0 (#509)
Bumps [cryptography](https://github.com/pyca/cryptography) from 48.0.1 to 49.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/48.0.1...49.0.0)
---
updated-dependencies:
- dependency-name: cryptography
dependency-version: 49.0.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mapyr <pyrka.marcin@gmail.com>
* chore(deps): update mkdocs-material requirement from >=9.5 to >=9.7.6 (#510)
Updates the requirements on [mkdocs-material](https://github.com/squidfunk/mkdocs-material) to permit the latest version.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.0...9.7.6)
---
updated-dependencies:
- dependency-name: mkdocs-material
dependency-version: 9.7.6
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mapyr <pyrka.marcin@gmail.com>
* chore(deps): update mkdocs-git-revision-date-localized-plugin requirement (#511)
Updates the requirements on [mkdocs-git-revision-date-localized-plugin](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin) to permit the latest version.
- [Release notes](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/releases)
- [Commits](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.2.0...v1.5.3)
---
updated-dependencies:
- dependency-name: mkdocs-git-revision-date-localized-plugin
dependency-version: 1.5.3
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mapyr <pyrka.marcin@gmail.com>
* ci(ci): allow deps-dev scope for Dependabot dev-dependency PRs (#520)
* chore(deps-dev): bump mypy from 1.19.1 to 2.3.0 (#515)
Bumps [mypy](https://github.com/python/mypy) from 1.19.1 to 2.3.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.19.1...v2.3.0)
---
updated-dependencies:
- dependency-name: mypy
dependency-version: 2.3.0
dependency-type: direct:development
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mapyr <pyrka.marcin@gmail.com>
* chore(deps): bump rich from 14.3.3 to 15.0.0 (#516)
Bumps [rich](https://github.com/Textualize/rich) from 14.3.3 to 15.0.0.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.3.3...v15.0.0)
---
updated-dependencies:
- dependency-name: rich
dependency-version: 15.0.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mapyr <pyrka.marcin@gmail.com>
* chore(deps): update mkdocstrings[python] requirement (#514)
Updates the requirements on [mkdocstrings[python]](https://github.com/mkdocstrings/mkdocstrings) to permit the latest version.
- [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases)
- [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.0...1.0.6)
---
updated-dependencies:
- dependency-name: mkdocstrings[python]
dependency-version: 1.0.6
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* ci(ci): add domain-lint caller (ADR-011) (#522)
* docs(repo): link the release compatibility matrix from the README (ADR-011) (#521)
The compatibility matrix (docs RELEASE_COMPATIBILITY.md) reconciles core,
operator, and chart versions but was unreachable from the flagship repo.
Link it from the README Documentation section per ADR-011 (matrix = the
versions/compat SSOT, reachable from core). Closes the reachability gap in
#489; the --http fail-closed auth behavior is already documented here (the
core README owns server security behavior per ADR-011).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci(ci): add interceptor-pin drift detection (ADR-012) (#523)
* security(core): validate WebSocket handshake Origin/Host at the edge (#524)
* security(core): validate WebSocket handshake Origin/Host at the edge
Non-/api/ WebSocket connections were forwarded to mcp_app with no Hangar-layer
Origin/Host check (asgi.py) -- the CVE-2026-59950 class at our own trust
boundary. Add handshake validation before forwarding:
- loopback connections are trusted (no cross-origin / rebinding threat);
- non-loopback is fail-closed: a *present* Origin must be allow-listed
(MCP_CORS_ORIGINS), a *missing* Origin is a non-browser client and is
allowed (auth still applies); the Host must be in the trusted-hosts
allowlist (MCP_TRUSTED_HOSTS -- same list the REST API uses; * disables).
Rejected handshakes close with 1008. Reuses the existing API-protection
allowlists so one config governs both surfaces. Independent of #497 (SDK floor).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: ruff-format asgi.py + add CHANGELOG entry for #498
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(core): add L7 egress policy engine (#526)
The core-side half of MCPEgressPolicy (operator epic #53, ADR-013). The
operator enforces L3/L4 (which upstream hosts a server may reach); this adds the
deterministic L7 enforcement on the connections Hangar already proxies:
- tool-call matching: glob on the MCP tool name -> allow / deny /
require-approval, with a policy default action; precedence deny >
require-approval > allow > default.
- argument scanning: named secret-pattern groups (reusing the output redactor's
value-regexes, plus PEM private-key blocks) and a payload-size limit. A secret
or oversized payload denies the call even for an allowed tool -- deny wins.
Pure and deterministic (no I/O, no ML) -- full DLP/ML detection are non-goals
(ADR-013). New module domain/policies/egress_l7.py with 16 tests. Wiring the
evaluator into the tool-invocation path (fed by the operator's compiled policy
document) is a follow-up.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(core): enforce L7 egress policy at the tool-invocation chokepoint (#527)
Wires the L7 engine (#526) into McpServer.invoke_tool -- the single method
every tool call funnels through. McpServer gains an optional l7_policy
(constructor arg + property + set_l7_policy setter); invoke_tool evaluates each
call against it before ensure_ready, so a denied or approval-gated call never
wakes the server or reaches the upstream:
- DENY (tool deny-rule/default, or a secret/oversized argument) -> EgressPolicyDeniedError
- REQUIRE_APPROVAL -> EgressPolicyApprovalRequiredError (fails closed; interactive
approval-queue routing is a follow-up)
No policy attached => the branch is skipped and behavior is unchanged. Per
ADR-011 (single source of truth), the policy is meant to come from the
operator's compiled MCPEgressPolicy, not a parallel config schema; this PR
builds the enforcement seam + setter, and the operator->core transport that
populates it is the remaining integration step.
New exceptions EgressPolicyDeniedError / EgressPolicyApprovalRequiredError
(ToolError subclasses; caller-facing message generic, reason in details for
audit). 6 tests; 222 existing invoke/tool tests still pass.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(core): receive compiled L7 egress policy over the REST API (#528)
The operator->core transport sink for MCPEgressPolicy L7 rules. Lets the
operator push a compiled L7 policy per mcp_server so the CRD drives the L7
enforcement that #527 wired into invoke_tool.
- L7Policy.from_dict: parse the operator's camelCase wire form
(tools.allow/deny/requireApproval, arguments.secretPatterns/maxPayloadBytes,
defaultAction), with strict validation.
- SetL7PolicyCommand + SetL7PolicyHandler: resolve the McpServer from the
repository and call set_l7_policy (None clears). 404 if unknown.
- Routes POST/PUT /api/mcp_servers/{id}/l7_policy (set/replace) and DELETE
(clear), guarded by the mcp_servers:write permission -- same guard as the
other provider mutations.
Additive and dormant until the operator pushes (next PR). 36 L7 tests
(from_dict parse/validation, handler set/clear/not-found, and a set->invoke
end-to-end that proves the transport meets the enforcement). ruff/mypy clean;
existing crud/provider-api tests unchanged.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(core): L7 argument scan fails closed on unserializable arguments (#529)
Adversarial testing found that scan_arguments serialized the tool-call
arguments unconditionally via json.dumps, so a pathological payload (e.g. a
circular reference) raised ValueError from inside the policy evaluation --
crashing the call instead of denying it. Tool arguments arrive as decoded JSON
in practice, but a policy control must not fail open (or crash) on odd input.
- _serialize_arguments returns None when the payload can't be serialized;
scan_arguments then reports a violation (fail closed) rather than raising.
- Short-circuit: when no secret patterns and no size limit are configured,
return early without serializing at all (cheaper, and cannot crash).
Two regression tests (unserializable -> violation; no-rules -> clean, no crash).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(core): redact secret values in logs and the log buffer (#530)
Observability audit finding P1/L1 (critical): the value-level OutputRedactor
was never installed in the logging pipeline, so a secret embedded in a log
message string or under a non-secret key reached the JSON logs; and raw MCP
server stderr was appended verbatim to the log buffer and served by
GET /mcp_servers/{id}/logs -- exposing any token/key an upstream printed to
stderr to anyone with mcp_servers:read.
- domain/security/redactor: add get_default_redactor() -- a shared, process-wide
OutputRedactor with long-string redaction OFF (only builtin token-shape
patterns, so real secrets are caught without mangling arbitrary long values).
- logging_config: new _redact_secret_values structlog processor (runs after the
key-name sanitizer, also in foreign_pre_chain so stdlib-routed logs are
covered) scrubbing every string in the record.
- mcp_server: redact each stderr line at the source in the reader thread before
it enters the buffer -- one choke that protects the /logs API and any future
log stream.
5 tests; existing redactor/logging/mcp_server suites unchanged (185 pass).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(core): scrub Langfuse tool inputs/outputs by default (#531)
Observability audit finding T3/P1 (security): the Langfuse exporter shipped
full tool-call inputs and outputs to Langfuse unless scrub_inputs/scrub_outputs
were explicitly enabled. Flip both defaults to true across LangfuseConfig, the
bootstrap LangfuseBootstrapConfig, the env-var fallbacks, and the doc example --
secure by default. When scrubbing is on the exporter sends only the argument/
result key names, not their values. Operators who want full content for
debugging can set them false.
Guarded by two tests asserting the on-by-default posture.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(metrics): stop poisoning the latency histogram; drop stream_id label (#532)
Observability audit Phase 2 (findings M4, M1/stream_id) -- two unambiguous
correctness fixes:
- tool_call_duration_seconds observed only on success. observe_tool_call
previously recorded duration=0.0 for every failed call (the failure path has
no real duration), injecting artificial 0-second samples that dragged the
latency percentiles down. Failures are still counted via
tool_call_errors_total.
- events_compacted_total no longer carries the unbounded stream_id label
(per-stream identifiers = cardinality bomb). record_events_compacted keeps the
arg for callers/logging but does not use it as a label. No alert/dashboard
referenced it.
3 tests parse the /metrics output; existing metrics suites pass (47).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(observability): correlate logs with traces (trace_id/span_id) (#533)
Observability audit finding P7/L2: OTel tracing existed but no trace_id/span_id
was ever injected into log records -- get_current_trace_id/span_id were dead
code and the structlog chain had no trace processor, so you could not pivot
from a log line to its trace.
Add a _add_trace_context structlog processor that stamps trace_id/span_id when
inside an active OTel span. Lazily imports the tracing helpers to avoid the
circular import (tracing imports this module for its logger), no-ops when
tracing is off / no active span, and is wrapped so a tracing error never breaks
a log call. 3 tests.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(core): consolidate discovery metrics onto the scraped registry (#534)
Observability audit finding M1: the discovery orchestrator recorded some metrics
to a second prometheus_client registry (application/discovery/discovery_metrics.py)
that the /metrics endpoint never serialized -- so discovery registrations,
errors, validation failures, and validation durations were silently dropped,
while cycle/quarantine/deregistration were double-recorded (once to the dead
registry, once to the scraped one).
- Delete the dead secondary system (discovery_metrics.py + its DiscoveryMetrics
class and unused @observe_* decorators) and its export.
- Add the two metrics it uniquely carried to the primary registry:
mcp_hangar_discovery_validation_failures_total and
mcp_hangar_discovery_validation_duration_seconds (registered + scraped).
- Rewire the orchestrator through the primary record_discovery_* fns; drop the
now-redundant double-records. Label naming is unified (source_type, not source).
A regression test asserts registrations/errors/validation now appear in the
/metrics output. 230 discovery/metrics tests pass.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(core): emit the cost-attribution metrics (#535)
Observability audit Phase 2 (dead-metric double-check -> wire): the cost handler
computed per-invocation cost via the ICostAttributor port and published a
CostReportGenerated event, but never fed the mcp_hangar_cost_cents_total /
mcp_hangar_cost_attributions_total metrics its own docstring promised -- so the
governance dashboard cost panels were always empty even with a real attributor.
Add record_cost(mcp_server, tool, cost_cents, cost_model) and call it in the
handler after a non-zero cost is computed. No-op under the default
NullCostAttributor (early return on zero cost). Cost is a real subsystem
(ICostAttributor / DefaultCostAttributor / pricing rules), so this wires rather
than deletes.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(core): wire connections_active; delete the redundant connection metrics (#536)
Observability audit Phase 2 (dead-metric double-check):
- WIRE connections_active: it has a provider-details "Active Connections" panel
and represents a real state, but was never emitted. Set 1 when a server's
client is created, 0 on close/shutdown (both teardown paths). The panel now
has data.
- DELETE connections_total and connection_duration_seconds: never emitted, no
dashboard/alert referenced them, and they duplicated the server-lifecycle
signals (starts_total, cold_start_seconds, server lifetime). Removed the defs
and their registration entries.
Tests: the gauge toggles 1/0; the two deleted metrics are absent from the
exposition and the module. 161 metrics/mcp_server tests pass.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(observability): trace the upstream call boundary (client spans, stdio propagation, sampler) (#537)
- CLIENT spans (SpanKind.CLIENT) around outgoing MCP RPCs in both stdio and
HTTP transports, named per OTel GenAI/MCP semconv (execute_tool {tool} with
gen_ai.tool.name / gen_ai.operation.name / mcp.method.name). Opened before
context injection so the upstream's server span parents to this CLIENT span.
- stdio transport propagates W3C trace context into the MCP _meta field,
mirroring the HTTP header injection (no caller-dict mutation; no-op when
tracing is off). Closes the stdio distributed-tracing gap.
- init_tracing honors OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG (the
hand-built TracerProvider previously ignored them despite the documented
contract).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(core): collapse the vestigial enterprise plugin boundary (#538)
The enterprise/SaaS product tier was retired (agent + Hangar Cloud archived);
its plugin-discovery boundary in bootstrap/enterprise.py was left behind as
dead indirection. Nothing ever registered a provider (register_enterprise_provider
was never called; the mcp_hangar.extensions entry-point group was empty save a
commented example), so every call already fell through to the built-in path that
loads the in-core auth / approvals modules.
Remove the EnterpriseProvider registry, entry-point discovery, and the
per-function provider loops; the public functions (load_enterprise_modules,
register_auth_cqrs, get_enterprise_api_routes, create_enterprise_event_store,
create_enterprise_observability_adapter) now invoke the built-in logic directly.
Behavior is unchanged — auth/approvals still load, empty config still yields
empty components. Also drops the dead pyproject entry-point group and the two
tests that exercised the removed plugin-discovery path.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(observability): align tool-invocation spans to OTel GenAI/MCP semconv (#539)
Follows the transport CLIENT-span work: bring the rest of the tool-invocation
telemetry onto OTel GenAI/MCP semantic conventions so GenAI-aware backends and
the OTel Collector recognize it without Hangar-specific mapping.
- Span name: application-layer `tool.invoke.{tool}` -> `execute_tool {tool}`
(matches the transport CLIENT span).
- Attributes: `mcp.tool.name` -> `gen_ai.tool.name`; `mcp.cost.input_tokens` /
`mcp.cost.output_tokens` -> `gen_ai.usage.input_tokens` /
`gen_ai.usage.output_tokens`; `gen_ai.operation.name` and `mcp.method.name`
now set. New `GenAI` conventions class; the `mcp.*` governance namespaces
(enforcement/risk/audit/behavioral/caller, cost cents/model/currency,
mcp.session.id) are unchanged -- no semconv equivalent.
Scope is spans only: metrics are a hand-rolled Prometheus registry (not OTel),
so their `mcp_hangar_*` names stay -- dotted semconv names would break the
scraped dashboards/alerts and don't fit Prometheus.
Also fixes a pre-existing botched `Provider`->`McpServer` rename that pinned
`OTEL_LOGS_AVAILABLE` to false (the `LoggerMcpServer` import always ImportError'd),
silently disabling OTLP audit-log export -- so the semconv-aligned audit
attributes now actually ship.
Breaking for consumers querying the old span/OTLP-audit attribute names.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(observability): wire transport message metrics; drop never-emitted pool/SSE gauges (#540)
Wire the JSON-RPC message metrics that were defined but never emitted, at the
stdio and HTTP transport boundaries:
- mcp_hangar_messages_sent (by method) + mcp_hangar_messages_received (by type:
response/notification/error) + mcp_hangar_message_size_bytes (by direction).
- Labeled per upstream server. StdioClient gains an mcp_server_id (the domain
tags stdio clients after launch; HTTP clients are labeled at construction).
- Sent is recorded in call() with the serialized request; received in the stdio
reader loop / from the HTTP response body, so there's no re-serialization on
the hot path. Added classify_jsonrpc_message + record_message_sent/received
helpers.
Remove three never-emitted metrics with nothing to populate them:
- mcp_hangar_http_connection_pool_size (httpx pool internals aren't exposed),
- mcp_hangar_http_sse_streams_active / mcp_hangar_http_sse_events (the
streaming-SSE reader path is unused; SSE responses are batch-parsed).
Repurpose the dead "Active SSE Streams" dashboard panel to a messages-sent rate.
Tested: unit (helpers, classify, both transports, removed-metric absence) +
live on the OSS stack — Prometheus scrapes messages_sent {initialize:1,
tools/list:1, tools/call:20} and messages_received{type=response}:22 labeled
mcp_server=math_subprocess; removed metrics absent from /metrics.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(observability): add telemetry-health alerts (OTLP export + discovery validation) (#541)
Two emitted signals had no alert coverage. Add:
- MCPHangarTelemetryExportFailing: rate(mcp_hangar_otlp_export_failures_total[5m]) > 0
for 10m. A silent OTLP export failure drops spans with no app-level error --
this guards exactly the "we've gone blind on tracing" class (cf. the OTLP
audit-log export that a botched rename had silently disabled).
- MCPHangarDiscoveryValidationFailing: rate(mcp_hangar_discovery_validation_failures_total[15m]) > 0
for 15m. Discovered servers failing validation are silently not registered.
Both warning severity (no runbook_url, matching the other warning alerts).
Validated with `promtool check rules` (30 rules, all valid). The packaged
helm-charts alert bundle should sync these separately.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(observability): stop logging expected stdio shutdowns as errors (#542)
Reviewing live logs surfaced a spurious ERROR on every idle-TTL shutdown:
close() sets self.closed=True before terminating the subprocess, so when the
reader thread hits EOF it called _capture_process_stderr(), which logged
`stdio_client_process_exited` (and any drained stderr) at ERROR unconditionally
-- even though we initiated the exit. That inflates the error stream and any
log-based alerting.
Pick the log level from self.closed: an exit we initiated is expected -> info
(with expected=true); an unsolicited process death is still an error. Same for
the EOF-on-stdout line. Unit tests cover both the expected and unexpected paths.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(repo): apply ruff format to restore green lint (#543)
`ruff format --check` (CI's ruff 0.14.13) was red on main for 12 files, so the
non-required `lint` check has been failing on every PR and masking real lint
regressions. Apply `ruff format` to the drifted files (pure formatting -- line
wrapping / spacing, no logic change). stdio_client.py is intentionally left to
PR #542, which already formats it alongside its fix.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(observability): mark a failed tool call's span as ERROR (#544)
Reviewing the error path on the live stack: a failing tool call (tool-level
error or ToolInvocationError) produced a trace where every span was UNSET --
in Jaeger the failing call looked completely successful and couldn't be filtered
as an error, even though tool_call_errors_total was incremented.
Cause: the batch executor handles failures as data (returns
CallResult(success=False)) inside the `batch.call.{tool}` span's with-block, so
the span never sees an exception and its status stays UNSET.
Set ERROR status on the span (with the failure message) when result.success is
false. Add a NoOp-safe mark_span_error() helper for the failure-as-data pattern.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(release): release 1.6.0 (#525)
Co-authored-by: mcp-hangar-release-bot[bot] <283430731+mcp-hangar-release-bot[bot]@users.noreply.github.com>
* test(core): update audit test for gen_ai.tool.name semconv rename
Reconcile fallout: main 1.6.0 (#539) renamed the span/audit attribute
mcp.tool.name -> gen_ai.tool.name, but this mcp2-side test still asserted
the old name. Git didn't flag it (code in main, test in mcp2), so it
only surfaced at runtime. Assert the new attribute.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: mcp-hangar-release-bot[bot] <283430731+mcp-hangar-release-bot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Addresses the first two items of #15 (the chain-orchestrator multi-server item follows in a separate PR). ## `mode`: keep `"active"`, add `"audit"` round-trip coverage > **Update:** an earlier revision of this PR renamed `mode` from `"active"` to `"enforce"`, following a SEP **draft**. The now-current revised SEP-1763 ([PR #2624](modelcontextprotocol/modelcontextprotocol#2624)) defines `mode?: "active" | "audit"` with default `"active"` — so `"active"` was correct all along. @jeongukjae flagged this; the rename has been reverted (commit 58edf2c). The C# SDK's wire value `"active"` matches the SEP and the TypeScript SDK. `InterceptorMode` keeps members `Active` (wire `"active"`, default) and `Audit` (wire `"audit"`). Added a deserialization test alongside the existing serialization test so both wire forms are covered. ## `priorityHint`: support `number | { request?, response? }` The SDK only supported a plain `int`. This adds a `PriorityHint` protocol type whose converter accepts both wire forms and preserves them on round-trip (a number stays a number, an object stays an object). Chain ordering now resolves the effective priority per phase exactly as the SEP's `resolvePriority` algorithm (unset → 0, alphabetical tie-break unchanged). - `Interceptor.PriorityHint` is now `PriorityHint?`; an implicit conversion from `int` keeps existing code compiling. - `[McpServerInterceptor]` gains `RequestPriorityHint` / `ResponsePriorityHint` (mutually exclusive with the scalar `PriorityHint`), following the official SDK's backing-field pattern for optional attribute values. - Behavior note: attribute-discovered interceptors with no priority set now omit `priorityHint` from the wire instead of emitting `0` — semantically identical per the SEP default, and consistent with the existing default-skipping for `Mode`/`FailOpen`. ## Testing `dotnet test` from `csharp/sdk/`: all passing, including new coverage for both `priorityHint` wire forms, partial objects, unknown-key tolerance, phase-dependent mutation ordering (the SEP's pii-redactor/compressor example), the attribute conflict error, and both `mode` wire values. Part of #15. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
@localden I noticed some inconsistency that might be important and thus want to flag with you. In this SEP, we use Also, this is a common best practice in the API world: use the plural form for the resource name so that it does not change depending on whether a list or a single object is returned. So, I highly recommend addressing this here before accepting the proposal and using |
|
Feedback from building the audit-mode shape against the current draft — a transport-level stdio proxy that observes Digests, not payloads, for anything audit mode retains. Security Best Practices #3 and the Compliance Considerations already require PII redaction and tamper-proof trails. The observation from implementing it is that those are prose obligations sitting on a schema whose default runs the other way: On the reserved
Without something like it, the field gets populated by whoever already holds the key and read downstream as more than authorization. A coverage gap in audit mode. Audit timeouts MUST be logged, but that log lands with the invoker: Cross-phase correlation. Happy to contribute vectors rather than prose for any of this. The disqualification is testable, and two-sided vectors for it exist already — tersignhq/evidence-record-conformance, Apache-2.0. The implementation is Disclosure: drafted with AI assistance; every claim about the SEP verified against the draft text and reviewed by a human. — Tersign |
…ture reference
## Summary
ask#7119 settled the genus noun for Minsky's ~90-entity enforcement corpus — **interceptor**, with
guard/detector/injector as computed family words — but the decision and the ontology beneath it
lived only in a closed ask and mt#3754's spec. This PR gives them a durable in-repo home:
`docs/architecture/interceptors.md`. mt#4006.
[no-deploy-impact]
## Motivation & Context
Five partially-overlapping words (guard, detector, observer, hook, interlock) each described part
of the corpus and were false for the rest. The catalog implementation and all later prose need one
vocabulary to inherit, and a spec section inside an IN-PROGRESS umbrella task is not where that
belongs. mt#4006's AT1 sets the bar: a reader answers "what is an interceptor, what are its axes,
and what is a guard vs a detector vs an injector" from the page alone.
## Design/Approach
**Judgment call — a docs page, no ADR.** mt#4006's spec left this open. `documentation-taxonomy`'s
ADR trigger ("committing to one architectural choice among alternatives") does describe the genus
decision, so an ADR was a live option. Three things decided against it: the commitment already
happened in ask#7119 and is already recorded in mt#3754, so an ADR would be a third copy with no
new reader; ADRs are immutable once Accepted, which is the wrong container for a model that will
grow as the catalog is built across ~89 heterogeneous entities; and splitting decision from
ontology guarantees a reader lands on one needing the other — the exact fragmentation the task
exists to end. The page is typed as an **architecture reference** (`docs/architecture/<feature>.md`,
descriptive title, living) and carries the decision inline in §6, with the three rejected naming
packages one line each so "why this and not that" is answerable in place. That rationale is also
written into the page itself, for the future reader who goes looking for an ADR.
**The amended model is the model.** mt#3754's field-alignment synthesis produced seven falsifiers
against the clean three-axis frame. The page carries all four resulting amendments as a
first-class section, not an appendix, and says so explicitly — a reader who takes away only the
three axes has taken away a model the codebase already contradicts.
## Key Changes
- **`docs/architecture/interceptors.md`** (new):
- §1 the genus definition, the entity-is-an-interception argument, and the has-a settlement
(classifier is a part, not a kind) with both arguments and its XACML/Invariant corroboration.
- §1 the one-sentence MCP disambiguation, deliberately unwrapped onto a single line so a
line-based grep can find it, written once in `docs/` — plus a footnote resolving the
two-identifier SEP question (see Notes below).
- §2 the three axes: interception point (9 values), intervention type (all 8 values defined in a
table, with the three that have no field standard and the three named false friends), decision
mechanism (ADR-024's ladder).
- §3 the four amendments — capability-set semantics, the audience parameter, the `record` split,
the `subject` marker — plus the two coverage gaps that bind the catalog (the registry sees ~37
of ~89; filenames disagree with axis 2). `auto-session-title` is recorded as a known rough edge
the genus definition fits worst, rather than papered over.
- §4 the computed families with definitions and the reason each survives; `observer` retired;
the coordinates grammar with four worked examples.
- §5 the entity strata: trajectory × role, and why mechanism decides who is bound.
- §6 the decision with provenance, the rejected packages, the two accepted costs, and the
storage-key policy (`guardName` not migrated; read-side alias only; adoption-cost tiers defined
in place).
- "What this page does not change" and "Adopted field vocabulary" sections.
- **`src/cockpit/CLAUDE.md §Guard/interlock vocabulary`**: records the genus decision and the
computed families, states explicitly that mt#2626 is not reopened, points at the page. The
existing mt#2626 paragraph is byte-unchanged.
- **mt#3754 `## Naming decision`**: cross-reference added pointing at the page as the durable
record, with what still belongs to the spec (the survey, the cost verdicts, the unverified-claims
ledger).
## Testing
No code changed and no test files were added, so no new tests apply. The acceptance tests are
documentary; the two mechanically checkable ones were run.
Execution evidence:
AT2 — the disambiguation sentence appears exactly once under `docs/`:
```
$ grep -rn 'Minsky interceptors are host- and process-side; MCP interceptors (SEP-2624) are the protocol-wire kind; the tool-call validator/mutator subset is shaped to become them.' docs/ | wc -l
1
docs/architecture/interceptors.md:64:Minsky interceptors are host- and process-side; MCP interceptors (SEP-2624) are the protocol-wire kind; the tool-call validator/mutator subset is shaped to become them.
```
This check drove a change: the sentence was originally wrapped across two lines, so the grep
returned zero. It is now deliberately unwrapped. Re-run after the footnote rewrite to confirm the
mandated sentence stayed byte-identical — still exactly 1.
AT3 — mt#2626's scoping restated unchanged in both surfaces:
```
$ grep -n 'registration mechanics' docs/architecture/interceptors.md src/cockpit/CLAUDE.md
docs/architecture/interceptors.md:306:- **`hook` still names registration mechanics only** — `.claude/hooks/`, `.minsky/hooks/`,
src/cockpit/CLAUDE.md:61:"Hook" names the Claude Code registration mechanics only (...)
src/cockpit/CLAUDE.md:65:**This does NOT reopen mt#2626, and the paragraph above stands unchanged:** ...
$ grep -n 'plant-UI deny noun\|interlock-history' docs/architecture/interceptors.md src/cockpit/CLAUDE.md
docs/architecture/interceptors.md:311:- **`interlock` remains the plant-UI deny noun** — the Plant Board's S2 valves, the
docs/architecture/interceptors.md:312: `/plant/interlock-history` route — per mt#2626, unchanged and shipped.
src/cockpit/CLAUDE.md:61:...the `/plant/interlock-history` page...
src/cockpit/CLAUDE.md:65:..."interlock" stays the plant-UI deny noun in shipped copy.
```
The `src/cockpit/CLAUDE.md:61` line is the pre-existing mt#2626 paragraph, unmodified by this PR.
AT1 (reader can answer the three questions from the page alone) is a judgment criterion, not a
runnable check. It is satisfied structurally: the genus definition (§1), all three axes with all
eight intervention-type values defined in place (§2), and the three family definitions (§4) are
each written out on the page with no forward reference to mt#3754 or ask#7119 required to read
them. Both source artifacts appear only under Cross-references.
Section anchors were verified against the emitted headings; one (`#the-genus-decision-ask7119`)
was wrong because the heading carries a numeric prefix, and is fixed.
## Ancillary Changes
The `Tier-2` cost citation in §6 arrived without its scale. SC5 requires every coined term to be
defined where it enters, so the four-tier scale is now defined in place. This is inside the task's
own criteria, not scope expansion.
## Notes for the reviewer
- **The two SEP numbers are both correct — please do not normalize them.** An earlier revision of
this PR flagged SEP-1763 vs SEP-2624 as a possible inconsistency in mt#3754's spec. It is not.
Checked against upstream primary sources:
[issue #1763](modelcontextprotocol/modelcontextprotocol#1763) is
"SEP-1763: Interceptors for Model Context Protocol", the originating proposal issue;
[PR #2624](modelcontextprotocol/modelcontextprotocol#2624) is
"SEP-2624: Interceptors for the Model Context Protocol", open, filed from a branch literally
named `SEP-1763`, and it carries the actual SEP document. The effort simply has two live
identifiers. The page cites SEP-2624 because that is what the live SEP document titles itself,
and §1's footnote now names both, links both, and says so — specifically so a later reader
cannot "fix" one into the other.
- **Counts are relayed, not recounted.** The ~89 / ~37 / 387k figures come from mt#3754's audit.
The page says so where it uses them and tells whatever consumes them to re-measure.
- Out of scope per the spec and untouched here: any code rename, storage migration, the
`/interceptors` cockpit route, and a rules-corpus sweep replacing "guard".
Co-Authored-By: minsky-ai[bot] <minsky-ai[bot]@users.noreply.github.com>
|
What's the state of this. At what point should core maintainers take a look? |
We had @clareliguori join the working group call last week and she provided the WG with feedback on how to structure the content before presenting to the core maintainers. The WG is updating the SEP contents per that feedback. cc @jeongukjae, @PederHP, @olaservo |
Summary
This PR introduces a SEP for an interceptor framework for the Model Context Protocol, defining a new MCP primitive that allows context operations to be intercepted, validated, and transformed at key points in the agentic lifecycle.
Motivation
The MCP ecosystem is rapidly developing a sprawling landscape of sidecars, proxies, client-wrappers and gateways to address cross-cutting context governance concerns (like PII Checking/reduction, Toxicity filtering, etc) . However, these implementations are largely:
Overall this is leading to an M × N problem: Each of M clients must integrate with each of N middleware solutions, resulting in M × N integration points and configurations.
What This SEP Proposes
This SEP proposes an Interceptor framework that effectively elevates middleware to an MCP primitive in-order to provide control over context at key points in the agentic lifecycle.
Towards this end, the SEP:
tools/call) to ensure governance over context is reliably applied before it crosses trust boundaries for both the server and the client.interceptors/listandinterceptor/invoke) that allow clients and servers to dynamically discover and trigger these primitives.Important: The SEP is fully backward compatible, interceptors are an optional server capability, no existing protocol methods are modified, and clients that do not support interceptors continue to work unchanged.
Other Relevant material
Relevant MCP Dev Summit (2016) Talks:
Reference Implementations (WIP):