claude-code-rust 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
5
5
  [![npm version](https://img.shields.io/npm/v/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
7
7
  [![CI](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml)
8
- [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
8
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
9
9
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](https://nodejs.org/)
10
10
 
11
11
  ## About
@@ -59,20 +59,14 @@ Three-layer design:
59
59
 
60
60
  **Agent Runtime** (Anthropic Agent SDK) - The TypeScript bridge drives `@anthropic-ai/claude-agent-sdk`, which manages authentication, session/query lifecycle, and tool execution.
61
61
 
62
- ## Known Limitations
63
-
64
- - The config view includes the Settings tab but the Status, Usage, and MCP tabs are not yet implemented.
65
-
66
62
  ## Status
67
63
 
68
64
  This project is pre-1.0 and under active development. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to get involved.
69
65
 
70
66
  ## License
71
67
 
72
- This project is licensed under the [GNU Affero General Public License v3.0 or later](LICENSE).
73
- This license was chosen because Claude Code is not open-source and this license allows everyone to use it while stopping Anthropic from implementing it in their closed-source version.
74
-
75
- By using this software, you agree to the terms of the AGPL-3.0. If you modify this software and make it available over a network, you must offer the source code to users of that service.
68
+ This project is licensed under the [Apache License 2.0](LICENSE).
69
+ Apache-2.0 was chosen to keep usage and redistribution straightforward for individual users, downstream packagers, and commercial adopters.
76
70
 
77
71
  ## Disclaimer
78
72
 
@@ -10,3 +10,8 @@ npm run build
10
10
  ```
11
11
 
12
12
  Build output is written to `dist/bridge.mjs`.
13
+
14
+ ## License
15
+
16
+ This bridge is part of the `claude-code-rust` project and is licensed under
17
+ the Apache License 2.0. See the repository root [LICENSE](../LICENSE).
@@ -48,38 +48,43 @@ function optionalLaunchSettings(record, key, context) {
48
48
  return {};
49
49
  }
50
50
  const parsed = asRecord(value, `${context}.${key}`);
51
- const model = optionalString(parsed, "model", `${context}.${key}`);
52
51
  const language = optionalString(parsed, "language", `${context}.${key}`);
53
- const permissionMode = optionalString(parsed, "permission_mode", `${context}.${key}`);
54
- const thinkingMode = optionalThinkingMode(parsed, "thinking_mode", `${context}.${key}`);
55
- const effortLevel = optionalEffortLevel(parsed, "effort_level", `${context}.${key}`);
52
+ const settings = optionalJsonObject(parsed, "settings", `${context}.${key}`);
53
+ const agentProgressSummaries = optionalBoolean(parsed, "agent_progress_summaries", `${context}.${key}`);
56
54
  return {
57
- ...(model ? { model } : {}),
58
55
  ...(language ? { language } : {}),
59
- ...(permissionMode ? { permission_mode: permissionMode } : {}),
60
- ...(thinkingMode ? { thinking_mode: thinkingMode } : {}),
61
- ...(effortLevel ? { effort_level: effortLevel } : {}),
56
+ ...(settings ? { settings } : {}),
57
+ ...(agentProgressSummaries !== undefined
58
+ ? { agent_progress_summaries: agentProgressSummaries }
59
+ : {}),
62
60
  };
63
61
  }
64
- function optionalThinkingMode(record, key, context) {
65
- const value = optionalString(record, key, context);
66
- if (value === undefined) {
62
+ function optionalBoolean(record, key, context) {
63
+ const value = record[key];
64
+ if (value === undefined || value === null) {
67
65
  return undefined;
68
66
  }
69
- if (value === "adaptive" || value === "disabled") {
70
- return value;
67
+ if (typeof value !== "boolean") {
68
+ throw new Error(`${context}.${key} must be a boolean when provided`);
71
69
  }
72
- throw new Error(`${context}.${key} must be "adaptive" or "disabled" when provided`);
70
+ return value;
73
71
  }
74
- function optionalEffortLevel(record, key, context) {
75
- const value = optionalString(record, key, context);
76
- if (value === undefined) {
72
+ function optionalJsonObject(record, key, context) {
73
+ const value = record[key];
74
+ if (value === undefined || value === null) {
77
75
  return undefined;
78
76
  }
79
- if (value === "low" || value === "medium" || value === "high") {
77
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
78
+ throw new Error(`${context}.${key} must be an object when provided`);
79
+ }
80
+ return value;
81
+ }
82
+ function expectElicitationAction(record, key, context) {
83
+ const value = expectString(record, key, context);
84
+ if (value === "accept" || value === "decline" || value === "cancel") {
80
85
  return value;
81
86
  }
82
- throw new Error(`${context}.${key} must be "low", "medium", or "high" when provided`);
87
+ throw new Error(`${context}.${key} must be one of accept, decline, cancel`);
83
88
  }
84
89
  function parsePromptChunks(record, context) {
85
90
  const rawChunks = record.chunks;
@@ -92,6 +97,41 @@ function parsePromptChunks(record, context) {
92
97
  return { kind, value: (parsed.value ?? null) };
93
98
  });
94
99
  }
100
+ function expectBoolean(record, key, context) {
101
+ const value = record[key];
102
+ if (typeof value !== "boolean") {
103
+ throw new Error(`${context}.${key} must be a boolean`);
104
+ }
105
+ return value;
106
+ }
107
+ function parseMcpServerConfig(value, context) {
108
+ const record = asRecord(value, context);
109
+ const type = expectString(record, "type", context);
110
+ switch (type) {
111
+ case "stdio":
112
+ return {
113
+ type,
114
+ command: expectString(record, "command", context),
115
+ ...(record.args === undefined ? {} : { args: expectStringArray(record, "args", context) }),
116
+ ...(record.env === undefined ? {} : { env: expectStringMap(record, "env", context) }),
117
+ };
118
+ case "sse":
119
+ case "http":
120
+ return {
121
+ type,
122
+ url: expectString(record, "url", context),
123
+ ...(record.headers === undefined
124
+ ? {}
125
+ : { headers: expectStringMap(record, "headers", context) }),
126
+ };
127
+ default:
128
+ throw new Error(`${context}.type must be one of stdio, sse, http`);
129
+ }
130
+ }
131
+ function parseMcpServersRecord(value, context) {
132
+ const record = asRecord(value, context);
133
+ return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, parseMcpServerConfig(entry, `${context}.${key}`)]));
134
+ }
95
135
  export function parseCommandEnvelope(line) {
96
136
  const raw = asRecord(JSON.parse(line), "command envelope");
97
137
  const requestId = typeof raw.request_id === "string" ? raw.request_id : undefined;
@@ -148,6 +188,48 @@ export function parseCommandEnvelope(line) {
148
188
  session_id: expectString(raw, "session_id", "set_mode"),
149
189
  mode: expectString(raw, "mode", "set_mode"),
150
190
  };
191
+ case "generate_session_title":
192
+ return {
193
+ command: "generate_session_title",
194
+ session_id: expectString(raw, "session_id", "generate_session_title"),
195
+ description: expectString(raw, "description", "generate_session_title"),
196
+ };
197
+ case "rename_session":
198
+ return {
199
+ command: "rename_session",
200
+ session_id: expectString(raw, "session_id", "rename_session"),
201
+ title: expectString(raw, "title", "rename_session"),
202
+ };
203
+ case "get_status_snapshot":
204
+ return {
205
+ command: "get_status_snapshot",
206
+ session_id: expectString(raw, "session_id", "get_status_snapshot"),
207
+ };
208
+ case "mcp_status":
209
+ case "get_mcp_snapshot":
210
+ return {
211
+ command: "mcp_status",
212
+ session_id: expectString(raw, "session_id", commandName),
213
+ };
214
+ case "mcp_reconnect":
215
+ return {
216
+ command: "mcp_reconnect",
217
+ session_id: expectString(raw, "session_id", "mcp_reconnect"),
218
+ server_name: expectString(raw, "server_name", "mcp_reconnect"),
219
+ };
220
+ case "mcp_toggle":
221
+ return {
222
+ command: "mcp_toggle",
223
+ session_id: expectString(raw, "session_id", "mcp_toggle"),
224
+ server_name: expectString(raw, "server_name", "mcp_toggle"),
225
+ enabled: expectBoolean(raw, "enabled", "mcp_toggle"),
226
+ };
227
+ case "mcp_set_servers":
228
+ return {
229
+ command: "mcp_set_servers",
230
+ session_id: expectString(raw, "session_id", "mcp_set_servers"),
231
+ servers: parseMcpServersRecord(raw.servers ?? {}, "mcp_set_servers.servers"),
232
+ };
151
233
  case "permission_response": {
152
234
  const outcome = asRecord(raw.outcome, "permission_response.outcome");
153
235
  const outcomeType = expectString(outcome, "outcome", "permission_response.outcome");
@@ -167,6 +249,57 @@ export function parseCommandEnvelope(line) {
167
249
  outcome: parsedOutcome,
168
250
  };
169
251
  }
252
+ case "question_response": {
253
+ const outcome = asRecord(raw.outcome, "question_response.outcome");
254
+ const outcomeType = expectString(outcome, "outcome", "question_response.outcome");
255
+ if (outcomeType !== "answered" && outcomeType !== "cancelled") {
256
+ throw new Error("question_response.outcome.outcome must be 'answered' or 'cancelled'");
257
+ }
258
+ const parsedOutcome = outcomeType === "answered"
259
+ ? {
260
+ outcome: "answered",
261
+ selected_option_ids: expectStringArray(outcome, "selected_option_ids", "question_response.outcome"),
262
+ ...(outcome.annotation === undefined || outcome.annotation === null
263
+ ? {}
264
+ : { annotation: parseQuestionAnnotation(outcome.annotation) }),
265
+ }
266
+ : { outcome: "cancelled" };
267
+ return {
268
+ command: "question_response",
269
+ session_id: expectString(raw, "session_id", "question_response"),
270
+ tool_call_id: expectString(raw, "tool_call_id", "question_response"),
271
+ outcome: parsedOutcome,
272
+ };
273
+ }
274
+ case "elicitation_response":
275
+ return {
276
+ command: "elicitation_response",
277
+ session_id: expectString(raw, "session_id", "elicitation_response"),
278
+ elicitation_request_id: expectString(raw, "elicitation_request_id", "elicitation_response"),
279
+ action: expectElicitationAction(raw, "action", "elicitation_response"),
280
+ ...(optionalJsonObject(raw, "content", "elicitation_response")
281
+ ? { content: optionalJsonObject(raw, "content", "elicitation_response") }
282
+ : {}),
283
+ };
284
+ case "mcp_authenticate":
285
+ return {
286
+ command: "mcp_authenticate",
287
+ session_id: expectString(raw, "session_id", "mcp_authenticate"),
288
+ server_name: expectString(raw, "server_name", "mcp_authenticate"),
289
+ };
290
+ case "mcp_clear_auth":
291
+ return {
292
+ command: "mcp_clear_auth",
293
+ session_id: expectString(raw, "session_id", "mcp_clear_auth"),
294
+ server_name: expectString(raw, "server_name", "mcp_clear_auth"),
295
+ };
296
+ case "mcp_oauth_callback_url":
297
+ return {
298
+ command: "mcp_oauth_callback_url",
299
+ session_id: expectString(raw, "session_id", "mcp_oauth_callback_url"),
300
+ server_name: expectString(raw, "server_name", "mcp_oauth_callback_url"),
301
+ callback_url: expectString(raw, "callback_url", "mcp_oauth_callback_url"),
302
+ };
170
303
  case "shutdown":
171
304
  return { command: "shutdown" };
172
305
  default:
@@ -175,6 +308,37 @@ export function parseCommandEnvelope(line) {
175
308
  })();
176
309
  return { requestId, command };
177
310
  }
311
+ function expectStringArray(record, key, context) {
312
+ const value = record[key];
313
+ if (!Array.isArray(value)) {
314
+ throw new Error(`${context}.${key} must be an array`);
315
+ }
316
+ return value.map((entry, index) => {
317
+ if (typeof entry !== "string") {
318
+ throw new Error(`${context}.${key}[${index}] must be a string`);
319
+ }
320
+ return entry;
321
+ });
322
+ }
323
+ function expectStringMap(record, key, context) {
324
+ const value = record[key];
325
+ const parsed = asRecord(value, `${context}.${key}`);
326
+ return Object.fromEntries(Object.entries(parsed).map(([entryKey, entryValue]) => {
327
+ if (typeof entryValue !== "string") {
328
+ throw new Error(`${context}.${key}.${entryKey} must be a string`);
329
+ }
330
+ return [entryKey, entryValue];
331
+ }));
332
+ }
333
+ function parseQuestionAnnotation(value) {
334
+ const record = asRecord(value, "question_response.outcome.annotation");
335
+ const preview = optionalString(record, "preview", "question_response.outcome.annotation");
336
+ const notes = optionalString(record, "notes", "question_response.outcome.annotation");
337
+ return {
338
+ ...(preview !== undefined ? { preview } : {}),
339
+ ...(notes !== undefined ? { notes } : {}),
340
+ };
341
+ }
178
342
  export function toPermissionMode(mode) {
179
343
  if (mode === "default" ||
180
344
  mode === "acceptEdits" ||
@@ -2,6 +2,16 @@ import { listSessions } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { buildModeState } from "./commands.js";
3
3
  import { mapSdkSessions } from "./history.js";
4
4
  const SESSION_LIST_LIMIT = 50;
5
+ let sessionListingDir;
6
+ export function buildSessionListOptions(dir, limit = SESSION_LIST_LIMIT) {
7
+ return dir ? { dir, includeWorktrees: true, limit } : { limit };
8
+ }
9
+ export function setSessionListingDir(dir) {
10
+ sessionListingDir = dir;
11
+ }
12
+ export function currentSessionListOptions() {
13
+ return buildSessionListOptions(sessionListingDir);
14
+ }
5
15
  export function writeEvent(event, requestId) {
6
16
  const envelope = {
7
17
  ...(requestId ? { request_id: requestId } : {}),
@@ -15,6 +25,9 @@ export function failConnection(message, requestId) {
15
25
  export function slashError(sessionId, message, requestId) {
16
26
  writeEvent({ event: "slash_error", session_id: sessionId, message }, requestId);
17
27
  }
28
+ export function emitMcpOperationError(sessionId, error, requestId) {
29
+ writeEvent({ event: "mcp_operation_error", session_id: sessionId, error }, requestId);
30
+ }
18
31
  export function emitSessionUpdate(sessionId, update) {
19
32
  writeEvent({ event: "session_update", session_id: sessionId, update });
20
33
  }
@@ -67,7 +80,7 @@ export function emitConnectEvent(session) {
67
80
  }
68
81
  export async function emitSessionsList(requestId) {
69
82
  try {
70
- const sdkSessions = await listSessions({ limit: SESSION_LIST_LIMIT });
83
+ const sdkSessions = await listSessions(currentSessionListOptions());
71
84
  writeEvent({ event: "sessions_listed", sessions: mapSdkSessions(sdkSessions, SESSION_LIST_LIMIT) }, requestId);
72
85
  }
73
86
  catch (error) {
@@ -48,7 +48,7 @@ function pushResumeToolResult(updates, toolCalls, block) {
48
48
  }
49
49
  const isError = Boolean(block.is_error);
50
50
  const base = toolCalls.get(toolUseId);
51
- const fields = buildToolResultFields(isError, block.content, base);
51
+ const fields = buildToolResultFields(isError, block.content, base, block);
52
52
  updates.push({ type: "tool_call_update", tool_call_update: { tool_call_id: toolUseId, fields } });
53
53
  if (!base) {
54
54
  return;
@@ -60,6 +60,9 @@ function pushResumeToolResult(updates, toolCalls, block) {
60
60
  if (fields.content) {
61
61
  base.content = fields.content;
62
62
  }
63
+ if (fields.output_metadata) {
64
+ base.output_metadata = fields.output_metadata;
65
+ }
63
66
  }
64
67
  function summaryFromSession(info) {
65
68
  return (nonEmptyTrimmed(info.summary) ??
@@ -0,0 +1,289 @@
1
+ import { emitMcpOperationError, slashError, writeEvent } from "./events.js";
2
+ export const MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS = 30_000;
3
+ const knownConnectedMcpServers = new Set();
4
+ function queryWithMcpAuth(session) {
5
+ return session.query;
6
+ }
7
+ async function callMcpAuthMethod(session, methodName, args) {
8
+ const query = queryWithMcpAuth(session);
9
+ switch (methodName) {
10
+ case "mcpAuthenticate":
11
+ if (typeof query.mcpAuthenticate !== "function") {
12
+ throw new Error("installed SDK does not support mcpAuthenticate");
13
+ }
14
+ return await query.mcpAuthenticate(args[0] ?? "");
15
+ case "mcpClearAuth":
16
+ if (typeof query.mcpClearAuth !== "function") {
17
+ throw new Error("installed SDK does not support mcpClearAuth");
18
+ }
19
+ return await query.mcpClearAuth(args[0] ?? "");
20
+ case "mcpSubmitOAuthCallbackUrl":
21
+ if (typeof query.mcpSubmitOAuthCallbackUrl !== "function") {
22
+ throw new Error("installed SDK does not support mcpSubmitOAuthCallbackUrl");
23
+ }
24
+ return await query.mcpSubmitOAuthCallbackUrl(args[0] ?? "", args[1] ?? "");
25
+ }
26
+ }
27
+ function extractMcpAuthRedirect(serverName, value) {
28
+ if (!value || typeof value !== "object") {
29
+ return null;
30
+ }
31
+ const authUrl = Reflect.get(value, "authUrl");
32
+ if (typeof authUrl !== "string" || authUrl.trim().length === 0) {
33
+ return null;
34
+ }
35
+ const requiresUserAction = Reflect.get(value, "requiresUserAction");
36
+ return {
37
+ server_name: serverName,
38
+ auth_url: authUrl,
39
+ requires_user_action: requiresUserAction === true,
40
+ };
41
+ }
42
+ function emitMcpCommandError(sessionId, operation, message, requestId, serverName) {
43
+ emitMcpOperationError(sessionId, {
44
+ ...(serverName ? { server_name: serverName } : {}),
45
+ operation,
46
+ message,
47
+ }, requestId);
48
+ }
49
+ export async function emitMcpSnapshotEvent(session, requestId) {
50
+ const servers = await session.query.mcpServerStatus();
51
+ let mapped = servers.map(mapMcpServerStatus);
52
+ mapped = await reconcileSuspiciousMcpStatuses(session, mapped);
53
+ rememberKnownConnectedMcpServers(mapped);
54
+ writeEvent({
55
+ event: "mcp_snapshot",
56
+ session_id: session.sessionId,
57
+ servers: mapped,
58
+ }, requestId);
59
+ return mapped;
60
+ }
61
+ export function staleMcpAuthCandidates(servers, knownConnectedServerNames, lastRevalidatedAt, now = Date.now(), cooldownMs = MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS) {
62
+ return servers
63
+ .filter((server) => {
64
+ if (server.status !== "needs-auth") {
65
+ return false;
66
+ }
67
+ if (!knownConnectedServerNames.has(server.name)) {
68
+ return false;
69
+ }
70
+ const lastAttempt = lastRevalidatedAt.get(server.name) ?? 0;
71
+ return now - lastAttempt >= cooldownMs;
72
+ })
73
+ .map((server) => server.name);
74
+ }
75
+ function rememberKnownConnectedMcpServers(servers) {
76
+ for (const server of servers) {
77
+ if (server.status === "connected") {
78
+ knownConnectedMcpServers.add(server.name);
79
+ }
80
+ }
81
+ }
82
+ function forgetKnownConnectedMcpServer(serverName) {
83
+ knownConnectedMcpServers.delete(serverName);
84
+ }
85
+ async function reconcileSuspiciousMcpStatuses(session, servers) {
86
+ const candidates = staleMcpAuthCandidates(servers, knownConnectedMcpServers, session.mcpStatusRevalidatedAt);
87
+ if (candidates.length === 0) {
88
+ return servers;
89
+ }
90
+ const now = Date.now();
91
+ for (const serverName of candidates) {
92
+ session.mcpStatusRevalidatedAt.set(serverName, now);
93
+ console.error(`[sdk mcp reconcile] session=${session.sessionId} server=${serverName} ` +
94
+ `status=needs-auth reason=previously-connected action=reconnect`);
95
+ try {
96
+ await session.query.reconnectMcpServer(serverName);
97
+ }
98
+ catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ console.error(`[sdk mcp reconcile] session=${session.sessionId} server=${serverName} ` +
101
+ `action=reconnect failed=${message}`);
102
+ }
103
+ }
104
+ return (await session.query.mcpServerStatus()).map(mapMcpServerStatus);
105
+ }
106
+ function shouldKeepMonitoringMcpAuth(server) {
107
+ return server?.status === "needs-auth" || server?.status === "pending";
108
+ }
109
+ function scheduleMcpAuthSnapshotMonitor(session, serverName, attempt = 0) {
110
+ const maxAttempts = 180;
111
+ const delayMs = 1000;
112
+ setTimeout(() => {
113
+ void monitorMcpAuthSnapshot(session, serverName, attempt + 1, maxAttempts, delayMs);
114
+ }, delayMs);
115
+ }
116
+ async function monitorMcpAuthSnapshot(session, serverName, attempt, maxAttempts, delayMs) {
117
+ try {
118
+ const servers = await emitMcpSnapshotEvent(session);
119
+ const server = servers.find((candidate) => candidate.name === serverName);
120
+ if (attempt < maxAttempts && shouldKeepMonitoringMcpAuth(server)) {
121
+ setTimeout(() => {
122
+ void monitorMcpAuthSnapshot(session, serverName, attempt + 1, maxAttempts, delayMs);
123
+ }, delayMs);
124
+ }
125
+ }
126
+ catch {
127
+ if (attempt < maxAttempts) {
128
+ setTimeout(() => {
129
+ void monitorMcpAuthSnapshot(session, serverName, attempt + 1, maxAttempts, delayMs);
130
+ }, delayMs);
131
+ }
132
+ }
133
+ }
134
+ export async function handleMcpStatusCommand(session, requestId) {
135
+ try {
136
+ await emitMcpSnapshotEvent(session, requestId);
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof Error ? error.message : String(error);
140
+ writeEvent({
141
+ event: "mcp_snapshot",
142
+ session_id: session.sessionId,
143
+ servers: [],
144
+ error: message,
145
+ }, requestId);
146
+ }
147
+ }
148
+ export async function handleMcpReconnectCommand(session, command, requestId) {
149
+ try {
150
+ await session.query.reconnectMcpServer(command.server_name);
151
+ }
152
+ catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ emitMcpCommandError(command.session_id, "reconnect", message, requestId, command.server_name);
155
+ }
156
+ }
157
+ export async function handleMcpToggleCommand(session, command, requestId) {
158
+ try {
159
+ await session.query.toggleMcpServer(command.server_name, command.enabled);
160
+ }
161
+ catch (error) {
162
+ const message = error instanceof Error ? error.message : String(error);
163
+ emitMcpCommandError(command.session_id, "toggle", message, requestId, command.server_name);
164
+ }
165
+ }
166
+ export async function handleMcpSetServersCommand(session, command, requestId) {
167
+ try {
168
+ await session.query.setMcpServers(command.servers);
169
+ }
170
+ catch (error) {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ slashError(command.session_id, `failed to set MCP servers: ${message}`, requestId);
173
+ }
174
+ }
175
+ export async function handleMcpAuthenticateCommand(session, command, requestId) {
176
+ try {
177
+ const result = await callMcpAuthMethod(session, "mcpAuthenticate", [command.server_name]);
178
+ const redirect = extractMcpAuthRedirect(command.server_name, result);
179
+ if (redirect) {
180
+ writeEvent({
181
+ event: "mcp_auth_redirect",
182
+ session_id: command.session_id,
183
+ redirect,
184
+ });
185
+ }
186
+ scheduleMcpAuthSnapshotMonitor(session, command.server_name);
187
+ }
188
+ catch (error) {
189
+ const message = error instanceof Error ? error.message : String(error);
190
+ emitMcpCommandError(command.session_id, "authenticate", message, requestId, command.server_name);
191
+ }
192
+ }
193
+ export async function handleMcpClearAuthCommand(session, command, requestId) {
194
+ try {
195
+ await callMcpAuthMethod(session, "mcpClearAuth", [command.server_name]);
196
+ forgetKnownConnectedMcpServer(command.server_name);
197
+ session.mcpStatusRevalidatedAt.delete(command.server_name);
198
+ }
199
+ catch (error) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ emitMcpCommandError(command.session_id, "clear-auth", message, requestId, command.server_name);
202
+ }
203
+ }
204
+ export async function handleMcpOauthCallbackUrlCommand(session, command, requestId) {
205
+ try {
206
+ await callMcpAuthMethod(session, "mcpSubmitOAuthCallbackUrl", [
207
+ command.server_name,
208
+ command.callback_url,
209
+ ]);
210
+ }
211
+ catch (error) {
212
+ const message = error instanceof Error ? error.message : String(error);
213
+ emitMcpCommandError(command.session_id, "submit-callback-url", message, requestId, command.server_name);
214
+ }
215
+ }
216
+ function mapMcpServerStatus(status) {
217
+ return {
218
+ name: status.name,
219
+ status: status.status,
220
+ ...(status.serverInfo
221
+ ? {
222
+ server_info: {
223
+ name: status.serverInfo.name,
224
+ version: status.serverInfo.version,
225
+ },
226
+ }
227
+ : {}),
228
+ ...(status.error ? { error: status.error } : {}),
229
+ ...(status.config ? { config: mapMcpServerStatusConfig(status.config) } : {}),
230
+ ...(status.scope ? { scope: status.scope } : {}),
231
+ tools: Array.isArray(status.tools)
232
+ ? status.tools.map((tool) => ({
233
+ name: tool.name,
234
+ ...(tool.description ? { description: tool.description } : {}),
235
+ ...(tool.annotations
236
+ ? {
237
+ annotations: {
238
+ ...(typeof tool.annotations.readOnly === "boolean"
239
+ ? { read_only: tool.annotations.readOnly }
240
+ : {}),
241
+ ...(typeof tool.annotations.destructive === "boolean"
242
+ ? { destructive: tool.annotations.destructive }
243
+ : {}),
244
+ ...(typeof tool.annotations.openWorld === "boolean"
245
+ ? { open_world: tool.annotations.openWorld }
246
+ : {}),
247
+ },
248
+ }
249
+ : {}),
250
+ }))
251
+ : [],
252
+ };
253
+ }
254
+ function mapMcpServerStatusConfig(config) {
255
+ switch (config.type) {
256
+ case "stdio":
257
+ return {
258
+ type: "stdio",
259
+ command: config.command,
260
+ ...(Array.isArray(config.args) && config.args.length > 0 ? { args: config.args } : {}),
261
+ ...(config.env ? { env: config.env } : {}),
262
+ };
263
+ case "sse":
264
+ return {
265
+ type: "sse",
266
+ url: config.url,
267
+ ...(config.headers ? { headers: config.headers } : {}),
268
+ };
269
+ case "http":
270
+ return {
271
+ type: "http",
272
+ url: config.url,
273
+ ...(config.headers ? { headers: config.headers } : {}),
274
+ };
275
+ case "sdk":
276
+ return {
277
+ type: "sdk",
278
+ name: config.name,
279
+ };
280
+ case "claudeai-proxy":
281
+ return {
282
+ type: "claudeai-proxy",
283
+ url: config.url,
284
+ id: config.id,
285
+ };
286
+ default:
287
+ throw new Error(`unsupported MCP status config: ${JSON.stringify(config)}`);
288
+ }
289
+ }
@@ -127,7 +127,7 @@ export function handleContentBlock(session, block) {
127
127
  return;
128
128
  }
129
129
  const isError = Boolean(block.is_error);
130
- emitToolResultUpdate(session, toolUseId, isError, block.content);
130
+ emitToolResultUpdate(session, toolUseId, isError, block.content, block);
131
131
  }
132
132
  }
133
133
  export function handleStreamEvent(session, event) {
@@ -356,7 +356,18 @@ export function handleSdkMessage(session, message) {
356
356
  return;
357
357
  }
358
358
  if (subtype === "elicitation_complete") {
359
- // No-op: elicitation flow is auto-canceled in the onElicitation callback.
359
+ const elicitationId = typeof msg.elicitation_id === "string" ? msg.elicitation_id : "";
360
+ if (!elicitationId) {
361
+ return;
362
+ }
363
+ writeEvent({
364
+ event: "elicitation_complete",
365
+ session_id: session.sessionId,
366
+ completion: {
367
+ elicitation_id: elicitationId,
368
+ ...(typeof msg.mcp_server_name === "string" ? { server_name: msg.mcp_server_name } : {}),
369
+ },
370
+ });
360
371
  return;
361
372
  }
362
373
  handleTaskSystemMessage(session, subtype, msg);
@@ -411,7 +422,7 @@ export function handleSdkMessage(session, message) {
411
422
  const toolUseId = typeof msg.parent_tool_use_id === "string" ? msg.parent_tool_use_id : "";
412
423
  if (toolUseId && "tool_use_result" in msg) {
413
424
  const parsed = unwrapToolUseResult(msg.tool_use_result);
414
- emitToolResultUpdate(session, toolUseId, parsed.isError, parsed.content);
425
+ emitToolResultUpdate(session, toolUseId, parsed.isError, parsed.content, msg.tool_use_result);
415
426
  }
416
427
  return;
417
428
  }