Skip to content

Conversation

@chelojimenez
Copy link
Contributor

@chelojimenez chelojimenez commented Nov 5, 2025

Note

Adds 2025-03-26 OAuth protocol to the debugger (UI, factory, types, state machine, diagram) and improves SSE debug proxy/handling; updates 2025-06-18/11-25 flows and client naming.

  • OAuth Protocols:
    • Add 2025-03-26 support:
      • New state machine client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts (direct RFC8414 discovery from MCP base URL, fallback endpoints, PKCE required; DCR/preregistered registration; no RFC9728/CIMD).
      • Update types.ts to include "2025-03-26", new step "discovery_start", and RegistrationStrategy2025_03_26.
      • Extend factory to create/select 2025-03-26 machines, validate strategies, defaults, supported strategies, and PROTOCOL_VERSION_INFO.
    • Update 2025-06-18 and 2025-11-25 state machines:
      • Switch client name to "MCPJam Inspector" and adjust authenticated initialize requests.
      • Handle SSE responses via structured proxy body (transport === "sse").
  • UI:
    • Add protocol option 2025-03-26 in components/OAuthFlowTab.tsx and persist preferences.
    • OAuthSequenceDiagram: conditionally omit initial 401/RFC9728 steps for 2025-03-26; update discovery/PKCE labels and descriptions.
  • Backend Debug Proxy (server/routes/mcp/oauth.ts):
    • Enhance SSE parsing to detect legacy HTTP+SSE transport; return structured payload with transport, events, isOldTransport, endpoint, mcpResponse, and rawBuffer.
    • Improve error details in SSE failures.

Written by Cursor Bugbot for commit 1607050. This will update automatically on new commits. Configure here.

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request labels Nov 5, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 5, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds protocol version "2025-03-26" across the client and factory, including a new discovery-first debug state machine with PKCE and MCP-aware flows, and a UI dropdown option. Updates the sequence diagram for protocol-specific branching. Extends factory config and supported registration strategies to include "2025-03-26". Renames client display text from "MCP Inspector" to "MCPJam Inspector" and refines SSE handling to detect transport type. Server proxy SSE parsing changed to line-buffered stream parsing with structured SSE responses and error reporting.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b34a3e2 and 1607050.

📒 Files selected for processing (8)
  • client/src/components/OAuthFlowTab.tsx (1 hunks)
  • client/src/components/OAuthSequenceDiagram.tsx (2 hunks)
  • client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts (1 hunks)
  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts (7 hunks)
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts (7 hunks)
  • client/src/lib/oauth/state-machines/factory.ts (6 hunks)
  • client/src/lib/oauth/state-machines/types.ts (2 hunks)
  • server/routes/mcp/oauth.ts (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts (2)

1207-1214: Update client name to match rebranding.

Apply this diff:

                     clientInfo: {
-                      name: "MCP Inspector",
+                      name: "MCPJam Inspector",
                       version: "1.0.0",
                     },

1267-1274: Update client name to match rebranding.

Apply this diff:

                     clientInfo: {
-                      name: "MCP Inspector",
+                      name: "MCPJam Inspector",
                       version: "1.0.0",
                     },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31ec7c2 and b34a3e2.

📒 Files selected for processing (8)
  • client/src/components/OAuthFlowTab.tsx (1 hunks)
  • client/src/components/OAuthSequenceDiagram.tsx (3 hunks)
  • client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts (1 hunks)
  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts (7 hunks)
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts (7 hunks)
  • client/src/lib/oauth/state-machines/factory.ts (6 hunks)
  • client/src/lib/oauth/state-machines/types.ts (2 hunks)
  • server/routes/mcp/oauth.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interfaces for defining object shapes
Constrain generics with appropriate type bounds
Use type guards to narrow unknown or union types before usage
Enforce import ordering consistently

**/*.{ts,tsx}: Prefer named exports in TypeScript modules
Use 2-space indentation
Declare types and interfaces using PascalCase

Files:

  • client/src/components/OAuthFlowTab.tsx
  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • client/src/lib/oauth/state-machines/types.ts
  • server/routes/mcp/oauth.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • client/src/components/OAuthSequenceDiagram.tsx
  • client/src/lib/oauth/state-machines/factory.ts
client/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Follow consistent React component structure in the frontend

Files:

  • client/src/components/OAuthFlowTab.tsx
  • client/src/components/OAuthSequenceDiagram.tsx
client/src/{app,components}/**/*.tsx

📄 CodeRabbit inference engine (client/CLAUDE.md)

client/src/{app,components}/**/*.tsx: Use React functional components with React.FC typing for all UI components and pages
Define explicit Props interfaces for components, including proper children prop handling
Type event handlers with React-provided types (e.g., React.MouseEvent, FormEvent)
Follow React 19 patterns: hooks-centric components, Suspense and Error Boundaries where appropriate, and concurrent features
Maintain component isolation: avoid excessive prop drilling, use Context wisely, favor composition, and optimize renders
Build responsive layouts using the established Tailwind breakpoint system, grids, flex, and container queries
Ensure accessibility: proper ARIA attributes, keyboard navigation, focus management, and screen reader support

Files:

  • client/src/components/OAuthFlowTab.tsx
  • client/src/components/OAuthSequenceDiagram.tsx
client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (client/CLAUDE.md)

Implement strict TypeScript types: strict prop types, event types, state interfaces, and utility types

In client code, use the @/ alias for imports

Files:

  • client/src/components/OAuthFlowTab.tsx
  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • client/src/lib/oauth/state-machines/types.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • client/src/components/OAuthSequenceDiagram.tsx
  • client/src/lib/oauth/state-machines/factory.ts
client/src/{app,components,hooks}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (client/CLAUDE.md)

Use React hooks for local state: apply useState/useReducer patterns, create custom hooks, and always clean up effects

Files:

  • client/src/components/OAuthFlowTab.tsx
  • client/src/components/OAuthSequenceDiagram.tsx
client/src/components/**/*.tsx

📄 CodeRabbit inference engine (client/CLAUDE.md)

Use Radix UI primitives for dialogs, dropdowns, form controls, and tooltips

React components in client/src/components should be functional and saved as PascalCase.tsx files

Files:

  • client/src/components/OAuthFlowTab.tsx
  • client/src/components/OAuthSequenceDiagram.tsx
client/src/{stores/servers,lib}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (client/CLAUDE.md)

Persist configuration: local storage, export/import, sync, and backup/restore options

Files:

  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • client/src/lib/oauth/state-machines/types.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • client/src/lib/oauth/state-machines/factory.ts
server/routes/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

server/routes/**/*.ts: Validate server responses (schema, content, status codes, headers) in route handlers
Perform input validation on incoming requests (types, sanitization, size, format)

Files:

  • server/routes/mcp/oauth.ts
server/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

server/**/*.ts: Implement robust error handling with safe error messages and log sanitization
Set appropriate security headers for backend responses
Apply rate limiting and backoff strategies to protect APIs

Use TypeScript for all backend source files

Files:

  • server/routes/mcp/oauth.ts
server/{app.ts,routes/**/*.ts}

📄 CodeRabbit inference engine (server/CLAUDE.md)

Use Hono.js for API routing and middleware in the app setup and all route handlers

Files:

  • server/routes/mcp/oauth.ts
server/{routes/mcp/**,utils/mcp-utils.ts,types/mcp.ts}

📄 CodeRabbit inference engine (server/CLAUDE.md)

Implement MCP protocol using Mastra (@mastra/core, @mastra/mcp) for MCP handling, helpers, and types

Files:

  • server/routes/mcp/oauth.ts
server/routes/mcp/**/*.ts

📄 CodeRabbit inference engine (server/CLAUDE.md)

Place all MCP protocol handlers under server/routes/mcp

Files:

  • server/routes/mcp/oauth.ts
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: server/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:07:08.982Z
Learning: Provide comprehensive MCP server testing (spec validation, OAuth flows, prompt/resource checks, error simulation)
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-19T23:05:54.474Z
Learning: Follow MCP specification: protocol versioning, message formats, error codes, and extensions
📚 Learning: 2025-10-19T23:06:33.450Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: client/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:06:33.450Z
Learning: Applies to client/src/{components/servers,hooks/{mcp,transport}}/**/*.{ts,tsx} : Provide transport selection UIs and logic: protocol options, configuration forms, validation rules, and presets

Applied to files:

  • client/src/components/OAuthFlowTab.tsx
📚 Learning: 2025-10-19T23:06:33.450Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: client/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:06:33.450Z
Learning: Applies to client/src/{hooks/mcp,stores/{chat,servers}}/**/*.{ts,tsx} : Synchronize MCP state: track connections, requests, responses, and errors consistently

Applied to files:

  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • server/routes/mcp/oauth.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
📚 Learning: 2025-10-19T23:07:08.982Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: server/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:07:08.982Z
Learning: Applies to server/utils/mcp-utils.ts : Put MCP helper utilities in server/utils/mcp-utils.ts

Applied to files:

  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • server/routes/mcp/oauth.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
📚 Learning: 2025-10-19T23:07:08.982Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: server/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:07:08.982Z
Learning: Applies to server/{routes/mcp/**,utils/mcp-utils.ts,types/mcp.ts} : Implement MCP protocol using Mastra (mastra/core, mastra/mcp) for MCP handling, helpers, and types

Applied to files:

  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • server/routes/mcp/oauth.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • client/src/components/OAuthSequenceDiagram.tsx
📚 Learning: 2025-10-19T23:06:33.450Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: client/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:06:33.450Z
Learning: Applies to client/src/{components/servers,hooks/mcp,lib/api}/**/*.{ts,tsx} : Implement authentication setup: OAuth configuration, token management, scopes, and refresh handling

Applied to files:

  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • client/src/lib/oauth/state-machines/types.ts
  • server/routes/mcp/oauth.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • client/src/components/OAuthSequenceDiagram.tsx
📚 Learning: 2025-10-19T23:07:08.982Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: server/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:07:08.982Z
Learning: Applies to server/types/mcp.ts : Define MCP types in server/types/mcp.ts

Applied to files:

  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • server/routes/mcp/oauth.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
📚 Learning: 2025-10-19T23:07:08.982Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: server/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:07:08.982Z
Learning: Provide comprehensive MCP server testing (spec validation, OAuth flows, prompt/resource checks, error simulation)

Applied to files:

  • client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • client/src/components/OAuthSequenceDiagram.tsx
📚 Learning: 2025-10-19T23:06:33.450Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: client/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:06:33.450Z
Learning: Applies to client/src/{lib/api,hooks/llm}/**/*.{ts,tsx} : Implement Anthropic Claude integration: authentication flow, request formatting, stream processing, and rate limiting

Applied to files:

  • client/src/lib/oauth/state-machines/types.ts
  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • client/src/components/OAuthSequenceDiagram.tsx
📚 Learning: 2025-10-19T23:07:08.982Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: server/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:07:08.982Z
Learning: Applies to server/routes/transport/sse/**/*.ts : Implement Server-Sent Events (streaming, connection handling, reconnection, message formatting) under server/routes/transport/sse

Applied to files:

  • server/routes/mcp/oauth.ts
📚 Learning: 2025-10-19T23:07:08.982Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: server/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:07:08.982Z
Learning: Applies to server/routes/mcp/**/*.ts : Place all MCP protocol handlers under server/routes/mcp

Applied to files:

  • server/routes/mcp/oauth.ts
📚 Learning: 2025-10-19T23:07:08.982Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: server/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:07:08.982Z
Learning: Applies to server/routes/transport/http/**/*.ts : Implement streamable HTTP (chunked encoding, pools, stream/timeout handling) under server/routes/transport/http

Applied to files:

  • server/routes/mcp/oauth.ts
📚 Learning: 2025-10-19T23:07:08.982Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: server/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:07:08.982Z
Learning: Applies to server/routes/transport/stdio/**/*.ts : Implement STDIO transport (process spawning, stream handling, buffering, error recovery) under server/routes/transport/stdio

Applied to files:

  • server/routes/mcp/oauth.ts
📚 Learning: 2025-10-19T23:06:33.450Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: client/CLAUDE.md:0-0
Timestamp: 2025-10-19T23:06:33.450Z
Learning: Applies to client/src/{hooks/llm,components/chat}/**/*.{ts,tsx} : Handle response streaming: token processing, UI updates, cancellation, and error states

Applied to files:

  • server/routes/mcp/oauth.ts
📚 Learning: 2025-10-19T23:05:54.474Z
Learnt from: CR
Repo: MCPJam/inspector PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-19T23:05:54.474Z
Learning: Follow MCP specification: protocol versioning, message formats, error codes, and extensions

Applied to files:

  • client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • client/src/lib/oauth/state-machines/factory.ts
🧬 Code graph analysis (3)
client/src/lib/oauth/state-machines/types.ts (2)
client/src/lib/debug-oauth-state-machine.ts (3)
  • RegistrationStrategy2025_06_18 (20-20)
  • RegistrationStrategy2025_11_25 (21-21)
  • OAuthProtocolVersion (19-19)
client/src/lib/oauth-flow-types.ts (1)
  • OAuthProtocolVersion (10-10)
client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts (2)
client/src/lib/oauth/state-machines/types.ts (4)
  • OAuthFlowState (32-124)
  • EMPTY_OAUTH_FLOW_STATE (127-133)
  • OAuthStateMachine (136-142)
  • RegistrationStrategy2025_03_26 (158-158)
client/src/lib/jwt-decoder.ts (2)
  • decodeJWT (5-31)
  • formatJWTTimestamp (58-64)
client/src/lib/oauth/state-machines/factory.ts (1)
client/src/lib/oauth/state-machines/types.ts (1)
  • RegistrationStrategy2025_03_26 (158-158)
🪛 Biome (2.1.2)
client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts

[error] 319-319: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 320-320: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 322-326: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 353-355: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 356-356: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 357-357: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 358-358: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 359-359: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 468-473: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 475-475: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 485-486: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 488-493: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 515-520: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 777-777: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 778-778: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 780-790: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 812-814: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 850-857: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 898-902: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 920-927: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 1196-1219: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 1219-1231: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔍 Remote MCP

I'll search for relevant context about the MCP protocol specifications and OAuth changes referenced in this PR.

Summary of Additional Context for PR Review

MCP OAuth Specification Timeline

In March 2025, the MCP specification took a major leap forward with the release of a new specification that standardizes Authorization using OAuth 2.1. This PR implements support for multiple OAuth protocol versions:

  • 2025-03-26 (March): OAuth 2.1 access token handling conforms to resource request requirements, with PKCE as a foundational security mechanism for public clients.

  • 2025-06-18 (June): The latest changelog, released on June 18, 2025, introduces updates that clarify how authorization should be handled for MCP Servers and how MCP Clients should implement Resource Indicators to prevent malicious servers from obtaining access tokens. Specifically, MCP servers are now officially classified as OAuth Resource Servers, which has significant implications for security and discovery.

PKCE (Proof Key for Code Exchange) Security Context

Mandating PKCE for all clients significantly raises the bar for security, protecting against common attacks right out of the box. The PR implements PKCE generation and integration throughout the state machines.

PKCE provides an additional security layer on top of the authorization code grant flow, ensuring that the application that starts the authentication flow is the same one that finishes it. OAuth 2.1 incorporates PKCE and requires or recommends it for all OAuth clients running the code flow - confidential as well as public ones.

Key Specification Changes

  1. Metadata Discovery: MCP specification strongly recommends metadata discovery implementation, which reduces the need for users to provide endpoints manually, and recommends implementation of dynamic client registration.

  2. Dynamic Client Registration: MCP clients and servers SHOULD support the OAuth 2.0 Dynamic Client Registration Protocol to allow MCP clients to obtain OAuth client IDs without user interaction, which is crucial for MCP because clients cannot know all possible servers in advance.

  3. Resource Indicators: MCP clients are now required to implement Resource Indicators, as specified in RFC 8707. By using a resource indicator in the token request, a client explicitly states the intended recipient (the "audience") of the access token, ensuring the Authorization Server issues a token that is tightly scoped and only valid for that specific MCP server.

SSE Transport Implementation

Server-Sent Events (SSE) is a server push technology enabling a client to receive automatic updates from a server via an HTTP connection, commonly used to send message updates or continuous data streams to a browser client. The PR updates SSE handling in the /debug/proxy path with improved line-based event parsing and transport type detection.

Notable Changes in This PR

The PR introduces backward-compatible type aliases and supports three protocol versions with distinct registration strategies. The renaming from "MCP Inspector" to "MCPJam Inspector" appears to reflect a rebranding effort, while the enhanced SSE handling improves compatibility with MCP's revised transport layer specifications from the 2025-06-18 update.

🔇 Additional comments (4)
client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts (1)

183-1576: Well-structured OAuth flow implementation.

The state machine implementation is comprehensive:

  • Discovery-first flow with path-aware metadata discovery and fallback endpoints
  • Proper PKCE generation and validation throughout
  • HTTP+SSE transport backward compatibility detection
  • JWT audience validation for security
  • Detailed logging and error handling at each step

The fallback endpoint logic (lines 408-442) and HTTP+SSE detection (lines 1295-1416) are particularly well-documented.

client/src/components/OAuthFlowTab.tsx (1)

643-645: New protocol version option added correctly.

The 2025-03-26 protocol option is added consistently with existing items in the dropdown.

client/src/components/OAuthSequenceDiagram.tsx (2)

312-391: Protocol-specific flow branching implemented correctly.

The conditional logic appropriately reflects the 2025-03-26 specification:

  • Skips initial unauthorized MCP request (starts with discovery)
  • Bypasses RFC9728 Protected Resource Metadata steps
  • Uses MCP base URL for direct OAuth discovery with fallbacks
  • Labels accurately describe discovery method per protocol version

The flow accurately represents the discovery-first approach for 2025-03-26.


574-582: PKCE label consistency maintained.

The PKCE generation labels correctly reflect that it's REQUIRED for both 2025-03-26 and 2025-11-25, while treating it as recommended for earlier versions. Resource parameter inclusion is consistently mentioned across all versions.

Comment on lines +1 to +10
/**
* OAuth 2.0 State Machine for MCP - 2025-03-26 Protocol
*
* This implementation follows the 2025-03-26 MCP OAuth specification:
* - Registration priority: DCR (SHOULD) > Pre-registered
* - Discovery: Direct OAuth 2.0 (RFC8414) from MCP server base URL with fallback endpoints
* - PKCE: Recommended but not strictly required
* - No Protected Resource Metadata (RFC9728) support
* - No Client ID Metadata Documents support
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix PKCE documentation inconsistency.

The header states "PKCE: Recommended but not strictly required" but the implementation treats it as mandatory (lines 528, 783, 961). Per the external specification context, PKCE is a foundational security mechanism for 2025-03-26.

Apply this diff:

- * - PKCE: Recommended but not strictly required
+ * - PKCE: REQUIRED for all clients (OAuth 2.1 foundational security)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* OAuth 2.0 State Machine for MCP - 2025-03-26 Protocol
*
* This implementation follows the 2025-03-26 MCP OAuth specification:
* - Registration priority: DCR (SHOULD) > Pre-registered
* - Discovery: Direct OAuth 2.0 (RFC8414) from MCP server base URL with fallback endpoints
* - PKCE: Recommended but not strictly required
* - No Protected Resource Metadata (RFC9728) support
* - No Client ID Metadata Documents support
*/
/**
* OAuth 2.0 State Machine for MCP - 2025-03-26 Protocol
*
* This implementation follows the 2025-03-26 MCP OAuth specification:
* - Registration priority: DCR (SHOULD) > Pre-registered
* - Discovery: Direct OAuth 2.0 (RFC8414) from MCP server base URL with fallback endpoints
* - PKCE: REQUIRED for all clients (OAuth 2.1 foundational security)
* - No Protected Resource Metadata (RFC9728) support
* - No Client ID Metadata Documents support
*/

Comment on lines +222 to +1541
switch (state.currentStep) {
case "idle":
// For March 2025-03-26 protocol: Start directly with OAuth discovery
// Per spec "Authorization Flow Steps", the flow starts with metadata discovery
// No initial MCP request is shown in the canonical flow diagram
updateState({
currentStep: "discovery_start",
serverUrl,
httpHistory: [],
isInitiatingAuth: false,
});

setTimeout(() => machine.proceedToNextStep(), 50);
return;

case "request_without_token":
// Step 2: Request MCP server and expect 401 or 200
if (!state.serverUrl) {
throw new Error("No server URL available");
}

try {
const response = await proxyFetch(state.serverUrl, {
method: "POST",
headers: mergeHeaders({
"Content-Type": "application/json",
}),
body: JSON.stringify({
jsonrpc: "2.0",
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: {
name: "MCP Inspector",
version: "1.0.0",
},
},
id: 1,
}),
});

const responseData = {
status: response.status,
statusText: response.statusText,
headers: response.headers,
body: response.body,
};

const updatedHistory = [...(state.httpHistory || [])];
if (updatedHistory.length > 0) {
const lastEntry = updatedHistory[updatedHistory.length - 1];
lastEntry.response = responseData;
lastEntry.duration =
Date.now() - (lastEntry.timestamp || Date.now());
}

if (response.status === 401 || response.status === 200) {
// Expected response - proceed with OAuth discovery
const infoLogs =
response.status === 200
? addInfoLog(
state,
"optional-auth",
"Optional Authentication",
{
message: "Server allows anonymous access",
note: "Proceeding with OAuth discovery for authenticated features",
},
)
: state.infoLogs;

updateState({
currentStep: "discovery_start",
lastResponse: responseData,
httpHistory: updatedHistory,
infoLogs,
isInitiatingAuth: false,
});
} else {
updateState({
lastResponse: responseData,
httpHistory: updatedHistory,
});
throw new Error(
`Expected 401 or 200 but got HTTP ${response.status}: ${response.body?.error?.message || response.statusText}`,
);
}
} catch (error) {
throw new Error(
`Failed to request MCP server: ${error instanceof Error ? error.message : String(error)}`,
);
}
break;

case "discovery_start":
// Step 3: Start authorization server metadata discovery
const authBaseUrl = buildAuthorizationBaseUrl(state.serverUrl!);
const authServerUrls = buildAuthServerMetadataUrls(authBaseUrl);

const authServerRequest = {
method: "GET",
url: authServerUrls[0],
headers: mergeHeaders({}),
};

updateState({
currentStep: "request_authorization_server_metadata",
authorizationServerUrl: authBaseUrl,
lastRequest: authServerRequest,
lastResponse: undefined,
httpHistory: [
...(state.httpHistory || []),
{
step: "request_authorization_server_metadata",
timestamp: Date.now(),
request: authServerRequest,
},
],
isInitiatingAuth: false,
});

setTimeout(() => machine.proceedToNextStep(), 50);
return;

case "request_authorization_server_metadata":
// Step 4: Fetch authorization server metadata with fallback
if (!state.authorizationServerUrl) {
throw new Error("No authorization server URL available");
}

const urlsToTry = buildAuthServerMetadataUrls(
state.authorizationServerUrl,
);
let authServerMetadata = null;
let lastError = null;
let successUrl = "";
let finalResponseData: any = null;

for (const url of urlsToTry) {
try {
const requestHeaders = mergeHeaders({});

const updatedHistoryForRetry = [...(state.httpHistory || [])];
if (updatedHistoryForRetry.length > 0) {
updatedHistoryForRetry[
updatedHistoryForRetry.length - 1
].request = {
method: "GET",
url: url,
headers: requestHeaders,
};
}

updateState({
lastRequest: {
method: "GET",
url: url,
headers: requestHeaders,
},
httpHistory: updatedHistoryForRetry,
});

const response = await proxyFetch(url, {
method: "GET",
headers: mergeHeaders({
"MCP-Protocol-Version": "2025-03-26",
}),
});

if (response.ok) {
authServerMetadata = response.body;
successUrl = url;
finalResponseData = response;
break;
} else if (response.status >= 400 && response.status < 500) {
continue;
} else {
lastError = new Error(`HTTP ${response.status} from ${url}`);
}
} catch (error) {
lastError = error;
continue;
}
}

// If discovery failed, use fallback endpoints
if (!authServerMetadata) {
const baseUrl = state.authorizationServerUrl;
authServerMetadata = {
issuer: baseUrl,
authorization_endpoint: `${baseUrl}/authorize`,
token_endpoint: `${baseUrl}/token`,
registration_endpoint: `${baseUrl}/register`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
};

const fallbackInfo = addInfoLog(
getCurrentState(),
"fallback-endpoints",
"Using Fallback Endpoints",
{
Note: "Metadata discovery failed, using default endpoints per 2025-03-26 spec",
"Authorization Endpoint":
authServerMetadata.authorization_endpoint,
"Token Endpoint": authServerMetadata.token_endpoint,
"Registration Endpoint":
authServerMetadata.registration_endpoint,
},
);

updateState({
currentStep: "received_authorization_server_metadata",
authorizationServerMetadata: authServerMetadata,
infoLogs: fallbackInfo,
isInitiatingAuth: false,
});
break;
}

// Validate required AS metadata fields
if (!authServerMetadata.issuer) {
throw new Error(
"Authorization server metadata missing required 'issuer' field",
);
}
if (!authServerMetadata.authorization_endpoint) {
throw new Error(
"Authorization server metadata missing 'authorization_endpoint'",
);
}
if (!authServerMetadata.token_endpoint) {
throw new Error(
"Authorization server metadata missing 'token_endpoint'",
);
}
if (
!authServerMetadata.response_types_supported?.includes("code")
) {
throw new Error(
"Authorization server does not support 'code' response type",
);
}

const authServerResponseData = {
status: finalResponseData.status,
statusText: finalResponseData.statusText,
headers: finalResponseData.headers,
body: authServerMetadata,
};

const updatedHistoryFinal = [...(state.httpHistory || [])];
if (updatedHistoryFinal.length > 0) {
const lastEntry =
updatedHistoryFinal[updatedHistoryFinal.length - 1];
lastEntry.response = authServerResponseData;
lastEntry.duration =
Date.now() - (lastEntry.timestamp || Date.now());
}

// PKCE validation (recommended but not required in 2025-03-26)
const supportedMethods =
authServerMetadata.code_challenge_methods_supported || [];

const metadata: Record<string, any> = {
Issuer: authServerMetadata.issuer,
"Authorization Endpoint":
authServerMetadata.authorization_endpoint,
"Token Endpoint": authServerMetadata.token_endpoint,
};

if (authServerMetadata.registration_endpoint) {
metadata["Registration Endpoint"] =
authServerMetadata.registration_endpoint;
}
if (authServerMetadata.code_challenge_methods_supported) {
metadata["PKCE Methods"] =
authServerMetadata.code_challenge_methods_supported;
}
if (authServerMetadata.grant_types_supported) {
metadata["Grant Types"] =
authServerMetadata.grant_types_supported;
}
if (authServerMetadata.response_types_supported) {
metadata["Response Types"] =
authServerMetadata.response_types_supported;
}
if (authServerMetadata.scopes_supported) {
metadata["Scopes"] = authServerMetadata.scopes_supported;
}

const infoLogs = addInfoLog(
getCurrentState(),
"as-metadata",
"Authorization Server Metadata",
metadata,
);

if (
!supportedMethods ||
supportedMethods.length === 0 ||
!supportedMethods.includes("S256")
) {
console.warn(
"Authorization server may not support PKCE S256 (REQUIRED for 2025-03-26 spec)",
);
}

updateState({
currentStep: "received_authorization_server_metadata",
authorizationServerMetadata: authServerMetadata,
lastResponse: authServerResponseData,
httpHistory: updatedHistoryFinal,
infoLogs,
isInitiatingAuth: false,
});
break;

case "received_authorization_server_metadata":
// Step 5: Client Registration
if (!state.authorizationServerMetadata) {
throw new Error("No authorization server metadata available");
}

if (registrationStrategy === "preregistered") {
const { clientId, clientSecret } =
loadPreregisteredCredentials(serverName);

if (!clientId) {
updateState({
error:
"Pre-registered client ID is required. Please configure OAuth credentials in the server settings.",
isInitiatingAuth: false,
});
return;
}

const preregInfo: Record<string, any> = {
"Client ID": clientId,
"Client Secret": clientSecret
? "Configured"
: "Not provided (public client)",
"Token Auth Method": clientSecret
? "client_secret_post"
: "none",
Note: "Using pre-registered client credentials (skipped DCR)",
};

const infoLogs = addInfoLog(
getCurrentState(),
"dcr",
"Pre-registered Client",
preregInfo,
);

updateState({
currentStep: "received_client_credentials",
clientId,
clientSecret: clientSecret || undefined,
tokenEndpointAuthMethod: clientSecret
? "client_secret_post"
: "none",
infoLogs,
isInitiatingAuth: false,
});
} else if (
state.authorizationServerMetadata.registration_endpoint
) {
const scopesSupported =
state.authorizationServerMetadata.scopes_supported;

const clientMetadata: Record<string, any> = {
client_name: "MCP Inspector Debug Client",
redirect_uris: [redirectUri],
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
token_endpoint_auth_method: "none",
};

if (scopesSupported && scopesSupported.length > 0) {
clientMetadata.scope = scopesSupported.join(" ");
}

const registrationRequest = {
method: "POST",
url: state.authorizationServerMetadata.registration_endpoint,
headers: mergeHeaders({
"Content-Type": "application/json",
}),
body: clientMetadata,
};

updateState({
currentStep: "request_client_registration",
lastRequest: registrationRequest,
lastResponse: undefined,
httpHistory: [
...(state.httpHistory || []),
{
step: "request_client_registration",
timestamp: Date.now(),
request: registrationRequest,
},
],
isInitiatingAuth: false,
});

setTimeout(() => machine.proceedToNextStep(), 50);
return;
} else {
// No registration endpoint - use mock client ID
updateState({
currentStep: "generate_pkce_parameters",
clientId: "mock-client-id-for-demo",
tokenEndpointAuthMethod: "none",
isInitiatingAuth: false,
});
}
break;

case "request_client_registration":
// Step 6: Dynamic Client Registration
if (!state.authorizationServerMetadata?.registration_endpoint) {
throw new Error("No registration endpoint available");
}

if (!state.lastRequest?.body) {
throw new Error("No client metadata in request");
}

try {
const response = await proxyFetch(
state.authorizationServerMetadata.registration_endpoint,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(state.lastRequest.body),
},
);

const registrationResponseData = {
status: response.status,
statusText: response.statusText,
headers: response.headers,
body: response.body,
};

const updatedHistoryReg = [...(state.httpHistory || [])];
if (updatedHistoryReg.length > 0) {
const lastEntry =
updatedHistoryReg[updatedHistoryReg.length - 1];
lastEntry.response = registrationResponseData;
lastEntry.duration =
Date.now() - (lastEntry.timestamp || Date.now());
}

if (!response.ok) {
updateState({
lastResponse: registrationResponseData,
httpHistory: updatedHistoryReg,
error: `Dynamic Client Registration failed (${response.status}). Using fallback client ID.`,
});

const fallbackClientId = "preregistered-client-id";

updateState({
currentStep: "received_client_credentials",
clientId: fallbackClientId,
clientSecret: undefined,
tokenEndpointAuthMethod: "none",
isInitiatingAuth: false,
});
} else {
const clientInfo = response.body;

const dcrInfo: Record<string, any> = {
"Client ID": clientInfo.client_id,
"Client Name": clientInfo.client_name,
"Token Auth Method":
clientInfo.token_endpoint_auth_method || "none",
"Redirect URIs": clientInfo.redirect_uris,
"Grant Types": clientInfo.grant_types,
"Response Types": clientInfo.response_types,
};

if (clientInfo.client_secret) {
dcrInfo["Client Secret"] =
clientInfo.client_secret.substring(0, 20) + "...";
dcrInfo["Note"] =
"Server issued client_secret - this will be used in token requests";
}

const infoLogs = addInfoLog(
getCurrentState(),
"dcr",
"Dynamic Client Registration",
dcrInfo,
);

updateState({
currentStep: "received_client_credentials",
clientId: clientInfo.client_id,
clientSecret: clientInfo.client_secret,
tokenEndpointAuthMethod:
clientInfo.token_endpoint_auth_method || "none",
lastResponse: registrationResponseData,
httpHistory: updatedHistoryReg,
infoLogs,
error: undefined,
isInitiatingAuth: false,
});
}
} catch (error) {
const errorResponse = {
status: 0,
statusText: "Network Error",
headers: mergeHeaders({}),
body: {
error: error instanceof Error ? error.message : String(error),
},
};

const updatedHistoryError = [...(state.httpHistory || [])];
if (updatedHistoryError.length > 0) {
const lastEntry =
updatedHistoryError[updatedHistoryError.length - 1];
lastEntry.response = errorResponse;
lastEntry.duration =
Date.now() - (lastEntry.timestamp || Date.now());
}

updateState({
lastResponse: errorResponse,
httpHistory: updatedHistoryError,
error: `Client registration failed: ${error instanceof Error ? error.message : String(error)}. Using fallback.`,
});

const fallbackClientId = "preregistered-client-id";

updateState({
currentStep: "received_client_credentials",
clientId: fallbackClientId,
clientSecret: undefined,
tokenEndpointAuthMethod: "none",
isInitiatingAuth: false,
});
}
break;

case "received_client_credentials":
// Step 7: Generate PKCE parameters (REQUIRED for 2025-03-26)
const codeVerifier = generateRandomString(43);
const codeChallenge = await generateCodeChallenge(codeVerifier);

const pkceInfoLogs = addInfoLog(
getCurrentState(),
"pkce-generation",
"Generate PKCE Parameters (REQUIRED)",
{
code_challenge: codeChallenge,
method: "S256",
resource: state.serverUrl || "Unknown",
note: "PKCE is REQUIRED for all clients in 2025-03-26 spec",
},
);

updateState({
currentStep: "generate_pkce_parameters",
codeVerifier,
codeChallenge,
codeChallengeMethod: "S256",
state: generateRandomString(16),
infoLogs: pkceInfoLogs,
isInitiatingAuth: false,
});
break;

case "generate_pkce_parameters":
// Step 8: Build authorization URL
if (
!state.authorizationServerMetadata?.authorization_endpoint ||
!state.clientId
) {
throw new Error("Missing authorization endpoint or client ID");
}

const authUrl = new URL(
state.authorizationServerMetadata.authorization_endpoint,
);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", state.clientId);
authUrl.searchParams.set("redirect_uri", redirectUri);
authUrl.searchParams.set(
"code_challenge",
state.codeChallenge || "",
);
authUrl.searchParams.set("code_challenge_method", "S256");
authUrl.searchParams.set("state", state.state || "");
if (state.serverUrl) {
authUrl.searchParams.set("resource", state.serverUrl);
}

// Add scopes
if (customScopes) {
authUrl.searchParams.set("scope", customScopes);
} else {
const scopesSupported =
state.authorizationServerMetadata.scopes_supported;

const scopes = new Set<string>();

if (scopesSupported && scopesSupported.length > 0) {
scopesSupported.forEach((scope) => scopes.add(scope));
}

if (scopesSupported?.includes("offline_access")) {
scopes.add("offline_access");
}

if (scopes.size > 0) {
authUrl.searchParams.set("scope", Array.from(scopes).join(" "));
}
}

const authUrlInfoLogs = addInfoLog(
getCurrentState(),
"auth-url",
"Authorization URL",
{
url: authUrl.toString(),
},
);

updateState({
currentStep: "authorization_request",
authorizationUrl: authUrl.toString(),
authorizationCode: undefined,
accessToken: undefined,
refreshToken: undefined,
tokenType: undefined,
expiresIn: undefined,
infoLogs: authUrlInfoLogs,
isInitiatingAuth: false,
});
break;

case "authorization_request":
// Step 9: Wait for authorization code
updateState({
currentStep: "received_authorization_code",
isInitiatingAuth: false,
});
break;

case "received_authorization_code":
// Step 10: Prepare token exchange
if (
!state.authorizationCode ||
state.authorizationCode.trim() === ""
) {
updateState({
error:
"Authorization code is required. Please paste the code you received from the authorization server.",
isInitiatingAuth: false,
});
return;
}

if (!state.authorizationServerMetadata?.token_endpoint) {
throw new Error("Missing token endpoint");
}

const tokenRequestBodyObj: Record<string, string> = {
grant_type: "authorization_code",
code: state.authorizationCode,
redirect_uri: redirectUri,
};

if (state.clientId) {
tokenRequestBodyObj.client_id = state.clientId;
}

if (state.clientSecret) {
tokenRequestBodyObj.client_secret = state.clientSecret;
}

if (state.codeVerifier) {
tokenRequestBodyObj.code_verifier = state.codeVerifier;
}

if (state.serverUrl) {
tokenRequestBodyObj.resource = state.serverUrl;
}

const tokenRequest = {
method: "POST",
url: state.authorizationServerMetadata.token_endpoint,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: tokenRequestBodyObj,
};

updateState({
currentStep: "token_request",
lastRequest: tokenRequest,
lastResponse: undefined,
accessToken: undefined,
refreshToken: undefined,
httpHistory: [
...(state.httpHistory || []),
{
step: "token_request",
timestamp: Date.now(),
request: tokenRequest,
},
],
isInitiatingAuth: false,
});

setTimeout(() => machine.proceedToNextStep(), 50);
return;

case "token_request":
// Step 11: Exchange authorization code for access token
if (!state.authorizationServerMetadata?.token_endpoint) {
throw new Error("Missing token endpoint");
}

if (!state.authorizationCode) {
throw new Error("Missing authorization code");
}

if (!state.codeVerifier) {
throw new Error(
"PKCE code_verifier is missing - REQUIRED by 2025-03-26 spec for token exchange",
);
}

try {
const tokenRequestBody = new URLSearchParams({
grant_type: "authorization_code",
code: state.authorizationCode,
redirect_uri: redirectUri,
client_id: state.clientId || "",
code_verifier: state.codeVerifier || "",
});

if (state.clientSecret) {
tokenRequestBody.set("client_secret", state.clientSecret);
}

if (state.serverUrl) {
tokenRequestBody.set("resource", state.serverUrl);
}

const response = await proxyFetch(
state.authorizationServerMetadata.token_endpoint,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: tokenRequestBody.toString(),
},
);

const tokenResponseData = {
status: response.status,
statusText: response.statusText,
headers: response.headers,
body: response.body,
};

const updatedHistoryToken = [...(state.httpHistory || [])];
if (updatedHistoryToken.length > 0) {
const lastEntry =
updatedHistoryToken[updatedHistoryToken.length - 1];
lastEntry.response = tokenResponseData;
lastEntry.duration =
Date.now() - (lastEntry.timestamp || Date.now());
}

if (!response.ok) {
updateState({
lastResponse: tokenResponseData,
httpHistory: updatedHistoryToken,
authorizationCode: undefined,
error: `Token request failed: ${response.body?.error || response.statusText} - ${response.body?.error_description || "Unknown error"}`,
isInitiatingAuth: false,
});
return;
}

const tokens = response.body;

const existingLogs = (getCurrentState().infoLogs || []).filter(
(log) =>
log.id !== "auth-code" &&
log.id !== "oauth-tokens" &&
log.id !== "token",
);

let tokenInfoLogs = existingLogs;

if (state.authorizationCode) {
tokenInfoLogs = [
...tokenInfoLogs,
{
id: "auth-code",
label: "Authorization Code",
data: {
code: state.authorizationCode,
},
timestamp: Date.now(),
},
];
}

if (tokens.access_token) {
const tokenData: Record<string, any> = {
access_token: tokens.access_token,
};

if (tokens.refresh_token) {
tokenData.refresh_token = tokens.refresh_token;
}

tokenInfoLogs = [
...tokenInfoLogs,
{
id: "oauth-tokens",
label: "OAuth Tokens",
data: tokenData,
timestamp: Date.now(),
},
];
}

if (tokens.access_token) {
const decoded = decodeJWT(tokens.access_token);
if (decoded) {
const formatted = { ...decoded };
if (formatted.exp) {
formatted.exp = `${formatted.exp} (${formatJWTTimestamp(formatted.exp)})`;
}
if (formatted.iat) {
formatted.iat = `${formatted.iat} (${formatJWTTimestamp(formatted.iat)})`;
}
if (formatted.nbf) {
formatted.nbf = `${formatted.nbf} (${formatJWTTimestamp(formatted.nbf)})`;
}

const audienceNote: Record<string, any> = {
...formatted,
};

if (formatted.aud) {
const expectedResource = state.serverUrl;
const audArray = Array.isArray(formatted.aud)
? formatted.aud
: [formatted.aud];

const isValidAudience = audArray.some(
(aud: string) => aud === expectedResource,
);

audienceNote._validation = {
expected_audience: expectedResource,
audience_matches: isValidAudience,
note: isValidAudience
? "✓ Token audience matches MCP server"
: "⚠ Token audience does not match MCP server (security risk)",
};
} else {
audienceNote._validation = {
note: "⚠ No audience claim found - server should validate token binding",
};
}

tokenInfoLogs = [
...tokenInfoLogs,
{
id: "token",
label: "Access Token (Decoded JWT)",
data: audienceNote,
timestamp: Date.now(),
},
];
}
}

if (tokens.id_token) {
const decodedIdToken = decodeJWT(tokens.id_token);
if (decodedIdToken) {
const formattedIdToken = { ...decodedIdToken };
if (formattedIdToken.exp) {
formattedIdToken.exp = `${formattedIdToken.exp} (${formatJWTTimestamp(formattedIdToken.exp)})`;
}
if (formattedIdToken.iat) {
formattedIdToken.iat = `${formattedIdToken.iat} (${formatJWTTimestamp(formattedIdToken.iat)})`;
}
if (formattedIdToken.nbf) {
formattedIdToken.nbf = `${formattedIdToken.nbf} (${formatJWTTimestamp(formattedIdToken.nbf)})`;
}
if (formattedIdToken.auth_time) {
formattedIdToken.auth_time = `${formattedIdToken.auth_time} (${formatJWTTimestamp(formattedIdToken.auth_time)})`;
}

formattedIdToken._note =
"OIDC ID Token - Used for user identity verification";

tokenInfoLogs = [
...tokenInfoLogs,
{
id: "id-token",
label: "ID Token (OIDC - Decoded JWT)",
data: formattedIdToken,
timestamp: Date.now(),
},
];
}
}

updateState({
currentStep: "received_access_token",
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
tokenType: tokens.token_type || "Bearer",
expiresIn: tokens.expires_in,
lastResponse: tokenResponseData,
httpHistory: updatedHistoryToken,
infoLogs: tokenInfoLogs,
error: undefined,
isInitiatingAuth: false,
});
} catch (error) {
const errorResponse = {
status: 0,
statusText: "Network Error",
headers: mergeHeaders({}),
body: {
error: error instanceof Error ? error.message : String(error),
},
};

const updatedHistoryError = [...(state.httpHistory || [])];
if (updatedHistoryError.length > 0) {
const lastEntry =
updatedHistoryError[updatedHistoryError.length - 1];
lastEntry.response = errorResponse;
lastEntry.duration =
Date.now() - (lastEntry.timestamp || Date.now());
}

updateState({
lastResponse: errorResponse,
httpHistory: updatedHistoryError,
error: `Token exchange failed: ${error instanceof Error ? error.message : String(error)}`,
isInitiatingAuth: false,
});
}
break;

case "received_access_token":
// Step 12: Make authenticated MCP request
if (!state.serverUrl || !state.accessToken) {
throw new Error("Missing server URL or access token");
}

const authenticatedRequest = {
method: "POST",
url: state.serverUrl,
headers: {
Authorization: `Bearer ${state.accessToken}`,
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
},
body: {
jsonrpc: "2.0",
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: {
name: "MCP Inspector",
version: "1.0.0",
},
},
id: 2,
},
};

const authenticatedRequestInfoLogs = addInfoLog(
getCurrentState(),
"authenticated-init",
"Authenticated MCP Initialize Request",
{
Request: "MCP initialize with OAuth bearer token",
"Protocol Version": "2025-03-26",
Client: "MCP Inspector v1.0.0",
Endpoint: state.serverUrl,
},
);

updateState({
currentStep: "authenticated_mcp_request",
lastRequest: authenticatedRequest,
lastResponse: undefined,
httpHistory: [
...(state.httpHistory || []),
{
step: "authenticated_mcp_request",
timestamp: Date.now(),
request: authenticatedRequest,
},
],
infoLogs: authenticatedRequestInfoLogs,
isInitiatingAuth: false,
});

setTimeout(() => machine.proceedToNextStep(), 50);
return;

case "authenticated_mcp_request":
// Step 13: Make actual authenticated request
if (!state.serverUrl || !state.accessToken) {
throw new Error("Missing server URL or access token");
}

try {
const response = await proxyFetch(state.serverUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${state.accessToken}`,
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: {
name: "MCP Inspector",
version: "1.0.0",
},
},
id: 2,
}),
});

const mcpResponseData = {
status: response.status,
statusText: response.statusText,
headers: response.headers,
body: response.body,
};

const updatedHistoryMcp = [...(state.httpHistory || [])];
if (updatedHistoryMcp.length > 0) {
const lastEntry =
updatedHistoryMcp[updatedHistoryMcp.length - 1];
lastEntry.response = mcpResponseData;
lastEntry.duration =
Date.now() - (lastEntry.timestamp || Date.now());
}

if (!response.ok) {
// Backwards compatibility: Check if this is using old HTTP+SSE transport
// Per spec: "If it fails with an HTTP 4xx status code (e.g., 405 Method Not Allowed or 404 Not Found):
// Issue a GET request to the server URL, expecting that this will open an SSE stream and return an endpoint event"
if (response.status >= 400 && response.status < 500) {
try {
// Add GET request to history
const getRequest = {
method: "GET",
url: state.serverUrl,
headers: {
Authorization: `Bearer ${state.accessToken}`,
Accept: "text/event-stream",
},
};

const getHistoryEntry = {
step: "http_sse_fallback",
timestamp: Date.now(),
request: getRequest,
};

updatedHistoryMcp.push(getHistoryEntry);

// Add info log for backwards compatibility attempt
let fallbackInfoLogs = addInfoLog(
getCurrentState(),
"http-sse-fallback-attempt",
"Backwards Compatibility Check",
{
Reason: `POST failed with ${response.status}, checking for old HTTP+SSE transport`,
"GET Request": state.serverUrl,
Note: "Attempting to detect SSE stream with 'endpoint' event",
},
);

updateState({
infoLogs: fallbackInfoLogs,
httpHistory: updatedHistoryMcp,
});

const getResponse = await proxyFetch(state.serverUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${state.accessToken}`,
Accept: "text/event-stream",
},
});

// Update history with response
getHistoryEntry.response = {
status: getResponse.status,
statusText: getResponse.statusText,
headers: getResponse.headers,
body: getResponse.body,
};
getHistoryEntry.duration =
Date.now() - getHistoryEntry.timestamp;

// Check if we got an SSE stream with an endpoint event
const sseBody = getResponse.body;

// Handle structured SSE response from debug proxy
if (
sseBody &&
typeof sseBody === "object" &&
sseBody.isOldTransport &&
sseBody.endpoint
) {
const endpoint = sseBody.endpoint;
const fullEndpoint = new URL(
endpoint,
state.serverUrl,
).toString();

const httpSseInfoLogs = addInfoLog(
getCurrentState(),
"http-sse-detected",
"HTTP+SSE Transport Detected (2024-11-05)",
{
Transport: "HTTP+SSE (2024-11-05 - Deprecated)",
"SSE Stream URL": state.serverUrl,
"POST Endpoint": fullEndpoint,
"First Event": sseBody.events?.[0],
Note: "Server uses the old HTTP+SSE transport. All subsequent MCP requests should be POSTed to the endpoint above.",
},
);

updateState({
currentStep: "complete",
lastResponse: {
status: getResponse.status,
statusText: "OK - HTTP+SSE Transport",
headers: getResponse.headers,
body: {
transport: "HTTP+SSE (2024-11-05)",
sseStreamUrl: state.serverUrl,
postEndpoint: fullEndpoint,
events: sseBody.events,
},
},
httpHistory: updatedHistoryMcp,
infoLogs: httpSseInfoLogs,
error: undefined,
isInitiatingAuth: false,
});
return;
}
} catch (getError) {
// If GET also fails, fall through to original error
console.error("GET fallback failed:", getError);
}
}

updateState({
lastResponse: mcpResponseData,
httpHistory: updatedHistoryMcp,
error: `Authenticated request failed: ${response.status} ${response.statusText}`,
isInitiatingAuth: false,
});
return;
}

let mcpInfoLogs = getCurrentState().infoLogs || [];

const contentType = response.headers["content-type"] || "";
const isSSE = contentType.includes("text/event-stream");

let mcpResponse = null;
// Handle structured SSE response from debug proxy
if (isSSE && response.body?.transport === "sse") {
mcpResponse = response.body.mcpResponse;
} else {
mcpResponse = response.body;
}

if (isSSE && mcpResponse?.result?.protocolVersion) {
const protocolInfo: Record<string, any> = {
Transport: "Streamable HTTP",
"Response Format": "Server-Sent Events (streaming)",
"Protocol Version": mcpResponse.result.protocolVersion,
};

if (mcpResponse.result.serverInfo) {
protocolInfo["Server Name"] =
mcpResponse.result.serverInfo.name;
protocolInfo["Server Version"] =
mcpResponse.result.serverInfo.version;
}

if (mcpResponse.result.capabilities) {
protocolInfo["Capabilities"] =
mcpResponse.result.capabilities;
}

mcpInfoLogs = addInfoLog(
getCurrentState(),
"mcp-protocol",
"MCP Server Information",
protocolInfo,
);
} else if (isSSE) {
mcpInfoLogs = addInfoLog(
getCurrentState(),
"mcp-transport",
"MCP Transport Detected",
{
Transport: "Streamable HTTP",
"Response Format": "Server-Sent Events (streaming)",
"Content-Type": contentType,
Note: "Server returned streaming response. Initialize response delivered via SSE stream.",
Events: response.body?.events
? `${response.body.events.length} events parsed`
: "No events parsed",
},
);
} else if (mcpResponse?.result?.protocolVersion) {
const protocolInfo: Record<string, any> = {
Transport: "Streamable HTTP",
"Response Format": "JSON",
"Protocol Version": mcpResponse.result.protocolVersion,
};

if (mcpResponse.result.serverInfo) {
protocolInfo["Server Name"] =
mcpResponse.result.serverInfo.name;
protocolInfo["Server Version"] =
mcpResponse.result.serverInfo.version;
}

if (mcpResponse.result.capabilities) {
protocolInfo["Capabilities"] =
mcpResponse.result.capabilities;
}

mcpInfoLogs = addInfoLog(
getCurrentState(),
"mcp-protocol",
"MCP Server Information",
protocolInfo,
);
}

updateState({
currentStep: "complete",
lastResponse: mcpResponseData,
httpHistory: updatedHistoryMcp,
infoLogs: mcpInfoLogs,
error: undefined,
isInitiatingAuth: false,
});
} catch (error) {
const errorResponse = {
status: 0,
statusText: "Network Error",
headers: mergeHeaders({}),
body: {
error: error instanceof Error ? error.message : String(error),
},
};

const updatedHistoryError = [...(state.httpHistory || [])];
if (updatedHistoryError.length > 0) {
const lastEntry =
updatedHistoryError[updatedHistoryError.length - 1];
lastEntry.response = errorResponse;
lastEntry.duration =
Date.now() - (lastEntry.timestamp || Date.now());
}

updateState({
lastResponse: errorResponse,
httpHistory: updatedHistoryError,
error: `Authenticated MCP request failed: ${error instanceof Error ? error.message : String(error)}`,
isInitiatingAuth: false,
});
}
break;

case "complete":
updateState({ isInitiatingAuth: false });
break;

default:
updateState({ isInitiatingAuth: false });
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Wrap switch case variable declarations in blocks.

Variables declared in switch cases can erroneously be accessed by other cases. Wrap declarations in block statements to restrict scope.

As per coding guidelines

Example fix for case "discovery_start":

           case "discovery_start":
+            {
             // Step 3: Start authorization server metadata discovery
             const authBaseUrl = buildAuthorizationBaseUrl(state.serverUrl!);
             const authServerUrls = buildAuthServerMetadataUrls(authBaseUrl);
             
             // ... rest of case logic ...
             
             setTimeout(() => machine.proceedToNextStep(), 50);
             return;
+            }

Apply similar wrapping to all other cases with variable declarations. The static analysis tool identified 22 instances across the switch statement.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (2.1.2)

[error] 319-319: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 320-320: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 322-326: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 353-355: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 356-356: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 357-357: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 358-358: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 359-359: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 468-473: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 475-475: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 485-486: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 488-493: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 515-520: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 777-777: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 778-778: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 780-790: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 812-814: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 850-857: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 898-902: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 920-927: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 1196-1219: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 1219-1231: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts around lines 222
to 1541: the switch contains many case branches that declare variables
(let/const/var) which leak scope into other cases; wrap each case body that
declares variables in its own block { ... } so declared variables are scoped to
that case (preserve existing control flow by keeping returns/breaks inside the
new block), apply this to all ~22 flagged instances, and run linter/static
analysis to confirm no remaining cross-case scope issues.

Comment on lines +252 to +259
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: {
name: "MCP Inspector",
version: "1.0.0",
},
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update client name to match rebranding.

The client info uses "MCP Inspector" but should be "MCPJam Inspector" per the rebranding mentioned in the AI summary.

Apply this diff:

                     clientInfo: {
-                      name: "MCP Inspector",
+                      name: "MCPJam Inspector",
                       version: "1.0.0",
                     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: {
name: "MCP Inspector",
version: "1.0.0",
},
},
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: {
name: "MCPJam Inspector",
version: "1.0.0",
},
},
🤖 Prompt for AI Agents
In client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts around lines
252 to 259, update the clientInfo.name value to reflect the rebranding: replace
"MCP Inspector" with "MCPJam Inspector" so the client identifies correctly;
ensure only the name string is changed and surrounding object structure and
other fields (version, protocolVersion, capabilities) remain untouched.

Comment on lines +114 to 157
const { done, value } = await Promise.race([
reader.read(),
new Promise<{ done: boolean; value: undefined }>((_, reject) =>
setTimeout(() => reject(new Error("Read timeout")), 1000),
),
]);

if (done) break;

buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
// Keep the last incomplete line in the buffer
buffer = lines.pop() || "";

for (const line of lines) {
if (line.startsWith("event:")) {
currentEvent.event = line.substring(6).trim();
} else if (line.startsWith("data:")) {
const data = line.substring(5).trim();
try {
currentEvent.data = JSON.parse(data);
} catch {
currentEvent.data = data;
}
} else if (line.startsWith("id:")) {
currentEvent.id = line.substring(3).trim();
} else if (line === "") {
// Empty line indicates end of event
if (Object.keys(currentEvent).length > 0) {
events.push({ ...currentEvent });
currentEvent = {};

// For backwards compatibility detection: check if first event is "endpoint"
if (events.length >= 1) {
// Got at least one event, that's enough for detection
break;
}
}
}
}

for (const line of lines) {
if (line.startsWith("event:")) {
currentEvent.event = line.substring(6).trim();
} else if (line.startsWith("data:")) {
const data = line.substring(5).trim();
try {
currentEvent.data = JSON.parse(data);
} catch {
currentEvent.data = data;
// Exit if we have at least one complete event
if (events.length >= 1) break;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix SSE timeout regression

Promise.race now rejects after a one‑second lull, so we throw, hit the catch block, and surface “Failed to parse SSE stream.” Real MCP servers frequently take longer than a second to deliver the first SSE frame, so every such response is now misreported as an error and the debugger never sees any events. Please make the timeout non‑fatal (e.g., resolve to a sentinel and keep waiting until maxReadTime elapses) so we only bail out on actual failures.

-              const { done, value } = await Promise.race([
-                reader.read(),
-                new Promise<{ done: boolean; value: undefined }>((_, reject) =>
-                  setTimeout(() => reject(new Error("Read timeout")), 1000),
-                ),
-              ]);
-
-              if (done) break;
+              const readResult = await Promise.race<
+                ReadableStreamReadResult<Uint8Array> | null
+              >([
+                reader.read(),
+                new Promise<null>((resolve) => setTimeout(() => resolve(null), 1000)),
+              ]);
+
+              if (readResult === null) {
+                if (Date.now() - startTime >= maxReadTime) {
+                  break;
+                }
+                continue;
+              }
+
+              const { done, value } = readResult;
+
+              if (done) break;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { done, value } = await Promise.race([
reader.read(),
new Promise<{ done: boolean; value: undefined }>((_, reject) =>
setTimeout(() => reject(new Error("Read timeout")), 1000),
),
]);
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
// Keep the last incomplete line in the buffer
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("event:")) {
currentEvent.event = line.substring(6).trim();
} else if (line.startsWith("data:")) {
const data = line.substring(5).trim();
try {
currentEvent.data = JSON.parse(data);
} catch {
currentEvent.data = data;
}
} else if (line.startsWith("id:")) {
currentEvent.id = line.substring(3).trim();
} else if (line === "") {
// Empty line indicates end of event
if (Object.keys(currentEvent).length > 0) {
events.push({ ...currentEvent });
currentEvent = {};
// For backwards compatibility detection: check if first event is "endpoint"
if (events.length >= 1) {
// Got at least one event, that's enough for detection
break;
}
}
}
}
for (const line of lines) {
if (line.startsWith("event:")) {
currentEvent.event = line.substring(6).trim();
} else if (line.startsWith("data:")) {
const data = line.substring(5).trim();
try {
currentEvent.data = JSON.parse(data);
} catch {
currentEvent.data = data;
// Exit if we have at least one complete event
if (events.length >= 1) break;
}
const readResult = await Promise.race<
ReadableStreamReadResult<Uint8Array> | null
>([
reader.read(),
new Promise<null>((resolve) => setTimeout(() => resolve(null), 1000)),
]);
if (readResult === null) {
if (Date.now() - startTime >= maxReadTime) {
break;
}
continue;
}
const { done, value } = readResult;
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
// Keep the last incomplete line in the buffer
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("event:")) {
currentEvent.event = line.substring(6).trim();
} else if (line.startsWith("data:")) {
const data = line.substring(5).trim();
try {
currentEvent.data = JSON.parse(data);
} catch {
currentEvent.data = data;
}
} else if (line.startsWith("id:")) {
currentEvent.id = line.substring(3).trim();
} else if (line === "") {
// Empty line indicates end of event
if (Object.keys(currentEvent).length > 0) {
events.push({ ...currentEvent });
currentEvent = {};
// For backwards compatibility detection: check if first event is "endpoint"
if (events.length >= 1) {
// Got at least one event, that's enough for detection
break;
}
}
}
}
// Exit if we have at least one complete event
if (events.length >= 1) break;
}
🤖 Prompt for AI Agents
In server/routes/mcp/oauth.ts around lines 114 to 157, the Promise.race timeout
currently rejects after 1s which throws and aborts SSE parsing; change the
timeout to resolve to a sentinel (e.g., { done: false, value: undefined,
timedOut: true }) instead of rejecting, adjust the surrounding logic to detect
that sentinel and simply continue the read loop (not push an error) until either
a real read arrives or the existing maxReadTime is exceeded, and ensure any
timer is cleared/ignored so only an actual read.done true or the maxReadTime
causes exiting.

@chelojimenez chelojimenez force-pushed the add-june-spec-support branch from b34a3e2 to 1607050 Compare November 5, 2025 01:08
body: getResponse.body,
};
getHistoryEntry.duration =
Date.now() - getHistoryEntry.timestamp;
Copy link

Choose a reason for hiding this comment

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

Bug: Type Mismatch in Mutated History Entry

The getHistoryEntry object is mutated after being pushed to updatedHistoryMcp array. This creates a reference that is modified later, which works correctly. However, the getHistoryEntry variable is declared with const but has a type that doesn't match the expected httpHistory entry type. The entry is missing the step property type which should be OAuthFlowStep, but "http_sse_fallback" is not a valid OAuthFlowStep value according to the types defined in types.ts. This will cause a type mismatch when the history is processed, as the step value won't match any of the expected OAuth flow steps.

Fix in Cursor Fix in Web

@chelojimenez chelojimenez merged commit 3c115cf into main Nov 5, 2025
3 checks passed
khandrew1 pushed a commit to khandrew1/inspector that referenced this pull request Nov 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants