mcp-data-platform-v1.61.4
Highlights
Fix: portal stopped attributing locally-decided revocations to the IdP (#405)
The connection status card previously rendered a single message for every revoked authorization_code connection:
"Previous session rejected by the upstream IdP. The token endpoint at <host> returned
refresh_expired."
For two of the three revocation causes, this wording was wrong. The IdP was never called on the tick that produced the deletion — the verdict was reached locally:
- IdP-disclosed deadline reached. A previous successful refresh response disclosed a hard
refresh_expires_invalue (e.g., Keycloak'sSsoSessionMaxLifespanshrinksrefresh_expires_intoward zero across rotations until it pins to an absolute ceiling). When local clock crossed that disclosed deadline, the platform stopped before contacting the IdP — but emitted an event row labeledrefresh_failed_revokedwithIDPErrorCode: "refresh_expired", then the banner read out that field as if the IdP had returned it. - No refresh token stored. When a row had no refresh token to exchange (e.g., scope without
offline_access), the platform short-circuited before any network call but produced the same misleading event shape. - IdP-rejected
invalid_grant. This is the only cause where the IdP actually rejected something — same event shape was used, but for this case the wording was correct.
The fix dispatches the leading event by cause:
| Cause | IdP called? | Lead event |
|---|---|---|
invalid_grant from IdP |
yes | TypeRefreshFailedRevoked (unchanged) |
| Local deadline reached | no | TypeRefreshSkippedExpired |
| No refresh token stored | no | TypeRefreshSkippedNoToken |
The two Skipped types were already defined in pkg/authevents and covered by writer tests, but had no production callers. They're now the canonical lead events for locally-decided verdicts.
The portal banner now branches on the persisted last_revocation.reason field and renders distinct headlines per cause — "Session reached the IdP-disclosed maximum lifetime", "Upstream IdP rejected the refresh token", or "No refresh token is stored for this connection". The History panel labels match. Legacy events still on disk from before this release render through a UI translation layer so (refresh_expired) becomes (IdP-disclosed deadline reached) etc.
Fix: deterministic ordering in the in-memory authevents store
MemoryStore.List returned events in undefined order when back-to-back inserts produced identical OccurredAt timestamps. On Apple Silicon, two consecutive Insert calls without -race slowing them down can both observe the same nanosecond timestamp; sort.Slice is not stable. TestMemoryStoreListFilters failed deterministically under go test and passed only because make verify uses -race. Added a monotonic insertion counter that breaks ties.
The Postgres backend has no comparable tie-breaker (UUID primary key, no secondary ORDER BY) and remains undefined under tied timestamps. The doc on MemoryStore calls this out explicitly: the only production consumer of either backend, admin.lastRevocationFor, tolerates either order because the lead and trail of a revocation pair both carry the same reason string.
Changelog
Bug Fixes
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.61.4Verification
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-data-platform_1.61.4_linux_amd64.tar.gz.sigstore.json \
mcp-data-platform_1.61.4_linux_amd64.tar.gz