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.
- package/API_STABILITY.md +57 -0
- package/CHANGELOG.md +131 -1
- package/README.md +10 -4
- package/dist/agent/capabilities.d.ts +3 -0
- package/dist/evaluate/media.d.ts +6 -2
- package/dist/evaluate/media.js +7 -21
- package/dist/index.d.ts +1 -1
- package/dist/mcp/client.d.ts +86 -4
- package/dist/mcp/client.js +191 -18
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/server.d.ts +13 -1
- package/dist/mcp/server.js +28 -6
- package/dist/protocols/acp-editor.d.ts +16 -2
- package/dist/protocols/acp-editor.js +56 -17
- package/dist/protocols/acp.d.ts +8 -0
- package/dist/protocols/acp.js +1 -0
- package/dist/router/failover.js +19 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/list-page.d.ts +38 -0
- package/dist/server/list-page.js +49 -0
- package/dist/server/regions.d.ts +15 -1
- package/dist/server/regions.js +70 -0
- package/dist/server/runs.d.ts +24 -0
- package/dist/server/runs.js +18 -4
- package/dist/server/server.js +17 -5
- package/dist/tracing/index.d.ts +2 -2
- package/dist/tracing/instrument.d.ts +7 -2
- package/dist/tracing/instrument.js +69 -2
- package/dist/tracing/otlp-proto.js +18 -0
- package/dist/tracing/otlp.d.ts +26 -1
- package/dist/tracing/otlp.js +19 -0
- package/dist/tracing/tracer.d.ts +36 -1
- package/dist/tracing/tracer.js +68 -2
- package/dist/types/response.d.ts +18 -0
- package/dist/types/tracing.d.ts +32 -0
- package/dist/utils/media-duration.d.ts +20 -0
- package/dist/utils/media-duration.js +334 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/video/google.d.ts +50 -11
- package/dist/video/google.js +270 -19
- package/dist/video/index.d.ts +64 -6
- package/dist/video/index.js +134 -15
- package/dist/video/mock.js +12 -1
- package/dist/video/runway.d.ts +10 -1
- package/dist/video/runway.js +102 -2
- package/dist-cjs/agent/capabilities.d.ts +3 -0
- package/dist-cjs/evaluate/media.d.ts +6 -2
- package/dist-cjs/evaluate/media.js +9 -21
- package/dist-cjs/index.d.ts +1 -1
- package/dist-cjs/mcp/client.d.ts +86 -4
- package/dist-cjs/mcp/client.js +193 -18
- package/dist-cjs/mcp/index.d.ts +2 -2
- package/dist-cjs/mcp/index.js +4 -1
- package/dist-cjs/mcp/server.d.ts +13 -1
- package/dist-cjs/mcp/server.js +29 -6
- package/dist-cjs/protocols/acp-editor.d.ts +16 -2
- package/dist-cjs/protocols/acp-editor.js +55 -16
- package/dist-cjs/protocols/acp.d.ts +8 -0
- package/dist-cjs/protocols/acp.js +1 -0
- package/dist-cjs/router/failover.js +19 -1
- package/dist-cjs/server/index.d.ts +1 -0
- package/dist-cjs/server/list-page.d.ts +38 -0
- package/dist-cjs/server/list-page.js +56 -0
- package/dist-cjs/server/regions.d.ts +15 -1
- package/dist-cjs/server/regions.js +70 -0
- package/dist-cjs/server/runs.d.ts +24 -0
- package/dist-cjs/server/runs.js +18 -4
- package/dist-cjs/server/server.js +17 -5
- package/dist-cjs/tracing/index.d.ts +2 -2
- package/dist-cjs/tracing/instrument.d.ts +7 -2
- package/dist-cjs/tracing/instrument.js +69 -2
- package/dist-cjs/tracing/otlp-proto.js +18 -0
- package/dist-cjs/tracing/otlp.d.ts +26 -1
- package/dist-cjs/tracing/otlp.js +19 -0
- package/dist-cjs/tracing/tracer.d.ts +36 -1
- package/dist-cjs/tracing/tracer.js +68 -2
- package/dist-cjs/types/response.d.ts +18 -0
- package/dist-cjs/types/tracing.d.ts +32 -0
- package/dist-cjs/utils/media-duration.d.ts +20 -0
- package/dist-cjs/utils/media-duration.js +337 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/dist-cjs/video/google.d.ts +50 -11
- package/dist-cjs/video/google.js +269 -18
- package/dist-cjs/video/index.d.ts +64 -6
- package/dist-cjs/video/index.js +134 -15
- package/dist-cjs/video/mock.js +12 -1
- package/dist-cjs/video/runway.d.ts +10 -1
- package/dist-cjs/video/runway.js +102 -2
- package/package.json +1 -1
package/dist/mcp/client.js
CHANGED
|
@@ -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?.(() =>
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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')
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -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>;
|
package/dist/mcp/server.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
11
|
-
const
|
|
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
|
-
:
|
|
23
|
-
?
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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, {
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
152
|
-
await Promise.all(
|
|
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
|
};
|
package/dist/protocols/acp.d.ts
CHANGED
|
@@ -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. */
|
package/dist/protocols/acp.js
CHANGED
|
@@ -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 } : {}),
|
package/dist/router/failover.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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 {};
|