feat!: streamable HTTP transport (hostable) + remove OIDC browser auth#15
Open
souf92i wants to merge 3 commits into
Open
feat!: streamable HTTP transport (hostable) + remove OIDC browser auth#15souf92i wants to merge 3 commits into
souf92i wants to merge 3 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a hostable streamable HTTP transport to horizon-mcp alongside the existing stdio transport, so one MCP instance can be hosted next to its Horizon and shared by multiple client sessions. Single-tenant by design: one MCP per Horizon,
HORIZON_URLalways from env, never client-supplied (no multi-tenant routing, no SSRF surface).Transport is selected by
HORIZON_TRANSPORT(stdiodefault, orhttp). HTTP mode runs an Express server on a single endpoint (default/mcp, POST/GET/DELETE with SSE responses) backed by the SDKStreamableHTTPServerTransport, with full per-caller session isolation.Authentication (Horizon stays the authority)
The MCP never makes authorization decisions; it forwards a Horizon credential and Horizon applies that principal's RBAC.
HORIZON_HTTP_AUTH_MODEfixes one of:service- the MCP holds one env credential (API key or mTLS to Horizon) and acts as a single identity for all callers. No per-caller fingerprint binding (the session id is a bearer), so the front door must be access-controlled by network placement or an authenticating edge; use a least-privileged identity.api-key(per caller) - the client sends its ownX-API-ID/X-API-KEY, forwarded to Horizon.mtls(per caller, terminate-and-forward, the focus) - the client presents a TLS client cert; the MCP (or a trusted ingress) terminates the TLS withoptional_no_casemantics (proving possession, not validating the chain) and forwards the cert to Horizon's Play backend inHORIZON_FORWARD_CERT_HEADER(defaultSSL_CLIENT_CERT). Horizon validates the chain, revocation, and identity. Forwards no long-lived secret. Most MCP clients cannot present a client cert, so a local mTLS proxy is documented.BREAKING CHANGE
OIDC browser login (Playwright) is removed in all transports, stdio included. Deployments that relied on it must switch to an API key or mTLS.
HORIZON_CLIENT_*andHORIZON_API_*are unchanged. A headless OIDC bearer flow is deferred until Horizon supports a forwardable token. Theplaywrightdependency,--external playwrightbuild flags, andHORIZON_LOGIN_TIMEOUTare gone.Security and isolation
HorizonClient+McpServerper session; ALS-routed per-session logging (fixes the prior module-level sink that would leak logs across concurrent sessions).req.headersandreq.rawHeadersbefore the SDK reads them (the SDK builds the Web request fromrawHeadersvia@hono/node-server), so credentials never surface in toolrequestInfo.HORIZON_PUBLIC_URL/HORIZON_TRUSTED_HOSTSrefuses to start; mTLS topology, header names, public URL, origins, and trusted-proxy CIDR are all validated;HORIZON_ALLOW_PRIVATE_TLS_PROBE=1is refused in HTTP mode.Adversarial review
A multi-dimension adversarial review was run against the implementation; all 10 confirmed findings are fixed in this PR, including a HIGH resource leak (an
initializethe SDK rejected after the client/auth were built was never tracked or reclaimed), a maxSessions TOCTOU, per-message rate accounting, an unbounded rate-limiter map, and config/credential validation hardening.Docs
README Transports section + full HTTP config table + auth modes + breaking-change note;
docs/authentication.md;docs/client-setup.mdwalkthrough for Claude Code / Claude Desktop / Codex (stdio + http, plus the mTLS local-proxy note);.env.example; aDockerfile+.dockerignore. Corrected the stale tool count to 211 tools across 12 domains (the config-CRUD suite was missing from the README counts since the last PR).Test plan
bun run format:check,bun run lint,bun run typecheckbun run build(express stays external; built bundle boots,/healthz-> 200, bad Host -> 421)bun run test- 1006 unit tests pass, including a real-HTTP integration suite (two-session identity isolation, no-credential rejection, session-id-replay rejection, service-mode credential rejection, teardown on close, and the resource-leak fix)bun run verify:truth,bun run test:scenarios(offline LLM eval)bun run test:e2e/bun run test:llm:liveagainst QA (needs.env.local) - recommended before mergeNote:
bun run docs:diffreports pre-existing drift insrc/generated/docs/*.json(live Horizon doc snapshots), unrelated to this change.