mcp-data-platform-v1.60.1
What's new in v1.60.1
This release unifies the OAuth 2.1 authorization_code flow for all outbound connection kinds (MCP gateway, HTTP API gateway, and any future kinds) onto one shared implementation, and adds a real Keycloak IdP to the dev environment so the full flow is testable end-to-end.
Why this matters
The MCP gateway and the HTTP API gateway each had their own OAuth implementation — separate HTTP handlers, separate token tables, separate refresh loops, separate frontend hooks, and separate status components (~95% duplicated). That divergence produced three user-visible symptoms:
- API connection "Connect" button surfaced a generic error on unsaved forms. Save-first was required by both backends, but only the API view showed it as a failure — because the API view had no OAuth status block to surface state.
- API connection view didn't show OAuth status at all. The status endpoint and the status component were built only for MCP.
- MCP refresh tokens appeared to vanish across replicas. The MCP toolkit's in-memory
oauthTokenSourcecachedloaded=trueafter the first read, so a Connect that landed on pod A was invisible to pod B until restart.
All three traced to one architectural cause: the same OAuth flow had been forked instead of shared. This release fixes that.
Backend changes
- New
pkg/connoauth/package — singleSource/Store/Exchangeflow keyed on(kind, name). Refresh usesgolang.org/x/oauth2and the package explicitly persists the rotated token (including any new refresh token) after every successful exchange, so refresh-token rotations survive process restarts and multi-replica reads. That is the bug-3 fix. - Migration 000039 —
connection_oauth_tokenstable backfilled from the priorgateway_oauth_tokensandapigateway_oauth_tokenstables. The old tables are retained for one release for emergency rollback; a follow-up migration drops them. - Unified admin handler (
pkg/admin/connection_oauth_handler.go) — single/api/v1/admin/connections/{kind}/{name}/oauth-{start,status,reacquire}route set, kind-dispatched through anOAuthKindHandlersregistry. The canonical callback URL/api/v1/admin/oauth/callbackis unchanged from the prior MCP path, so customer IdP configurations (Keycloak / Auth0 / Okta / Microsoft App Registrations) do not need to be reconfigured. The legacy/api/v1/admin/api-gateway/oauth/callbackURL is registered as an alias so existing API-gateway IdP configurations continue to resolve. - Per-kind
OAuthKindHandlerimplementations in both toolkit packages, plusconnoauth-backedTokenStoreadapters so the toolkits' existingAuthenticators read and write the unified table. - Reacquire endpoint returns the post-refresh
OAuthStatusbody, so the portal status card updates immediately instead of waiting for the next 10-second poll.
Frontend changes
- Shared
ConnectionOAuthStatusCardrendered for every connection kind with an identical surface (Connect, Reconnect, Refresh now, status grid, error band). Replaces the MCP-only inline card. Keyed on(kind, name)so the card fully remounts on connection switch — no more stale "Token refreshed" banners bleeding from one connection to another. - Single unified hook set:
useStartConnectionOAuth(kind),useConnectionOAuthStatus(kind, name),useReacquireConnectionOAuth(). The legacy MCP/API helpers now delegate to these. - Connection selection mirrors into the URL as
?kind=...&name=...viahistory.replaceState, so the OAuth callback'sreturnURLrestores the same connection the operator was viewing. Default selection follows the alphabetical kind order shown in the left sidebar instead of whatever order the backend returned first. - Removed the useless "Show sensitive" toggle on the connection view — it only flipped between two opaque masks (
********↔[REDACTED]), so it served no purpose. LoginFormrebuilt with logo + portal title + tagline matching the rest of the brand surface. Newportal.taglinefield flows through/api/v1/admin/public/brandingso deployments can override the strapline alongside title and logo.
Dev environment
- Real Keycloak container in
make dev, backed by the existing Postgres (auxiliarykeycloakdatabase created idempotently instart.sh— works on both fresh volumes and pre-existing volumes that pre-date the keycloak addition). - Realm
mcp-platformpre-seeded with:- Three OAuth clients:
mcp-data-platform-portal(operator login),oauth-mcp-dev(MCP fixture connection),oauth-api-dev(API fixture connection). - A realm-role mapper so realm roles flow into the ID token (Keycloak's default puts them only on the access token).
- Two test users:
admin@example.com / admin-password(roledp_admin) andanalyst@example.com / analyst-password(roledp_analyst).
- Three OAuth clients:
- Vite proxy honors
X-Forwarded-Hostso OAuth start, callback, and logout all stay on the operator's original origin (:5173vs:8080). Previously the OAuth callback would silently bounce sessions started on the Vite dev server over to the Go server, leaving the operator viewing the embedded (compiled-in) UI bundle instead of the live source. browsersession.LogoutHandlerderivespost_logout_redirect_urifrom the request scheme + host so dev sessions return to their source origin rather than a static configured target./portal/auth/*added to the Vite proxy so OIDC login flows reach the Go backend instead of falling through to the SPA's 404 (which previously made the login page appear to reload itself when the operator clicked Sign in with OIDC).- Persona role definitions accept both unprefixed (API-key auth) and
dp_-prefixed (Keycloak OIDC, filtered byrole_prefix "dp_") variants so the same persona matches every login path.
Bug fixes shipped
| # | Symptom | Root cause | Fix |
|---|---|---|---|
| 1 | API Connect button generic error | API view had no OAuth status block | Shared ConnectionOAuthStatusCard rendered for every kind |
| 2 | No OAuth status on API view | Status endpoint + component were MCP-only | Unified /oauth-status route + shared card |
| 3 | MCP refresh tokens vanishing across replicas | In-memory loaded=true cache in toolkit |
connoauth.Source reads store on every call; refresh explicitly persists rotated tokens |
| 4 | Browser silently jumped to :8080 after OAuth |
Vite proxy used changeOrigin: true without X-Forwarded-Host |
xfwd: true on the Vite proxy + dynamic callback-URL derivation in the Go handler |
| 5 | Logout returned "Invalid parameter: id_token_hint" | Realm-level accessTokenLifespan: 60, so id_tokens expired before the operator clicked Sign out |
Bumped to 1800s in the realm import |
| 6 | "Refresh now" UI threw Unexpected end of JSON input |
Reacquire endpoint returned empty 200; apiFetch always parses JSON |
Endpoint now returns the post-refresh OAuthStatus body |
| 7 | Action banners bled across connections | Inner component reused across kind/name prop changes |
Card keyed on (kind, name) so it fully remounts |
| 8 | Trino auto-selected on first Connections visit | Default picked connections[0] (backend order) |
Default now follows the alphabetical kind order shown in the sidebar |
| 9 | OAuth callback dropped operator on a different connection | Selection was component state only | URL mirroring via history.replaceState + initial-selection restore |
Stats
- 51 files changed, ~6,100 insertions, ~450 deletions
- 1 new package (
pkg/connoauth/) - 1 new migration (
000039_connection_oauth_tokens) - 1 new admin route group
- 1 new shared React component (
ConnectionOAuthStatusCard) - Test coverage: total 87%;
pkg/connoauth91.3%,pkg/admin89.3%,pkg/toolkits/gateway95.3%,pkg/toolkits/apigateway93.1%.
Operator upgrade notes
- No database migration is required by the operator. Migration 000039 applies automatically on startup. Existing tokens in
gateway_oauth_tokensandapigateway_oauth_tokensare backfilled into the new unifiedconnection_oauth_tokenstable; the old tables remain readable for one release. - No IdP reconfiguration is required. The canonical OAuth callback URL
/api/v1/admin/oauth/callbackis unchanged. The legacy/api/v1/admin/api-gateway/oauth/callbackURL continues to resolve as an alias. - New optional config field:
portal.tagline(string) appears in/api/v1/admin/public/brandingasportal_taglineand is rendered on the login screen below the portal title. Defaults to"Sign in to access the platform."when unset. make devnow requires Docker pull ofquay.io/keycloak/keycloak:25.0.6on first start (~500 MB). Subsequent runs reuse the image.
Follow-ups (deferred, not in this release)
- Drop migration for
gateway_oauth_tokensandapigateway_oauth_tokensafter one stable release on the unified table. - Deletion of the legacy
pkg/admin/gateway_oauth_handler.goandpkg/admin/api_gateway_oauth_handler.gofiles (kept as fallback whenConnOAuthStoreis nil; routes are not registered when the unified path is active). - Deletion of the
/api/v1/admin/api-gateway/oauth/callbacklegacy alias after one release.
Changelog
Installation
Homebrew (macOS)
brew install txn2/tap/mcp-data-platformClaude Code CLI
claude mcp add mcp-data-platform -- mcp-data-platformDocker
docker pull ghcr.io/txn2/mcp-data-platform:v1.60.1Verification
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-data-platform_1.60.1_linux_amd64.tar.gz.sigstore.json \
mcp-data-platform_1.60.1_linux_amd64.tar.gz