Skip to content

MCP registry and authorization

halos.yaml mcp is the list of MCP servers your org approves (ADR-0029). Releases render each ring’s and unit’s servers into every harness’s native MCP settings, halod reports servers on machines that are not in it, and halo-server can be the authorization server for your own remote servers.

Terminal window
halo mcp registry import io.github.microsoft/playwright-mcp
# add under mcp.servers in halos.yaml:
- id: playwright-mcp
registryName: io.github.microsoft/playwright-mcp
version: 0.0.82
package:
type: npm
identifier: '@playwright/mcp'
version: 0.0.82

The argument is a name in the official MCP Registry (fetched read-only), an https URL serving a server.json, or a local file. --remote pins the streamable-HTTP endpoint instead of the package; an OCI package needs --sha256 with the image digest. Notes on stderr list what the entry does not carry: arguments, required environment variables, headers. Nothing is written.

halos.yaml
mcp:
mode: allowlist # default. warn: render, block nothing else, report drift
servers:
- id: playwright-mcp
package: {type: npm, identifier: "@playwright/mcp", version: 0.0.82}
rings: [ring0-harness-team]
- id: wiki
url: https://mcp-wiki.internal.acme.example/mcp
rings: [ring1-canary, ring2-early, ring3-ga]
units: [payments] # also members of payments and units below it
tools:
allow: [search, fetch]
deny: [delete_page]
auth: {mode: halos}

halo validate checks the pins (exact versions only, image digests, https URLs, ${VAR} header values), that rings and units exist, and in allowlist mode that profiles, unit overlays and toggles name only registry servers. Ship it like any policy change: halo plan, a ring rollout, eval gates.

Moving from mcp.servers in a profile: list the servers in the registry with the profile’s rings, delete them from the profile, then drop mode: warn.

Server allowlist Tool lists Auth
Claude Code allowManagedMcpServersOnly + allowedMcpServers deny only oauth; halos via headersHelper
Codex requirements.toml identities allow and deny warns
Gemini CLI mcp.allowed (names) allow and deny oauth
Copilot CLI allowedMcpServers warns warns
Cursor CLI warns (dashboard) warns warns
OpenCode warns (no allowlist) deny only oauth

Everything a harness cannot express is a warning in the release manifest; halo plan shows them. Details and sources: harness matrix.

Each halod cycle reads the console user’s own MCP configs for the harnesses the release ships and reports in its status:

"mcpDrift": [
{"harness": "claude-code", "file": "/Users/dev/.claude.json", "server": "playwright", "kind": "version-drift",
"transport": "stdio", "definitionSha256": "93098e84bb3984832b76cd6c573afb9fcfff0a17cf58956f9703e827e3f81a30",
"detail": "defined as a stdio command (definition sha256 93098e84bb39); the registry pins npx -y @playwright/mcp@0.0.82",
"fix": "change \"playwright\" to npx -y @playwright/mcp@0.0.82, or remove it (the managed config already provides it)"},
{"harness": "codex", "file": "/Users/dev/.codex/config.toml", "server": "scraper", "kind": "unapproved", ...}
]

halod never edits user files. Project .mcp.json files are not scanned. It reads only regular files, opened inside the user’s home (os.Root: no symlinked directory or .. leads out of it), never through a final symlink, without blocking, and only if the opened file is the one it checked (device and inode). Findings leave the device, so they name the server, its transport (stdio or http), an http server’s host (no userinfo, path or query) and the sha256 of the user’s full definition (URL, or command and arguments separated by NUL bytes; compare with printf 'npx\0-y\0@playwright/mcp@latest\0' | shasum -a 256). They never carry the command, its arguments or a URL path, any of which can hold a credential.

For a server you run, auth: {mode: halos} makes halo-server mint its tokens: 5 minutes at most, aud the server’s URL, sub the person (and actors the agent chain, when an agent asks). The caller’s own credential is sent only to halo-server.

Claude Code gets a headersHelper that runs, on every connection:

Terminal window
halo mcp token --resource https://mcp-wiki.internal.acme.example/mcp --format headers
# {"Authorization":"Bearer eyJ..."}

It needs HALO_SERVER and HALO_SESSION (or, in CI, HALO_GATEWAY_TOKEN from halo ci token) in the user’s environment, and halo-server --workload-key-file. Each exchange is audited as mcp.token.exchange or mcp.token.denied.

The server verifies the token with halo-server’s workload public key, the same PEM the gateways trust:

c, err := identity.VerifyMCPToken(keys, bearer, "https://mcp-wiki.internal.acme.example/mcp", time.Now())

or any JWT library: alg EdDSA, typ halo-mcp+jwt, iss halos, aud its own canonical URL, exp/nbf/iat, mcp_server its registry id.

Other modes: oauth renders a pre-registered clientID and scopes where the harness supports it (users still sign in with /mcp); clientIDMetadataURL and enterprise (Enterprise-Managed Authorization through identity.issuer) are policy for now, rendered by no CLI, and warn.