claude-code-rust 0.12.0 → 0.12.2

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.
@@ -130,6 +130,7 @@ function eventToolCallId(event) {
130
130
  case "elicitation_complete":
131
131
  case "mcp_auth_redirect":
132
132
  case "mcp_operation_error":
133
+ case "mcp_set_servers_result":
133
134
  case "turn_complete":
134
135
  case "turn_error":
135
136
  case "slash_error":
@@ -173,6 +174,7 @@ function protocolEventLevel(event) {
173
174
  case "session_update":
174
175
  case "permission_request":
175
176
  case "question_request":
177
+ case "user_dialog_request":
176
178
  case "elicitation_request":
177
179
  case "elicitation_complete":
178
180
  case "mcp_auth_redirect":
@@ -182,6 +184,7 @@ function protocolEventLevel(event) {
182
184
  case "status_snapshot":
183
185
  case "context_usage":
184
186
  case "runtime_reload_completed":
187
+ case "mcp_set_servers_result":
185
188
  case "mcp_snapshot":
186
189
  return "debug";
187
190
  }
@@ -1,5 +1,6 @@
1
1
  import { emitMcpOperationError, slashError, writeEvent } from "./events.js";
2
2
  import { bridgeLogger, LOG_TARGETS } from "./logger.js";
3
+ import { bridgeMcpServersToSdk, mapMcpServerStatus, summarizeMcpServersForDiagnostics, } from "./mcp_metadata.js";
3
4
  export const MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS = 30_000;
4
5
  const knownConnectedMcpServers = new Set();
5
6
  function logMcpSuccess(eventName, message, sessionId, requestId, fields) {
@@ -30,6 +31,22 @@ function logMcpFailure(eventName, message, sessionId, errorMessage, requestId, f
30
31
  function queryWithMcpAuth(session) {
31
32
  return session.query;
32
33
  }
34
+ function mapMcpSetServersResult(result) {
35
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
36
+ return { added: [], removed: [], errors: {} };
37
+ }
38
+ const record = result;
39
+ const added = Array.isArray(record.added)
40
+ ? record.added.filter((value) => typeof value === "string")
41
+ : [];
42
+ const removed = Array.isArray(record.removed)
43
+ ? record.removed.filter((value) => typeof value === "string")
44
+ : [];
45
+ const errors = record.errors && typeof record.errors === "object" && !Array.isArray(record.errors)
46
+ ? Object.fromEntries(Object.entries(record.errors).filter((entry) => typeof entry[1] === "string"))
47
+ : {};
48
+ return { added, removed, errors };
49
+ }
33
50
  async function callMcpAuthMethod(session, methodName, args) {
34
51
  const query = queryWithMcpAuth(session);
35
52
  switch (methodName) {
@@ -72,17 +89,31 @@ function emitMcpCommandError(sessionId, operation, message, requestId, serverNam
72
89
  message,
73
90
  }, requestId);
74
91
  }
75
- export async function emitMcpSnapshotEvent(session, requestId) {
92
+ export async function emitMcpSnapshotEvent(session, requestId, source = "mcp_status") {
76
93
  const servers = await session.query.mcpServerStatus();
77
94
  let mapped = servers.map(mapMcpServerStatus);
78
95
  mapped = await reconcileSuspiciousMcpStatuses(session, mapped);
96
+ return emitMcpSnapshotFromMappedStatuses(session, mapped, source, requestId);
97
+ }
98
+ export function emitMcpSnapshotFromStatuses(session, servers, source, requestId) {
99
+ return emitMcpSnapshotFromMappedStatuses(session, servers.map(mapMcpServerStatus), source, requestId);
100
+ }
101
+ export async function emitReconciledMcpSnapshotFromStatuses(session, servers, source, requestId) {
102
+ let mapped = servers.map(mapMcpServerStatus);
103
+ mapped = await reconcileSuspiciousMcpStatuses(session, mapped);
104
+ return emitMcpSnapshotFromMappedStatuses(session, mapped, source, requestId);
105
+ }
106
+ function emitMcpSnapshotFromMappedStatuses(session, mapped, source, requestId) {
79
107
  rememberKnownConnectedMcpServers(mapped);
80
108
  logMcpSuccess("mcp_snapshot_emitted", "MCP snapshot emitted", session.sessionId, requestId, {
109
+ source,
81
110
  server_count: mapped.length,
111
+ servers: summarizeMcpServersForDiagnostics(mapped),
82
112
  });
83
113
  writeEvent({
84
114
  event: "mcp_snapshot",
85
115
  session_id: session.sessionId,
116
+ source,
86
117
  servers: mapped,
87
118
  }, requestId);
88
119
  return mapped;
@@ -181,9 +212,9 @@ async function monitorMcpAuthSnapshot(session, serverName, attempt, maxAttempts,
181
212
  }
182
213
  }
183
214
  }
184
- export async function handleMcpStatusCommand(session, requestId) {
215
+ export async function handleMcpStatusCommand(session, requestId, source = "mcp_status") {
185
216
  try {
186
- await emitMcpSnapshotEvent(session, requestId);
217
+ await emitMcpSnapshotEvent(session, requestId, source);
187
218
  }
188
219
  catch (error) {
189
220
  const message = error instanceof Error ? error.message : String(error);
@@ -191,6 +222,7 @@ export async function handleMcpStatusCommand(session, requestId) {
191
222
  writeEvent({
192
223
  event: "mcp_snapshot",
193
224
  session_id: session.sessionId,
225
+ source,
194
226
  servers: [],
195
227
  error: message,
196
228
  }, requestId);
@@ -222,12 +254,24 @@ export async function handleMcpToggleCommand(session, command, requestId) {
222
254
  }
223
255
  export async function handleMcpSetServersCommand(session, command, requestId) {
224
256
  try {
225
- await session.query.setMcpServers(command.servers);
226
- logMcpSuccess("mcp_servers_set_completed", "MCP server configuration updated", command.session_id, requestId, { server_count: Object.keys(command.servers).length });
257
+ const result = mapMcpSetServersResult(await session.query.setMcpServers(bridgeMcpServersToSdk(command.servers)));
258
+ logMcpSuccess("mcp_servers_set_completed", "MCP server configuration updated", command.session_id, requestId, {
259
+ server_count: Object.keys(command.servers).length,
260
+ added_count: result.added.length,
261
+ removed_count: result.removed.length,
262
+ error_count: Object.keys(result.errors).length,
263
+ });
264
+ writeEvent({
265
+ event: "mcp_set_servers_result",
266
+ session_id: command.session_id,
267
+ result,
268
+ }, requestId);
269
+ await handleMcpStatusCommand(session, requestId, "mcp_set_servers");
227
270
  }
228
271
  catch (error) {
229
272
  const message = error instanceof Error ? error.message : String(error);
230
273
  logMcpFailure("mcp_servers_set_failed", "failed to update MCP server configuration", command.session_id, message, requestId, { server_count: Object.keys(command.servers).length });
274
+ emitMcpOperationError(command.session_id, { operation: "set-servers", message }, requestId);
231
275
  slashError(command.session_id, `failed to set MCP servers: ${message}`, requestId);
232
276
  }
233
277
  }
@@ -289,77 +333,3 @@ export async function handleMcpOauthCallbackUrlCommand(session, command, request
289
333
  emitMcpCommandError(command.session_id, "submit-callback-url", message, requestId, command.server_name);
290
334
  }
291
335
  }
292
- function mapMcpServerStatus(status) {
293
- return {
294
- name: status.name,
295
- status: status.status,
296
- ...(status.serverInfo
297
- ? {
298
- server_info: {
299
- name: status.serverInfo.name,
300
- version: status.serverInfo.version,
301
- },
302
- }
303
- : {}),
304
- ...(status.error ? { error: status.error } : {}),
305
- ...(status.config ? { config: mapMcpServerStatusConfig(status.config) } : {}),
306
- ...(status.scope ? { scope: status.scope } : {}),
307
- tools: Array.isArray(status.tools)
308
- ? status.tools.map((tool) => ({
309
- name: tool.name,
310
- ...(tool.description ? { description: tool.description } : {}),
311
- ...(tool.annotations
312
- ? {
313
- annotations: {
314
- ...(typeof tool.annotations.readOnly === "boolean"
315
- ? { read_only: tool.annotations.readOnly }
316
- : {}),
317
- ...(typeof tool.annotations.destructive === "boolean"
318
- ? { destructive: tool.annotations.destructive }
319
- : {}),
320
- ...(typeof tool.annotations.openWorld === "boolean"
321
- ? { open_world: tool.annotations.openWorld }
322
- : {}),
323
- },
324
- }
325
- : {}),
326
- }))
327
- : [],
328
- };
329
- }
330
- function mapMcpServerStatusConfig(config) {
331
- switch (config.type) {
332
- case "stdio":
333
- return {
334
- type: "stdio",
335
- command: config.command,
336
- ...(Array.isArray(config.args) && config.args.length > 0 ? { args: config.args } : {}),
337
- ...(config.env ? { env: config.env } : {}),
338
- };
339
- case "sse":
340
- return {
341
- type: "sse",
342
- url: config.url,
343
- ...(config.headers ? { headers: config.headers } : {}),
344
- };
345
- case "http":
346
- return {
347
- type: "http",
348
- url: config.url,
349
- ...(config.headers ? { headers: config.headers } : {}),
350
- };
351
- case "sdk":
352
- return {
353
- type: "sdk",
354
- name: config.name,
355
- };
356
- case "claudeai-proxy":
357
- return {
358
- type: "claudeai-proxy",
359
- url: config.url,
360
- id: config.id,
361
- };
362
- default:
363
- throw new Error(`unsupported MCP status config: ${JSON.stringify(config)}`);
364
- }
365
- }
@@ -0,0 +1,369 @@
1
+ import { bridgeLogger, LOG_TARGETS } from "./logger.js";
2
+ const TOOL_PERMISSION_POLICIES = new Set([
3
+ "always_allow",
4
+ "always_ask",
5
+ "always_deny",
6
+ ]);
7
+ const ORG_MAX_PERMISSIONS = new Set([
8
+ "allow",
9
+ "ask",
10
+ "blocked",
11
+ ]);
12
+ function asRecord(value, context) {
13
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
14
+ throw new Error(`${context} must be an object`);
15
+ }
16
+ return value;
17
+ }
18
+ function optionalStringArray(record, key, context) {
19
+ const value = record[key];
20
+ if (value === undefined) {
21
+ return undefined;
22
+ }
23
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
24
+ throw new Error(`${context}.${key} must be an array of strings`);
25
+ }
26
+ return value;
27
+ }
28
+ function optionalStringMap(record, key, context) {
29
+ const value = record[key];
30
+ if (value === undefined) {
31
+ return undefined;
32
+ }
33
+ const map = asRecord(value, `${context}.${key}`);
34
+ for (const [entryKey, entryValue] of Object.entries(map)) {
35
+ if (typeof entryValue !== "string") {
36
+ throw new Error(`${context}.${key}.${entryKey} must be a string`);
37
+ }
38
+ }
39
+ return map;
40
+ }
41
+ function optionalTimeout(record, context) {
42
+ const value = record.timeout;
43
+ if (value === undefined) {
44
+ return undefined;
45
+ }
46
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1000) {
47
+ throw new Error(`${context}.timeout must be an integer >= 1000`);
48
+ }
49
+ return value;
50
+ }
51
+ function optionalAlwaysLoad(record, context) {
52
+ const value = record.always_load;
53
+ if (value === undefined) {
54
+ return undefined;
55
+ }
56
+ if (typeof value !== "boolean") {
57
+ throw new Error(`${context}.always_load must be a boolean`);
58
+ }
59
+ return value;
60
+ }
61
+ function optionalToolPolicies(record, context) {
62
+ const value = record.tools;
63
+ if (value === undefined) {
64
+ return undefined;
65
+ }
66
+ if (!Array.isArray(value)) {
67
+ throw new Error(`${context}.tools must be an array`);
68
+ }
69
+ return value.map((entry, index) => {
70
+ const item = asRecord(entry, `${context}.tools[${index}]`);
71
+ const name = item.name;
72
+ if (typeof name !== "string") {
73
+ throw new Error(`${context}.tools[${index}].name must be a string`);
74
+ }
75
+ const policy = { name };
76
+ const permissionPolicy = item.permission_policy;
77
+ if (permissionPolicy !== undefined) {
78
+ if (typeof permissionPolicy !== "string" || !TOOL_PERMISSION_POLICIES.has(permissionPolicy)) {
79
+ throw new Error(`${context}.tools[${index}].permission_policy must be one of always_allow, always_ask, always_deny`);
80
+ }
81
+ policy.permission_policy = permissionPolicy;
82
+ }
83
+ const orgMaxPermission = item.org_max_permission;
84
+ if (orgMaxPermission !== undefined) {
85
+ if (typeof orgMaxPermission !== "string" || !ORG_MAX_PERMISSIONS.has(orgMaxPermission)) {
86
+ throw new Error(`${context}.tools[${index}].org_max_permission must be one of allow, ask, blocked`);
87
+ }
88
+ policy.org_max_permission = orgMaxPermission;
89
+ }
90
+ return policy;
91
+ });
92
+ }
93
+ export function parseMcpServerConfig(value, context) {
94
+ const record = asRecord(value, context);
95
+ const rawType = record.type;
96
+ const type = rawType === undefined ? "stdio" : rawType;
97
+ if (typeof type !== "string") {
98
+ throw new Error(`${context}.type must be a string`);
99
+ }
100
+ const timeout = optionalTimeout(record, context);
101
+ const alwaysLoad = optionalAlwaysLoad(record, context);
102
+ switch (type) {
103
+ case "stdio": {
104
+ if (record.tools !== undefined) {
105
+ throw new Error(`${context}.tools is only supported for http and sse MCP servers`);
106
+ }
107
+ const command = record.command;
108
+ if (typeof command !== "string") {
109
+ throw new Error(`${context}.command must be a string`);
110
+ }
111
+ return {
112
+ type,
113
+ command,
114
+ ...(optionalStringArray(record, "args", context) ? { args: optionalStringArray(record, "args", context) } : {}),
115
+ ...(optionalStringMap(record, "env", context) ? { env: optionalStringMap(record, "env", context) } : {}),
116
+ ...(timeout === undefined ? {} : { timeout }),
117
+ ...(alwaysLoad === undefined ? {} : { always_load: alwaysLoad }),
118
+ };
119
+ }
120
+ case "sse":
121
+ case "http": {
122
+ const url = record.url;
123
+ if (typeof url !== "string") {
124
+ throw new Error(`${context}.url must be a string`);
125
+ }
126
+ const tools = optionalToolPolicies(record, context);
127
+ return {
128
+ type,
129
+ url,
130
+ ...(optionalStringMap(record, "headers", context) ? { headers: optionalStringMap(record, "headers", context) } : {}),
131
+ ...(tools === undefined ? {} : { tools }),
132
+ ...(timeout === undefined ? {} : { timeout }),
133
+ ...(alwaysLoad === undefined ? {} : { always_load: alwaysLoad }),
134
+ };
135
+ }
136
+ default:
137
+ throw new Error(`${context}.type must be one of stdio, sse, http`);
138
+ }
139
+ }
140
+ export function parseMcpServersRecord(value, context) {
141
+ const record = asRecord(value, context);
142
+ return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, parseMcpServerConfig(entry, `${context}.${key}`)]));
143
+ }
144
+ function toSdkToolPolicies(tools) {
145
+ return tools?.map((tool) => ({
146
+ name: tool.name,
147
+ ...(tool.permission_policy === undefined ? {} : { permission_policy: tool.permission_policy }),
148
+ ...(tool.org_max_permission === undefined ? {} : { org_max_permission: tool.org_max_permission }),
149
+ }));
150
+ }
151
+ export function bridgeMcpConfigToSdk(config) {
152
+ switch (config.type) {
153
+ case "stdio":
154
+ return {
155
+ type: "stdio",
156
+ command: config.command,
157
+ ...(config.args ? { args: config.args } : {}),
158
+ ...(config.env ? { env: config.env } : {}),
159
+ ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
160
+ ...(config.always_load === undefined ? {} : { alwaysLoad: config.always_load }),
161
+ };
162
+ case "sse":
163
+ return {
164
+ type: "sse",
165
+ url: config.url,
166
+ ...(config.headers ? { headers: config.headers } : {}),
167
+ ...(config.tools ? { tools: toSdkToolPolicies(config.tools) } : {}),
168
+ ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
169
+ ...(config.always_load === undefined ? {} : { alwaysLoad: config.always_load }),
170
+ };
171
+ case "http":
172
+ return {
173
+ type: "http",
174
+ url: config.url,
175
+ ...(config.headers ? { headers: config.headers } : {}),
176
+ ...(config.tools ? { tools: toSdkToolPolicies(config.tools) } : {}),
177
+ ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
178
+ ...(config.always_load === undefined ? {} : { alwaysLoad: config.always_load }),
179
+ };
180
+ }
181
+ }
182
+ export function bridgeMcpServersToSdk(servers) {
183
+ return Object.fromEntries(Object.entries(servers).map(([name, config]) => [name, bridgeMcpConfigToSdk(config)]));
184
+ }
185
+ function mapSdkToolPolicies(tools) {
186
+ if (!Array.isArray(tools)) {
187
+ return undefined;
188
+ }
189
+ return tools
190
+ .map((tool) => {
191
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
192
+ return null;
193
+ }
194
+ const raw = tool;
195
+ if (typeof raw.name !== "string") {
196
+ return null;
197
+ }
198
+ const policy = { name: raw.name };
199
+ if (raw.permission_policy !== undefined) {
200
+ if (typeof raw.permission_policy !== "string" || !TOOL_PERMISSION_POLICIES.has(raw.permission_policy)) {
201
+ return null;
202
+ }
203
+ policy.permission_policy = raw.permission_policy;
204
+ }
205
+ if (raw.org_max_permission !== undefined) {
206
+ if (typeof raw.org_max_permission !== "string" || !ORG_MAX_PERMISSIONS.has(raw.org_max_permission)) {
207
+ return null;
208
+ }
209
+ policy.org_max_permission = raw.org_max_permission;
210
+ }
211
+ return policy;
212
+ })
213
+ .filter((tool) => tool !== null);
214
+ }
215
+ export function mapMcpServerStatus(status) {
216
+ return {
217
+ name: status.name,
218
+ status: status.status,
219
+ ...(status.serverInfo
220
+ ? {
221
+ server_info: {
222
+ name: status.serverInfo.name,
223
+ version: status.serverInfo.version,
224
+ },
225
+ }
226
+ : {}),
227
+ ...(status.error ? { error: status.error } : {}),
228
+ ...(status.config ? { config: mapMcpServerStatusConfig(status.config) } : {}),
229
+ ...(status.scope ? { scope: status.scope } : {}),
230
+ tools: Array.isArray(status.tools)
231
+ ? status.tools.map((tool) => ({
232
+ name: tool.name,
233
+ ...(tool.description ? { description: tool.description } : {}),
234
+ ...(tool.annotations
235
+ ? {
236
+ annotations: {
237
+ ...(typeof tool.annotations.readOnly === "boolean"
238
+ ? { read_only: tool.annotations.readOnly }
239
+ : {}),
240
+ ...(typeof tool.annotations.destructive === "boolean"
241
+ ? { destructive: tool.annotations.destructive }
242
+ : {}),
243
+ ...(typeof tool.annotations.openWorld === "boolean"
244
+ ? { open_world: tool.annotations.openWorld }
245
+ : {}),
246
+ },
247
+ }
248
+ : {}),
249
+ }))
250
+ : [],
251
+ };
252
+ }
253
+ export function mapMcpServerStatusConfig(config) {
254
+ switch (config.type) {
255
+ case "stdio":
256
+ return {
257
+ type: "stdio",
258
+ command: config.command,
259
+ ...(Array.isArray(config.args) && config.args.length > 0 ? { args: config.args } : {}),
260
+ ...(config.env ? { env: config.env } : {}),
261
+ ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
262
+ ...(config.alwaysLoad === undefined ? {} : { always_load: config.alwaysLoad }),
263
+ };
264
+ case "sse": {
265
+ const tools = mapSdkToolPolicies(config.tools);
266
+ return {
267
+ type: "sse",
268
+ url: config.url,
269
+ ...(config.headers ? { headers: config.headers } : {}),
270
+ ...(tools === undefined ? {} : { tools }),
271
+ ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
272
+ ...(config.alwaysLoad === undefined ? {} : { always_load: config.alwaysLoad }),
273
+ };
274
+ }
275
+ case "http": {
276
+ const tools = mapSdkToolPolicies(config.tools);
277
+ return {
278
+ type: "http",
279
+ url: config.url,
280
+ ...(config.headers ? { headers: config.headers } : {}),
281
+ ...(tools === undefined ? {} : { tools }),
282
+ ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
283
+ ...(config.alwaysLoad === undefined ? {} : { always_load: config.alwaysLoad }),
284
+ };
285
+ }
286
+ case "sdk":
287
+ return {
288
+ type: "sdk",
289
+ name: config.name,
290
+ };
291
+ case "claudeai-proxy":
292
+ return {
293
+ type: "claudeai-proxy",
294
+ url: config.url,
295
+ id: config.id,
296
+ ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
297
+ };
298
+ default: {
299
+ const raw = config;
300
+ const rawType = typeof raw.type === "string" ? raw.type : "unknown";
301
+ bridgeLogger.warn({
302
+ target: LOG_TARGETS.BRIDGE_MCP,
303
+ eventName: "mcp_status_config_unknown",
304
+ message: "unknown MCP status config type",
305
+ outcome: "ignored",
306
+ fields: { config_type: rawType },
307
+ });
308
+ return { type: "unknown", raw_type: rawType };
309
+ }
310
+ }
311
+ }
312
+ function mcpStatusConfigDiagnostics(config) {
313
+ if (!config) {
314
+ return {
315
+ config_type: "missing",
316
+ configured_tool_policy_count: 0,
317
+ };
318
+ }
319
+ switch (config.type) {
320
+ case "stdio":
321
+ return {
322
+ config_type: "stdio",
323
+ ...(config.timeout === undefined ? {} : { timeout_ms: config.timeout }),
324
+ ...(config.always_load === undefined ? {} : { always_load: config.always_load }),
325
+ configured_tool_policy_count: 0,
326
+ };
327
+ case "sse":
328
+ case "http":
329
+ return {
330
+ config_type: config.type,
331
+ ...(config.timeout === undefined ? {} : { timeout_ms: config.timeout }),
332
+ ...(config.always_load === undefined ? {} : { always_load: config.always_load }),
333
+ configured_tool_policy_count: config.tools?.length ?? 0,
334
+ };
335
+ case "sdk":
336
+ return {
337
+ config_type: "sdk",
338
+ configured_tool_policy_count: 0,
339
+ };
340
+ case "claudeai-proxy":
341
+ return {
342
+ config_type: "claudeai-proxy",
343
+ ...(config.timeout === undefined ? {} : { timeout_ms: config.timeout }),
344
+ configured_tool_policy_count: 0,
345
+ };
346
+ case "unknown":
347
+ return {
348
+ config_type: `unknown:${config.raw_type}`,
349
+ configured_tool_policy_count: 0,
350
+ };
351
+ }
352
+ }
353
+ export function summarizeMcpServersForDiagnostics(servers) {
354
+ return servers.map((server) => {
355
+ const config = mcpStatusConfigDiagnostics(server.config);
356
+ return {
357
+ name: server.name,
358
+ status: server.status,
359
+ config_type: config.config_type,
360
+ ...(server.scope ? { scope: server.scope } : {}),
361
+ ...(config.timeout_ms === undefined ? {} : { timeout_ms: config.timeout_ms }),
362
+ ...(config.always_load === undefined ? {} : { always_load: config.always_load }),
363
+ tool_count: server.tools.length,
364
+ configured_tool_policy_count: config.configured_tool_policy_count,
365
+ has_error: typeof server.error === "string" && server.error.length > 0,
366
+ has_server_info: server.server_info !== undefined,
367
+ };
368
+ });
369
+ }