@agentionai/agents 1.0.2 → 1.2.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/dist/agents/Agent.d.ts +1 -1
- package/dist/agents/AgentConfig.d.ts +17 -1
- package/dist/agents/BaseAgent.d.ts +82 -0
- package/dist/agents/BaseAgent.js +107 -0
- package/dist/agents/anthropic/ClaudeAgent.d.ts +0 -2
- package/dist/agents/anthropic/ClaudeAgent.js +11 -21
- package/dist/agents/google/GeminiAgent.d.ts +1 -2
- package/dist/agents/google/GeminiAgent.js +12 -11
- package/dist/agents/mistral/MistralAgent.d.ts +0 -2
- package/dist/agents/mistral/MistralAgent.js +4 -10
- package/dist/agents/model-types.d.ts +77 -1
- package/dist/agents/model-types.js +33 -0
- package/dist/agents/ollama/OllamaAgent.d.ts +0 -2
- package/dist/agents/ollama/OllamaAgent.js +12 -10
- package/dist/agents/openai/OpenAiAgent.d.ts +55 -9
- package/dist/agents/openai/OpenAiAgent.js +70 -35
- package/dist/agents/openai-compatible/OpenAICompatibleAgent.d.ts +0 -1
- package/dist/agents/openai-compatible/OpenAICompatibleAgent.js +47 -21
- package/dist/history/transformers.d.ts +10 -0
- package/dist/history/transformers.js +24 -0
- package/dist/mcp/MCPClient.d.ts +185 -8
- package/dist/mcp/MCPClient.js +459 -70
- package/dist/mcp/content.d.ts +28 -0
- package/dist/mcp/content.js +90 -0
- package/dist/mcp/errors.d.ts +62 -0
- package/dist/mcp/errors.js +74 -0
- package/dist/mcp/index.d.ts +20 -2
- package/dist/mcp/index.js +26 -1
- package/dist/mcp/types.d.ts +278 -11
- package/package.json +2 -1
package/dist/mcp/MCPClient.js
CHANGED
|
@@ -32,9 +32,50 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.MCPClient = void 0;
|
|
39
|
+
exports.MCPClient = exports.MCPClientEvent = void 0;
|
|
40
|
+
const events_1 = __importDefault(require("events"));
|
|
37
41
|
const Tool_1 = require("../tools/Tool");
|
|
42
|
+
const content_1 = require("./content");
|
|
43
|
+
const errors_1 = require("./errors");
|
|
44
|
+
/**
|
|
45
|
+
* Names of the events an {@link MCPClient} emits.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* mcp.on(MCPClientEvent.DISCONNECTED, ({ willReconnect }) => {
|
|
50
|
+
* if (!willReconnect) scheduleManualReconnect();
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
class MCPClientEvent {
|
|
55
|
+
}
|
|
56
|
+
exports.MCPClientEvent = MCPClientEvent;
|
|
57
|
+
/** Emitted after a successful {@link MCPClient.connect}. */
|
|
58
|
+
MCPClientEvent.CONNECTED = "connected";
|
|
59
|
+
/** Emitted whenever the connection is lost, deliberately or not. */
|
|
60
|
+
MCPClientEvent.DISCONNECTED = "disconnected";
|
|
61
|
+
/** Emitted before each automatic reconnect attempt. */
|
|
62
|
+
MCPClientEvent.RECONNECTING = "reconnecting";
|
|
63
|
+
/** Emitted when an automatic reconnect succeeds. */
|
|
64
|
+
MCPClientEvent.RECONNECTED = "reconnected";
|
|
65
|
+
/** Emitted when the server's tool list changes. */
|
|
66
|
+
MCPClientEvent.TOOLS_CHANGED = "toolsChanged";
|
|
67
|
+
/**
|
|
68
|
+
* Emitted for out-of-band transport errors and for failures inside background
|
|
69
|
+
* work such as reconnection or tool refresh.
|
|
70
|
+
*/
|
|
71
|
+
MCPClientEvent.ERROR = "error";
|
|
72
|
+
const DEFAULT_RECONNECT = {
|
|
73
|
+
enabled: false,
|
|
74
|
+
maxRetries: 5,
|
|
75
|
+
initialDelayMs: 500,
|
|
76
|
+
maxDelayMs: 30000,
|
|
77
|
+
backoffFactor: 2,
|
|
78
|
+
};
|
|
38
79
|
/**
|
|
39
80
|
* MCPClient connects to an MCP (Model Context Protocol) server and converts its
|
|
40
81
|
* tools into agention-lib {@link Tool} instances that can be passed to any agent.
|
|
@@ -43,6 +84,9 @@ const Tool_1 = require("../tools/Tool");
|
|
|
43
84
|
* - **stdio** — spawns a local process and communicates over stdin/stdout
|
|
44
85
|
* - **http** — connects to a remote MCP server over Streamable HTTP
|
|
45
86
|
*
|
|
87
|
+
* The client is an `EventEmitter`; see {@link MCPClientEvent} for the lifecycle
|
|
88
|
+
* events a host can watch.
|
|
89
|
+
*
|
|
46
90
|
* @requires @modelcontextprotocol/sdk - Install as a peer dependency:
|
|
47
91
|
* ```
|
|
48
92
|
* npm install @modelcontextprotocol/sdk
|
|
@@ -81,6 +125,20 @@ const Tool_1 = require("../tools/Tool");
|
|
|
81
125
|
* agent.addTools(mcp.getTools());
|
|
82
126
|
* ```
|
|
83
127
|
*
|
|
128
|
+
* @example Cancellable, time-boxed tool calls
|
|
129
|
+
* ```typescript
|
|
130
|
+
* let turn = new AbortController();
|
|
131
|
+
*
|
|
132
|
+
* const mcp = MCPClient.fromUrl("https://my-mcp-server.com/mcp", {
|
|
133
|
+
* // Resolved per call, so each agent turn gets the current signal
|
|
134
|
+
* callOptions: () => ({ signal: turn.signal, timeout: 20_000 }),
|
|
135
|
+
* reconnect: { enabled: true },
|
|
136
|
+
* });
|
|
137
|
+
*
|
|
138
|
+
* // Interrupting the turn now aborts any in-flight MCP call
|
|
139
|
+
* turn.abort();
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
84
142
|
* @example HTTP with OAuth
|
|
85
143
|
* ```typescript
|
|
86
144
|
* import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
@@ -90,25 +148,37 @@ const Tool_1 = require("../tools/Tool");
|
|
|
90
148
|
* });
|
|
91
149
|
* ```
|
|
92
150
|
*/
|
|
93
|
-
class MCPClient {
|
|
151
|
+
class MCPClient extends events_1.default {
|
|
94
152
|
constructor(transportConfig, options = {}) {
|
|
153
|
+
super();
|
|
95
154
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
155
|
this.sdkClient = null;
|
|
97
156
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
98
157
|
this.sdkTransport = null;
|
|
99
158
|
this._tools = [];
|
|
100
|
-
|
|
159
|
+
/** Signature per tool name, used to keep Tool identity stable across refreshes. */
|
|
160
|
+
this.toolSignatures = new Map();
|
|
161
|
+
this._state = "disconnected";
|
|
162
|
+
this.connectPromise = null;
|
|
163
|
+
this.reconnectPromise = null;
|
|
164
|
+
this.reconnectAttempts = 0;
|
|
165
|
+
this.reconnectTimer = null;
|
|
166
|
+
this.reconnectWake = null;
|
|
167
|
+
this.closedByUser = false;
|
|
101
168
|
this.transportConfig = transportConfig;
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
169
|
+
this.clientName = options.clientName ?? "agention-mcp-client";
|
|
170
|
+
this.clientVersion = options.clientVersion ?? "1.0.0";
|
|
171
|
+
this.callOptions = options.callOptions;
|
|
172
|
+
this.throwOnToolError = options.throwOnToolError ?? true;
|
|
173
|
+
this.refreshToolsOnListChanged = options.refreshToolsOnListChanged ?? true;
|
|
174
|
+
this.formatResult = options.formatResult;
|
|
175
|
+
this.reconnectOptions = { ...DEFAULT_RECONNECT, ...options.reconnect };
|
|
106
176
|
}
|
|
107
177
|
/**
|
|
108
178
|
* Create an MCPClient that connects to a local MCP server process via stdio.
|
|
109
179
|
*
|
|
110
180
|
* @param config - Command and arguments to spawn the MCP server process
|
|
111
|
-
* @param options - Optional client identification options
|
|
181
|
+
* @param options - Optional client identification and behaviour options
|
|
112
182
|
*/
|
|
113
183
|
static fromStdio(config, options) {
|
|
114
184
|
return new MCPClient({ type: "stdio", config }, options);
|
|
@@ -130,14 +200,158 @@ class MCPClient {
|
|
|
130
200
|
/**
|
|
131
201
|
* Connect to the MCP server and discover all available tools.
|
|
132
202
|
*
|
|
133
|
-
* This method is idempotent — calling it when already connected is a no-op
|
|
203
|
+
* This method is idempotent — calling it when already connected is a no-op, and
|
|
204
|
+
* concurrent calls share a single connection attempt.
|
|
134
205
|
* Must be called before {@link getTools}.
|
|
135
206
|
*
|
|
136
207
|
* @throws If the MCP server cannot be reached or the SDK is not installed
|
|
137
208
|
*/
|
|
138
209
|
async connect() {
|
|
139
|
-
if (this.connected)
|
|
210
|
+
if (this._state === "connected")
|
|
140
211
|
return;
|
|
212
|
+
if (this.connectPromise)
|
|
213
|
+
return this.connectPromise;
|
|
214
|
+
this.closedByUser = false;
|
|
215
|
+
this._state = "connecting";
|
|
216
|
+
this.connectPromise = this.establish()
|
|
217
|
+
.then(() => {
|
|
218
|
+
this._state = "connected";
|
|
219
|
+
this.reconnectAttempts = 0;
|
|
220
|
+
this.emitEvent(MCPClientEvent.CONNECTED, { tools: this._tools });
|
|
221
|
+
})
|
|
222
|
+
.catch((error) => {
|
|
223
|
+
this._state = "disconnected";
|
|
224
|
+
throw error;
|
|
225
|
+
})
|
|
226
|
+
.finally(() => {
|
|
227
|
+
this.connectPromise = null;
|
|
228
|
+
});
|
|
229
|
+
return this.connectPromise;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Return all tools discovered from the MCP server as agention-lib Tool instances.
|
|
233
|
+
*
|
|
234
|
+
* Returns an empty array if {@link connect} has not been called yet.
|
|
235
|
+
* The returned tools can be passed directly to any agent via the `tools` config
|
|
236
|
+
* option or {@link BaseAgent.addTools}.
|
|
237
|
+
*
|
|
238
|
+
* Tool instances are stable across reconnects and tool-list refreshes: a tool
|
|
239
|
+
* whose definition is unchanged keeps its identity, so agents already holding it
|
|
240
|
+
* keep working. Listen for {@link MCPClientEvent.TOOLS_CHANGED} to learn when the
|
|
241
|
+
* list itself changed.
|
|
242
|
+
*/
|
|
243
|
+
getTools() {
|
|
244
|
+
return this._tools;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Current connection state.
|
|
248
|
+
*
|
|
249
|
+
* @see {@link MCPConnectionState}
|
|
250
|
+
*/
|
|
251
|
+
getState() {
|
|
252
|
+
return this._state;
|
|
253
|
+
}
|
|
254
|
+
/** Whether the client currently has a usable connection. */
|
|
255
|
+
isConnected() {
|
|
256
|
+
return this._state === "connected";
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Replace the default options applied to every tool call.
|
|
260
|
+
*
|
|
261
|
+
* Pass a function to have them resolved per call — the usual way to scope an
|
|
262
|
+
* `AbortSignal` to the current agent turn.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* mcp.setCallOptions(() => ({ signal: currentTurn.signal, timeout: 15_000 }));
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
setCallOptions(options) {
|
|
270
|
+
this.callOptions = options;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Call an MCP tool directly and receive the raw `CallToolResult`, including any
|
|
274
|
+
* image, audio or resource blocks and the `isError` flag.
|
|
275
|
+
*
|
|
276
|
+
* Unlike the wrapped {@link Tool} instances from {@link getTools}, this does not
|
|
277
|
+
* render the result or throw on `isError` — it is the escape hatch for hosts
|
|
278
|
+
* that want to handle MCP content themselves.
|
|
279
|
+
*
|
|
280
|
+
* @param name - Name of the tool as advertised by the server
|
|
281
|
+
* @param input - Arguments for the tool
|
|
282
|
+
* @param options - Per-call options, merged over the client defaults
|
|
283
|
+
*/
|
|
284
|
+
async callTool(name, input = {}, options) {
|
|
285
|
+
return this.invokeTool(name, input, options);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Re-run tool discovery against the connected server.
|
|
289
|
+
*
|
|
290
|
+
* Called automatically when the server sends `notifications/tools/list_changed`
|
|
291
|
+
* (unless `refreshToolsOnListChanged` is disabled) and after a successful
|
|
292
|
+
* reconnect. Emits {@link MCPClientEvent.TOOLS_CHANGED} when the list differs.
|
|
293
|
+
*
|
|
294
|
+
* @returns The refreshed tool list
|
|
295
|
+
*/
|
|
296
|
+
async refreshTools() {
|
|
297
|
+
const sdkClient = this.sdkClient;
|
|
298
|
+
if (!sdkClient) {
|
|
299
|
+
throw new errors_1.MCPNotConnectedError("MCPClient: Cannot refresh tools — client is not connected", "", this._state);
|
|
300
|
+
}
|
|
301
|
+
const definitions = await this.listAllTools(sdkClient);
|
|
302
|
+
this.applyToolDefinitions(definitions);
|
|
303
|
+
return this._tools;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Disconnect from the MCP server and release all resources.
|
|
307
|
+
*
|
|
308
|
+
* This method is idempotent — calling it when not connected is a no-op. It also
|
|
309
|
+
* cancels any pending automatic reconnect. After disconnecting, {@link getTools}
|
|
310
|
+
* returns an empty array; call {@link connect} again to start over.
|
|
311
|
+
*/
|
|
312
|
+
async disconnect() {
|
|
313
|
+
const wasActive = this._state !== "disconnected" || this.sdkClient !== null;
|
|
314
|
+
this.closedByUser = true;
|
|
315
|
+
this.cancelReconnectWait();
|
|
316
|
+
if (this.reconnectPromise) {
|
|
317
|
+
await this.reconnectPromise.catch(() => undefined);
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
if (this.transportConfig.type === "http" && this.sdkTransport) {
|
|
321
|
+
await this.sdkTransport.terminateSession?.();
|
|
322
|
+
}
|
|
323
|
+
if (this.sdkClient) {
|
|
324
|
+
await this.sdkClient.close();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
this.sdkClient = null;
|
|
329
|
+
this.sdkTransport = null;
|
|
330
|
+
this._tools = [];
|
|
331
|
+
this.toolSignatures.clear();
|
|
332
|
+
this.reconnectAttempts = 0;
|
|
333
|
+
this._state = "disconnected";
|
|
334
|
+
}
|
|
335
|
+
if (wasActive) {
|
|
336
|
+
this.emitEvent(MCPClientEvent.DISCONNECTED, {
|
|
337
|
+
deliberate: true,
|
|
338
|
+
willReconnect: false,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
343
|
+
on(event, listener) {
|
|
344
|
+
return super.on(event, listener);
|
|
345
|
+
}
|
|
346
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
347
|
+
once(event, listener) {
|
|
348
|
+
return super.once(event, listener);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Open the SDK client and transport, wire up lifecycle handlers and discover
|
|
352
|
+
* tools. Shared by {@link connect} and the reconnect loop.
|
|
353
|
+
*/
|
|
354
|
+
async establish() {
|
|
141
355
|
// Build module paths at runtime so tsc does not attempt to resolve them at
|
|
142
356
|
// compile time. @modelcontextprotocol/sdk is an optional peer dependency and
|
|
143
357
|
// may not be installed on the build host.
|
|
@@ -145,7 +359,7 @@ class MCPClient {
|
|
|
145
359
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
360
|
const { Client } = await Promise.resolve(`${`${pkg}/client/index.js`}`).then(s => __importStar(require(s)));
|
|
147
361
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
-
const sdkClient = new Client({ name: this.
|
|
362
|
+
const sdkClient = new Client({ name: this.clientName, version: this.clientVersion }, { capabilities: {} });
|
|
149
363
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
364
|
let transport;
|
|
151
365
|
if (this.transportConfig.type === "stdio") {
|
|
@@ -166,53 +380,249 @@ class MCPClient {
|
|
|
166
380
|
...(authProvider ? { authProvider } : {}),
|
|
167
381
|
});
|
|
168
382
|
}
|
|
383
|
+
// Registered before connect() so a notification arriving immediately after the
|
|
384
|
+
// handshake is not missed.
|
|
385
|
+
await this.subscribeToToolListChanges(sdkClient, pkg);
|
|
386
|
+
sdkClient.onclose = () => this.handleClose(sdkClient);
|
|
387
|
+
sdkClient.onerror = (error) => this.handleTransportError(sdkClient, error);
|
|
169
388
|
await sdkClient.connect(transport);
|
|
170
389
|
this.sdkClient = sdkClient;
|
|
171
390
|
this.sdkTransport = transport;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
391
|
+
const definitions = await this.listAllTools(sdkClient);
|
|
392
|
+
this.applyToolDefinitions(definitions, { silent: this._tools.length === 0 });
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Ask the server to notify us about tool list changes. Failures here are not
|
|
396
|
+
* fatal — an older SDK or a server without the capability simply means the
|
|
397
|
+
* cached list stays as it was at connect time.
|
|
398
|
+
*/
|
|
399
|
+
async subscribeToToolListChanges(
|
|
400
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
401
|
+
sdkClient, pkg) {
|
|
402
|
+
if (!this.refreshToolsOnListChanged)
|
|
403
|
+
return;
|
|
404
|
+
try {
|
|
405
|
+
const { ToolListChangedNotificationSchema } = (await Promise.resolve(`${
|
|
406
|
+
/* @vite-ignore */ `${pkg}/types.js`}`).then(s => __importStar(require(s))));
|
|
407
|
+
sdkClient.setNotificationHandler(ToolListChangedNotificationSchema, () => {
|
|
408
|
+
void this.refreshTools().catch((error) => this.emitError(error));
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
this.emitError(error);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** Page through `tools/list` until the server stops handing back a cursor. */
|
|
416
|
+
async listAllTools(
|
|
417
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
418
|
+
sdkClient) {
|
|
419
|
+
const definitions = [];
|
|
175
420
|
let cursor;
|
|
176
421
|
do {
|
|
177
422
|
const response = await sdkClient.listTools(cursor ? { cursor } : {});
|
|
178
|
-
|
|
423
|
+
definitions.push(...response.tools);
|
|
179
424
|
cursor = response.nextCursor;
|
|
180
425
|
} while (cursor);
|
|
181
|
-
|
|
426
|
+
return definitions;
|
|
182
427
|
}
|
|
183
428
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* The returned tools can be passed directly to any agent via the `tools` config
|
|
188
|
-
* option or {@link BaseAgent.addTools}.
|
|
429
|
+
* Reconcile the cached tool list with a fresh set of definitions, reusing the
|
|
430
|
+
* existing {@link Tool} instance for every definition that has not changed so
|
|
431
|
+
* agents holding a reference keep working.
|
|
189
432
|
*/
|
|
190
|
-
|
|
191
|
-
|
|
433
|
+
applyToolDefinitions(definitions, { silent = false } = {}) {
|
|
434
|
+
const previous = new Map(this._tools.map((tool) => [tool.name, tool]));
|
|
435
|
+
const previousSignatures = this.toolSignatures;
|
|
436
|
+
const tools = [];
|
|
437
|
+
const signatures = new Map();
|
|
438
|
+
const added = [];
|
|
439
|
+
let changed = false;
|
|
440
|
+
for (const definition of definitions) {
|
|
441
|
+
const signature = JSON.stringify({
|
|
442
|
+
description: definition.description ?? null,
|
|
443
|
+
inputSchema: definition.inputSchema ?? null,
|
|
444
|
+
});
|
|
445
|
+
const existing = previous.get(definition.name);
|
|
446
|
+
if (existing && previousSignatures.get(definition.name) === signature) {
|
|
447
|
+
tools.push(existing);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
tools.push(this.wrapMcpTool(definition));
|
|
451
|
+
changed = true;
|
|
452
|
+
if (!existing)
|
|
453
|
+
added.push(definition.name);
|
|
454
|
+
}
|
|
455
|
+
signatures.set(definition.name, signature);
|
|
456
|
+
previous.delete(definition.name);
|
|
457
|
+
}
|
|
458
|
+
const removed = [...previous.keys()];
|
|
459
|
+
this._tools = tools;
|
|
460
|
+
this.toolSignatures = signatures;
|
|
461
|
+
if (!silent && (changed || removed.length > 0)) {
|
|
462
|
+
this.emitEvent(MCPClientEvent.TOOLS_CHANGED, { tools, added, removed });
|
|
463
|
+
}
|
|
192
464
|
}
|
|
193
465
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* This method is idempotent — calling it when not connected is a no-op.
|
|
197
|
-
* After disconnecting, {@link getTools} returns an empty array.
|
|
466
|
+
* Handle the transport closing. Deliberate closes are reported by
|
|
467
|
+
* {@link disconnect} itself; everything else is an unexpected drop.
|
|
198
468
|
*/
|
|
199
|
-
|
|
200
|
-
|
|
469
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
470
|
+
handleClose(sdkClient) {
|
|
471
|
+
if (this.closedByUser)
|
|
201
472
|
return;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
473
|
+
// A close from a client we have already replaced or abandoned is stale.
|
|
474
|
+
if (this.sdkClient !== sdkClient)
|
|
475
|
+
return;
|
|
476
|
+
this.sdkClient = null;
|
|
477
|
+
this.sdkTransport = null;
|
|
478
|
+
const willReconnect = this.reconnectOptions.enabled && this.reconnectAttempts < this.reconnectOptions.maxRetries;
|
|
479
|
+
// A transport that fails reports the cause through onerror just before it
|
|
480
|
+
// closes, so the most recent one is the reason for this drop.
|
|
481
|
+
const error = this.lastTransportError;
|
|
482
|
+
this.lastTransportError = undefined;
|
|
483
|
+
this._state = willReconnect ? "reconnecting" : "failed";
|
|
484
|
+
this.emitEvent(MCPClientEvent.DISCONNECTED, {
|
|
485
|
+
...(error ? { error } : {}),
|
|
486
|
+
deliberate: false,
|
|
487
|
+
willReconnect,
|
|
488
|
+
});
|
|
489
|
+
if (willReconnect)
|
|
490
|
+
this.startReconnectLoop();
|
|
491
|
+
}
|
|
492
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
493
|
+
handleTransportError(sdkClient, error) {
|
|
494
|
+
if (this.sdkClient !== null && this.sdkClient !== sdkClient)
|
|
495
|
+
return;
|
|
496
|
+
this.lastTransportError = error instanceof Error ? error : new Error(String(error));
|
|
497
|
+
this.emitError(error);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Retry {@link establish} with exponential backoff until it succeeds, the retry
|
|
501
|
+
* budget runs out, or {@link disconnect} is called.
|
|
502
|
+
*/
|
|
503
|
+
startReconnectLoop() {
|
|
504
|
+
if (this.reconnectPromise)
|
|
505
|
+
return;
|
|
506
|
+
this.reconnectPromise = (async () => {
|
|
507
|
+
while (!this.closedByUser) {
|
|
508
|
+
const attempt = ++this.reconnectAttempts;
|
|
509
|
+
const delayMs = Math.min(this.reconnectOptions.initialDelayMs *
|
|
510
|
+
Math.pow(this.reconnectOptions.backoffFactor, attempt - 1), this.reconnectOptions.maxDelayMs);
|
|
511
|
+
this.emitEvent(MCPClientEvent.RECONNECTING, { attempt, delayMs });
|
|
512
|
+
await this.waitBeforeReconnect(delayMs);
|
|
513
|
+
if (this.closedByUser)
|
|
514
|
+
return;
|
|
515
|
+
try {
|
|
516
|
+
await this.establish();
|
|
517
|
+
this._state = "connected";
|
|
518
|
+
this.reconnectAttempts = 0;
|
|
519
|
+
this.emitEvent(MCPClientEvent.RECONNECTED, { tools: this._tools });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
this.emitError(error);
|
|
524
|
+
if (attempt >= this.reconnectOptions.maxRetries) {
|
|
525
|
+
this._state = "failed";
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
208
529
|
}
|
|
530
|
+
})().finally(() => {
|
|
531
|
+
this.reconnectPromise = null;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
waitBeforeReconnect(delayMs) {
|
|
535
|
+
return new Promise((resolve) => {
|
|
536
|
+
this.reconnectWake = resolve;
|
|
537
|
+
this.reconnectTimer = setTimeout(() => {
|
|
538
|
+
this.reconnectTimer = null;
|
|
539
|
+
this.reconnectWake = null;
|
|
540
|
+
resolve();
|
|
541
|
+
}, delayMs);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
cancelReconnectWait() {
|
|
545
|
+
if (this.reconnectTimer) {
|
|
546
|
+
clearTimeout(this.reconnectTimer);
|
|
547
|
+
this.reconnectTimer = null;
|
|
209
548
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.
|
|
213
|
-
|
|
214
|
-
|
|
549
|
+
if (this.reconnectWake) {
|
|
550
|
+
const wake = this.reconnectWake;
|
|
551
|
+
this.reconnectWake = null;
|
|
552
|
+
wake();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* `EventEmitter` throws when an `error` event has no listener, which would turn
|
|
557
|
+
* a background transport hiccup into a process crash. Only emit when someone is
|
|
558
|
+
* actually listening.
|
|
559
|
+
*/
|
|
560
|
+
emitError(error) {
|
|
561
|
+
if (this.listenerCount(MCPClientEvent.ERROR) === 0)
|
|
562
|
+
return;
|
|
563
|
+
this.emit(MCPClientEvent.ERROR, error instanceof Error ? error : new Error(String(error)));
|
|
564
|
+
}
|
|
565
|
+
emitEvent(event, payload) {
|
|
566
|
+
this.emit(event, payload);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Resolve the effective request options for a call: client defaults (static or
|
|
570
|
+
* per-call), with explicit per-call options layered on top.
|
|
571
|
+
*/
|
|
572
|
+
resolveCallOptions(context, overrides) {
|
|
573
|
+
const defaults = typeof this.callOptions === "function" ? this.callOptions(context) : this.callOptions;
|
|
574
|
+
const merged = { ...defaults, ...overrides };
|
|
575
|
+
const hasOption = Object.values(merged).some((value) => value !== undefined);
|
|
576
|
+
return hasOption ? merged : undefined;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Wait for the client to become usable, then issue the call. Wrapping every
|
|
580
|
+
* transport-level failure keeps the error surface stable, while `cause` lets a
|
|
581
|
+
* host tell an abort apart from a server error.
|
|
582
|
+
*/
|
|
583
|
+
async invokeTool(name, input, overrides) {
|
|
584
|
+
const callOptions = this.resolveCallOptions({ toolName: name, input }, overrides);
|
|
585
|
+
const sdkClient = await this.awaitUsableClient(name, callOptions?.signal);
|
|
586
|
+
try {
|
|
587
|
+
const params = { name, arguments: input };
|
|
588
|
+
return callOptions
|
|
589
|
+
? await sdkClient.callTool(params, undefined, callOptions)
|
|
590
|
+
: await sdkClient.callTool(params);
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
594
|
+
throw new errors_1.MCPCallError(`MCPClient: Tool "${name}" execution failed: ${message}`, name, error);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Return the SDK client once one is usable. While a reconnect is in flight the
|
|
599
|
+
* call waits for it rather than failing outright, so a transient drop does not
|
|
600
|
+
* surface as a tool error — but an aborted signal still wins immediately.
|
|
601
|
+
*/
|
|
602
|
+
async awaitUsableClient(toolName, signal
|
|
603
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
604
|
+
) {
|
|
605
|
+
if (signal?.aborted) {
|
|
606
|
+
throw new errors_1.MCPCallError(`MCPClient: Tool "${toolName}" execution failed: request was aborted`, toolName, signal.reason);
|
|
607
|
+
}
|
|
608
|
+
if (this._state === "reconnecting" && this.reconnectPromise) {
|
|
609
|
+
await this.raceAbort(this.reconnectPromise, toolName, signal);
|
|
215
610
|
}
|
|
611
|
+
if (this._state !== "connected" || !this.sdkClient) {
|
|
612
|
+
throw new errors_1.MCPNotConnectedError(`MCPClient: Cannot execute tool "${toolName}" — client is not connected`, toolName, this._state);
|
|
613
|
+
}
|
|
614
|
+
return this.sdkClient;
|
|
615
|
+
}
|
|
616
|
+
raceAbort(promise, toolName, signal) {
|
|
617
|
+
if (!signal)
|
|
618
|
+
return promise;
|
|
619
|
+
return new Promise((resolve, reject) => {
|
|
620
|
+
const onAbort = () => reject(new errors_1.MCPCallError(`MCPClient: Tool "${toolName}" execution failed: request was aborted`, toolName, signal.reason));
|
|
621
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
622
|
+
promise
|
|
623
|
+
.then(resolve, reject)
|
|
624
|
+
.finally(() => signal.removeEventListener("abort", onAbort));
|
|
625
|
+
});
|
|
216
626
|
}
|
|
217
627
|
wrapMcpTool(mcpTool) {
|
|
218
628
|
const inputSchema = {
|
|
@@ -225,37 +635,16 @@ class MCPClient {
|
|
|
225
635
|
description: mcpTool.description ?? mcpTool.name,
|
|
226
636
|
inputSchema,
|
|
227
637
|
execute: async (input) => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
result = await this.sdkClient.callTool({
|
|
235
|
-
name: mcpTool.name,
|
|
236
|
-
arguments: input,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
catch (error) {
|
|
240
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
241
|
-
throw new Error(`MCPClient: Tool "${mcpTool.name}" execution failed: ${message}`);
|
|
242
|
-
}
|
|
243
|
-
// Extract text content items from MCP result
|
|
244
|
-
if (result?.content && Array.isArray(result.content)) {
|
|
245
|
-
const textItems = result.content
|
|
246
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
-
.filter((item) => item.type === "text")
|
|
248
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
249
|
-
.map((item) => item.text);
|
|
250
|
-
if (textItems.length > 0) {
|
|
251
|
-
return textItems.length === 1 ? textItems[0] : textItems.join("\n");
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
// Fall back to structured content or full JSON serialization
|
|
255
|
-
if (result?.structuredContent !== undefined) {
|
|
256
|
-
return result.structuredContent;
|
|
638
|
+
const result = await this.invokeTool(mcpTool.name, input ?? {});
|
|
639
|
+
const context = { toolName: mcpTool.name, input: input ?? {} };
|
|
640
|
+
if (result?.isError && this.throwOnToolError) {
|
|
641
|
+
const rendered = (0, content_1.renderToolResult)(result);
|
|
642
|
+
const detail = typeof rendered === "string" ? rendered : JSON.stringify(rendered);
|
|
643
|
+
throw new errors_1.MCPToolError(`MCPClient: Tool "${mcpTool.name}" reported an error: ${detail}`, mcpTool.name, result);
|
|
257
644
|
}
|
|
258
|
-
return
|
|
645
|
+
return this.formatResult
|
|
646
|
+
? this.formatResult(result, context)
|
|
647
|
+
: (0, content_1.renderToolResult)(result);
|
|
259
648
|
},
|
|
260
649
|
});
|
|
261
650
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MCPCallToolResult, MCPContentBlock } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Render a single MCP content block as text.
|
|
4
|
+
*
|
|
5
|
+
* Text blocks are returned verbatim. Binary blocks (image, audio, blob
|
|
6
|
+
* resources) cannot be represented as text, so they are rendered as a
|
|
7
|
+
* descriptive placeholder carrying their mime type and size — enough for a model
|
|
8
|
+
* to know the content exists and to ask for it another way. Text-bearing
|
|
9
|
+
* resources are inlined under a header naming their URI.
|
|
10
|
+
*
|
|
11
|
+
* Unrecognised block types — content types added to the protocol after this
|
|
12
|
+
* release — are serialised as JSON rather than dropped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderContentBlock(block: MCPContentBlock): string;
|
|
15
|
+
/**
|
|
16
|
+
* Convert a raw MCP `CallToolResult` into the value handed to the agent.
|
|
17
|
+
*
|
|
18
|
+
* Resolution order:
|
|
19
|
+
* 1. When the result has content blocks, every block is rendered and the
|
|
20
|
+
* segments are joined with newlines. `structuredContent` is appended as JSON
|
|
21
|
+
* when the result carries no text block, so structured output is never lost
|
|
22
|
+
* behind a binary-only result.
|
|
23
|
+
* 2. Otherwise `structuredContent` is returned as-is, so tools with an
|
|
24
|
+
* `outputSchema` keep giving the agent a real object.
|
|
25
|
+
* 3. Otherwise the whole result is JSON-serialised.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderToolResult(result: MCPCallToolResult | null | undefined): unknown;
|
|
28
|
+
//# sourceMappingURL=content.d.ts.map
|