nexus-ai-pro 2.15.1 → 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 (57) hide show
  1. package/API_STABILITY.md +37 -0
  2. package/CHANGELOG.md +85 -1
  3. package/README.md +9 -3
  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/a2a-grpc.d.ts +63 -0
  12. package/dist/protocols/a2a-grpc.js +374 -0
  13. package/dist/protocols/a2a-proto.d.ts +26 -0
  14. package/dist/protocols/a2a-proto.js +592 -0
  15. package/dist/protocols/a2a.d.ts +92 -7
  16. package/dist/protocols/a2a.js +451 -110
  17. package/dist/protocols/acp-editor.d.ts +16 -2
  18. package/dist/protocols/acp-editor.js +21 -9
  19. package/dist/protocols/acp.d.ts +8 -0
  20. package/dist/protocols/acp.js +1 -0
  21. package/dist/version.d.ts +1 -1
  22. package/dist/version.js +1 -1
  23. package/dist/video/google.d.ts +42 -11
  24. package/dist/video/google.js +249 -20
  25. package/dist/video/index.d.ts +49 -6
  26. package/dist/video/index.js +91 -15
  27. package/dist/video/mock.js +10 -1
  28. package/dist/video/runway.d.ts +8 -1
  29. package/dist/video/runway.js +68 -2
  30. package/dist-cjs/agent/capabilities.d.ts +3 -0
  31. package/dist-cjs/mcp/client.d.ts +62 -3
  32. package/dist-cjs/mcp/client.js +156 -16
  33. package/dist-cjs/mcp/index.d.ts +2 -2
  34. package/dist-cjs/mcp/index.js +4 -1
  35. package/dist-cjs/mcp/server.d.ts +12 -0
  36. package/dist-cjs/mcp/server.js +25 -5
  37. package/dist-cjs/protocols/a2a-grpc.d.ts +63 -0
  38. package/dist-cjs/protocols/a2a-grpc.js +381 -0
  39. package/dist-cjs/protocols/a2a-proto.d.ts +26 -0
  40. package/dist-cjs/protocols/a2a-proto.js +599 -0
  41. package/dist-cjs/protocols/a2a.d.ts +92 -7
  42. package/dist-cjs/protocols/a2a.js +453 -110
  43. package/dist-cjs/protocols/acp-editor.d.ts +16 -2
  44. package/dist-cjs/protocols/acp-editor.js +20 -8
  45. package/dist-cjs/protocols/acp.d.ts +8 -0
  46. package/dist-cjs/protocols/acp.js +1 -0
  47. package/dist-cjs/version.d.ts +1 -1
  48. package/dist-cjs/version.js +1 -1
  49. package/dist-cjs/video/google.d.ts +42 -11
  50. package/dist-cjs/video/google.js +248 -19
  51. package/dist-cjs/video/index.d.ts +49 -6
  52. package/dist-cjs/video/index.js +91 -15
  53. package/dist-cjs/video/mock.js +10 -1
  54. package/dist-cjs/video/runway.d.ts +8 -1
  55. package/dist-cjs/video/runway.js +68 -2
  56. package/llms.txt +1 -1
  57. package/package.json +14 -1
package/API_STABILITY.md CHANGED
@@ -41,6 +41,43 @@ 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
+
66
+ ## 2.16 stage (2.16.0)
67
+
68
+ Everything 2.16 adds is stable from the start and additive, unless this section says otherwise.
69
+
70
+ New and stable:
71
+ - `nexus-ai-pro/protocols/a2a/grpc`: `a2aGrpcService()`, `a2aGrpcClient()`, `A2aGrpcService`,
72
+ `A2aGrpcServiceOptions`, `A2aGrpcClient`, and `A2aGrpcClientOptions`;
73
+ - in `nexus-ai-pro/protocols/a2a`: the HTTP+JSON routes, `A2aServerOptions.bindings` and
74
+ `interfaces`, `A2aHandler.call()`, `card`, and `authenticate()`, `A2aBinding`, `A2aCallContext`,
75
+ `A2aCallOutcome`, `A2aErrorStatus`, `a2aErrorStatus()`, `a2aErrorCode()`,
76
+ `A2aClientOptions.binding`, `A2aPushOptions.text`, and `A2aPushNotificationConfig.authentication`.
77
+
78
+ The routes, the gRPC service, and their errors follow A2A 1.0's bindings: a change the specification
79
+ makes in a later version comes with that version, behind `A2A_PROTOCOL_VERSION`.
80
+
44
81
  ## 2.15 stage (2.15.0)
45
82
 
46
83
  Everything 2.15 adds is stable from the start and additive, unless this section says otherwise.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,88 @@ 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
+
59
+ ## [2.16.0] - 2026-10-20
60
+
61
+ A2A on all three of its bindings over one set of tasks, and webhooks told the answer as it is written.
62
+
63
+ ### Added
64
+
65
+ - **A2A over HTTP+JSON and gRPC.** `a2aHandler()` serves the protocol's HTTP+JSON binding beside
66
+ JSON-RPC, at routes under the card's URL, its errors AIP-193's JSON with A2A's `ErrorInfo` reason.
67
+ `a2aGrpcService()`, on the new `nexus-ai-pro/protocols/a2a/grpc`, serves the gRPC binding,
68
+ `lf.a2a.v1.A2AService`, over `node:http2` with no dependency. The card names each binding served
69
+ (`bindings`, `interfaces`). All three run through `A2aHandler.call()` over one task store, so a
70
+ caller on any binding continues, reads, cancels, or follows a task another started, and reads the
71
+ same history.
72
+ - **Clients on every binding.** `a2aClient({ binding: 'HTTP+JSON' })` calls the HTTP+JSON binding, and
73
+ `a2aGrpcClient()` the gRPC one, with every method `a2aClient()` has. An error on any binding raises
74
+ an `A2aError` with A2A's code; `a2aErrorStatus()` and `a2aErrorCode()` map a code to its HTTP
75
+ status, gRPC status, and reason, and back.
76
+ - **A webhook told the answer as it is written.** `pushNotifications.text` pushes each piece of
77
+ streamed text to a task's webhooks, the same pieces a stream sends, before the state that follows
78
+ them; `text: { everyMs }` gathers the pieces into fewer deliveries. A push config's
79
+ `authentication` is sent as each delivery's `Authorization` header.
80
+ - Push configs take protocol 1.0's flat shape on every binding, and JSON-RPC answers both it and the
81
+ drafts' nested one; `ListTaskPushNotificationConfigs` is answered under its 1.0 name too.
82
+
83
+ ### Changed
84
+
85
+ - The nightly interoperability check drives `@a2a-js/sdk` over each of the three bindings, and a
86
+ task across them. It drives `@agentclientprotocol/sdk` 2.0, which dropped
87
+ `ClientSideConnection`, through its new connection API, and 1.x as before.
88
+
7
89
  ## [2.15.1] - 2026-10-19
8
90
 
9
91
  A test that could fail by chance, made exact. The package is unchanged from 2.15.0.
@@ -3021,7 +3103,9 @@ shared policy vocabulary is in place for a future release that revisits this.
3021
3103
  - URL fetching rejects unsafe private-network targets and limits response reads.
3022
3104
  - CLI and audit findings no longer disclose detected secret values.
3023
3105
 
3024
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.1...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
3108
+ [2.16.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.1...v2.16.0
3025
3109
  [2.15.1]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.0...v2.15.1
3026
3110
  [2.15.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.14.0...v2.15.0
3027
3111
  [2.14.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.13.0...v2.14.0
package/README.md CHANGED
@@ -321,12 +321,18 @@ Shipped so far:
321
321
  - 2.15 a compatibility record for community adapters: `nexus adapters verify` seals the contract an
322
322
  adapter passed, against which release, with its benchmark; `nexus adapters recheck` turns a listed
323
323
  adapter red before a release that would break it; and one
324
- [page](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/COMPATIBILITY.md) lists them.
324
+ [page](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/COMPATIBILITY.md) lists them;
325
+ - 2.16 A2A over every binding: one agent served over JSON-RPC, HTTP+JSON, and gRPC from one task
326
+ store, so a caller on any binding continues a task another started, and webhooks told the answer
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.
325
331
 
326
332
  Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
327
333
  [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
328
- Next, 2.16 serves A2A over its HTTP+JSON and gRPC bindings beside JSON-RPC, one task store behind
329
- all three, and lets a push webhook receive each piece of streamed text. 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
330
336
  embedding batch tier, media understanding, video, Gemini Live, SIP, and both phone providers
331
337
  graduate once the live workflow passes against each. The
332
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 ?? '');
@@ -0,0 +1,63 @@
1
+ /**
2
+ * A2A's gRPC binding: the service `lf.a2a.v1.A2AService` of protocol 1.0, served over the same
3
+ * `a2aHandler()` as its JSON-RPC and HTTP+JSON bindings, and so over the same tasks, and a client that
4
+ * calls an agent over it. Over HTTP/2 from `node:http2`, with no dependency.
5
+ *
6
+ * ```ts
7
+ * const handler = a2aHandler(agent, { card, interfaces: [{ url: 'agents.example.com:50051', protocolBinding: 'GRPC' }] });
8
+ * const grpc = a2aGrpcService(handler);
9
+ * await grpc.listen(50051);
10
+ * ```
11
+ */
12
+ import { type Http2Server, type IncomingHttpHeaders, type ServerHttp2Stream } from 'node:http2';
13
+ import { type A2aClient, type A2aClientOptions, type A2aHandler } from './a2a.js';
14
+ /** Options for `a2aGrpcService()`. */
15
+ export interface A2aGrpcServiceOptions {
16
+ /** Largest request message taken, in bytes. Defaults to 4 MiB, gRPC's own default. */
17
+ maxMessageBytes?: number;
18
+ }
19
+ /** The gRPC binding of one A2A handler. */
20
+ export interface A2aGrpcService {
21
+ /**
22
+ * Answers one HTTP/2 stream: give it each stream of a server of your own, such as one over TLS,
23
+ * as `server.on('stream', service.handle)`.
24
+ */
25
+ handle(stream: ServerHttp2Stream, headers: IncomingHttpHeaders): void;
26
+ /**
27
+ * Starts a server over plain HTTP/2 on `port`, 0 for any, on `host`: for a server behind a proxy
28
+ * that ends TLS, and for tests. Resolves with the address to list on the card, `host:port`. Its
29
+ * `close()` stops taking calls, tells each connected client to go, and resolves once the calls in
30
+ * flight finish.
31
+ */
32
+ listen(port?: number, host?: string): Promise<{
33
+ url: string;
34
+ port: number;
35
+ server: Http2Server;
36
+ close(): Promise<void>;
37
+ }>;
38
+ }
39
+ /**
40
+ * The gRPC binding over an `a2aHandler()`: each method of `lf.a2a.v1.A2AService`, its message read as
41
+ * the JSON the handler takes and its answer written back, a stream as a server-streaming call, and an
42
+ * error as a gRPC status with A2A's `ErrorInfo` in `grpc-status-details-bin`. The caller is read by the
43
+ * handler's own `principal`, from a `Request` with the call's metadata as its headers.
44
+ */
45
+ export declare function a2aGrpcService(handler: A2aHandler, options?: A2aGrpcServiceOptions): A2aGrpcService;
46
+ /** Options for `a2aGrpcClient()`. */
47
+ export interface A2aGrpcClientOptions extends Omit<A2aClientOptions, 'binding' | 'url'> {
48
+ /** Where the agent's gRPC service is: `host:port`, as its card lists it, or an `http://` or `https://` URL. */
49
+ url: string;
50
+ /** Where its card is, for `card()`. Without it, `card()` reads the extended card. */
51
+ cardUrl?: string;
52
+ }
53
+ /** A client for an A2A agent over gRPC, with `close()` for its connection. */
54
+ export interface A2aGrpcClient extends A2aClient {
55
+ /** Closes the HTTP/2 connection. */
56
+ close(): Promise<void>;
57
+ }
58
+ /**
59
+ * A client for one A2A agent over its gRPC binding: the same methods as `a2aClient()`, each a call of
60
+ * `lf.a2a.v1.A2AService` over one HTTP/2 connection, a gRPC error raised as an `A2aError` with the
61
+ * protocol's code. Plain HTTP/2 for `host:port` and `http://`; TLS for `https://`.
62
+ */
63
+ export declare function a2aGrpcClient(options: A2aGrpcClientOptions): A2aGrpcClient;