claude-code-rust 0.7.1 → 0.8.1
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/LICENSE +202 -661
- package/README.md +3 -9
- package/agent-sdk/README.md +5 -0
- package/agent-sdk/dist/bridge/commands.js +183 -19
- package/agent-sdk/dist/bridge/events.js +14 -1
- package/agent-sdk/dist/bridge/history.js +4 -1
- package/agent-sdk/dist/bridge/mcp.js +289 -0
- package/agent-sdk/dist/bridge/message_handlers.js +14 -3
- package/agent-sdk/dist/bridge/session_lifecycle.js +139 -41
- package/agent-sdk/dist/bridge/tool_calls.js +12 -3
- package/agent-sdk/dist/bridge/tooling.js +272 -7
- package/agent-sdk/dist/bridge/user_interaction.js +76 -28
- package/agent-sdk/dist/bridge.js +158 -7
- package/agent-sdk/dist/bridge.test.js +846 -23
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
|
|
|
5
5
|
[](https://www.npmjs.com/package/claude-code-rust)
|
|
6
6
|
[](https://www.npmjs.com/package/claude-code-rust)
|
|
7
7
|
[](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml)
|
|
8
|
-
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
9
9
|
[](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 [
|
|
73
|
-
|
|
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
|
|
package/agent-sdk/README.md
CHANGED
|
@@ -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
|
|
54
|
-
const
|
|
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
|
-
...(
|
|
60
|
-
...(
|
|
61
|
-
|
|
56
|
+
...(settings ? { settings } : {}),
|
|
57
|
+
...(agentProgressSummaries !== undefined
|
|
58
|
+
? { agent_progress_summaries: agentProgressSummaries }
|
|
59
|
+
: {}),
|
|
62
60
|
};
|
|
63
61
|
}
|
|
64
|
-
function
|
|
65
|
-
const value =
|
|
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 (
|
|
70
|
-
|
|
67
|
+
if (typeof value !== "boolean") {
|
|
68
|
+
throw new Error(`${context}.${key} must be a boolean when provided`);
|
|
71
69
|
}
|
|
72
|
-
|
|
70
|
+
return value;
|
|
73
71
|
}
|
|
74
|
-
function
|
|
75
|
-
const value =
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
}
|