nexus-ai-pro 2.16.0 → 2.17.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 (44) hide show
  1. package/API_STABILITY.md +22 -0
  2. package/CHANGELOG.md +54 -1
  3. package/README.md +6 -4
  4. package/dist/agent/capabilities.d.ts +3 -0
  5. package/dist/mcp/client.d.ts +62 -3
  6. package/dist/mcp/client.js +154 -16
  7. package/dist/mcp/index.d.ts +2 -2
  8. package/dist/mcp/index.js +2 -2
  9. package/dist/mcp/server.d.ts +12 -0
  10. package/dist/mcp/server.js +24 -5
  11. package/dist/protocols/acp-editor.d.ts +16 -2
  12. package/dist/protocols/acp-editor.js +21 -9
  13. package/dist/protocols/acp.d.ts +8 -0
  14. package/dist/protocols/acp.js +1 -0
  15. package/dist/version.d.ts +1 -1
  16. package/dist/version.js +1 -1
  17. package/dist/video/google.d.ts +42 -11
  18. package/dist/video/google.js +249 -20
  19. package/dist/video/index.d.ts +49 -6
  20. package/dist/video/index.js +91 -15
  21. package/dist/video/mock.js +10 -1
  22. package/dist/video/runway.d.ts +8 -1
  23. package/dist/video/runway.js +68 -2
  24. package/dist-cjs/agent/capabilities.d.ts +3 -0
  25. package/dist-cjs/mcp/client.d.ts +62 -3
  26. package/dist-cjs/mcp/client.js +156 -16
  27. package/dist-cjs/mcp/index.d.ts +2 -2
  28. package/dist-cjs/mcp/index.js +4 -1
  29. package/dist-cjs/mcp/server.d.ts +12 -0
  30. package/dist-cjs/mcp/server.js +25 -5
  31. package/dist-cjs/protocols/acp-editor.d.ts +16 -2
  32. package/dist-cjs/protocols/acp-editor.js +20 -8
  33. package/dist-cjs/protocols/acp.d.ts +8 -0
  34. package/dist-cjs/protocols/acp.js +1 -0
  35. package/dist-cjs/version.d.ts +1 -1
  36. package/dist-cjs/version.js +1 -1
  37. package/dist-cjs/video/google.d.ts +42 -11
  38. package/dist-cjs/video/google.js +248 -19
  39. package/dist-cjs/video/index.d.ts +49 -6
  40. package/dist-cjs/video/index.js +91 -15
  41. package/dist-cjs/video/mock.js +10 -1
  42. package/dist-cjs/video/runway.d.ts +8 -1
  43. package/dist-cjs/video/runway.js +68 -2
  44. package/package.json +1 -1
package/API_STABILITY.md CHANGED
@@ -41,6 +41,28 @@ Both are covered by this policy:
41
41
  Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
42
42
  one of its explicit subpaths.
43
43
 
44
+ ## 2.17 stage (2.17.0)
45
+
46
+ Everything 2.17 adds is stable from the start and additive, unless this section says otherwise.
47
+
48
+ New and stable:
49
+ - in `nexus-ai-pro/mcp`: `createSseTransport()`, `SseClientOptions`, `mcpToolCapabilities()`,
50
+ `mcpToolAnnotations()`, `McpToolAnnotations`, `McpToolsOptions`, and `McpToolDescriptor.annotations`;
51
+ the capability kinds `mcp.read`, `mcp.write`, and `mcp.destructive`;
52
+ - in `nexus-ai-pro/protocols/acp/editor`: `AcpEditorOptions.mcpToolCapabilities` and
53
+ `AcpEditor.mcpCapabilities`; in `nexus-ai-pro/protocols/acp`: `AcpEditorBridge.mcpCapabilities`;
54
+ - in `nexus-ai-pro/video`, experimental with the family: `VideoSource`, `VideoGenerateRequest.video`,
55
+ `VideoProviderRequest.video`, the `VideoInputKind` values `extend` and `edit`,
56
+ `VideoModelCapabilities.extendDurations`, `requires`, and `continuesJobs`,
57
+ `VideoProviderCapabilities.inputVideoMimeTypes`, and `VideoConformanceOptions.video`; Gemini Omni
58
+ Flash in `nexus-ai-pro/video/google`, and video to video in `nexus-ai-pro/video/runway`.
59
+
60
+ Changed on purpose: `acpEditor()` reads MCP tool annotations by default. A policy that allowed the
61
+ editor's MCP tools as undeclared (`undeclared: 'allow'`) now decides annotated tools by their
62
+ capabilities; `mcpToolCapabilities: false` keeps the earlier decisions. `GoogleVideoProvider`'s
63
+ default models follow Google's shutdown of the Veo 3.1 previews: Gemini Omni Flash on the Gemini API,
64
+ and the generally available Veo 3.1 models on Vertex AI.
65
+
44
66
  ## 2.16 stage (2.16.0)
45
67
 
46
68
  Everything 2.16 adds is stable from the start and additive, unless this section says otherwise.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.17.0] - 2026-10-21
8
+
9
+ The editor's MCP servers over every transport, decided by what their servers say they do; and video
10
+ extended or edited on every provider, with Gemini Omni Flash on Google's Interactions API.
11
+
12
+ ### Added
13
+
14
+ - **Video extended or edited.** A `VideoGenerateRequest` takes a `video`, a `VideoSource`: `extend`
15
+ continues it, and `edit` changes it as the prompt says. The video is an input (bytes, a URL, or a
16
+ stored asset), or `from` a finished job of the same provider, which continues from what it holds
17
+ with nothing uploaded. Models declare `extend` and `edit` among their `inputs`, with
18
+ `extendDurations`, `continuesJobs`, and `requires`, and providers their `inputVideoMimeTypes`;
19
+ anything a model cannot do is refused before it is sent, and the input travels with a durable
20
+ generation. The conformance suite checks extending and editing, and continuing a job.
21
+ - **Gemini Omni Flash on the Interactions API.** `GoogleVideoProvider` serves `gemini-omni-1.1-flash`
22
+ as a background interaction: 3 to 10 seconds at 360p to 4k, from a prompt, a first frame, or
23
+ reference images, extending and editing videos, continuing its own interactions by id, cancelled and
24
+ deleted with them. It is priced at its 720p token rate.
25
+ - **Veo extends on Vertex AI.** Veo 3.1 extends a video in Cloud Storage by seven seconds, or a job of
26
+ its own when `storageUri` keeps its videos there, and makes up to four videos a request.
27
+ - **Runway's video to video.** `gemini_omni_flash_1.1` extends and edits, `gemini_omni_flash` edits,
28
+ and `aleph2`, now among the models, only edits, from a video by URL, inline up to 5 MB, or a task's
29
+ own video.
30
+
31
+ - **MCP over SSE.** `createSseTransport()`, on `nexus-ai-pro/mcp`, speaks to a server over HTTP with
32
+ Server-Sent Events, MCP's transport before streamable HTTP: one `GET` on the event stream, a `POST`
33
+ per message to the endpoint the stream names, kept to the stream's origin. An ACP agent connects the
34
+ SSE servers an editor offers, beside stdio and HTTP.
35
+ - **MCP tools that declare what they do.** `toNexusTools({ capabilities: true })` gives each tool the
36
+ capabilities its server's annotations describe, from `mcpToolCapabilities()`: `mcp.read`,
37
+ `mcp.write`, or `mcp.destructive` under the server's name, and `network` for one that reaches
38
+ beyond it, with the specification's defaults for a hint left out. A permission policy then decides a
39
+ server's tools as it decides the agent's own: `{ custom: { 'mcp.read': 'allow' } }` runs a lookup
40
+ and refuses a delete, with no rule naming either. A function decides per tool instead.
41
+ - **The package's MCP server says what its tools do.** `McpServer` lists each tool with the
42
+ annotations its capabilities give it, from `mcpToolAnnotations()`, never claiming less than the
43
+ tool may do.
44
+
45
+ ### Changed
46
+
47
+ - **Google's video defaults follow its shutdown of the Veo 3.1 previews on 2026-10-22.** On the Gemini
48
+ API, `GoogleVideoProvider` serves Gemini Omni Flash by default; on Vertex AI, the generally
49
+ available `veo-3.1-generate-001` and `veo-3.1-fast-generate-001`, and `veo-3.1-lite-generate-001`.
50
+ Name the preview models in `models` to keep them until Google stops serving them.
51
+ - `acpEditor()` reads its MCP servers' annotations by default (`mcpToolCapabilities`), so an
52
+ annotated tool is decided by its capabilities rather than as undeclared; `mcpToolCapabilities: false`
53
+ keeps the 2.16 behaviour. A tool its server says nothing about is undeclared, as before.
54
+ - The ACP agent tells the editor, in `initialize`, that it connects MCP servers over HTTP and SSE, so
55
+ an editor that follows the protocol offers them; before, it offered only stdio servers.
56
+ - The nightly interoperability check offers the ACP agent MCP servers built with
57
+ `@modelcontextprotocol/sdk` over stdio, HTTP, and SSE.
58
+
7
59
  ## [2.16.0] - 2026-10-20
8
60
 
9
61
  A2A on all three of its bindings over one set of tasks, and webhooks told the answer as it is written.
@@ -3051,7 +3103,8 @@ shared policy vocabulary is in place for a future release that revisits this.
3051
3103
  - URL fetching rejects unsafe private-network targets and limits response reads.
3052
3104
  - CLI and audit findings no longer disclose detected secret values.
3053
3105
 
3054
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.16.0...HEAD
3106
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.17.0...HEAD
3107
+ [2.17.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.16.0...v2.17.0
3055
3108
  [2.16.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.1...v2.16.0
3056
3109
  [2.15.1]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.0...v2.15.1
3057
3110
  [2.15.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.14.0...v2.15.0
package/README.md CHANGED
@@ -324,13 +324,15 @@ Shipped so far:
324
324
  [page](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/COMPATIBILITY.md) lists them;
325
325
  - 2.16 A2A over every binding: one agent served over JSON-RPC, HTTP+JSON, and gRPC from one task
326
326
  store, so a caller on any binding continues a task another started, and webhooks told the answer
327
- as it is written.
327
+ as it is written;
328
+ - 2.17 the editor's MCP servers over every transport, and video you can extend or edit: an ACP agent
329
+ connects stdio, HTTP, and SSE servers and decides each tool by what its server says it does, and a
330
+ video is extended from its own job or edited on Gemini Omni Flash, Veo, and Runway.
328
331
 
329
332
  Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
330
333
  [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
331
- Next, 2.17 connects the MCP servers an editor gives an ACP agent over SSE as well as stdio and HTTP,
332
- and reads what their tools declare they do, so a permission policy covers them without a rule for
333
- each. Pinecone, images, the Gemini
334
+ Next, 2.18 records events and links on spans, as OpenTelemetry has them, and reads the length of
335
+ compressed audio and video, so an evaluation holds a clip to its duration. Pinecone, images, the Gemini
334
336
  embedding batch tier, media understanding, video, Gemini Live, SIP, and both phone providers
335
337
  graduate once the live workflow passes against each. The
336
338
  [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
@@ -9,6 +9,9 @@ import type { ToolDefinition } from '../types/messages.js';
9
9
  * | `network`, `network:<host or URL>` | Opens a network connection, to a host when given |
10
10
  * | `shell`, `shell:<command line>` | Runs a shell command |
11
11
  * | `code` | Runs code it was given, as an interpreter does |
12
+ * | `mcp.read:<server>/<tool>` | An MCP tool its server says only reads |
13
+ * | `mcp.write:<server>/<tool>` | An MCP tool its server says changes things but destroys nothing |
14
+ * | `mcp.destructive:<server>/<tool>` | An MCP tool its server says may destroy or overwrite |
12
15
  * | `<name>`, `<name>:<detail>` | Anything the application names, such as `payments:refund` |
13
16
  *
14
17
  * A tool that declares `capabilities: []` does nothing to the world; one that declares none at all
@@ -1,5 +1,21 @@
1
1
  import type { ToolDefinition } from '../types/messages.js';
2
2
  import { type McpTransport } from './protocol.js';
3
+ /**
4
+ * What a server says a tool does, as MCP's tool annotations: hints, which a client trusts only as far
5
+ * as it trusts the server. A hint left out takes the specification's default.
6
+ */
7
+ export interface McpToolAnnotations {
8
+ /** A name for people. */
9
+ title?: string;
10
+ /** It changes nothing. Defaults to false. */
11
+ readOnlyHint?: boolean;
12
+ /** When it changes things, it may destroy or overwrite them, not only add. Defaults to true. */
13
+ destructiveHint?: boolean;
14
+ /** Calling it again with the same arguments changes nothing more. Defaults to false. */
15
+ idempotentHint?: boolean;
16
+ /** It reaches things beyond the server, such as the web. Defaults to true. */
17
+ openWorldHint?: boolean;
18
+ }
3
19
  /** A tool an MCP server offers. */
4
20
  export interface McpToolDescriptor {
5
21
  /** The tool's name. */
@@ -8,6 +24,33 @@ export interface McpToolDescriptor {
8
24
  description?: string;
9
25
  /** JSON Schema for the arguments. */
10
26
  inputSchema?: Record<string, unknown>;
27
+ /** What the server says the tool does. */
28
+ annotations?: McpToolAnnotations;
29
+ }
30
+ /**
31
+ * The capabilities an MCP tool declares, from what its server says of it in its annotations, so a
32
+ * permission policy decides it as it decides any tool:
33
+ * - `mcp.read:<server>/<tool>` for a tool that only reads;
34
+ * - `mcp.write:<server>/<tool>` for one that changes things but destroys nothing;
35
+ * - `mcp.destructive:<server>/<tool>` for one that may destroy or overwrite;
36
+ * - and `network`, beside it, for one that reaches beyond its server.
37
+ *
38
+ * A hint left out takes the specification's default: not read-only, destructive, and open. A tool
39
+ * with no annotations at all declares nothing, `undefined`, so a policy decides it as undeclared.
40
+ */
41
+ export declare function mcpToolCapabilities(tool: McpToolDescriptor, server: string): string[] | undefined;
42
+ /** Options for `McpClient.toNexusTools()`. */
43
+ export interface McpToolsOptions {
44
+ /** Put before each tool's name with an underscore, so two servers' `search` tools differ. */
45
+ prefix?: string;
46
+ /**
47
+ * The capabilities each tool declares: `true` reads them from the server's annotations, with
48
+ * `mcpToolCapabilities()`, under the server's name (the prefix, or the name the server gives); a
49
+ * function decides them per tool. Without it, a tool declares none, as before 2.17.
50
+ */
51
+ capabilities?: boolean | ((tool: McpToolDescriptor) => readonly string[] | undefined);
52
+ /** The server's name in each capability, when `capabilities` is `true`. Defaults to the prefix. */
53
+ server?: string;
11
54
  }
12
55
  /** A resource an MCP server offers. */
13
56
  export interface McpResourceDescriptor {
@@ -109,9 +152,7 @@ export declare class McpClient {
109
152
  * Names are prefixed when asked, because two servers can both offer `search` and a model has to be
110
153
  * able to tell them apart.
111
154
  */
112
- toNexusTools(options?: {
113
- prefix?: string;
114
- }): Promise<ToolDefinition[]>;
155
+ toNexusTools(options?: McpToolsOptions): Promise<ToolDefinition[]>;
115
156
  /** Closes the connection and rejects every request still waiting. */
116
157
  close(): Promise<void>;
117
158
  private request;
@@ -168,3 +209,21 @@ export interface HttpClientOptions {
168
209
  * calling a tool needs.
169
210
  */
170
211
  export declare function createHttpTransport(options: HttpClientOptions): McpTransport;
212
+ /** Connects to a remote MCP server over HTTP with Server-Sent Events, the transport before streamable HTTP. */
213
+ export interface SseClientOptions {
214
+ /** The server's event stream: a `GET` here opens it, and the server names where to post messages. */
215
+ url: string;
216
+ /** Headers sent with the stream's request and with every message, such as authorization. */
217
+ headers?: Record<string, string>;
218
+ /** Replaces the global `fetch`. */
219
+ fetch?: typeof globalThis.fetch;
220
+ /** How long to wait for the server to name where messages go, in milliseconds. Defaults to 10 seconds. */
221
+ timeoutMs?: number;
222
+ }
223
+ /**
224
+ * Speaks to an MCP server over HTTP with Server-Sent Events, as MCP's 2024-11-05 transport: one
225
+ * long `GET` the server answers on, and a `POST` per message to the endpoint the stream names
226
+ * first. The endpoint must be on the stream's own origin, so a server cannot send the messages, and
227
+ * their headers, anywhere else. When the stream ends, the transport closes.
228
+ */
229
+ export declare function createSseTransport(options: SseClientOptions): McpTransport;
@@ -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;
@@ -79,22 +86,34 @@ export class McpClient {
79
86
  }
80
87
  async toNexusTools(options = {}) {
81
88
  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
- }));
89
+ const server = options.server ?? options.prefix ?? this.serverInfo.name ?? 'mcp';
90
+ const declared = (descriptor) => {
91
+ const { capabilities } = options;
92
+ if (!capabilities)
93
+ return undefined;
94
+ const list = capabilities === true ? mcpToolCapabilities(descriptor, server) : capabilities(descriptor);
95
+ return list === undefined ? undefined : [...list];
96
+ };
97
+ return tools.map((descriptor) => {
98
+ const capabilities = declared(descriptor);
99
+ return {
100
+ name: options.prefix ? `${options.prefix}_${descriptor.name}` : descriptor.name,
101
+ description: descriptor.description ?? `MCP tool ${descriptor.name}`,
102
+ parameters: descriptor.inputSchema ?? { type: 'object', properties: {} },
103
+ ...(capabilities ? { capabilities } : {}),
104
+ execute: async (args, context) => {
105
+ const progress = context?.progress;
106
+ const result = await this.callTool(descriptor.name, args, progress ? { progress } : {});
107
+ const text = result.content
108
+ .map((part) => part.text ?? (part.type === 'text' ? '' : JSON.stringify(part)))
109
+ .filter(Boolean)
110
+ .join('\n');
111
+ if (result.isError)
112
+ throw new McpError(text || `MCP tool ${descriptor.name} failed`);
113
+ return text;
114
+ },
115
+ };
116
+ });
98
117
  }
99
118
  async close() {
100
119
  this.failAll(new McpError('The MCP client was closed'));
@@ -297,6 +316,125 @@ export function createHttpTransport(options) {
297
316
  },
298
317
  };
299
318
  }
319
+ export function createSseTransport(options) {
320
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
321
+ let onMessage = () => undefined;
322
+ const onClose = [];
323
+ let endpoint;
324
+ let opening;
325
+ let closed = false;
326
+ const stream = new AbortController();
327
+ const ended = () => {
328
+ if (closed)
329
+ return;
330
+ closed = true;
331
+ for (const handler of onClose)
332
+ handler();
333
+ };
334
+ const open = async () => {
335
+ const response = await fetchImplementation(options.url, {
336
+ headers: { accept: 'text/event-stream', ...options.headers },
337
+ signal: stream.signal,
338
+ });
339
+ if (!response.ok || !response.body)
340
+ throw new McpError(`MCP SSE transport failed with ${response.status}`);
341
+ const reader = response.body.getReader();
342
+ await new Promise((resolve, reject) => {
343
+ const timer = setTimeout(() => {
344
+ reject(new McpError(`The MCP server named no endpoint within ${options.timeoutMs ?? 10_000}ms`));
345
+ stream.abort();
346
+ }, options.timeoutMs ?? 10_000);
347
+ const event = (name, data) => {
348
+ if (name === 'endpoint') {
349
+ if (endpoint)
350
+ return;
351
+ const target = new URL(data.trim(), options.url);
352
+ if (target.origin !== new URL(options.url).origin) {
353
+ clearTimeout(timer);
354
+ reject(new McpError(`The MCP server named an endpoint on another origin, ${target.origin}`));
355
+ stream.abort();
356
+ return;
357
+ }
358
+ endpoint = target.href;
359
+ clearTimeout(timer);
360
+ resolve();
361
+ }
362
+ else if (name === 'message') {
363
+ try {
364
+ onMessage(JSON.parse(data));
365
+ }
366
+ catch {
367
+ }
368
+ }
369
+ };
370
+ void readEvents(reader, event).then(() => {
371
+ clearTimeout(timer);
372
+ reject(new McpError('The MCP event stream ended'));
373
+ ended();
374
+ }, (error) => {
375
+ clearTimeout(timer);
376
+ reject(error instanceof McpError ? error : new McpError(`The MCP event stream failed: ${String(error)}`));
377
+ ended();
378
+ });
379
+ });
380
+ };
381
+ const started = () => {
382
+ opening ??= open();
383
+ return opening;
384
+ };
385
+ return {
386
+ start: started,
387
+ async send(message) {
388
+ await started();
389
+ if (closed || !endpoint)
390
+ throw new McpError('The MCP event stream is closed');
391
+ const response = await fetchImplementation(endpoint, {
392
+ method: 'POST',
393
+ headers: { 'content-type': 'application/json', ...options.headers },
394
+ body: JSON.stringify(message),
395
+ });
396
+ await response.body?.cancel().catch(() => undefined);
397
+ if (!response.ok)
398
+ throw new McpError(`MCP SSE transport failed with ${response.status}`);
399
+ },
400
+ onMessage(handler) {
401
+ onMessage = handler;
402
+ },
403
+ onClose(handler) {
404
+ onClose.push(handler);
405
+ },
406
+ close() {
407
+ stream.abort();
408
+ ended();
409
+ },
410
+ };
411
+ }
412
+ async function readEvents(reader, event) {
413
+ const decoder = new TextDecoder();
414
+ let buffer = '';
415
+ while (true) {
416
+ const { value, done } = await reader.read();
417
+ if (done)
418
+ return;
419
+ buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, '\n');
420
+ let boundary = buffer.indexOf('\n\n');
421
+ while (boundary !== -1) {
422
+ const frame = buffer.slice(0, boundary);
423
+ buffer = buffer.slice(boundary + 2);
424
+ let name = 'message';
425
+ const data = [];
426
+ for (const line of frame.split('\n')) {
427
+ if (line.startsWith('event:'))
428
+ name = line.slice(6).trim();
429
+ else if (line.startsWith('data:'))
430
+ data.push(line.slice(5).replace(/^ /, ''));
431
+ }
432
+ if (data.length)
433
+ event(name, data.join('\n'));
434
+ boundary = buffer.indexOf('\n\n');
435
+ }
436
+ }
437
+ }
300
438
  function sseData(body) {
301
439
  return body
302
440
  .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
  *
@@ -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();
@@ -57,11 +72,15 @@ export class McpServer {
57
72
  return {};
58
73
  case 'tools/list':
59
74
  return {
60
- tools: [...this.tools.values()].map((item) => ({
61
- name: item.name,
62
- description: item.description,
63
- inputSchema: item.parameters ?? { type: 'object', properties: {} },
64
- })),
75
+ tools: [...this.tools.values()].map((item) => {
76
+ const annotations = mcpToolAnnotations(item);
77
+ return {
78
+ name: item.name,
79
+ description: item.description,
80
+ inputSchema: item.parameters ?? { type: 'object', properties: {} },
81
+ ...(annotations ? { annotations } : {}),
82
+ };
83
+ }),
65
84
  };
66
85
  case 'tools/call': {
67
86
  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();
@@ -9,9 +9,18 @@ export function acpEditor(options = {}) {
9
9
  connected = (async () => {
10
10
  const clients = [];
11
11
  const tools = [];
12
+ const declare = options.mcpToolCapabilities ?? true;
12
13
  for (const offered of session.mcpServers) {
13
14
  const name = String(offered.name ?? 'mcp');
15
+ const prefix = name.replace(/[^A-Za-z0-9_-]/g, '_');
14
16
  try {
17
+ const remote = offered.url
18
+ ? {
19
+ url: offered.url,
20
+ headers: Object.fromEntries((offered.headers ?? []).map((item) => [item.name, item.value])),
21
+ ...(options.mcpFetch ? { fetch: options.mcpFetch } : {}),
22
+ }
23
+ : undefined;
15
24
  const transport = offered.command
16
25
  ? createStdioTransport({
17
26
  command: offered.command,
@@ -19,18 +28,20 @@ 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
+ : remote && offered.type === 'sse'
32
+ ? createSseTransport({ ...remote, timeoutMs: options.mcpTimeoutMs ?? 30_000 })
33
+ : remote && (offered.type === 'http' || offered.type === undefined)
34
+ ? createHttpTransport(remote)
35
+ : undefined;
29
36
  if (!transport)
30
37
  throw new Error(`MCP server "${name}" uses a transport this agent does not speak`);
31
38
  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
39
  clients.push(client);
40
+ tools.push(...(await client.toNexusTools({
41
+ prefix,
42
+ server: prefix,
43
+ capabilities: typeof declare === 'function' ? (tool) => declare(tool, prefix) : declare,
44
+ })));
34
45
  }
35
46
  catch (error) {
36
47
  options.onError?.(error, name);
@@ -130,6 +141,7 @@ export function acpEditor(options = {}) {
130
141
  return list;
131
142
  };
132
143
  return {
144
+ mcpCapabilities: { http: options.mcp !== false, sse: options.mcp !== false },
133
145
  attach(next) {
134
146
  connection = next;
135
147
  },
@@ -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 } : {}),
package/dist/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * This package's version, recorded where provenance matters, such as on every experiment. A test
3
3
  * keeps it equal to `package.json`.
4
4
  */
5
- export declare const NEXUS_VERSION = "2.16.0";
5
+ export declare const NEXUS_VERSION = "2.17.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const NEXUS_VERSION = '2.16.0';
1
+ export const NEXUS_VERSION = '2.17.0';