summaryrefslogtreecommitdiffhomepage
path: root/src/features/mcp/logic/view-model.ts
blob: fdff78a2098d6b75271af9bad7273ada53c80adf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import type { McpServerInfo, McpServerState } from "@dispatch/transport-contract";

/**
 * Pure core for the mcp feature — zero DOM, zero effects, zero Svelte.
 *
 * The mcp feature exposes the live status of the MCP (Model Context Protocol)
 * servers configured for a conversation's working directory, fetched from
 * `GET /conversations/:id/mcp`. This module holds the pure logic: the mapping
 * of a backend `McpServerState` to a display badge + label, and a one-line
 * server summary. The effect (the HTTP get MCP status) is INJECTED via the
 * `LoadMcpStatus` port below; the composition root implements it.
 */

// ── Injected port (consumer-defines-port; the composition root adapts the
//    store's HTTP call to this shape). ──────────────────────────────────────────

/** Outcome of `GET /conversations/:id/mcp`; `null` when no real conversation is focused. */
export type McpStatusResult =
  | { readonly ok: true; readonly cwd: string | null; readonly servers: readonly McpServerInfo[] }
  | { readonly ok: false; readonly error: string };

export type LoadMcpStatus = () => Promise<McpStatusResult | null>;

// ── MCP server status → display view ───────────────────────────────────────────

export type Badge = "success" | "warning" | "error" | "neutral";

export interface McpServerView {
  readonly id: string;
  readonly state: McpServerState;
  readonly statusLabel: string;
  readonly badge: Badge;
  /** True while the state is transient (show a spinner). */
  readonly busy: boolean;
  /** The error reason when `state === "error"`, else null. */
  readonly error: string | null;
  /** Number of tools discovered from this server. */
  readonly toolCount: number;
  /** Which config source the server was resolved from, else null. */
  readonly configSource: string | null;
}

/**
 * Map a server's state to a display label + badge severity + busy flag. Mirrors
 * the LSP status visual treatment: `connected` → success, `connecting` (the
 * transient state, analogous to LSP's `starting`) → warning + spinner, `error`
 * → error, and `disconnected` (a stable idle state) → neutral.
 */
export function viewMcpServer(server: McpServerInfo): McpServerView {
  let statusLabel: string;
  let badge: Badge;
  let busy = false;
  switch (server.state) {
    case "connected":
      statusLabel = "Connected";
      badge = "success";
      break;
    case "connecting":
      statusLabel = "Connecting…";
      badge = "warning";
      busy = true;
      break;
    case "disconnected":
      statusLabel = "Disconnected";
      badge = "neutral";
      break;
    case "error":
      statusLabel = "Error";
      badge = "error";
      break;
  }
  return {
    id: server.id,
    state: server.state,
    statusLabel,
    badge,
    busy,
    error: server.state === "error" ? (server.error ?? "Failed to connect") : null,
    toolCount: server.toolCount,
    configSource: server.configSource ?? null,
  };
}

export function viewMcpServers(servers: readonly McpServerInfo[]): readonly McpServerView[] {
  return servers.map(viewMcpServer);
}

/**
 * A short one-line summary, e.g. "2 connected" / "1 connected, 1 connecting,
 * 1 error". Only non-zero buckets are listed.
 */
export function summarizeMcpServers(servers: readonly McpServerInfo[]): string {
  if (servers.length === 0) return "No MCP servers";
  let connected = 0;
  let connecting = 0;
  let disconnected = 0;
  let errored = 0;
  for (const s of servers) {
    if (s.state === "connected") connected++;
    else if (s.state === "error") errored++;
    else if (s.state === "connecting") connecting++;
    else disconnected++;
  }
  const parts: string[] = [];
  if (connected > 0) parts.push(`${connected} connected`);
  if (connecting > 0) parts.push(`${connecting} connecting`);
  if (disconnected > 0) parts.push(`${disconnected} disconnected`);
  if (errored > 0) parts.push(`${errored} error${errored === 1 ? "" : "s"}`);
  return parts.join(", ");
}