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.
1. Import a server
Section titled “1. Import a server”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.82The 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.
2. Add it to policy
Section titled “2. Add it to policy”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.
3. What each harness gets
Section titled “3. What each harness gets”| 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.
4. Drift
Section titled “4. Drift”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.
5. Halos as the authorization server
Section titled “5. Halos as the authorization server”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:
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.