Repository navigation
Conversation
Injects the git commit SHA at Docker build time via ldflags and replaces http.DefaultTransport at startup with a wrapper that sets User-Agent on every outgoing request. Format follows RFC 9110: mcp-front/1.0 (build; <sha>). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a token refresh fails with invalid_grant, invalid_client, or unauthorized_client, the refresh token is permanently invalid. Delete the stored token so /my/tokens and /tools accurately reflect that the user needs to re-authenticate instead of showing a stale "connected" status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The service OAuth flow now uses PKCE (S256) for all providers—the verifier is embedded in the signed state parameter so it survives the redirect. Removes the hardcoded access_type=offline param which is Google-specific and rejected by some authorization servers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends the inline tool system with two generic capabilities: GET support for HTTP tools — when method is GET, tool arguments are appended as query parameters instead of sent as a JSON body. Static params can be baked into the URL. HTML fetch tool type — a new execution mode that fetches a URL, validates it against an allowed domain list, and extracts text content using a CSS selector. Strips script, style, nav, and footer elements. Adds help-docs inline server with search and fetch tools for Vori's HubSpot-hosted knowledge base at help.vori.com. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Services can now configure clientCredentials with clientId, clientSecret, tokenUrl, and optional scopes. mcp-front exchanges the credentials for a Bearer token and refreshes it automatically via oauth2.TokenSource. This is useful for services like Mintlify that use machine-to-machine auth rather than per-user OAuth flows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- GKE tools returned 401 because `forwardAuthToken` forwarded mcp-front JWTs (not GCP tokens) to `container.googleapis.com` - Adds `gcpAuth` config option that uses Application Default Credentials to get GCP access tokens - Token source handles caching and auto-refresh, solving the 1-hour GCP token lifespan - GKE uses the cached backend path for fast tool calls (no per-call init overhead) - [x] All unit tests pass - [x] Deployed and verified `gke__kube_get` and `gke__list_clusters` work in production 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow GCP service accounts to exchange their short-lived access tokens for long-lived JWT + refresh token pairs. This enables Coder workspaces to maintain persistent MCP gateway authentication instead of relying on static tokens that expire after ~1 hour. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mitted When requireResourceParam is false (i.e. dangerouslyAcceptIssuerAudience is enabled), the authorize endpoint allowed requests without the RFC 8707 resource parameter but issued tokens with an empty audience. These tokens then failed ValidateAudienceForService because an empty audience matches neither the per-service URI nor the issuer URI. Now falls back to the base issuer URI as audience, which the token validation middleware already accepts when acceptIssuerAudience is enabled. Fixes VOR-16272 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Renders tools grouped by backend service with annotation badges (readOnlyHint, destructiveHint, idempotentHint, openWorldHint). Uses the same browser SSO middleware as /my/tokens so users can view available tools after authenticating with Google. The tools handler queries the aggregate's cached tool schemas via ListToolsByBackend, which reuses the existing discovery cache rather than triggering fresh discovery. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds opt-in response streamlining to aggregate servers via streamlineResponses config flag. Strips embedded documentation (XML example/hints blocks), truncates long descriptions, removes parameter descriptions, simplifies nullable types, and strips validation-only schema fields. Reduces tools/list response size significantly for deployments aggregating many tools with verbose documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Service OAuth configurations can now omit clientId, clientSecret, and scopes — mcp-front performs RFC 7591 dynamic client registration with the upstream service when these fields are absent. Only authorizationUrl and tokenUrl are strictly required. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
html/template URL-escapes values interpolated into href attributes. Pre-escaping with url.QueryEscape causes double encoding: the = at the end of a base64 signature becomes %3D, then %253D, making the signature unverifiable when the link is clicked. Pass the raw signed state and returnURL to the template and let html/template handle escaping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Go's http.ServeMux 301-redirects /gateway to /gateway/ when only the trailing-slash variant is registered. HTTP clients often convert the redirected POST into a GET, which breaks MCP streamable-http handshakes with cryptic timeouts. Register the handler at both paths so the redirect never happens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Streamable-http MCP servers return Mcp-Session-Id in the initialize response header. Clients must echo it in subsequent requests. Without Access-Control-Expose-Headers, browsers and strict HTTP libraries can't read the header, so clients send follow-up notifications without a session ID and the server returns 400. Also add Mcp-Session-Id to Allow-Headers so clients can include it in request headers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ChainMiddleware applies middlewares in order, meaning the last entry becomes the outermost. Previously auth wrapped CORS, so OPTIONS preflights were rejected with 401 before CORS could short-circuit them. This broke browser-based clients trying to read the Mcp-Session-Id response header. New order (outer → inner): logger → recover → CORS → auth → serviceAuth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mcpserver.NewStreamableHTTPServer takes a WithHTTPContextFunc that runs after the library populates the context with the session. Our callback returned r.Context() — throwing away the session. Downstream, ClientSessionFromContext(ctx) returned nil during tools/list, so session-specific tools (set by our onRegisterSession hook) weren't found and the aggregate returned an empty tool list. Drop the callback entirely. If we ever need per-request context, it must extend the passed ctx, not replace it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Standalone reproducer that starts a mock MCP backend with 3 tools, wires it through an aggregate server with streamable-http transport, and verifies that tools/list returns the backend's tools rather than an empty list. Catches the session-context bug fixed in the previous commit. Run with: go run ./internal/aggregate/streamable_race_test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cube server implements the inline.MCPServer interface, providing meta, query, and dimension_search tools against a Cube API. Uses JWT auth with HMAC-SHA256 signing and automatic token refresh. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vori-specific deployment: tracks vori/config.json (aggregate gateway
plus 15+ backend MCP servers), the Docker build script with git-SHA
version injection, a local dev run script, and a Dockerfile tweak to
COPY vori/config.json as the default /app/config.json.
Un-ignores vori/config.json so the deployment config travels with the
fork. No secrets — environment variables are referenced via {"$env":
"VAR_NAME"}.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backends with gcpAuth: true had no Authorization header attached to outbound requests. The dead newGCPTokenSource function was never wired to the new aggregate package after the upstream rebase, so GKE tool calls reached container.googleapis.com without credentials and silently returned no useful data. Adds a generic TokenSources map[backendName]oauth2.TokenSource on aggregate.ServerConfig. In createConn, after any user-token substitution, the matching token source (if any) is fetched and applied as a Bearer Authorization header via a new copy-on-write WithBearerToken method on MCPClientConfig. Sources for gcpAuth backends are built by buildGCPTokenSources (one shared google.DefaultTokenSource keyed by each backend name) and passed through to every aggregate.
Token sources were built by buildClientCredentialsSources but never reached the aggregate, so backends like Mintlify that authenticate via the client_credentials grant got no Authorization header and servers rejected requests with "No authorization provided". Merge the client-credentials sources into the aggregate's TokenSources map alongside GCP sources.
Restores the GCP fallback in NewValidateTokenMiddleware that the upstream rebase removed. Service-to-service callers (e.g. opsbot) authenticate by sending their raw GCP access token as a Bearer token rather than going through the RFC 8693 token-exchange flow, and the JWT-only validator was returning 401 for every such request. When ValidateAccessToken rejects the token, fall back to gcpValidator.Validate; on success use the GCP-resolved email as the user identity, store the raw token on the context for backends that forward auth, and honor X-On-Behalf-Of impersonation against allowedDomains.
Streamable-http session state (per-session tool registrations) lives in mcp-go's in-process sessionToolsStore and is lost when the pod restarts. Clients holding the old Mcp-Session-Id got 400 Invalid session ID on every follow-up call until they reinitialized, which most MCP clients don't do on their own. Switch the aggregate's streamable transport to StatelessGeneratingSessionIdManager so the session id is validated by format only — stale ids from restarted-over sessions pass validation and produce an empty ephemeral session instead of a 400. Then, in BeforeListTools and BeforeCallTool hooks, lazily rebuild the per-session tool list from the aggregate's cached discovery when it is missing, so that rehydrated sessions see the same tools they had before the restart. The onRegisterSession path is reused via a shared ensureSessionTools helper that's idempotent against already-populated sessions. Bumps mcp-go v0.43.0 → v0.44.1 for StatelessGeneratingSessionIdManager, which generates proper per-client UUIDs (v0.43.0's StatelessSessionIdManager returns an empty string, collapsing all users onto one shared session).
Drops the custom "cube" transport type and the bundled internal/cube package. Cube integration now lives in a standalone cube-mcp server; mcp-front treats it like any other streamable-http backend. Follow-up commits add the replacement configuration.
Lets a backend declare `hmacJWT` in its config with a secret, algorithm (HS256), arbitrary claims, and TTL. mcp-front mints a short-lived HMAC-signed JWT and attaches it as a Bearer token on outbound requests. The implementation plugs into the existing backend TokenSources map via a new oauth2.TokenSource and refreshes tokens automatically before expiry. Wires the Vori-side cube backend through this mechanism pointing at the standalone cube-mcp sidecar on localhost:8003, and adds cube to each aggregate's server list.
## Summary
Adds the Materialize developer MCP server as a backend in the Vori
`mcp-front` gateway, so developers can query Materialize's system
catalog through the OAuth proxy alongside the other internal tools.
Wiring it up surfaced two pre-existing bugs in `mcp-front`, which this
PR also fixes — so the change is broader than just a config entry.
## Changes
### Materialize backend — `vori/config.json`
New `materialize` server: `streamable-http` transport pointed at the
Materialize developer MCP endpoint (`/api/mcp/developer`), added to both
the `gateway` and `gateway-streamable` aggregate servers. Auth is a
per-user manual token — each developer generates a personal MCP token in
the Materialize Console and pastes it into `/my/tokens`; `mcp-front`
injects it as `Authorization: Basic <token>`. Per-user tokens preserve
audit attribution in Materialize.
### Per-user tool discovery in the aggregate gateway —
`internal/aggregate/server.go`
The aggregate cached discovered tools in a single process-global entry
(60s TTL), populated by whichever user happened to trigger a discovery
cycle. That is incorrect for `requiresUserToken` backends: such a
backend was visible or absent for *every* user depending on whether the
triggering user had a token — so token-gated tools (`materialize`,
`linear`, `notion`, `sentry`, `coder`) flickered in and out of
everyone's tool list every 60 seconds.
Discovery is now split: backends needing no user token are discovered
once and cached globally; token-gated backends are discovered and cached
per-user, limited to the backends each user actually has a token for.
New regression test `TestPerUserTokenBackendIsolation`.
### mcp-go v0.44.1 → v0.49.0 and empty-`params` stripping — `go.mod`,
`internal/client/client.go`
mcp-go v0.44.1 serialized the required MCP `notifications/initialized`
message — and `tools/list` requests — with an empty `"params":{}` object
(`omitempty` does not apply to struct-typed fields). Materialize's MCP
server rejects an empty `params` with HTTP 422, so the materialize
handshake never completed.
- The mcp-go bump to v0.49.0 fixes the notification (`omitempty` →
`omitzero`) and trims the indirect-dependency set.
- For the request path, `stripEmptyParamsTransport` — an
`http.RoundTripper` on streamable-http transports — drops an empty
`"params":{}` / `null` from outgoing JSON-RPC bodies. An empty `params`
is equivalent to an absent one per JSON-RPC 2.0, so this is safe for
every backend and keeps `mcp-front` compatible with strict MCP servers.
## Test plan
- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `go test ./internal/...` — full suite passes, including
`TestPerUserTokenBackendIsolation` and the `stripEmptyParams` tests
- [x] Verified directly against the live Materialize endpoint with
`curl`: `initialize`, `notifications/initialized` (no `params`), and
`tools/list` (no `params`) all succeed; `tools/list` returns
`query_system_catalog`
- [x] Deployed the mcp-go bump to the production gateway — the
materialize handshake now completes (`Backend connection established` in
the logs)
- [ ] End-to-end through Claude: `materialize__query_system_catalog`
callable once the empty-`params` stripping change is deployed
## Linear
VOR-18099
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The aggregate keeps a long-lived pooled connection per (user, backend) and reuses the backend's `Mcp-Session-Id` across calls. When a streamable-http backend expires that session server-side before the idle connection is evicted (idle timeout is 5 minutes), the next pooled call sends a stale session id and the backend rejects it. Datadog returns "Invalid session ID"; mcp-go surfaces this as an error and does not re-initialize on its own. The aggregate evicted the dead connection but proxied the error to the client, so the next call self-healed and the user saw intermittent "Invalid session ID" failures. Production logs confirmed the pattern: dozens of `Evicted broken backend connection backend=datadog` events clustered seconds apart, with no accompanying retry. The literal error was never logged — it was serialized into the MCP tool result and proxied to the client — so this also adds the missing backend-error log. `withConnRetry` now evicts the broken connection and retries once against a freshly-initialized connection (which mints a new session) when the error indicates session invalidation: `transport.ErrSessionTerminated` for an HTTP 404, or an error whose text mentions a session for backends that reject with a different 4xx (Datadog's 400 "Invalid session ID"). The retry covers both tool calls and tool discovery. Non-session errors keep the previous evict-and-return behavior. The fix generalizes to every streamable-http backend, which share the same session-expiry behavior.
Upstream's new trier-middleware and redirect-URI tests call NewValidateTokenMiddleware and NewAuthHandlers with the upstream arity; our fork extends both with a GCP access token validator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The middleware has sent Mcp-Session-Id in Access-Control-Allow-Headers since it was added for streamable-http clients; the test assertion was never updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream now rejects DCR redirect URIs whose origin is not allowlisted, closing an authorization-code-theft vector. An audit of all 1,854 registered clients in Firestore found exactly three origins in use: http://localhost, http://127.0.0.1, and https://claude.ai. claude.com is included for Anthropic's domain migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aggregate tool calls intermittently fail with "invalid character '\x1f'" when a still-gzipped body from gke or datadog reaches mcp-go's JSON decoder. A response-side RoundTripper on streamable backends now sniffs Content-Encoding and the gzip magic bytes on application/json bodies, decompresses (looping for double-encoded bodies), and logs a WARN with the trigger so production occurrences reveal whether the backend double-encodes or Go skipped transparent decompression. SSE responses pass through untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tics Production showed the guard firing with trigger=header on gke, meaning Go skipped transparent decompression. Nothing in the stack sets Accept-Encoding, so logging the outgoing header plus request method and response status narrows down why Go did not request or strip gzip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mcp-go stamps every inbound HTTP request's headers onto the parsed request structs (PR #480) and its streamable client sends request.Header verbatim to the backend (PR #546, since v0.43.0). A proxy passing the request struct through therefore forwards the end client's headers — Accept-Encoding, cookies, auth — to backends. An explicit Accept-Encoding disables Go's transparent gzip decompression, which is how still-gzipped gke/datadog responses reached the JSON decoder. Clears request.Header at every backend-forwarding boundary: the aggregate tool handler and the proxy tool, prompt, resource, and resource-template handlers. The gzip guard transport remains as defense-in-depth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This repository no longer includes Vori's configuration. That data now resides in our backend repo. Closes VOR-18857
The system now properly revokes tokens for deactivated Google accounts. This is achieved by re-verifying the user against the Google directory when the client refreshes their token. If the provider no longer recognizes them, refresh is denied and stored credentials/sessions are purged.
…12) Two fixes for an incident where MCP clients repeatedly lost their connection to the proxy. ## Gateway streamable-session memory leak (the reconnects) The aggregate gateway attaches a per-session tool map to every session via `SetSessionTools`, and mints a fresh session on every client `initialize` (`StatelessGeneratingSessionIdManager`). MCP clients rarely send an HTTP `DELETE`, so mcp-go never frees those per-session maps — this is mcp-go issue [#723](mark3labs/mcp-go#723). Memory grows linearly with the number of sessions ever created, until the process is killed. As it nears the limit the runtime GC-thrashes and requests begin exceeding their deadlines, which surfaces to clients as the server going unresponsive. The remedy is mcp-go's built-in idle sweeper, `WithSessionIdleTTL` ([PR #724](mark3labs/mcp-go#724)). It is opt-in and off by default, which is why the leak persisted. Enabling it with a 10-minute TTL bounds retained per-session state to recent activity. Swept sessions rebuild their tools lazily from cached discovery on the next request, and `StatelessGeneratingSessionIdManager.Terminate` is a no-op with format-only validation, so a session ID reused after a sweep rehydrates transparently with no client-visible rejection. ## Service-account revocation churn The refresh-time revocation check denies any principal without a stored Google IdP refresh token. Service accounts that authenticate via RFC 8693 token exchange never have one, so every refresh was denied and they were forced to re-authenticate continuously. The identity's provider type now threads into the check, and the IdP replay runs only for Google identities. Others are validated by their provider at exchange time and are exempt. Deactivated-Google-user enforcement is unchanged.
A backend authenticating with an OAuth client-credentials grant whose access token has a short TTL flaps: `Backend connection established` succeeds, then once the token's lifetime elapses `Backend discovery failed … authorization required` and `Evicted broken backend connection`, repeating for every user (these backends use no per-user token, so it is global). ## Root cause `createConn` fetches a token from the per-backend `oauth2.TokenSource` and bakes the bearer into the pooled connection's static HTTP headers. That connection is reused for its whole lifetime. The `clientcredentials` `ReuseTokenSource` refreshes correctly, but that refresh never reaches an already-established pooled connection — it keeps sending the bearer captured at creation time. Once the captured token expires, the next discovery or tool call sends a stale bearer and the backend returns HTTP 401. `withConnRetry` evicts on any error but only retries when the error is a stale-session error. A 401 is not a session error, so the connection is evicted and the error is returned: discovery fails and the backend drops from the aggregated tool list. The next request mints a fresh connection with a fresh token, works until the next expiry, and the cycle repeats. Refreshing the token source proactively does not help — the staleness lives on the pooled connection, not in the source. The fix belongs at the connection level. ## Fix An authorization error is now treated like a stale session: evict, recreate the connection (which re-mints the token via the source and re-applies any user token), and retry once, making the 401 invisible to the client. The auth retry is gated on the backend having a re-mintable credential (a client-credentials token source or a per-connection user token), so a genuinely misconfigured backend is not retried with the same rejected bearer. Auth errors are detected via mcp-go's `transport.ErrAuthorizationRequired`/`ErrUnauthorized` sentinels, mirroring the existing session-error check. No thundering herd: oauth2's `ReuseTokenSource` serializes refresh, so concurrent recreations hit the token endpoint once. This also benefits user-token backends — an expired user token now recreates the connection and re-fetches the refreshed token rather than evicting.
mcp-go v0.55.1 fixes the upstream bug where the client forwarded inbound HTTP headers to backend MCP servers (mark3labs/mcp-go#905, fixed by mark3labs/mcp-go#909). The new `outboundHeader()` helper returns `nil` for any request decoded from an inbound JSON-RPC message — exactly what the local workaround did by clearing `request.Header` at every backend-forwarding boundary. With the fix upstream, the workaround is redundant. Removed: - the `stripInboundHeaders` wrapper and its call site in the tool-call path - the inline `request.Header = nil` clears in the prompt, resource, resource-template, and aggregate tool-call paths - the two tests that asserted the old behavior (`TestStripInboundHeaders`, `TestToolHandlerDropsInboundHeaders`) `gzipGuardTransport` stays as defense-in-depth. Its trigger (a leaked `Accept-Encoding`) disappears with this upgrade, but the gzip root cause was never fully pinned down, so its WARN log remains a useful signal of any new unaccounted encoding source. `copyRequestHeaders` (the SSE proxy header allowlist) and `stripEmptyParamsTransport` are unrelated and untouched.
Tools from an integration would disappear from the gateway whenever that integration was briefly slow to respond, and anyone who connected during that window lost them for the rest of their session. An integration's tools now survive a slow moment upstream. They are still withdrawn when access is genuinely revoked, and an integration that stays unreachable still falls away rather than being offered indefinitely.
Account executives can now generate onboarding links. Relates to GTM-1362
…nfig (#18) A gateway server entry for an OAuth-protected backend now needs only its URL. The endpoints a user is sent to, and the client registration behind them, are read from what the backend advertises, so adding one takes what the vendor's own setup instructions give you rather than four values copied by hand. A backend that cannot be discovered, or one that needs different values, still takes them from config as before. Registration also now works where a backend puts its authorization endpoint on a different host than its issuer, which previously failed outright.
An upstream server entry can now name the authenticated caller in a request header. Previously an upstream saw either a static token or a per-user OAuth token, and a shared-secret backend had no way to learn which person or service account was behind a call. With a `$userEmail` header value, the gateway stamps the caller's email on every request it opens for them, whether they signed in with Google or presented a service-account token.
Author
|
Opened against the wrong repository by mistake. This belongs on the voriteam fork. |
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.
An upstream server entry can now name the authenticated caller in a request header. Previously an upstream saw either a static token or a per-user OAuth token, and a shared-secret backend had no way to learn which person or service account was behind a call. With a
$userEmailheader value, the gateway stamps the caller's email on every request it opens for them, whether they signed in with Google or presented a service-account token.Closes VOR-20327