Ctrl K
ComunidadGuías…

src/mcp_oauth/transition.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { sameInvocationRef } from "@/state_machines/invocation_ref";
import {
  change,
  ignore as ignoreTransition,
  type IgnoreReason as SharedIgnoreReason,
  type TransitionResult,
} from "@/state_machines/types";
import type {
  McpOAuthEvent,
  McpOAuthFlowIdentity,
  McpOAuthState,
} from "./state";

export type IgnoreReason = SharedIgnoreReason<
  | "stale-invocation"
  | "state-mismatch"
  | "invalid-in-current-state"
  | "no-active-flow"
  | "duplicate-connect"
>;

export type McpOAuthTransitionResult = TransitionResult<
  McpOAuthState,
  never,
  IgnoreReason
>;

function eventIdentity(
  event: Extract<McpOAuthEvent, { type: "CONNECT" }>,
): McpOAuthFlowIdentity {
  return {
    invocationRef: event.invocationRef,
    rendererMessageId: event.rendererMessageId,
    expectedState: event.expectedState,
    serverId: event.serverId,
  };
}

function hasCurrentInvocation(
  state: McpOAuthState,
  event: Exclude<McpOAuthEvent, { type: "CONNECT" }>,
): boolean {
  if (state.status === "idle") return false;
  const active =
    state.status === "superseding"
      ? state.closing.invocationRef
      : state.invocationRef;
  return sameInvocationRef(active, event.invocationRef);
}

export function transition(
  state: McpOAuthState,
  event: McpOAuthEvent,
): McpOAuthTransitionResult {
  if (event.type === "CONNECT") {
    const next = eventIdentity(event);
    switch (state.status) {
      case "idle":
      case "connected":
      case "failed":
      case "timedOut":
        return change({ status: "binding", ...next });
      case "superseding":
        if (sameInvocationRef(state.next.invocationRef, next.invocationRef)) {
          return ignoreTransition(state, "duplicate-connect");
        }
        return change({ ...state, next });
      case "binding":
      case "awaitingCallback":
      case "exchanging":
        return change({
          status: "superseding",
          closing: {
            invocationRef: state.invocationRef,
            rendererMessageId: state.rendererMessageId,
            expectedState: state.expectedState,
            serverId: state.serverId,
          },
          next,
        });
      default:
        return unreachable(state);
    }
  }

  if (state.status === "idle") {
    return ignoreTransition(state, "no-active-flow");
  }
  if (!hasCurrentInvocation(state, event)) {
    return ignoreTransition(state, "stale-invocation");
  }

  switch (event.type) {
    case "SOCKETS_CLOSED":
      return state.status === "superseding"
        ? change({ status: "binding", ...state.next })
        : ignoreTransition(state, "invalid-in-current-state");
    case "BINDS_SETTLED":
      if (state.status !== "binding") {
        return ignoreTransition(state, "invalid-in-current-state");
      }
      if (event.anyInUse) {
        return change({
          ...state,
          status: "failed",
          message: "callback-port-in-use",
        });
      }
      if (event.boundHosts.length === 0) {
        return change({
          ...state,
          status: "failed",
          message: "callback-bind-failed",
        });
      }
      return change({ ...state, status: "awaitingCallback" });
    case "AUTHORIZED_SILENTLY":
      return state.status === "awaitingCallback"
        ? change({ ...state, status: "connected" })
        : ignoreTransition(state, "invalid-in-current-state");
    case "CALLBACK":
      if (state.status !== "awaitingCallback") {
        return ignoreTransition(state, "invalid-in-current-state");
      }
      if (event.state !== state.expectedState) {
        return ignoreTransition(state, "state-mismatch");
      }
      return event.code
        ? change({ ...state, status: "exchanging" })
        : change({
            ...state,
            status: "failed",
            message: `OAuth callback error: ${event.error ?? "missing code"}`,
          });
    case "TIMEOUT":
      return state.status === "binding" || state.status === "awaitingCallback"
        ? change({ ...state, status: "timedOut" })
        : ignoreTransition(state, "invalid-in-current-state");
    case "EXCHANGE_OK":
      return state.status === "exchanging"
        ? change({ ...state, status: "connected" })
        : ignoreTransition(state, "invalid-in-current-state");
    case "EXCHANGE_FAILED":
      return state.status === "binding" ||
        state.status === "awaitingCallback" ||
        state.status === "exchanging"
        ? change({ ...state, status: "failed", message: event.message })
        : ignoreTransition(state, "invalid-in-current-state");
    default: {
      const exhaustive: never = event;
      return exhaustive;
    }
  }
}

function unreachable(state: never): never {
  throw new Error(`Unreachable MCP OAuth state: ${JSON.stringify(state)}`);
}