Skip to content

fix(mcp): stabilize authenticated tool discovery#5754

Open
j15z wants to merge 10 commits into
stagingfrom
fix/mcp-discovery-races
Open

fix(mcp): stabilize authenticated tool discovery#5754
j15z wants to merge 10 commits into
stagingfrom
fix/mcp-discovery-races

Conversation

@j15z

@j15z j15z commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Authenticated MCP servers now keep the newest valid discovery result instead of resurfacing stale tools, stale status, or an OAuth prompt from an older request. Static-header authorization failures remain header-auth failures, while OAuth servers that do not advertise dynamic client registration return an actionable 422 directing the user to configure a token.

Discovery publication is ordered end to end. Redis and memory cache adapters allocate a monotonic millisecond mutation token; conditional cache publication and the database lastToolsRefresh CAS use that exact same token, so retries cannot make the two layers choose different winners. A credential-safe SHA-256 discovery revision covers every connection-affecting field; after a status CAS race, discovery re-reads that revision so metadata-only edits keep valid live tools while connection changes invalidate stale results. Failure counters recompute through an optimistic JSONB CAS when a success races them, and a full cache clear invalidates existing mutation owners before deleting entries.

Mutation ownership is retried once. If ordering or an atomic cache transition remains unavailable, freshly fetched tools are returned to the caller but unordered cache/database state is not persisted. If another mutation supersedes the request, discovery reloads the winning positive-cache entry; stale-configuration results with no valid winner still fail closed. Explicit configuration and list-change invalidation remains best-effort during cache degradation.

Bulk discovery awaits each publication before returning, failures persist a zero tool count, and refresh consumes the discovery publication metadata. It re-reads current server metadata, reports unavailable or winner-cache results with the live tool count, and compares the published lastToolsRefresh token instead of a wall-clock timestamp before applying a failure. Raw upstream errors are replaced with allowlisted UI messages plus shared credential-free structural diagnostics.

Investigation confirmed @modelcontextprotocol/sdk 1.29 already captures Mcp-Session-Id from initialize and replays it on tools/list. The client records the request phase and whether a session id was present without logging the identifier or credentials.

Related: #5665 and #5595.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

  • DATABASE_URL=postgresql://postgres:postgres@localhost:5432/sim bun run --cwd apps/sim test -- lib/mcp 'app/api/mcp/servers/[id]/refresh/route.test.ts' — 357 tests passed across 19 files.
  • bun run check:api-validation:strict — passed with no boundary-policy drift.
  • Biome check and git diff --check — passed.
  • Direct GitHub Copilot MCP probes completed initialize and tools/list with 47 tools using both the normal and SSRF-pinned transports; the SDK transport carried the session id.
  • bun run --cwd apps/sim type-check currently reaches only unrelated shared-worktree dependency errors in fork audit events, recordAuditBatch, and @google-cloud/storage; no MCP TypeScript errors are reported.

Reviewer focus: the unified cache/database publication token, explicit discovery revision, status JSONB CAS loop, Redis conditional-mutation scripts, and degraded-cache response path are the highest-value concurrency review points.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Screenshots/Videos

N/A — API and backend behavior only.

@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 18, 2026 3:16am

Request Review

@cursor

cursor Bot commented Jul 18, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Large concurrency and publication changes across MCP discovery, Redis Lua scripts, and status CAS; incorrect ordering could still show stale tools or status under race conditions.

Overview
MCP tool discovery now publishes cache and database state through a shared monotonic mutation token, with CAS on lastToolsRefresh, discovery-revision guards, and bulk discovery awaiting publication before returning. When ordering or atomic cache writes fail, callers still get live tools but unordered state is not persisted; superseded requests reload the winning cache entry.

Cache adapters (memory and Redis) gain beginMutation, conditional writes, and applyMutationIfCurrent; workspace clears invalidate mutation owners before deleting keys. Refresh uses discoverServerToolsWithMetadata, re-reads server rows after discovery, treats unavailable / winner-cache as connected with live tool counts, and races failures on lastToolsRefresh instead of overwriting status locally.

Client and errors: header-auth UnauthorizedError is no longer treated as OAuth; logging uses getMcpSafeErrorDiagnostics (no messages, credentials, or session ids). OAuth start returns 422 when dynamic client registration is unsupported. Server updates clear cache on transport/auth/headers changes. User-facing discovery failures use allowlisted messages instead of raw upstream text.

Reviewed by Cursor Bugbot for commit d4bdbe8. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/sim/lib/mcp/service.ts Outdated
Comment thread apps/sim/lib/mcp/client.ts
@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR orders authenticated MCP discovery results so older requests cannot replace newer state. The main changes are:

  • Generation-based conditional mutations for Redis and memory caches.
  • Database publication guards for configuration and discovery ordering.
  • Current metadata reads and zero tool counts in refresh responses.
  • Safer authentication errors and credential-free diagnostics.

Confidence Score: 4/5

The database publication predicate needs a precision-safe configuration generation before merging.

  • PostgreSQL timestamps can retain precision that JavaScript dates discard.
  • An exact mismatch suppresses status and cache publication for an unchanged server.
  • The cache mutation ordering and authentication handling otherwise appear consistent.

apps/sim/lib/mcp/service.ts

Important Files Changed

Filename Overview
apps/sim/lib/mcp/service.ts Adds ordered database and cache publication, but exact timestamp equality can suppress valid discovery results.
apps/sim/lib/mcp/storage/redis-cache.ts Adds atomic generation checks for conditional Redis writes and deletes.
apps/sim/lib/mcp/storage/memory-cache.ts Adds per-server mutation generations and invalidates them during cache clearing.
apps/sim/app/api/mcp/servers/[id]/refresh/route.ts Reads current persisted server metadata after discovery and preserves newer successful state.
apps/sim/lib/mcp/client.ts Separates static-header authorization failures from OAuth prompts and sanitizes diagnostics.
apps/sim/app/api/mcp/oauth/start/route.ts Returns an actionable response when dynamic OAuth client registration is unavailable.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant DB as PostgreSQL
    participant Discovery as MCP discovery
    participant Cache as MCP cache
    DB-->>Discovery: updatedAt .123456 becomes JS Date .123
    Discovery->>Discovery: tools/list succeeds
    Discovery->>DB: "UPDATE WHERE updatedAt = .123000"
    DB-->>Discovery: zero rows updated
    Discovery-->>Cache: publication skipped
    Discovery-->>Discovery: discovered tools discarded
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant DB as PostgreSQL
    participant Discovery as MCP discovery
    participant Cache as MCP cache
    DB-->>Discovery: updatedAt .123456 becomes JS Date .123
    Discovery->>Discovery: tools/list succeeds
    Discovery->>DB: "UPDATE WHERE updatedAt = .123000"
    DB-->>Discovery: zero rows updated
    Discovery-->>Cache: publication skipped
    Discovery-->>Discovery: discovered tools discarded
Loading

Reviews (1): Last reviewed commit: "fix(mcp): stabilize authenticated tool d..." | Re-trigger Greptile

Comment thread apps/sim/lib/mcp/service.ts Outdated
- preserve typed static-header authentication failures\n- order discovery publication by start time\n- atomically publish tools and failure cache state
Comment thread apps/sim/lib/mcp/service.ts Outdated
Comment thread apps/sim/lib/mcp/service.ts
Match configuration generations within the JavaScript millisecond window so PostgreSQL microseconds do not suppress valid publication.
@j15z

j15z commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

The database publication predicate needs a precision-safe configuration generation.

Addressed in 89dce25: both status-publication predicates now match the JavaScript millisecond window, accepting PostgreSQL sub-millisecond precision while still rejecting the next generation. A boundary regression covers the behavior.

Comment thread apps/sim/lib/mcp/service.ts
- fall back to best-effort cache writes when mutation ordering is unavailable\n- share bounded redacted MCP error diagnostics across client and service
Comment thread apps/sim/lib/mcp/service.ts Outdated
Retry cache mutation ownership before publishing discovery state, avoid unordered publication that older writers can supersede, and keep explicit invalidation best effort.
Comment thread apps/sim/lib/mcp/service.ts
Comment thread apps/sim/lib/mcp/storage/redis-cache.ts
Use timestamp-based mutation tokens for both cache ownership and database CAS ordering, and invalidate Redis mutation owners before deleting entries during a full clear.
Comment thread apps/sim/lib/mcp/service.ts Outdated
Comment thread apps/sim/lib/mcp/service.ts Outdated
Recompute consecutive failures after status CAS conflicts, reload winning cached tools for superseded discoveries, and return live tools without unordered publication when cache ordering is unavailable.
Comment thread apps/sim/app/api/mcp/servers/[id]/refresh/route.ts
Comment thread apps/sim/app/api/mcp/servers/[id]/refresh/route.ts
Expose discovery publication metadata to refresh callers, report live tools during cache degradation, and compare lastToolsRefresh mutation tokens instead of wall clocks when preserving a newer success.
Comment thread apps/sim/lib/mcp/service.ts Outdated
Probe mutation ownership after database CAS misses, keep valid live tools for metadata-only edits, and invalidate ownership for transport and auth-type changes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c1764a1. Configure here.

Comment thread apps/sim/lib/mcp/service.ts Outdated
Hash connection-affecting server fields and recheck the fresh revision after database CAS misses so metadata edits retain valid tools while URL, credential, and transport changes fail closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant