nexus-ai-pro 2.16.0 → 2.18.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.
Files changed (92) hide show
  1. package/API_STABILITY.md +57 -0
  2. package/CHANGELOG.md +131 -1
  3. package/README.md +10 -4
  4. package/dist/agent/capabilities.d.ts +3 -0
  5. package/dist/evaluate/media.d.ts +6 -2
  6. package/dist/evaluate/media.js +7 -21
  7. package/dist/index.d.ts +1 -1
  8. package/dist/mcp/client.d.ts +86 -4
  9. package/dist/mcp/client.js +191 -18
  10. package/dist/mcp/index.d.ts +2 -2
  11. package/dist/mcp/index.js +2 -2
  12. package/dist/mcp/server.d.ts +13 -1
  13. package/dist/mcp/server.js +28 -6
  14. package/dist/protocols/acp-editor.d.ts +16 -2
  15. package/dist/protocols/acp-editor.js +56 -17
  16. package/dist/protocols/acp.d.ts +8 -0
  17. package/dist/protocols/acp.js +1 -0
  18. package/dist/router/failover.js +19 -1
  19. package/dist/server/index.d.ts +1 -0
  20. package/dist/server/list-page.d.ts +38 -0
  21. package/dist/server/list-page.js +49 -0
  22. package/dist/server/regions.d.ts +15 -1
  23. package/dist/server/regions.js +70 -0
  24. package/dist/server/runs.d.ts +24 -0
  25. package/dist/server/runs.js +18 -4
  26. package/dist/server/server.js +17 -5
  27. package/dist/tracing/index.d.ts +2 -2
  28. package/dist/tracing/instrument.d.ts +7 -2
  29. package/dist/tracing/instrument.js +69 -2
  30. package/dist/tracing/otlp-proto.js +18 -0
  31. package/dist/tracing/otlp.d.ts +26 -1
  32. package/dist/tracing/otlp.js +19 -0
  33. package/dist/tracing/tracer.d.ts +36 -1
  34. package/dist/tracing/tracer.js +68 -2
  35. package/dist/types/response.d.ts +18 -0
  36. package/dist/types/tracing.d.ts +32 -0
  37. package/dist/utils/media-duration.d.ts +20 -0
  38. package/dist/utils/media-duration.js +334 -0
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/dist/video/google.d.ts +50 -11
  42. package/dist/video/google.js +270 -19
  43. package/dist/video/index.d.ts +64 -6
  44. package/dist/video/index.js +134 -15
  45. package/dist/video/mock.js +12 -1
  46. package/dist/video/runway.d.ts +10 -1
  47. package/dist/video/runway.js +102 -2
  48. package/dist-cjs/agent/capabilities.d.ts +3 -0
  49. package/dist-cjs/evaluate/media.d.ts +6 -2
  50. package/dist-cjs/evaluate/media.js +9 -21
  51. package/dist-cjs/index.d.ts +1 -1
  52. package/dist-cjs/mcp/client.d.ts +86 -4
  53. package/dist-cjs/mcp/client.js +193 -18
  54. package/dist-cjs/mcp/index.d.ts +2 -2
  55. package/dist-cjs/mcp/index.js +4 -1
  56. package/dist-cjs/mcp/server.d.ts +13 -1
  57. package/dist-cjs/mcp/server.js +29 -6
  58. package/dist-cjs/protocols/acp-editor.d.ts +16 -2
  59. package/dist-cjs/protocols/acp-editor.js +55 -16
  60. package/dist-cjs/protocols/acp.d.ts +8 -0
  61. package/dist-cjs/protocols/acp.js +1 -0
  62. package/dist-cjs/router/failover.js +19 -1
  63. package/dist-cjs/server/index.d.ts +1 -0
  64. package/dist-cjs/server/list-page.d.ts +38 -0
  65. package/dist-cjs/server/list-page.js +56 -0
  66. package/dist-cjs/server/regions.d.ts +15 -1
  67. package/dist-cjs/server/regions.js +70 -0
  68. package/dist-cjs/server/runs.d.ts +24 -0
  69. package/dist-cjs/server/runs.js +18 -4
  70. package/dist-cjs/server/server.js +17 -5
  71. package/dist-cjs/tracing/index.d.ts +2 -2
  72. package/dist-cjs/tracing/instrument.d.ts +7 -2
  73. package/dist-cjs/tracing/instrument.js +69 -2
  74. package/dist-cjs/tracing/otlp-proto.js +18 -0
  75. package/dist-cjs/tracing/otlp.d.ts +26 -1
  76. package/dist-cjs/tracing/otlp.js +19 -0
  77. package/dist-cjs/tracing/tracer.d.ts +36 -1
  78. package/dist-cjs/tracing/tracer.js +68 -2
  79. package/dist-cjs/types/response.d.ts +18 -0
  80. package/dist-cjs/types/tracing.d.ts +32 -0
  81. package/dist-cjs/utils/media-duration.d.ts +20 -0
  82. package/dist-cjs/utils/media-duration.js +337 -0
  83. package/dist-cjs/version.d.ts +1 -1
  84. package/dist-cjs/version.js +1 -1
  85. package/dist-cjs/video/google.d.ts +50 -11
  86. package/dist-cjs/video/google.js +269 -18
  87. package/dist-cjs/video/index.d.ts +64 -6
  88. package/dist-cjs/video/index.js +134 -15
  89. package/dist-cjs/video/mock.js +12 -1
  90. package/dist-cjs/video/runway.d.ts +10 -1
  91. package/dist-cjs/video/runway.js +102 -2
  92. package/package.json +1 -1
@@ -1,4 +1,11 @@
1
1
  import { isNotification, isRequest, isResponse, LineDecoder, MCP_PROTOCOL_VERSION, McpError, } from './protocol.js';
2
+ export function mcpToolCapabilities(tool, server) {
3
+ const hints = tool.annotations;
4
+ if (!hints || typeof hints !== 'object')
5
+ return undefined;
6
+ const effect = hints.readOnlyHint === true ? 'mcp.read' : hints.destructiveHint === false ? 'mcp.write' : 'mcp.destructive';
7
+ return [`${effect}:${server}/${tool.name}`, ...(hints.openWorldHint === false ? [] : ['network'])];
8
+ }
2
9
  export class McpClient {
3
10
  transport;
4
11
  options;
@@ -8,11 +15,23 @@ export class McpClient {
8
15
  initialized = false;
9
16
  connecting;
10
17
  serverInfo = {};
18
+ toolsChanged = new Set();
19
+ stopped = false;
11
20
  constructor(transport, options = {}) {
12
21
  this.transport = transport;
13
22
  this.options = options;
14
23
  transport.onMessage((message) => this.receive(message));
15
- transport.onClose?.(() => this.failAll(new McpError('The MCP connection closed')));
24
+ transport.onClose?.(() => {
25
+ this.failAll(new McpError('The MCP connection closed'));
26
+ if (this.options.reconnect && !this.stopped)
27
+ this.initialized = false;
28
+ });
29
+ }
30
+ onToolsChanged(handler) {
31
+ this.toolsChanged.add(handler);
32
+ return () => {
33
+ this.toolsChanged.delete(handler);
34
+ };
16
35
  }
17
36
  async connect() {
18
37
  if (this.initialized)
@@ -79,24 +98,37 @@ export class McpClient {
79
98
  }
80
99
  async toNexusTools(options = {}) {
81
100
  const tools = await this.listTools();
82
- return tools.map((descriptor) => ({
83
- name: options.prefix ? `${options.prefix}_${descriptor.name}` : descriptor.name,
84
- description: descriptor.description ?? `MCP tool ${descriptor.name}`,
85
- parameters: descriptor.inputSchema ?? { type: 'object', properties: {} },
86
- execute: async (args, context) => {
87
- const progress = context?.progress;
88
- const result = await this.callTool(descriptor.name, args, progress ? { progress } : {});
89
- const text = result.content
90
- .map((part) => part.text ?? (part.type === 'text' ? '' : JSON.stringify(part)))
91
- .filter(Boolean)
92
- .join('\n');
93
- if (result.isError)
94
- throw new McpError(text || `MCP tool ${descriptor.name} failed`);
95
- return text;
96
- },
97
- }));
101
+ const server = options.server ?? options.prefix ?? this.serverInfo.name ?? 'mcp';
102
+ const declared = (descriptor) => {
103
+ const { capabilities } = options;
104
+ if (!capabilities)
105
+ return undefined;
106
+ const list = capabilities === true ? mcpToolCapabilities(descriptor, server) : capabilities(descriptor);
107
+ return list === undefined ? undefined : [...list];
108
+ };
109
+ return tools.map((descriptor) => {
110
+ const capabilities = declared(descriptor);
111
+ return {
112
+ name: options.prefix ? `${options.prefix}_${descriptor.name}` : descriptor.name,
113
+ description: descriptor.description ?? `MCP tool ${descriptor.name}`,
114
+ parameters: descriptor.inputSchema ?? { type: 'object', properties: {} },
115
+ ...(capabilities ? { capabilities } : {}),
116
+ execute: async (args, context) => {
117
+ const progress = context?.progress;
118
+ const result = await this.callTool(descriptor.name, args, progress ? { progress } : {});
119
+ const text = result.content
120
+ .map((part) => part.text ?? (part.type === 'text' ? '' : JSON.stringify(part)))
121
+ .filter(Boolean)
122
+ .join('\n');
123
+ if (result.isError)
124
+ throw new McpError(text || `MCP tool ${descriptor.name} failed`);
125
+ return text;
126
+ },
127
+ };
128
+ });
98
129
  }
99
130
  async close() {
131
+ this.stopped = true;
100
132
  this.failAll(new McpError('The MCP client was closed'));
101
133
  await this.transport.close();
102
134
  }
@@ -122,6 +154,16 @@ export class McpClient {
122
154
  this.settle(message.id, message.result, message.error ? new McpError(message.error.message, message.error.code, message.error.data) : undefined);
123
155
  return;
124
156
  }
157
+ if (isNotification(message) && message.method === 'notifications/tools/list_changed') {
158
+ for (const handler of this.toolsChanged) {
159
+ try {
160
+ handler();
161
+ }
162
+ catch {
163
+ }
164
+ }
165
+ return;
166
+ }
125
167
  if (isNotification(message) && message.method === 'notifications/progress') {
126
168
  const params = (message.params ?? {});
127
169
  const report = this.progress.get(String(params.progressToken));
@@ -216,11 +258,11 @@ export function serverEnvironment(source, options = {}) {
216
258
  }
217
259
  export function createStdioTransport(options) {
218
260
  let child;
219
- const decoder = new LineDecoder();
220
261
  let onMessage = () => undefined;
221
262
  let onClose = () => undefined;
222
263
  return {
223
264
  async start() {
265
+ const decoder = new LineDecoder();
224
266
  const { spawn } = await import('node:child_process');
225
267
  const spawned = spawn(options.command, options.args ?? [], {
226
268
  cwd: options.cwd,
@@ -297,6 +339,137 @@ export function createHttpTransport(options) {
297
339
  },
298
340
  };
299
341
  }
342
+ export function createSseTransport(options) {
343
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
344
+ let onMessage = () => undefined;
345
+ const onClose = [];
346
+ let endpoint;
347
+ let opening;
348
+ let closed = false;
349
+ let stopped = false;
350
+ let stream = new AbortController();
351
+ const ended = (from) => {
352
+ if (closed || from !== stream)
353
+ return;
354
+ closed = true;
355
+ for (const handler of onClose)
356
+ handler();
357
+ };
358
+ const open = async () => {
359
+ const own = new AbortController();
360
+ stream = own;
361
+ try {
362
+ const response = await fetchImplementation(options.url, {
363
+ headers: { accept: 'text/event-stream', ...options.headers },
364
+ signal: own.signal,
365
+ });
366
+ if (!response.ok || !response.body)
367
+ throw new McpError(`MCP SSE transport failed with ${response.status}`);
368
+ const reader = response.body.getReader();
369
+ await new Promise((resolve, reject) => {
370
+ const timer = setTimeout(() => reject(new McpError(`The MCP server named no endpoint within ${options.timeoutMs ?? 10_000}ms`)), options.timeoutMs ?? 10_000);
371
+ const event = (name, data) => {
372
+ if (name === 'endpoint') {
373
+ if (endpoint)
374
+ return;
375
+ const target = new URL(data.trim(), options.url);
376
+ if (target.origin !== new URL(options.url).origin) {
377
+ clearTimeout(timer);
378
+ reject(new McpError(`The MCP server named an endpoint on another origin, ${target.origin}`));
379
+ return;
380
+ }
381
+ endpoint = target.href;
382
+ clearTimeout(timer);
383
+ resolve();
384
+ }
385
+ else if (name === 'message') {
386
+ try {
387
+ onMessage(JSON.parse(data));
388
+ }
389
+ catch {
390
+ }
391
+ }
392
+ };
393
+ void readEvents(reader, event).then(() => {
394
+ clearTimeout(timer);
395
+ reject(new McpError('The MCP event stream ended'));
396
+ ended(own);
397
+ }, (error) => {
398
+ clearTimeout(timer);
399
+ reject(error instanceof McpError ? error : new McpError(`The MCP event stream failed: ${String(error)}`));
400
+ ended(own);
401
+ });
402
+ });
403
+ }
404
+ catch (error) {
405
+ own.abort();
406
+ ended(own);
407
+ throw error;
408
+ }
409
+ };
410
+ const started = () => {
411
+ if (closed && options.reopen && !stopped) {
412
+ closed = false;
413
+ endpoint = undefined;
414
+ opening = undefined;
415
+ }
416
+ opening ??= open();
417
+ return opening;
418
+ };
419
+ return {
420
+ start: started,
421
+ async send(message) {
422
+ await started();
423
+ if (closed || !endpoint)
424
+ throw new McpError('The MCP event stream is closed');
425
+ const response = await fetchImplementation(endpoint, {
426
+ method: 'POST',
427
+ headers: { 'content-type': 'application/json', ...options.headers },
428
+ body: JSON.stringify(message),
429
+ });
430
+ await response.body?.cancel().catch(() => undefined);
431
+ if (!response.ok)
432
+ throw new McpError(`MCP SSE transport failed with ${response.status}`);
433
+ },
434
+ onMessage(handler) {
435
+ onMessage = handler;
436
+ },
437
+ onClose(handler) {
438
+ onClose.push(handler);
439
+ },
440
+ close() {
441
+ stopped = true;
442
+ stream.abort();
443
+ ended(stream);
444
+ },
445
+ };
446
+ }
447
+ async function readEvents(reader, event) {
448
+ const decoder = new TextDecoder();
449
+ let buffer = '';
450
+ while (true) {
451
+ const { value, done } = await reader.read();
452
+ if (done)
453
+ return;
454
+ buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, '\n');
455
+ let boundary = buffer.indexOf('\n\n');
456
+ while (boundary !== -1) {
457
+ const frame = buffer.slice(0, boundary);
458
+ buffer = buffer.slice(boundary + 2);
459
+ let name = 'message';
460
+ const data = [];
461
+ for (const line of frame.split('\n')) {
462
+ if (line.startsWith('event:'))
463
+ name = line.slice(6).trim();
464
+ else if (line.startsWith('data:'))
465
+ data.push(line.slice(5).replace(/^ /, ''));
466
+ }
467
+ if (data.length)
468
+ event(name, data.join('\n'));
469
+ boundary = buffer.indexOf('\n\n');
470
+ }
471
+ }
472
+ }
300
473
  function sseData(body) {
301
474
  return body
302
475
  .split('\n')
@@ -1,3 +1,3 @@
1
- export { createHttpTransport, createStdioTransport, type HttpClientOptions, McpClient, type McpClientOptions, type McpContent, type McpResourceDescriptor, type McpToolDescriptor, type McpToolResult, type StdioClientOptions, } from './client.js';
1
+ export { createHttpTransport, createSseTransport, createStdioTransport, type HttpClientOptions, McpClient, type McpClientOptions, type McpContent, type McpResourceDescriptor, type McpToolAnnotations, type McpToolDescriptor, type McpToolResult, type McpToolsOptions, mcpToolCapabilities, type SseClientOptions, type StdioClientOptions, } from './client.js';
2
2
  export { JSON_RPC_ERRORS, type JsonRpcMessage, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, LineDecoder, MCP_PROTOCOL_VERSION, McpError, type McpTransport, } from './protocol.js';
3
- export { createStdioServerTransport, McpServer, type McpServerOptions } from './server.js';
3
+ export { createStdioServerTransport, McpServer, type McpServerOptions, mcpToolAnnotations } from './server.js';
package/dist/mcp/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { createHttpTransport, createStdioTransport, McpClient, } from './client.js';
1
+ export { createHttpTransport, createSseTransport, createStdioTransport, McpClient, mcpToolCapabilities, } from './client.js';
2
2
  export { JSON_RPC_ERRORS, LineDecoder, MCP_PROTOCOL_VERSION, McpError, } from './protocol.js';
3
- export { createStdioServerTransport, McpServer } from './server.js';
3
+ export { createStdioServerTransport, McpServer, mcpToolAnnotations } from './server.js';
@@ -1,4 +1,5 @@
1
1
  import type { ToolDefinition } from '../types/messages.js';
2
+ import type { McpToolAnnotations } from './client.js';
2
3
  import { type JsonRpcMessage, type McpTransport } from './protocol.js';
3
4
  /** Options for an MCP server. */
4
5
  export interface McpServerOptions {
@@ -17,6 +18,17 @@ export interface McpServerOptions {
17
18
  read(): Promise<unknown> | unknown;
18
19
  }>;
19
20
  }
21
+ /**
22
+ * The annotations a tool is served with, from the capabilities it declares, read so a client is never
23
+ * told less than the tool may do:
24
+ * - read-only only when every capability reads, `filesystem:read` or `mcp.read`, or there are none;
25
+ * - not destructive only when everything else it does is `mcp.write`;
26
+ * - closed to the world only when every capability is the filesystem's or an MCP tool's effect.
27
+ *
28
+ * A tool whose capabilities depend on its arguments, or that declares none, is served without
29
+ * annotations, which a client reads as the specification's defaults: it may do anything.
30
+ */
31
+ export declare function mcpToolAnnotations(tool: Pick<ToolDefinition, 'capabilities'>): McpToolAnnotations | undefined;
20
32
  /**
21
33
  * Exposes tools over MCP, so other assistants can call them.
22
34
  *
@@ -29,7 +41,7 @@ export declare class McpServer {
29
41
  private readonly tools;
30
42
  private transport;
31
43
  constructor(options?: McpServerOptions);
32
- /** Adds a tool. Returns the server, for chaining. */
44
+ /** Adds a tool, and tells a connected client its tools changed. Returns the server, for chaining. */
33
45
  register(tool: ToolDefinition): this;
34
46
  /** Starts serving requests over a transport. */
35
47
  connect(transport: McpTransport): Promise<void>;
@@ -1,4 +1,19 @@
1
+ import { parseCapability } from '../agent/capabilities.js';
1
2
  import { isNotification, isRequest, JSON_RPC_ERRORS, LineDecoder, MCP_PROTOCOL_VERSION, McpError, textContent, } from './protocol.js';
3
+ const CLOSED = new Set(['filesystem', 'mcp.read', 'mcp.write', 'mcp.destructive']);
4
+ export function mcpToolAnnotations(tool) {
5
+ const declared = tool.capabilities;
6
+ if (!declared || typeof declared === 'function')
7
+ return undefined;
8
+ const parsed = declared.map((capability) => parseCapability(capability));
9
+ const reads = (item) => (item.kind === 'filesystem' && item.access === 'read') || item.kind === 'mcp.read';
10
+ const readOnly = parsed.every(reads);
11
+ return {
12
+ readOnlyHint: readOnly,
13
+ ...(readOnly ? {} : { destructiveHint: !parsed.every((item) => reads(item) || item.kind === 'mcp.write') }),
14
+ openWorldHint: !parsed.every((item) => CLOSED.has(item.kind)),
15
+ };
16
+ }
2
17
  export class McpServer {
3
18
  options;
4
19
  tools = new Map();
@@ -10,6 +25,9 @@ export class McpServer {
10
25
  }
11
26
  register(tool) {
12
27
  this.tools.set(tool.name, tool);
28
+ if (this.transport) {
29
+ void Promise.resolve(this.transport.send({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' })).catch(() => undefined);
30
+ }
13
31
  return this;
14
32
  }
15
33
  async connect(transport) {
@@ -48,7 +66,7 @@ export class McpServer {
48
66
  return {
49
67
  protocolVersion: MCP_PROTOCOL_VERSION,
50
68
  capabilities: {
51
- tools: { listChanged: false },
69
+ tools: { listChanged: true },
52
70
  ...(this.options.resources?.length ? { resources: { listChanged: false } } : {}),
53
71
  },
54
72
  serverInfo: { name: this.options.name ?? 'nexus-ai-pro', version: this.options.version ?? '1.x' },
@@ -57,11 +75,15 @@ export class McpServer {
57
75
  return {};
58
76
  case 'tools/list':
59
77
  return {
60
- tools: [...this.tools.values()].map((item) => ({
61
- name: item.name,
62
- description: item.description,
63
- inputSchema: item.parameters ?? { type: 'object', properties: {} },
64
- })),
78
+ tools: [...this.tools.values()].map((item) => {
79
+ const annotations = mcpToolAnnotations(item);
80
+ return {
81
+ name: item.name,
82
+ description: item.description,
83
+ inputSchema: item.parameters ?? { type: 'object', properties: {} },
84
+ ...(annotations ? { annotations } : {}),
85
+ };
86
+ }),
65
87
  };
66
88
  case 'tools/call': {
67
89
  const name = String(params.name ?? '');
@@ -10,9 +10,11 @@
10
10
  *
11
11
  * A tool is offered only when the editor said it can do that thing, in `initialize`. Each declares
12
12
  * what it touches, as `filesystem:read:<path>`, `filesystem:write:<path>`, or `shell:<command>`, so a
13
- * permission policy decides it like any other tool, and an approval asks the person in the editor.
13
+ * permission policy decides it like any other tool, and an approval asks the person in the editor. An
14
+ * MCP server's tools, over stdio, HTTP, or SSE, declare what their server says they do.
14
15
  */
15
16
  import type { ToolSourceContext } from '../agent/create-agent.js';
17
+ import { type McpToolDescriptor } from '../mcp/client.js';
16
18
  import type { ToolDefinition } from '../types/messages.js';
17
19
  import type { AcpEditorBridge } from './acp.js';
18
20
  /** Options for `acpEditor()`. */
@@ -25,8 +27,15 @@ export interface AcpEditorOptions {
25
27
  mcp?: boolean;
26
28
  /** How long a request to an MCP server may take, in milliseconds. Defaults to 30 seconds. */
27
29
  mcpTimeoutMs?: number;
28
- /** Replaces `fetch` for MCP servers reached over HTTP: a proxy, or a test. */
30
+ /** Replaces `fetch` for MCP servers reached over HTTP or SSE: a proxy, or a test. */
29
31
  mcpFetch?: typeof globalThis.fetch;
32
+ /**
33
+ * What an MCP server's tools declare they do, for a permission policy. `true`, the default, reads
34
+ * it from the server's annotations, with `mcpToolCapabilities()`: `mcp.read`, `mcp.write`, or
35
+ * `mcp.destructive` under the server's name, and `network` for a tool that reaches beyond it. A
36
+ * function decides per tool, and `false` declares nothing, as before 2.17.
37
+ */
38
+ mcpToolCapabilities?: boolean | ((tool: McpToolDescriptor, server: string) => readonly string[] | undefined);
30
39
  /** The most terminal output a command returns, in bytes. Defaults to 64 KB. */
31
40
  maxOutputBytes?: number;
32
41
  /** Receives an MCP server that failed to start, whose tools are then not offered. */
@@ -34,6 +43,11 @@ export interface AcpEditorOptions {
34
43
  }
35
44
  /** The editor, as tools for an agent. */
36
45
  export interface AcpEditor extends AcpEditorBridge {
46
+ /** The MCP transports it connects, which the agent tells the editor in `initialize`. */
47
+ readonly mcpCapabilities: {
48
+ http: boolean;
49
+ sse: boolean;
50
+ };
37
51
  /** The tools of one session, for `CreateAgentOptions.toolSource`: the session is the run's thread. */
38
52
  toolSource(context: ToolSourceContext): Promise<ToolDefinition[]>;
39
53
  /** Closes every MCP server it connected. */
@@ -1,4 +1,4 @@
1
- import { createHttpTransport, createStdioTransport, McpClient } from '../mcp/client.js';
1
+ import { createHttpTransport, createSseTransport, createStdioTransport, McpClient, } from '../mcp/client.js';
2
2
  export function acpEditor(options = {}) {
3
3
  let connection;
4
4
  const servers = new Map();
@@ -7,11 +7,20 @@ export function acpEditor(options = {}) {
7
7
  let connected = servers.get(session.id);
8
8
  if (!connected) {
9
9
  connected = (async () => {
10
- const clients = [];
11
- const tools = [];
10
+ const entries = [];
11
+ const declare = options.mcpToolCapabilities ?? true;
12
12
  for (const offered of session.mcpServers) {
13
13
  const name = String(offered.name ?? 'mcp');
14
+ const prefix = name.replace(/[^A-Za-z0-9_-]/g, '_');
14
15
  try {
16
+ const remote = offered.url
17
+ ? {
18
+ url: offered.url,
19
+ headers: Object.fromEntries((offered.headers ?? []).map((item) => [item.name, item.value])),
20
+ ...(options.mcpFetch ? { fetch: options.mcpFetch } : {}),
21
+ }
22
+ : undefined;
23
+ const sse = remote && offered.type === 'sse';
15
24
  const transport = offered.command
16
25
  ? createStdioTransport({
17
26
  command: offered.command,
@@ -19,24 +28,40 @@ export function acpEditor(options = {}) {
19
28
  env: Object.fromEntries((offered.env ?? []).map((item) => [item.name, item.value])),
20
29
  ...(session.cwd ? { cwd: session.cwd } : {}),
21
30
  })
22
- : offered.url && (offered.type === 'http' || offered.type === undefined)
23
- ? createHttpTransport({
24
- url: offered.url,
25
- headers: Object.fromEntries((offered.headers ?? []).map((item) => [item.name, item.value])),
26
- ...(options.mcpFetch ? { fetch: options.mcpFetch } : {}),
27
- })
28
- : undefined;
31
+ : sse
32
+ ?
33
+ createSseTransport({ ...remote, timeoutMs: options.mcpTimeoutMs ?? 30_000, reopen: true })
34
+ : remote && (offered.type === 'http' || offered.type === undefined)
35
+ ? createHttpTransport(remote)
36
+ : undefined;
29
37
  if (!transport)
30
38
  throw new Error(`MCP server "${name}" uses a transport this agent does not speak`);
31
- const client = new McpClient(transport, { timeoutMs: options.mcpTimeoutMs ?? 30_000 });
32
- tools.push(...(await client.toNexusTools({ prefix: name.replace(/[^A-Za-z0-9_-]/g, '_') })));
33
- clients.push(client);
39
+ const client = new McpClient(transport, {
40
+ timeoutMs: options.mcpTimeoutMs ?? 30_000,
41
+ ...(sse ? { reconnect: true } : {}),
42
+ });
43
+ const entry = {
44
+ name,
45
+ client,
46
+ tools: [],
47
+ stale: false,
48
+ list: () => client.toNexusTools({
49
+ prefix,
50
+ server: prefix,
51
+ capabilities: typeof declare === 'function' ? (tool) => declare(tool, prefix) : declare,
52
+ }),
53
+ };
54
+ entries.push(entry);
55
+ client.onToolsChanged(() => {
56
+ entry.stale = true;
57
+ });
58
+ entry.tools = await entry.list();
34
59
  }
35
60
  catch (error) {
36
61
  options.onError?.(error, name);
37
62
  }
38
63
  }
39
- return { clients, tools };
64
+ return entries;
40
65
  })();
41
66
  servers.set(session.id, connected);
42
67
  }
@@ -130,6 +155,7 @@ export function acpEditor(options = {}) {
130
155
  return list;
131
156
  };
132
157
  return {
158
+ mcpCapabilities: { http: options.mcp !== false, sse: options.mcp !== false },
133
159
  attach(next) {
134
160
  connection = next;
135
161
  },
@@ -143,13 +169,26 @@ export function acpEditor(options = {}) {
143
169
  const own = tools(session);
144
170
  if (options.mcp === false || session.mcpServers.length === 0)
145
171
  return own;
146
- return [...own, ...(await mcpTools(session)).tools];
172
+ const entries = await mcpTools(session);
173
+ await Promise.all(entries
174
+ .filter((entry) => entry.stale)
175
+ .map(async (entry) => {
176
+ entry.stale = false;
177
+ try {
178
+ entry.tools = await entry.list();
179
+ }
180
+ catch (error) {
181
+ entry.stale = true;
182
+ options.onError?.(error, entry.name);
183
+ }
184
+ }));
185
+ return [...own, ...entries.flatMap((entry) => entry.tools)];
147
186
  },
148
187
  async close() {
149
188
  const connected = [...servers.values()];
150
189
  servers.clear();
151
- for (const { clients } of await Promise.all(connected)) {
152
- await Promise.all(clients.map((client) => client.close().catch(() => undefined)));
190
+ for (const entries of await Promise.all(connected)) {
191
+ await Promise.all(entries.map((entry) => entry.client.close().catch(() => undefined)));
153
192
  }
154
193
  },
155
194
  };
@@ -104,6 +104,14 @@ export interface AcpConnection {
104
104
  }
105
105
  /** Something that uses the editor's connection, such as `acpEditor()`. `serveAcp()` attaches it when it starts. */
106
106
  export interface AcpEditorBridge {
107
+ /**
108
+ * The MCP transports it connects beyond stdio, which the agent tells the editor in `initialize` so
109
+ * the editor offers servers over them too.
110
+ */
111
+ readonly mcpCapabilities?: {
112
+ http?: boolean;
113
+ sse?: boolean;
114
+ };
107
115
  /** Receives the connection to the editor. */
108
116
  attach(connection: AcpConnection): void;
109
117
  /** Called when the editor closes its end, or the server is closed: the place to stop what it started. */
@@ -249,6 +249,7 @@ export function serveAcp(graph, streams, options = {}) {
249
249
  agentCapabilities: {
250
250
  loadSession: true,
251
251
  promptCapabilities: { image: true, audio: true, embeddedContext: true },
252
+ ...(options.editor?.mcpCapabilities ? { mcpCapabilities: options.editor.mcpCapabilities } : {}),
252
253
  },
253
254
  authMethods: [],
254
255
  ...(options.agentInfo ? { agentInfo: options.agentInfo } : {}),
@@ -7,6 +7,7 @@ export class FailoverExecutor {
7
7
  async complete(request, decision, providers, options = {}) {
8
8
  const attempts = attemptsOf(decision);
9
9
  const errors = [];
10
+ const failures = [];
10
11
  const retry = this.mergeRetry(options.retry, request.retry);
11
12
  const timeoutMs = this.resolveTimeout(request.timeoutMs, options.timeoutMs);
12
13
  const refreshed = new Set();
@@ -37,6 +38,8 @@ export class FailoverExecutor {
37
38
  if (retryIndex > 0) {
38
39
  response.meta.guardrailsApplied.push(`provider-retry-${retryIndex}`);
39
40
  }
41
+ if (failures.length)
42
+ response.meta.failedAttempts = failures;
40
43
  return response;
41
44
  }
42
45
  catch (caught) {
@@ -53,6 +56,7 @@ export class FailoverExecutor {
53
56
  }
54
57
  const message = `${attempt.providerName}/${attempt.model}: ${error instanceof Error ? error.message : String(error)}`;
55
58
  errors.push(message);
59
+ failures.push(failureOf(attempt, error));
56
60
  if (error instanceof UnsettledProviderAttemptError) {
57
61
  break attemptsLoop;
58
62
  }
@@ -108,6 +112,7 @@ export class FailoverExecutor {
108
112
  const retry = this.mergeRetry(options.retry, request.retry);
109
113
  const timeoutMs = this.resolveTimeout(request.timeoutMs, options.timeoutMs);
110
114
  const errors = [];
115
+ const failures = [];
111
116
  const refreshed = new Set();
112
117
  attemptsLoop: for (const attempt of attempts) {
113
118
  const provider = providers.get(attempt.providerName);
@@ -151,7 +156,9 @@ export class FailoverExecutor {
151
156
  if (chunk.type === 'error')
152
157
  throw new Error(chunk.error || 'stream error');
153
158
  emitted = emitted || chunk.type === 'text' || chunk.type === 'tool_call';
154
- yield chunk;
159
+ yield chunk.type === 'done' && failures.length
160
+ ? { ...chunk, meta: { ...chunk.meta, failedAttempts: [...failures] } }
161
+ : chunk;
155
162
  }
156
163
  }
157
164
  catch (caught) {
@@ -184,6 +191,7 @@ export class FailoverExecutor {
184
191
  return;
185
192
  }
186
193
  errors.push(`${attempt.providerName}/${attempt.model}: ${attemptError instanceof Error ? attemptError.message : String(attemptError)}`);
194
+ failures.push(failureOf(attempt, attemptError));
187
195
  if (attemptError instanceof UnsettledProviderAttemptError)
188
196
  break attemptsLoop;
189
197
  if (emitted)
@@ -423,3 +431,13 @@ function attemptsOf(decision) {
423
431
  function maxRetriesFor(attempt, retry) {
424
432
  return Math.max(retry.maxRetries, attempt.rateLimit?.maxRetries ?? 0);
425
433
  }
434
+ function failureOf(attempt, error) {
435
+ const category = error instanceof NexusProviderError ? error.category : undefined;
436
+ return {
437
+ provider: attempt.providerName,
438
+ model: attempt.model,
439
+ error: error instanceof Error ? error.message : String(error),
440
+ ...(category ? { category } : {}),
441
+ at: new Date().toISOString(),
442
+ };
443
+ }
@@ -12,6 +12,7 @@ export { CronScheduler, type CronSchedulerOptions, type DueCronJob, parseCron }
12
12
  export { AssistantCapabilityError, BadRequestError, ForbiddenError, NotFoundError, RunHandOffError, ServerError, type TenantLimit, TenantLimitError, ThreadBusyError, UnauthorizedError, } from './errors.js';
13
13
  export { MemoryRunEventLog, type MemoryRunEventLogOptions, type RedisEventLogLikeClient, RedisRunEventLog, type RedisRunEventLogOptions, } from './events.js';
14
14
  export { type NodeListenerOptions, type NodeRequestLike, type NodeResponseLike, toNodeListener, } from './node.js';
15
+ export type { ListPage } from './list-page.js';
15
16
  export { type DrainResult, RunManager, type RunManagerOptions, type RunQueueOptions, type StartRunOptions, } from './runs.js';
16
17
  export { type AgentServer, type AgentServerOptions, createAgentServer } from './server.js';
17
18
  export { fromStore, MemoryServerStore, type StoreLike } from './state.js';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The order an agent server lists threads and runs in, and the cursor that pages through it. The
3
+ * regional gateway reads both, so a cursor one region gave pages every region the same way.
4
+ */
5
+ /** A page of a list, newest first, and the cursor of the page after it. */
6
+ export interface ListPage<T> {
7
+ /** The page's records, newest first. */
8
+ items: T[];
9
+ /** Asks for the page after this one, as `cursor`. Absent on the last page. */
10
+ nextCursor?: string;
11
+ }
12
+ /** A record as a list places it: by its last write, then its id. */
13
+ interface Listed {
14
+ id?: unknown;
15
+ updatedAt?: unknown;
16
+ createdAt?: unknown;
17
+ }
18
+ /** Where a record falls in a list. */
19
+ interface Place {
20
+ at: string;
21
+ id: string;
22
+ }
23
+ /** Every list's order: the newest written first, and by id among records written at the same time. */
24
+ export declare function newestFirst(a: Listed, b: Listed): number;
25
+ /** The cursor of the page after one that ends with `record`. */
26
+ export declare function cursorAfter(record: Listed): string;
27
+ /** The place a cursor names, or `undefined` for one no list gave. */
28
+ export declare function readCursor(cursor: string): Place | undefined;
29
+ /** Whether `record` comes after `place`, so belongs to the pages after the cursor that names it. */
30
+ export declare function isAfter(record: Listed, place: Place): boolean;
31
+ /**
32
+ * The page of the records `keep` takes, after `after`, from a store that lists newest written first.
33
+ * It reads more of the store until the page is full or the store ends. Until the store ends, only
34
+ * records newer than the oldest one read are placed for sure: another written in that same instant
35
+ * may be unread yet, and come before them by id.
36
+ */
37
+ export declare function pageOf<R extends Listed>(list: (limit: number) => Promise<R[]> | R[], keep: (record: R) => boolean, limit: number, after: Place | undefined): Promise<ListPage<R>>;
38
+ export {};