Skip to content

[Security follow-up C-3] Replace MCPPairingPanel imperative flow with state machineΒ #44

@hoainho

Description

@hoainho

🌟 Before claiming this issue

Two quick steps before you open a PR:

  1. ⭐ Star the repository β€” low-friction signal that you'll follow through
  2. πŸ’¬ Comment I'll take this (or similar) below β€” prevents two contributors racing on the same issue

Full policy: CONTRIBUTING.md β†’ How to claim. If a claim is older than 7 days with no PR, you can reclaim it politely.


Tracks follow-up #3 from PR #17 security review. Blocked on mcp-server-v1 M3 (~Jul 28, 2026).

What

The MCPPairingPanel handshake currently flows in a loose imperative sequence (enter token β†’ submit β†’ spinner β†’ success-or-error). Replace this with an explicit state machine so invalid transitions are unreachable.

Why

The current code has implicit states (idle, validating, paired, error) tracked across multiple useState hooks. This creates classes of bugs that are hard to catch:

  • User clicks Submit during the spinner β†’ request fires twice
  • Race between "paired" success and a subsequent re-validate
  • Error state can be reached without going through validating
  • Reset button works in some states but not others

An explicit state machine with 'idle' | 'validating' | 'paired' | 'error' makes invalid transitions a TypeScript error, not a runtime bug.

Acceptance criteria

  • MCPPairingPanel.tsx has a single state variable pairingState: PairingState (the union above) instead of multiple booleans
  • Transitions go through a reducer or useReducer so each event has a documented effect per state
  • Unit test: clicking Submit while in validating state is a no-op (no duplicate requests)
  • Unit test: reaching paired from error requires going through validating first

Implementation hint

type PairingState =
  | { kind: 'idle' }
  | { kind: 'validating'; token: string }
  | { kind: 'paired'; token: string; pairedAt: number }
  | { kind: 'error'; reason: 'invalid_format' | 'network' | 'rejected'; token: string };

type PairingEvent =
  | { type: 'submit'; token: string }
  | { type: 'validation_succeeded'; token: string }
  | { type: 'validation_failed'; reason: PairingState extends { kind: 'error' } ? ... : never }
  | { type: 'reset' };

function pairingReducer(state: PairingState, event: PairingEvent): PairingState {
  // Idle β†’ validating on submit; everything else from idle is rejected
  if (state.kind === 'idle' && event.type === 'submit') {
    return { kind: 'validating', token: event.token };
  }
  // Validating β†’ paired on success
  if (state.kind === 'validating' && event.type === 'validation_succeeded') {
    return { kind: 'paired', token: event.token, pairedAt: Date.now() };
  }
  // etc...
  return state; // invalid transition: no-op
}

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is neededsecuritySecurity-related (see SECURITY.md for vulnerabilities)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions