@alvin0/ai-agent-sdk-mcp 0.1.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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/client-ChEdrVJ_.d.ts +310 -0
- package/dist/client-ChEdrVJ_.d.ts.map +1 -0
- package/dist/client-D7Th3S7z.js +1250 -0
- package/dist/client-D7Th3S7z.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/package.json +91 -0
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
import { Client, InsufficientScopeError, SSEClientTransport, StreamableHTTPClientTransport, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
2
|
+
import { ToolRegistry } from "@alvin0/ai-agent-sdk-core/tools";
|
|
3
|
+
import { isJsonValue, waitForSettlement } from "@alvin0/ai-agent-sdk-core";
|
|
4
|
+
|
|
5
|
+
//#region src/client/result.ts
|
|
6
|
+
var McpRemoteToolError = class extends Error {
|
|
7
|
+
result;
|
|
8
|
+
constructor(serverName, toolName, result) {
|
|
9
|
+
super(`MCP tool '${serverName}/${toolName}' failed: ${resultText(result)}`);
|
|
10
|
+
this.name = "McpRemoteToolError";
|
|
11
|
+
this.result = result;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var McpConnectionError = class extends Error {
|
|
15
|
+
stage;
|
|
16
|
+
failure;
|
|
17
|
+
cleanup;
|
|
18
|
+
code = "MCP_CONNECT_FAILED";
|
|
19
|
+
constructor(stage, failure, cleanup, cause) {
|
|
20
|
+
super(`MCP connection failed during ${stage}`, { cause });
|
|
21
|
+
this.stage = stage;
|
|
22
|
+
this.failure = failure;
|
|
23
|
+
this.cleanup = cleanup;
|
|
24
|
+
this.name = "McpConnectionError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function connectionFailureStage(status) {
|
|
28
|
+
if (status === "authentication-failed" || status === "authentication-required" || status === "oauth-authorization-required" || status === "scope-authorization-required") return "authentication";
|
|
29
|
+
if (status === "connecting") return "handshake";
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
function protocolState(client, transport, fallback) {
|
|
33
|
+
const version = client.getNegotiatedProtocolVersion();
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
era: client.getProtocolEra() ?? "legacy",
|
|
36
|
+
...version === void 0 ? {} : { version },
|
|
37
|
+
transport,
|
|
38
|
+
fallback
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function normalizeResult(result) {
|
|
42
|
+
const content = result.content.map((block, index) => {
|
|
43
|
+
if (!isJsonValue(block)) throw new TypeError(`MCP result content[${index}] is not lossless JSON`);
|
|
44
|
+
return block;
|
|
45
|
+
});
|
|
46
|
+
const structured = result.structuredContent;
|
|
47
|
+
if (structured !== void 0 && !isJsonValue(structured)) throw new TypeError("MCP structuredContent is not lossless JSON");
|
|
48
|
+
return {
|
|
49
|
+
content,
|
|
50
|
+
...structured === void 0 ? {} : { structuredContent: structured }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function renderMcpResult(value) {
|
|
54
|
+
if (!isJsonObject$1(value) || !Array.isArray(value.content)) return [{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: value === void 0 ? "(no output)" : JSON.stringify(value, null, 2)
|
|
57
|
+
}];
|
|
58
|
+
const blocks = value.content.flatMap((block) => renderRemoteBlock(block));
|
|
59
|
+
return blocks.length === 0 ? [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: "(no output)"
|
|
62
|
+
}] : blocks;
|
|
63
|
+
}
|
|
64
|
+
function renderRemoteBlock(value) {
|
|
65
|
+
if (!isJsonObject$1(value) || typeof value.type !== "string") return [{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: JSON.stringify(value)
|
|
68
|
+
}];
|
|
69
|
+
if (value.type === "text" && typeof value.text === "string") return [{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: value.text
|
|
72
|
+
}];
|
|
73
|
+
if (value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string" && isImageMediaType(value.mimeType)) return [{
|
|
74
|
+
type: "image",
|
|
75
|
+
source: {
|
|
76
|
+
kind: "base64",
|
|
77
|
+
mediaType: value.mimeType,
|
|
78
|
+
data: value.data
|
|
79
|
+
}
|
|
80
|
+
}];
|
|
81
|
+
if (value.type === "resource" && isJsonObject$1(value.resource) && typeof value.resource.text === "string") return [{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: value.resource.text
|
|
84
|
+
}];
|
|
85
|
+
if (value.type === "resource_link" && typeof value.uri === "string") return [{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `[MCP resource: ${typeof value.name === "string" ? value.name : value.uri}](${value.uri})`
|
|
88
|
+
}];
|
|
89
|
+
return [{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: JSON.stringify(value, null, 2)
|
|
92
|
+
}];
|
|
93
|
+
}
|
|
94
|
+
function resultText(result) {
|
|
95
|
+
return result.content.filter((block) => typeof block === "object" && block !== null && "type" in block && block.type === "text" && "text" in block && typeof block.text === "string").map((block) => block.text).join("\n").trim() || "remote tool returned an error";
|
|
96
|
+
}
|
|
97
|
+
function isJsonObject$1(value) {
|
|
98
|
+
return isJsonValue(value) && typeof value === "object" && value !== null && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
function isImageMediaType(value) {
|
|
101
|
+
return value === "image/jpeg" || value === "image/png" || value === "image/gif" || value === "image/webp";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/common/integration-operation.ts
|
|
106
|
+
const MCP_INTEGRATION_OPERATIONS = Object.freeze({
|
|
107
|
+
"mcp-http-client": Object.freeze([
|
|
108
|
+
"connect",
|
|
109
|
+
"authenticate",
|
|
110
|
+
"catalog-refresh",
|
|
111
|
+
"reconnect",
|
|
112
|
+
"tool-call",
|
|
113
|
+
"close"
|
|
114
|
+
]),
|
|
115
|
+
"mcp-stdio-client": Object.freeze([
|
|
116
|
+
"connect",
|
|
117
|
+
"catalog-refresh",
|
|
118
|
+
"reconnect",
|
|
119
|
+
"tool-call",
|
|
120
|
+
"close"
|
|
121
|
+
]),
|
|
122
|
+
"mcp-web-server": Object.freeze([
|
|
123
|
+
"request",
|
|
124
|
+
"tool-call",
|
|
125
|
+
"agent-call"
|
|
126
|
+
]),
|
|
127
|
+
"mcp-stdio-server": Object.freeze([
|
|
128
|
+
"request",
|
|
129
|
+
"tool-call",
|
|
130
|
+
"agent-call",
|
|
131
|
+
"close"
|
|
132
|
+
])
|
|
133
|
+
});
|
|
134
|
+
const START_MESSAGE = "SDK integration operation started";
|
|
135
|
+
const ATTEMPT_START_MESSAGE = "SDK integration attempt started";
|
|
136
|
+
const SUCCESS_MESSAGE = "SDK integration operation completed";
|
|
137
|
+
const FAILURE_MESSAGE = "SDK integration operation failed";
|
|
138
|
+
const ABORT_MESSAGE = "SDK integration operation aborted";
|
|
139
|
+
function beginIntegrationOperation(logger, family, operation) {
|
|
140
|
+
assertIdentity(operation, 64, "integration operation");
|
|
141
|
+
const operationId = operationIdentity();
|
|
142
|
+
const startedAt = monotonicNow();
|
|
143
|
+
emit(logger, "info", START_MESSAGE, {
|
|
144
|
+
integrationSchemaVersion: 1,
|
|
145
|
+
integrationFamily: family,
|
|
146
|
+
integrationOperation: operation,
|
|
147
|
+
operationId,
|
|
148
|
+
kind: "logical-start"
|
|
149
|
+
});
|
|
150
|
+
let terminal = false;
|
|
151
|
+
const finish = (status, errorCode) => {
|
|
152
|
+
if (terminal) return;
|
|
153
|
+
terminal = true;
|
|
154
|
+
const fields = {
|
|
155
|
+
integrationSchemaVersion: 1,
|
|
156
|
+
integrationFamily: family,
|
|
157
|
+
integrationOperation: operation,
|
|
158
|
+
operationId,
|
|
159
|
+
kind: "logical-terminal",
|
|
160
|
+
status,
|
|
161
|
+
durationMs: durationSince(startedAt),
|
|
162
|
+
...errorCode === void 0 ? {} : { errorCode: boundedCode(errorCode) }
|
|
163
|
+
};
|
|
164
|
+
emit(logger, status === "error" ? "error" : "info", status === "success" ? SUCCESS_MESSAGE : status === "error" ? FAILURE_MESSAGE : ABORT_MESSAGE, fields);
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
attempt(attemptNumber) {
|
|
168
|
+
if (!Number.isSafeInteger(attemptNumber) || attemptNumber < 1) throw new TypeError("integration attemptNumber must be a positive safe integer");
|
|
169
|
+
const attemptId = operationIdentity(), attemptStartedAt = monotonicNow();
|
|
170
|
+
emit(logger, "info", ATTEMPT_START_MESSAGE, {
|
|
171
|
+
integrationSchemaVersion: 1,
|
|
172
|
+
integrationFamily: family,
|
|
173
|
+
integrationOperation: operation,
|
|
174
|
+
operationId,
|
|
175
|
+
kind: "attempt-start",
|
|
176
|
+
attemptId,
|
|
177
|
+
attemptNumber
|
|
178
|
+
});
|
|
179
|
+
let attemptTerminal = false;
|
|
180
|
+
const finishAttempt = (status, errorCode) => {
|
|
181
|
+
if (attemptTerminal) return;
|
|
182
|
+
attemptTerminal = true;
|
|
183
|
+
emit(logger, status === "error" ? "error" : "info", status === "success" ? SUCCESS_MESSAGE : status === "error" ? FAILURE_MESSAGE : ABORT_MESSAGE, {
|
|
184
|
+
integrationSchemaVersion: 1,
|
|
185
|
+
integrationFamily: family,
|
|
186
|
+
integrationOperation: operation,
|
|
187
|
+
operationId,
|
|
188
|
+
kind: "attempt-terminal",
|
|
189
|
+
attemptId,
|
|
190
|
+
attemptNumber,
|
|
191
|
+
status,
|
|
192
|
+
durationMs: durationSince(attemptStartedAt),
|
|
193
|
+
...errorCode === void 0 ? {} : { errorCode: boundedCode(errorCode) }
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
return {
|
|
197
|
+
success: () => finishAttempt("success"),
|
|
198
|
+
fail: (code) => finishAttempt("error", code),
|
|
199
|
+
abort: () => finishAttempt("aborted")
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
success: () => finish("success"),
|
|
203
|
+
fail: (code) => finish("error", code),
|
|
204
|
+
abort: () => finish("aborted")
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function integrationErrorCode(error) {
|
|
208
|
+
if (typeof error === "object" && error !== null) {
|
|
209
|
+
const code = Object.getOwnPropertyDescriptor(error, "code");
|
|
210
|
+
if (code !== void 0 && "value" in code && typeof code.value === "string") return boundedCode(code.value);
|
|
211
|
+
const name = Object.getOwnPropertyDescriptor(error, "name");
|
|
212
|
+
if (name !== void 0 && "value" in name && typeof name.value === "string") return boundedCode(name.value);
|
|
213
|
+
}
|
|
214
|
+
return "INTEGRATION_ERROR";
|
|
215
|
+
}
|
|
216
|
+
function emit(logger, level, message, fields) {
|
|
217
|
+
try {
|
|
218
|
+
logger?.[level](message, fields);
|
|
219
|
+
} catch {}
|
|
220
|
+
}
|
|
221
|
+
function operationIdentity() {
|
|
222
|
+
return globalThis.crypto.randomUUID();
|
|
223
|
+
}
|
|
224
|
+
function monotonicNow() {
|
|
225
|
+
return globalThis.performance?.now() ?? Date.now();
|
|
226
|
+
}
|
|
227
|
+
function durationSince(startedAt) {
|
|
228
|
+
const value = monotonicNow() - startedAt;
|
|
229
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
230
|
+
}
|
|
231
|
+
function boundedCode(value) {
|
|
232
|
+
const normalized = value.replace(/[^A-Za-z0-9_.:-]/g, "_");
|
|
233
|
+
return (normalized.length === 0 ? "INTEGRATION_ERROR" : normalized).slice(0, 128);
|
|
234
|
+
}
|
|
235
|
+
function assertIdentity(value, limit, label) {
|
|
236
|
+
if (value.length === 0 || value.length > limit) throw new TypeError(`${label} must contain 1-${limit} characters`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/common/support-error.ts
|
|
241
|
+
const NO_USAGE = Object.freeze({
|
|
242
|
+
logicalCalls: 0,
|
|
243
|
+
attempts: 0,
|
|
244
|
+
complete: 0,
|
|
245
|
+
partial: 0,
|
|
246
|
+
estimated: 0,
|
|
247
|
+
missing: 0,
|
|
248
|
+
notApplicable: 0,
|
|
249
|
+
possiblyBilledAttemptsWithoutUsage: 0
|
|
250
|
+
});
|
|
251
|
+
function mcpSupportError(code, stage, message) {
|
|
252
|
+
return Object.freeze({
|
|
253
|
+
code,
|
|
254
|
+
stage,
|
|
255
|
+
message,
|
|
256
|
+
usageCoverage: NO_USAGE,
|
|
257
|
+
possiblyBilledAttemptsWithoutUsage: 0
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/client/close.ts
|
|
263
|
+
/** Run every cleanup task under one deadline and retain only support-safe outcomes. */
|
|
264
|
+
async function executeMcpClosePlan(plan) {
|
|
265
|
+
const operation = beginIntegrationOperation(plan.logger, plan.family, "close");
|
|
266
|
+
const attempt = operation.attempt(1);
|
|
267
|
+
const states = plan.tasks.map(() => "pending");
|
|
268
|
+
const tasks = plan.tasks.map(async (task, index) => {
|
|
269
|
+
try {
|
|
270
|
+
await Promise.resolve().then(task);
|
|
271
|
+
states[index] = "succeeded";
|
|
272
|
+
} catch {
|
|
273
|
+
states[index] = "failed";
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
let aborted = plan.signal?.aborted === true;
|
|
277
|
+
let removeAbort = () => void 0;
|
|
278
|
+
const abortObserved = plan.signal === void 0 ? new Promise(() => void 0) : new Promise((resolve) => {
|
|
279
|
+
const observe = () => {
|
|
280
|
+
aborted = true;
|
|
281
|
+
resolve();
|
|
282
|
+
};
|
|
283
|
+
plan.signal?.addEventListener("abort", observe, { once: true });
|
|
284
|
+
removeAbort = () => plan.signal?.removeEventListener("abort", observe);
|
|
285
|
+
if (plan.signal?.aborted === true) observe();
|
|
286
|
+
});
|
|
287
|
+
await Promise.race([waitForSettlement(Promise.all(tasks), plan.timeoutMs), abortObserved]);
|
|
288
|
+
removeAbort();
|
|
289
|
+
const unsettledOperations = states.filter((state) => state === "pending").length;
|
|
290
|
+
const failed = states.includes("failed");
|
|
291
|
+
const error = aborted && unsettledOperations > 0 ? mcpSupportError("MCP_CLOSE_ABORTED", "mcp-close", "MCP cleanup was interrupted before it settled") : unsettledOperations > 0 ? mcpSupportError("MCP_CLOSE_TIMEOUT", "mcp-close", "MCP cleanup did not settle before its deadline") : failed ? mcpSupportError("MCP_CLOSE_FAILED", "mcp-close", "MCP cleanup failed") : void 0;
|
|
292
|
+
if (error === void 0) {
|
|
293
|
+
attempt.success();
|
|
294
|
+
operation.success();
|
|
295
|
+
} else {
|
|
296
|
+
attempt.fail(error.code);
|
|
297
|
+
operation.fail(error.code);
|
|
298
|
+
}
|
|
299
|
+
return Object.freeze({
|
|
300
|
+
state: "closed",
|
|
301
|
+
deadlineReached: !aborted && unsettledOperations > 0,
|
|
302
|
+
unsettledOperations,
|
|
303
|
+
...error === void 0 ? {} : { error }
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/client/config.ts
|
|
309
|
+
/** Internal defaults shared by MCP client lifecycle, HTTP policy, and reconnect code. */
|
|
310
|
+
const MCP_CLIENT_DEFAULTS = Object.freeze({
|
|
311
|
+
toolCallTimeoutMs: 12e4,
|
|
312
|
+
operationTimeoutMs: 12e4,
|
|
313
|
+
closeTimeoutMs: 3e4,
|
|
314
|
+
maxTools: 1024,
|
|
315
|
+
maxCatalogBytes: 4194304,
|
|
316
|
+
maxToolResultBytes: 4194304,
|
|
317
|
+
maxTransportBytes: 16777216,
|
|
318
|
+
maxRedirectHops: 10
|
|
319
|
+
});
|
|
320
|
+
const MCP_RECONNECT_DEFAULTS = Object.freeze({
|
|
321
|
+
enabled: true,
|
|
322
|
+
initialDelayMs: 500,
|
|
323
|
+
maxDelayMs: 3e4,
|
|
324
|
+
maxAttempts: 10
|
|
325
|
+
});
|
|
326
|
+
const MCP_HTTP_REDIRECT_STATUSES = Object.freeze([
|
|
327
|
+
301,
|
|
328
|
+
302,
|
|
329
|
+
303,
|
|
330
|
+
307,
|
|
331
|
+
308
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/client/runtime-helpers.ts
|
|
336
|
+
function resolveMcpReconnectOptions(input) {
|
|
337
|
+
if (input === false) return Object.freeze({
|
|
338
|
+
...MCP_RECONNECT_DEFAULTS,
|
|
339
|
+
enabled: false
|
|
340
|
+
});
|
|
341
|
+
const resolved = {
|
|
342
|
+
enabled: input?.enabled ?? MCP_RECONNECT_DEFAULTS.enabled,
|
|
343
|
+
initialDelayMs: input?.initialDelayMs ?? MCP_RECONNECT_DEFAULTS.initialDelayMs,
|
|
344
|
+
maxDelayMs: input?.maxDelayMs ?? MCP_RECONNECT_DEFAULTS.maxDelayMs,
|
|
345
|
+
maxAttempts: input?.maxAttempts ?? MCP_RECONNECT_DEFAULTS.maxAttempts
|
|
346
|
+
};
|
|
347
|
+
timeoutMilliseconds(resolved.initialDelayMs, "reconnect.initialDelayMs");
|
|
348
|
+
timeoutMilliseconds(resolved.maxDelayMs, "reconnect.maxDelayMs");
|
|
349
|
+
if (resolved.initialDelayMs > resolved.maxDelayMs) throw new TypeError("reconnect.initialDelayMs must be less than or equal to reconnect.maxDelayMs");
|
|
350
|
+
if (!Number.isInteger(resolved.maxAttempts) || resolved.maxAttempts < 1) throw new TypeError("reconnect.maxAttempts must be a positive integer");
|
|
351
|
+
return Object.freeze(resolved);
|
|
352
|
+
}
|
|
353
|
+
function authenticationKindOf(provider, headers) {
|
|
354
|
+
if (provider === void 0) return headers.has("authorization") ? "bearer" : "none";
|
|
355
|
+
return isOAuthClientProvider(provider) ? "oauth" : "bearer";
|
|
356
|
+
}
|
|
357
|
+
function isOAuthClientProvider(provider) {
|
|
358
|
+
if (typeof provider !== "object" || provider === null) return false;
|
|
359
|
+
const candidate = provider;
|
|
360
|
+
return typeof candidate.clientInformation === "function" && typeof candidate.tokens === "function" && typeof candidate.saveTokens === "function" && typeof candidate.redirectToAuthorization === "function" && typeof candidate.saveCodeVerifier === "function" && typeof candidate.codeVerifier === "function";
|
|
361
|
+
}
|
|
362
|
+
function filterRemoteTools(tools, filter) {
|
|
363
|
+
const allow = filter?.allow === void 0 ? void 0 : new Set(filter.allow);
|
|
364
|
+
const deny = new Set(filter?.deny ?? []);
|
|
365
|
+
return tools.filter((tool) => (allow === void 0 || allow.has(tool.name)) && !deny.has(tool.name));
|
|
366
|
+
}
|
|
367
|
+
function publicToolName(serverName, remoteName, prefixed) {
|
|
368
|
+
return prefixed ? `mcp__${serverName}__${remoteName}` : remoteName;
|
|
369
|
+
}
|
|
370
|
+
function isJsonObject(value) {
|
|
371
|
+
return isJsonValue(value) && typeof value === "object" && value !== null && !Array.isArray(value);
|
|
372
|
+
}
|
|
373
|
+
function assertServerName(name) {
|
|
374
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name)) throw new TypeError("MCP serverName must match /^[A-Za-z][A-Za-z0-9_-]{0,63}$/");
|
|
375
|
+
}
|
|
376
|
+
function positiveSafeInteger(value, field) {
|
|
377
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${field} must be a positive safe integer`);
|
|
378
|
+
return value;
|
|
379
|
+
}
|
|
380
|
+
function timeoutMilliseconds(value, field) {
|
|
381
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647) throw new RangeError(`${field} must be an integer between 1 and 2147483647 milliseconds`);
|
|
382
|
+
return value;
|
|
383
|
+
}
|
|
384
|
+
function serializedBytes(value) {
|
|
385
|
+
const serialized = JSON.stringify(value);
|
|
386
|
+
if (serialized === void 0) throw new TypeError("MCP value is not JSON serializable");
|
|
387
|
+
return new TextEncoder().encode(serialized).byteLength;
|
|
388
|
+
}
|
|
389
|
+
var McpOperationTimeoutError = class extends Error {
|
|
390
|
+
constructor(message) {
|
|
391
|
+
super(message);
|
|
392
|
+
this.name = "McpOperationTimeoutError";
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
function createAbortTimeoutScope(timeoutMs, message, callerSignal) {
|
|
396
|
+
timeoutMilliseconds(timeoutMs, "timeoutMs");
|
|
397
|
+
const controller = new AbortController();
|
|
398
|
+
const timeout = new McpOperationTimeoutError(message);
|
|
399
|
+
const timer = setTimeout(() => controller.abort(timeout), timeoutMs);
|
|
400
|
+
const signal = callerSignal === void 0 ? controller.signal : AbortSignal.any([callerSignal, controller.signal]);
|
|
401
|
+
let active = true;
|
|
402
|
+
const dispose = () => {
|
|
403
|
+
if (!active) return;
|
|
404
|
+
active = false;
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
signal.removeEventListener("abort", dispose);
|
|
407
|
+
};
|
|
408
|
+
signal.addEventListener("abort", dispose, { once: true });
|
|
409
|
+
if (signal.aborted) dispose();
|
|
410
|
+
return Object.freeze({
|
|
411
|
+
signal,
|
|
412
|
+
dispose
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function withAbortTimeout(operation, timeoutMs, message, callerSignal) {
|
|
416
|
+
const scope = createAbortTimeoutScope(timeoutMs, message, callerSignal);
|
|
417
|
+
let pending;
|
|
418
|
+
try {
|
|
419
|
+
scope.signal.throwIfAborted();
|
|
420
|
+
pending = Promise.resolve(operation(scope.signal));
|
|
421
|
+
} catch (error) {
|
|
422
|
+
scope.dispose();
|
|
423
|
+
return Promise.reject(error);
|
|
424
|
+
}
|
|
425
|
+
return raceAbort(pending, scope.signal).finally(scope.dispose);
|
|
426
|
+
}
|
|
427
|
+
function raceAbort(promise, signal) {
|
|
428
|
+
if (signal.aborted) {
|
|
429
|
+
promise.catch(() => void 0);
|
|
430
|
+
return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("MCP operation aborted"));
|
|
431
|
+
}
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
const abort = () => {
|
|
434
|
+
signal.removeEventListener("abort", abort);
|
|
435
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("MCP operation aborted"));
|
|
436
|
+
};
|
|
437
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
438
|
+
promise.then((value) => {
|
|
439
|
+
signal.removeEventListener("abort", abort);
|
|
440
|
+
resolve(value);
|
|
441
|
+
}, (error) => {
|
|
442
|
+
signal.removeEventListener("abort", abort);
|
|
443
|
+
reject(error);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function errorOf(value) {
|
|
448
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/client/connection.ts
|
|
453
|
+
/** MCP client lifecycle and remote-tool bridge for Universal runtimes. */
|
|
454
|
+
function isOAuthCapableTransport(transport) {
|
|
455
|
+
return transport instanceof StreamableHTTPClientTransport || transport instanceof SSEClientTransport;
|
|
456
|
+
}
|
|
457
|
+
function transportKindOf(transport) {
|
|
458
|
+
if (transport instanceof StreamableHTTPClientTransport) return "streamable-http";
|
|
459
|
+
if (transport instanceof SSEClientTransport) return "sse";
|
|
460
|
+
return "custom";
|
|
461
|
+
}
|
|
462
|
+
function shouldTryLegacyTransport(error) {
|
|
463
|
+
if (UnauthorizedError.isInstance(error) || InsufficientScopeError.isInstance(error)) return false;
|
|
464
|
+
return !(error instanceof Error && error.name === "AbortError");
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Owns one MCP server connection across transport generations.
|
|
468
|
+
*
|
|
469
|
+
* The ToolCatalog object is stable for the lifetime of this connection. A
|
|
470
|
+
* successful list refresh swaps its registrations synchronously; a failed
|
|
471
|
+
* refresh leaves the last-known-good catalog intact.
|
|
472
|
+
*/
|
|
473
|
+
var McpClientConnection = class {
|
|
474
|
+
kind = "tool-source";
|
|
475
|
+
apiVersion = 1;
|
|
476
|
+
id;
|
|
477
|
+
serverName;
|
|
478
|
+
tools;
|
|
479
|
+
options;
|
|
480
|
+
transportFactory;
|
|
481
|
+
fallbackTransportFactory;
|
|
482
|
+
authenticationKind;
|
|
483
|
+
integrationFamily;
|
|
484
|
+
toolCallTimeoutMs;
|
|
485
|
+
operationTimeoutMs;
|
|
486
|
+
closeTimeoutMs;
|
|
487
|
+
maxTools;
|
|
488
|
+
maxCatalogBytes;
|
|
489
|
+
maxToolResultBytes;
|
|
490
|
+
reconnect;
|
|
491
|
+
registry = new ToolRegistry();
|
|
492
|
+
toolDisposers = [];
|
|
493
|
+
current;
|
|
494
|
+
pendingAuthorization;
|
|
495
|
+
connecting;
|
|
496
|
+
syncTail = Promise.resolve();
|
|
497
|
+
pendingToolSyncs = 0;
|
|
498
|
+
reconnectTimer;
|
|
499
|
+
reconnectAttempts = 0;
|
|
500
|
+
catalogRevision = 0;
|
|
501
|
+
connectedAt;
|
|
502
|
+
closed = false;
|
|
503
|
+
closeTask;
|
|
504
|
+
currentState;
|
|
505
|
+
constructor(options, transportFactory, runtime = {}) {
|
|
506
|
+
assertServerName(options.serverName);
|
|
507
|
+
this.toolCallTimeoutMs = timeoutMilliseconds(options.toolCallTimeoutMs ?? MCP_CLIENT_DEFAULTS.toolCallTimeoutMs, "toolCallTimeoutMs");
|
|
508
|
+
this.operationTimeoutMs = timeoutMilliseconds(options.operationTimeoutMs ?? MCP_CLIENT_DEFAULTS.operationTimeoutMs, "operationTimeoutMs");
|
|
509
|
+
this.closeTimeoutMs = timeoutMilliseconds(options.closeTimeoutMs ?? MCP_CLIENT_DEFAULTS.closeTimeoutMs, "closeTimeoutMs");
|
|
510
|
+
this.maxTools = positiveSafeInteger(options.maxTools ?? MCP_CLIENT_DEFAULTS.maxTools, "maxTools");
|
|
511
|
+
this.maxCatalogBytes = positiveSafeInteger(options.maxCatalogBytes ?? MCP_CLIENT_DEFAULTS.maxCatalogBytes, "maxCatalogBytes");
|
|
512
|
+
this.maxToolResultBytes = positiveSafeInteger(options.maxToolResultBytes ?? MCP_CLIENT_DEFAULTS.maxToolResultBytes, "maxToolResultBytes");
|
|
513
|
+
const toolFilter = options.toolFilter === void 0 ? void 0 : Object.freeze({
|
|
514
|
+
...options.toolFilter.allow === void 0 ? {} : { allow: Object.freeze([...options.toolFilter.allow]) },
|
|
515
|
+
...options.toolFilter.deny === void 0 ? {} : { deny: Object.freeze([...options.toolFilter.deny]) }
|
|
516
|
+
});
|
|
517
|
+
this.reconnect = resolveMcpReconnectOptions(options.reconnect);
|
|
518
|
+
this.options = Object.freeze({
|
|
519
|
+
...options,
|
|
520
|
+
...toolFilter === void 0 ? {} : { toolFilter }
|
|
521
|
+
});
|
|
522
|
+
this.transportFactory = transportFactory;
|
|
523
|
+
this.fallbackTransportFactory = runtime.fallbackTransportFactory;
|
|
524
|
+
this.authenticationKind = runtime.authenticationKind ?? "unknown";
|
|
525
|
+
this.integrationFamily = runtime.integrationFamily ?? "mcp-http-client";
|
|
526
|
+
this.serverName = options.serverName;
|
|
527
|
+
this.id = options.serverName;
|
|
528
|
+
this.tools = this.registry;
|
|
529
|
+
this.currentState = Object.freeze({
|
|
530
|
+
status: "idle",
|
|
531
|
+
serverName: options.serverName,
|
|
532
|
+
attempt: 0,
|
|
533
|
+
catalogRevision: 0
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
get state() {
|
|
537
|
+
return this.currentState;
|
|
538
|
+
}
|
|
539
|
+
snapshot(options) {
|
|
540
|
+
options.signal.throwIfAborted();
|
|
541
|
+
return Object.freeze({
|
|
542
|
+
revision: String(this.catalogRevision),
|
|
543
|
+
tools: Object.freeze(this.registry.names().map((name) => this.registry.get(name)))
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async withClient(operation) {
|
|
547
|
+
const generation = this.current;
|
|
548
|
+
if (generation === void 0) throw new Error(`MCP server '${this.serverName}' is not connected`);
|
|
549
|
+
const message = `MCP operation on '${this.serverName}' exceeded ${this.operationTimeoutMs}ms`;
|
|
550
|
+
try {
|
|
551
|
+
return await withAbortTimeout((signal) => Promise.resolve().then(() => operation(generation, signal)), this.operationTimeoutMs, message);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
if (error instanceof McpOperationTimeoutError && this.current === generation) {
|
|
554
|
+
this.current = void 0;
|
|
555
|
+
this.clearTools();
|
|
556
|
+
await this.closeGeneration(generation);
|
|
557
|
+
if (!this.closed) this.scheduleReconnect(error);
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/** Connect, negotiate capabilities, and publish the first tool snapshot. */
|
|
563
|
+
connect() {
|
|
564
|
+
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error(`MCP client '${this.serverName}' is closed`));
|
|
565
|
+
if (this.current !== void 0 && (this.currentState.status === "ready" || this.currentState.status === "scope-authorization-required")) return Promise.resolve();
|
|
566
|
+
if (this.pendingAuthorization !== void 0) return Promise.reject(/* @__PURE__ */ new Error(`MCP client '${this.serverName}' is waiting for its OAuth callback`));
|
|
567
|
+
if (this.connecting !== void 0) return this.connecting;
|
|
568
|
+
if (this.reconnectTimer !== void 0) {
|
|
569
|
+
clearTimeout(this.reconnectTimer);
|
|
570
|
+
this.reconnectTimer = void 0;
|
|
571
|
+
}
|
|
572
|
+
const tracked = this.connectGeneration(this.currentState.status === "reconnecting").finally(() => {
|
|
573
|
+
if (this.connecting === tracked) this.connecting = void 0;
|
|
574
|
+
});
|
|
575
|
+
this.connecting = tracked;
|
|
576
|
+
return tracked;
|
|
577
|
+
}
|
|
578
|
+
/** Force a fresh tools/list and atomically replace the published snapshot. */
|
|
579
|
+
refreshTools(options = {}) {
|
|
580
|
+
const generation = this.current;
|
|
581
|
+
if (generation === void 0) return Promise.reject(/* @__PURE__ */ new Error(`MCP server '${this.serverName}' is not connected`));
|
|
582
|
+
return this.enqueueToolSync(generation, void 0, options.signal);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Validate an OAuth callback, exchange its authorization code on the pending
|
|
586
|
+
* HTTP transport, then reconnect with a fresh transport generation.
|
|
587
|
+
*/
|
|
588
|
+
async finishOAuth(callbackParams, options) {
|
|
589
|
+
if (this.closed) throw new Error(`MCP client '${this.serverName}' is closed`);
|
|
590
|
+
const pending = this.pendingAuthorization;
|
|
591
|
+
if (pending === void 0) throw new Error(`MCP client '${this.serverName}' has no pending OAuth authorization`);
|
|
592
|
+
if (options.expectedState.length === 0 || callbackParams.get("state") !== options.expectedState) throw new Error(`MCP client '${this.serverName}' rejected an OAuth callback with mismatched state`);
|
|
593
|
+
if (callbackParams.has("error")) throw new Error(`MCP client '${this.serverName}' OAuth authorization was denied or failed`);
|
|
594
|
+
const operation = beginIntegrationOperation(this.options.logger, this.integrationFamily, "authenticate");
|
|
595
|
+
const attempt = operation.attempt(1);
|
|
596
|
+
this.pendingAuthorization = void 0;
|
|
597
|
+
try {
|
|
598
|
+
await withAbortTimeout(() => pending.transport.finishAuth(callbackParams), this.operationTimeoutMs, `MCP OAuth callback exceeded ${this.operationTimeoutMs}ms`, options.signal);
|
|
599
|
+
attempt.success();
|
|
600
|
+
operation.success();
|
|
601
|
+
} catch (error) {
|
|
602
|
+
attempt.fail(integrationErrorCode(error));
|
|
603
|
+
operation.fail(integrationErrorCode(error));
|
|
604
|
+
const failure = errorOf(error);
|
|
605
|
+
this.publish("failed", this.reconnectAttempts, failure);
|
|
606
|
+
throw failure;
|
|
607
|
+
} finally {
|
|
608
|
+
await this.closeGeneration(pending.client);
|
|
609
|
+
}
|
|
610
|
+
await this.connect();
|
|
611
|
+
}
|
|
612
|
+
/** Stop reconnecting, close the live generation, and unregister its tools. */
|
|
613
|
+
async close() {
|
|
614
|
+
await this.closeWithReport();
|
|
615
|
+
}
|
|
616
|
+
closeWithReport(options = {}) {
|
|
617
|
+
if (this.closeTask !== void 0) return this.closeTask;
|
|
618
|
+
this.closeTask = this.performClose(options.signal);
|
|
619
|
+
return this.closeTask;
|
|
620
|
+
}
|
|
621
|
+
async performClose(signal) {
|
|
622
|
+
this.closed = true;
|
|
623
|
+
if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
|
|
624
|
+
this.reconnectTimer = void 0;
|
|
625
|
+
const generation = this.current;
|
|
626
|
+
this.current = void 0;
|
|
627
|
+
const tasks = [];
|
|
628
|
+
if (generation !== void 0) {
|
|
629
|
+
try {
|
|
630
|
+
const transport = generation.transport;
|
|
631
|
+
if (transport instanceof StreamableHTTPClientTransport && transport.sessionId !== void 0) tasks.push(() => transport.terminateSession());
|
|
632
|
+
} catch {}
|
|
633
|
+
tasks.push(() => generation.close());
|
|
634
|
+
}
|
|
635
|
+
const pending = this.pendingAuthorization;
|
|
636
|
+
this.pendingAuthorization = void 0;
|
|
637
|
+
if (pending !== void 0 && pending.client !== generation) tasks.push(() => pending.client.close());
|
|
638
|
+
const connecting = this.connecting;
|
|
639
|
+
if (connecting !== void 0) tasks.push(() => connecting);
|
|
640
|
+
if (this.pendingToolSyncs > 0) tasks.push(() => this.syncTail);
|
|
641
|
+
const report = await executeMcpClosePlan({
|
|
642
|
+
...this.options.logger === void 0 ? {} : { logger: this.options.logger },
|
|
643
|
+
family: this.integrationFamily,
|
|
644
|
+
...signal === void 0 ? {} : { signal },
|
|
645
|
+
timeoutMs: this.closeTimeoutMs,
|
|
646
|
+
tasks
|
|
647
|
+
});
|
|
648
|
+
this.clearTools();
|
|
649
|
+
this.publish("closed", this.reconnectAttempts);
|
|
650
|
+
return report;
|
|
651
|
+
}
|
|
652
|
+
async connectGeneration(reconnecting) {
|
|
653
|
+
const attempt = reconnecting ? this.reconnectAttempts : 0;
|
|
654
|
+
const operation = beginIntegrationOperation(this.options.logger, this.integrationFamily, reconnecting ? "reconnect" : "connect");
|
|
655
|
+
this.publish(reconnecting ? "reconnecting" : "connecting", attempt);
|
|
656
|
+
const factories = [this.transportFactory, this.fallbackTransportFactory].filter((factory) => factory !== void 0);
|
|
657
|
+
let lastFailure;
|
|
658
|
+
for (let index = 0; index < factories.length; index++) {
|
|
659
|
+
const physicalAttempt = operation.attempt(index + 1);
|
|
660
|
+
let generation;
|
|
661
|
+
try {
|
|
662
|
+
generation = this.createGeneration();
|
|
663
|
+
} catch (error) {
|
|
664
|
+
const code = integrationErrorCode(error);
|
|
665
|
+
physicalAttempt.fail(code);
|
|
666
|
+
operation.fail(code);
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
669
|
+
let starting = true;
|
|
670
|
+
let transport;
|
|
671
|
+
this.current = generation;
|
|
672
|
+
generation.onclose = () => {
|
|
673
|
+
if (!starting) this.generationDown(generation);
|
|
674
|
+
};
|
|
675
|
+
try {
|
|
676
|
+
const openedTransport = factories[index]();
|
|
677
|
+
transport = openedTransport;
|
|
678
|
+
await withAbortTimeout(() => generation.connect(openedTransport), this.operationTimeoutMs, `MCP connection '${this.serverName}' exceeded ${this.operationTimeoutMs}ms`, this.options.signal);
|
|
679
|
+
if (this.current !== generation || this.closed) throw new Error(`MCP connection '${this.serverName}' closed during startup`);
|
|
680
|
+
await this.enqueueToolSync(generation, void 0, this.options.signal);
|
|
681
|
+
if (this.current !== generation || this.closed) throw new Error(`MCP connection '${this.serverName}' closed during tool discovery`);
|
|
682
|
+
this.connectedAt = Date.now();
|
|
683
|
+
starting = false;
|
|
684
|
+
this.publish("ready", this.reconnectAttempts, void 0, { protocol: protocolState(generation, transportKindOf(transport), index > 0) });
|
|
685
|
+
physicalAttempt.success();
|
|
686
|
+
operation.success();
|
|
687
|
+
if (this.authenticationKind !== "none" && this.authenticationKind !== "unknown") {
|
|
688
|
+
const authentication = beginIntegrationOperation(this.options.logger, this.integrationFamily, "authenticate");
|
|
689
|
+
authentication.attempt(1).success();
|
|
690
|
+
authentication.success();
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
} catch (error) {
|
|
694
|
+
physicalAttempt.fail(integrationErrorCode(error));
|
|
695
|
+
starting = false;
|
|
696
|
+
const failure = errorOf(error);
|
|
697
|
+
lastFailure = failure;
|
|
698
|
+
if (this.current === generation) this.current = void 0;
|
|
699
|
+
if (UnauthorizedError.isInstance(error)) {
|
|
700
|
+
if (this.authenticationKind === "oauth" && isOAuthCapableTransport(transport)) {
|
|
701
|
+
this.pendingAuthorization = {
|
|
702
|
+
client: generation,
|
|
703
|
+
transport
|
|
704
|
+
};
|
|
705
|
+
this.publish("oauth-authorization-required", this.reconnectAttempts, failure, { authorization: {
|
|
706
|
+
kind: "oauth",
|
|
707
|
+
reason: "authorization-code-required"
|
|
708
|
+
} });
|
|
709
|
+
operation.fail(integrationErrorCode(error));
|
|
710
|
+
this.logAuthenticationFailure(error);
|
|
711
|
+
throw failure;
|
|
712
|
+
}
|
|
713
|
+
await this.closeGeneration(generation);
|
|
714
|
+
const status = this.authenticationKind === "bearer" ? "authentication-failed" : "authentication-required";
|
|
715
|
+
this.publish(status, this.reconnectAttempts, failure, { authorization: {
|
|
716
|
+
kind: this.authenticationKind,
|
|
717
|
+
reason: this.authenticationKind === "bearer" ? "invalid-credentials" : "credentials-required"
|
|
718
|
+
} });
|
|
719
|
+
operation.fail(integrationErrorCode(error));
|
|
720
|
+
this.logAuthenticationFailure(error);
|
|
721
|
+
throw failure;
|
|
722
|
+
}
|
|
723
|
+
await this.closeGeneration(generation);
|
|
724
|
+
if (index + 1 < factories.length && shouldTryLegacyTransport(error)) continue;
|
|
725
|
+
if (!this.closed) this.scheduleReconnect(failure);
|
|
726
|
+
operation.fail(integrationErrorCode(error));
|
|
727
|
+
throw failure;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const failure = lastFailure ?? /* @__PURE__ */ new Error(`MCP connection '${this.serverName}' has no transport candidate`);
|
|
731
|
+
if (!this.closed) this.scheduleReconnect(failure);
|
|
732
|
+
operation.fail(integrationErrorCode(failure));
|
|
733
|
+
throw failure;
|
|
734
|
+
}
|
|
735
|
+
createGeneration() {
|
|
736
|
+
let generation;
|
|
737
|
+
generation = new Client({
|
|
738
|
+
name: this.options.clientName ?? "ai-agent-sdk",
|
|
739
|
+
version: this.options.clientVersion ?? "0.0.0"
|
|
740
|
+
}, this.clientOptions((error, items) => {
|
|
741
|
+
if (this.current !== generation || this.closed) return;
|
|
742
|
+
if (error !== null || items === null) {
|
|
743
|
+
try {
|
|
744
|
+
this.options.onStateChange?.(Object.freeze({
|
|
745
|
+
...this.currentState,
|
|
746
|
+
...error === null ? {} : { error },
|
|
747
|
+
...error === null ? {} : { supportError: mcpSupportError(integrationErrorCode(error), "mcp-client", "MCP client operation failed") }
|
|
748
|
+
}));
|
|
749
|
+
} catch {}
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
this.enqueueToolSync(generation, items).catch((error) => {
|
|
753
|
+
try {
|
|
754
|
+
this.options.onStateChange?.(Object.freeze({
|
|
755
|
+
...this.currentState,
|
|
756
|
+
error: errorOf(error),
|
|
757
|
+
supportError: mcpSupportError(integrationErrorCode(error), "mcp-client", "MCP client operation failed")
|
|
758
|
+
}));
|
|
759
|
+
} catch {}
|
|
760
|
+
});
|
|
761
|
+
}));
|
|
762
|
+
return generation;
|
|
763
|
+
}
|
|
764
|
+
clientOptions(onToolsChanged) {
|
|
765
|
+
return {
|
|
766
|
+
capabilities: {},
|
|
767
|
+
versionNegotiation: { mode: this.options.protocol ?? "auto" },
|
|
768
|
+
listChanged: { tools: {
|
|
769
|
+
autoRefresh: true,
|
|
770
|
+
onChanged: onToolsChanged
|
|
771
|
+
} }
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
generationDown(generation) {
|
|
775
|
+
if (this.closed || this.current !== generation) return;
|
|
776
|
+
this.current = void 0;
|
|
777
|
+
this.scheduleReconnect(/* @__PURE__ */ new Error(`MCP connection '${this.serverName}' closed`));
|
|
778
|
+
}
|
|
779
|
+
scheduleReconnect(error) {
|
|
780
|
+
if (this.closed || this.reconnectTimer !== void 0) return;
|
|
781
|
+
const policy = this.reconnect;
|
|
782
|
+
if (!policy.enabled) {
|
|
783
|
+
this.publish("failed", this.reconnectAttempts, error);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (this.connectedAt !== void 0 && Date.now() - this.connectedAt >= policy.maxDelayMs) this.reconnectAttempts = 0;
|
|
787
|
+
this.connectedAt = void 0;
|
|
788
|
+
this.reconnectAttempts += 1;
|
|
789
|
+
if (this.reconnectAttempts > policy.maxAttempts) {
|
|
790
|
+
this.clearTools();
|
|
791
|
+
this.publish("failed", policy.maxAttempts, error);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const delay = Math.min(policy.maxDelayMs, policy.initialDelayMs * 2 ** (this.reconnectAttempts - 1));
|
|
795
|
+
this.publish("reconnecting", this.reconnectAttempts, error);
|
|
796
|
+
this.reconnectTimer = setTimeout(() => {
|
|
797
|
+
this.reconnectTimer = void 0;
|
|
798
|
+
this.connect().catch(() => void 0);
|
|
799
|
+
}, delay);
|
|
800
|
+
this.reconnectTimer.unref?.();
|
|
801
|
+
}
|
|
802
|
+
enqueueToolSync(generation, supplied, callerSignal) {
|
|
803
|
+
this.pendingToolSyncs += 1;
|
|
804
|
+
const tracked = this.syncTail.then(async () => {
|
|
805
|
+
if (this.closed || this.current !== generation) return;
|
|
806
|
+
const operation = beginIntegrationOperation(this.options.logger, this.integrationFamily, "catalog-refresh");
|
|
807
|
+
const attempt = operation.attempt(1);
|
|
808
|
+
try {
|
|
809
|
+
const tools = supplied ?? (await withAbortTimeout((signal) => generation.listTools(void 0, {
|
|
810
|
+
cacheMode: "refresh",
|
|
811
|
+
signal
|
|
812
|
+
}), this.operationTimeoutMs, `MCP tool discovery exceeded ${this.operationTimeoutMs}ms`, callerSignal)).tools;
|
|
813
|
+
if (this.closed || this.current !== generation) {
|
|
814
|
+
attempt.abort();
|
|
815
|
+
operation.abort();
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
this.swapTools(tools);
|
|
819
|
+
attempt.success();
|
|
820
|
+
operation.success();
|
|
821
|
+
} catch (error) {
|
|
822
|
+
attempt.fail(integrationErrorCode(error));
|
|
823
|
+
operation.fail(integrationErrorCode(error));
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}).finally(() => {
|
|
827
|
+
this.pendingToolSyncs -= 1;
|
|
828
|
+
});
|
|
829
|
+
this.syncTail = tracked.catch(() => void 0);
|
|
830
|
+
return tracked;
|
|
831
|
+
}
|
|
832
|
+
swapTools(remoteTools) {
|
|
833
|
+
if (remoteTools.length > this.maxTools) throw new RangeError(`MCP server '${this.serverName}' exceeds the ${this.maxTools}-tool limit`);
|
|
834
|
+
if (serializedBytes(remoteTools) > this.maxCatalogBytes) throw new RangeError(`MCP server '${this.serverName}' catalog exceeds the ${this.maxCatalogBytes}-byte limit`);
|
|
835
|
+
const next = new ToolRegistry();
|
|
836
|
+
const seen = /* @__PURE__ */ new Set();
|
|
837
|
+
for (const remote of filterRemoteTools(remoteTools, this.options.toolFilter)) {
|
|
838
|
+
const name = publicToolName(this.serverName, remote.name, this.options.prefixToolNames !== false);
|
|
839
|
+
if (seen.has(name)) throw new Error(`MCP server '${this.serverName}' produced duplicate tool name '${name}'`);
|
|
840
|
+
seen.add(name);
|
|
841
|
+
next.register(this.bridgeTool(name, remote));
|
|
842
|
+
}
|
|
843
|
+
this.clearTools(false);
|
|
844
|
+
this.toolDisposers = next.names().map((name) => this.registry.register(next.get(name)));
|
|
845
|
+
this.bumpCatalogRevision();
|
|
846
|
+
}
|
|
847
|
+
bridgeTool(publicName, remote) {
|
|
848
|
+
const inputSchema = isJsonObject(remote.inputSchema) ? structuredClone(remote.inputSchema) : {
|
|
849
|
+
type: "object",
|
|
850
|
+
additionalProperties: true
|
|
851
|
+
};
|
|
852
|
+
return {
|
|
853
|
+
name: publicName,
|
|
854
|
+
description: remote.description?.trim() || `Tool '${remote.name}' from MCP server '${this.serverName}'.`,
|
|
855
|
+
parameters: inputSchema,
|
|
856
|
+
timeoutMs: this.toolCallTimeoutMs,
|
|
857
|
+
parse: (raw) => {
|
|
858
|
+
if (!isJsonObject(raw)) throw new TypeError("MCP tool arguments must be a JSON object");
|
|
859
|
+
return raw;
|
|
860
|
+
},
|
|
861
|
+
execute: async (args, context) => {
|
|
862
|
+
const generation = this.current;
|
|
863
|
+
if (generation === void 0) throw new Error(`MCP server '${this.serverName}' is not connected`);
|
|
864
|
+
const operation = beginIntegrationOperation(context.logger, this.integrationFamily, "tool-call");
|
|
865
|
+
const attempt = operation.attempt(1);
|
|
866
|
+
try {
|
|
867
|
+
const result = await raceAbort(generation.callTool({
|
|
868
|
+
name: remote.name,
|
|
869
|
+
arguments: args
|
|
870
|
+
}, {
|
|
871
|
+
signal: context.signal,
|
|
872
|
+
toolDefinition: remote,
|
|
873
|
+
timeout: this.toolCallTimeoutMs
|
|
874
|
+
}), context.signal);
|
|
875
|
+
if (serializedBytes(result) > this.maxToolResultBytes) throw new RangeError(`MCP tool '${this.serverName}/${remote.name}' result exceeds the ${this.maxToolResultBytes}-byte limit`);
|
|
876
|
+
if (result.isError === true) throw new McpRemoteToolError(this.serverName, remote.name, result);
|
|
877
|
+
const normalized = normalizeResult(result);
|
|
878
|
+
attempt.success();
|
|
879
|
+
operation.success();
|
|
880
|
+
return normalized;
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (context.signal.aborted) {
|
|
883
|
+
attempt.abort();
|
|
884
|
+
operation.abort();
|
|
885
|
+
} else {
|
|
886
|
+
attempt.fail(integrationErrorCode(error));
|
|
887
|
+
operation.fail(integrationErrorCode(error));
|
|
888
|
+
}
|
|
889
|
+
if (InsufficientScopeError.isInstance(error)) this.publish("scope-authorization-required", this.reconnectAttempts, errorOf(error), {
|
|
890
|
+
authorization: {
|
|
891
|
+
kind: this.authenticationKind,
|
|
892
|
+
reason: "insufficient-scope",
|
|
893
|
+
...error.requiredScope === void 0 ? {} : { requiredScope: error.requiredScope }
|
|
894
|
+
},
|
|
895
|
+
...this.currentState.protocol === void 0 ? {} : { protocol: this.currentState.protocol }
|
|
896
|
+
});
|
|
897
|
+
else if (UnauthorizedError.isInstance(error)) {
|
|
898
|
+
const transport = generation.transport;
|
|
899
|
+
if (this.current === generation) this.current = void 0;
|
|
900
|
+
if (this.authenticationKind === "oauth" && isOAuthCapableTransport(transport)) {
|
|
901
|
+
this.pendingAuthorization = {
|
|
902
|
+
client: generation,
|
|
903
|
+
transport
|
|
904
|
+
};
|
|
905
|
+
this.publish("oauth-authorization-required", this.reconnectAttempts, errorOf(error), {
|
|
906
|
+
authorization: {
|
|
907
|
+
kind: "oauth",
|
|
908
|
+
reason: "authorization-code-required"
|
|
909
|
+
},
|
|
910
|
+
...this.currentState.protocol === void 0 ? {} : { protocol: this.currentState.protocol }
|
|
911
|
+
});
|
|
912
|
+
} else {
|
|
913
|
+
await this.closeGeneration(generation);
|
|
914
|
+
this.publish(this.authenticationKind === "bearer" ? "authentication-failed" : "authentication-required", this.reconnectAttempts, errorOf(error), {
|
|
915
|
+
authorization: {
|
|
916
|
+
kind: this.authenticationKind,
|
|
917
|
+
reason: this.authenticationKind === "bearer" ? "invalid-credentials" : "credentials-required"
|
|
918
|
+
},
|
|
919
|
+
...this.currentState.protocol === void 0 ? {} : { protocol: this.currentState.protocol }
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
render: (value) => renderMcpResult(value),
|
|
927
|
+
meta: () => ({
|
|
928
|
+
kind: "mcp",
|
|
929
|
+
serverName: this.serverName,
|
|
930
|
+
remoteToolName: remote.name
|
|
931
|
+
}),
|
|
932
|
+
...this.options.trustReadOnlyAnnotations === true && remote.annotations?.readOnlyHint === true ? { isConcurrencySafe: () => true } : {}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
clearTools(recordRevision = true) {
|
|
936
|
+
if (this.toolDisposers.length === 0) return;
|
|
937
|
+
for (const dispose of this.toolDisposers) dispose();
|
|
938
|
+
this.toolDisposers = [];
|
|
939
|
+
if (recordRevision) this.bumpCatalogRevision();
|
|
940
|
+
}
|
|
941
|
+
bumpCatalogRevision() {
|
|
942
|
+
if (this.catalogRevision < Number.MAX_SAFE_INTEGER) this.catalogRevision += 1;
|
|
943
|
+
this.currentState = Object.freeze({
|
|
944
|
+
...this.currentState,
|
|
945
|
+
catalogRevision: this.catalogRevision
|
|
946
|
+
});
|
|
947
|
+
try {
|
|
948
|
+
this.options.onStateChange?.(this.currentState);
|
|
949
|
+
} catch {}
|
|
950
|
+
}
|
|
951
|
+
logAuthenticationFailure(error) {
|
|
952
|
+
const operation = beginIntegrationOperation(this.options.logger, this.integrationFamily, "authenticate");
|
|
953
|
+
const attempt = operation.attempt(1);
|
|
954
|
+
const code = integrationErrorCode(error);
|
|
955
|
+
attempt.fail(code);
|
|
956
|
+
operation.fail(code);
|
|
957
|
+
}
|
|
958
|
+
async closeGeneration(generation) {
|
|
959
|
+
return (await executeMcpClosePlan({
|
|
960
|
+
family: this.integrationFamily,
|
|
961
|
+
timeoutMs: this.closeTimeoutMs,
|
|
962
|
+
tasks: [() => generation.close()]
|
|
963
|
+
})).error === void 0;
|
|
964
|
+
}
|
|
965
|
+
publish(status, attempt, error, details = {}) {
|
|
966
|
+
this.currentState = Object.freeze({
|
|
967
|
+
status,
|
|
968
|
+
serverName: this.serverName,
|
|
969
|
+
attempt,
|
|
970
|
+
catalogRevision: this.catalogRevision,
|
|
971
|
+
...error === void 0 ? {} : { error },
|
|
972
|
+
...error === void 0 ? {} : { supportError: mcpSupportError(integrationErrorCode(error), "mcp-client", "MCP client operation failed") },
|
|
973
|
+
...details.authorization === void 0 ? {} : { authorization: Object.freeze(details.authorization) },
|
|
974
|
+
...details.protocol === void 0 ? {} : { protocol: Object.freeze(details.protocol) }
|
|
975
|
+
});
|
|
976
|
+
try {
|
|
977
|
+
this.options.onStateChange?.(this.currentState);
|
|
978
|
+
} catch {}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/client/http-security.ts
|
|
984
|
+
function snapshotHttpSecurityOptions(options) {
|
|
985
|
+
const allowedOrigins = options.allowedOrigins?.map((origin, index) => {
|
|
986
|
+
let url;
|
|
987
|
+
try {
|
|
988
|
+
url = new URL(origin);
|
|
989
|
+
} catch {
|
|
990
|
+
throw new TypeError(`allowedOrigins[${index}] must be an absolute URL`);
|
|
991
|
+
}
|
|
992
|
+
if (url.username.length > 0 || url.password.length > 0) throw new TypeError(`allowedOrigins[${index}] must not contain credentials`);
|
|
993
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new TypeError(`allowedOrigins[${index}] must use http or https`);
|
|
994
|
+
return url.origin;
|
|
995
|
+
});
|
|
996
|
+
return Object.freeze({
|
|
997
|
+
...allowedOrigins === void 0 ? {} : { allowedOrigins: Object.freeze([...new Set(allowedOrigins)]) },
|
|
998
|
+
requireHttps: options.requireHttps !== false,
|
|
999
|
+
allowPrivateNetwork: options.allowPrivateNetwork === true,
|
|
1000
|
+
allowRedirects: options.allowRedirects === true,
|
|
1001
|
+
...options.validateEndpoint === void 0 ? {} : { validateEndpoint: options.validateEndpoint },
|
|
1002
|
+
maxTransportBytes: positiveSafeInteger(options.maxTransportBytes ?? MCP_CLIENT_DEFAULTS.maxTransportBytes, "maxTransportBytes"),
|
|
1003
|
+
timeoutMs: timeoutMilliseconds(options.operationTimeoutMs ?? MCP_CLIENT_DEFAULTS.operationTimeoutMs, "operationTimeoutMs"),
|
|
1004
|
+
teardownTimeoutMs: timeoutMilliseconds(options.closeTimeoutMs ?? MCP_CLIENT_DEFAULTS.closeTimeoutMs, "closeTimeoutMs")
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
function validateHttpEndpoint(value, options) {
|
|
1008
|
+
const url = new URL(value);
|
|
1009
|
+
if (url.username.length > 0 || url.password.length > 0) throw new TypeError("MCP HTTP endpoint URL must not contain credentials");
|
|
1010
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new TypeError("MCP HTTP endpoint URL must use http or https");
|
|
1011
|
+
if (!options.allowPrivateNetwork && isPrivateHostname(url.hostname)) throw new TypeError(`MCP HTTP endpoint host '${url.hostname}' is private or local`);
|
|
1012
|
+
if (options.requireHttps && url.protocol !== "https:") throw new TypeError("MCP HTTP endpoint URL must use https under the configured policy");
|
|
1013
|
+
if (options.allowedOrigins !== void 0 && !options.allowedOrigins.includes(url.origin)) throw new TypeError(`MCP HTTP endpoint origin '${url.origin}' is not allowed`);
|
|
1014
|
+
return url;
|
|
1015
|
+
}
|
|
1016
|
+
function createGuardedMcpFetch(baseFetch, options) {
|
|
1017
|
+
if (typeof baseFetch !== "function") throw new TypeError("MCP HTTP transport requires fetch");
|
|
1018
|
+
return ((input, init) => {
|
|
1019
|
+
const scope = createAbortTimeoutScope(options.timeoutMs, `MCP HTTP operation exceeded its ${options.timeoutMs}ms deadline`, init?.signal ?? void 0);
|
|
1020
|
+
const pending = (async () => {
|
|
1021
|
+
const signal = scope.signal;
|
|
1022
|
+
signal.throwIfAborted();
|
|
1023
|
+
let currentUrl = validateHttpEndpoint(input, options);
|
|
1024
|
+
await validateBeforeFetch(currentUrl, options, signal);
|
|
1025
|
+
let requestInit = {
|
|
1026
|
+
...init,
|
|
1027
|
+
signal,
|
|
1028
|
+
redirect: "manual"
|
|
1029
|
+
};
|
|
1030
|
+
let response;
|
|
1031
|
+
for (let hop = 0;; hop++) {
|
|
1032
|
+
signal.throwIfAborted();
|
|
1033
|
+
const fetching = Promise.resolve(baseFetch(currentUrl, requestInit));
|
|
1034
|
+
try {
|
|
1035
|
+
response = await raceAbort(fetching, signal);
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
fetching.then((late) => cancelResponse(late, options.teardownTimeoutMs), () => void 0).catch(() => void 0);
|
|
1038
|
+
throw error;
|
|
1039
|
+
}
|
|
1040
|
+
if (response.type === "opaqueredirect") {
|
|
1041
|
+
await cancelResponse(response, options.teardownTimeoutMs);
|
|
1042
|
+
throw new Error("MCP HTTP transport rejected an opaque redirect");
|
|
1043
|
+
}
|
|
1044
|
+
if (!MCP_HTTP_REDIRECT_STATUSES.includes(response.status)) break;
|
|
1045
|
+
if (!options.allowRedirects) {
|
|
1046
|
+
await cancelResponse(response, options.teardownTimeoutMs);
|
|
1047
|
+
throw new Error("MCP HTTP transport rejected a redirect");
|
|
1048
|
+
}
|
|
1049
|
+
if (hop >= MCP_CLIENT_DEFAULTS.maxRedirectHops) {
|
|
1050
|
+
await cancelResponse(response, options.teardownTimeoutMs);
|
|
1051
|
+
throw new Error(`MCP HTTP transport exceeded the ${MCP_CLIENT_DEFAULTS.maxRedirectHops}-redirect limit`);
|
|
1052
|
+
}
|
|
1053
|
+
const location = response.headers.get("location");
|
|
1054
|
+
if (location === null) {
|
|
1055
|
+
await cancelResponse(response, options.teardownTimeoutMs);
|
|
1056
|
+
throw new Error("MCP HTTP transport received a redirect without a location");
|
|
1057
|
+
}
|
|
1058
|
+
await cancelResponse(response, options.teardownTimeoutMs);
|
|
1059
|
+
const nextUrl = validateHttpEndpoint(new URL(location, currentUrl), options);
|
|
1060
|
+
await validateBeforeFetch(nextUrl, options, signal);
|
|
1061
|
+
const crossesOrigin = nextUrl.origin !== currentUrl.origin;
|
|
1062
|
+
requestInit = redirectInit(requestInit, response.status, crossesOrigin);
|
|
1063
|
+
currentUrl = nextUrl;
|
|
1064
|
+
}
|
|
1065
|
+
if (response.url.length > 0) try {
|
|
1066
|
+
if (validateHttpEndpoint(response.url, options).origin !== currentUrl.origin) throw new Error("MCP HTTP transport response escaped the validated origin");
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
await cancelResponse(response, options.teardownTimeoutMs);
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
const declared = Number(response.headers.get("content-length"));
|
|
1072
|
+
if (Number.isFinite(declared) && declared > options.maxTransportBytes) {
|
|
1073
|
+
if (response.body !== null) await waitForSettlement(response.body.cancel().catch(() => void 0), options.teardownTimeoutMs);
|
|
1074
|
+
throw new Error(`MCP HTTP response exceeds the ${options.maxTransportBytes}-byte limit`);
|
|
1075
|
+
}
|
|
1076
|
+
if (response.body === null) {
|
|
1077
|
+
scope.dispose();
|
|
1078
|
+
return response;
|
|
1079
|
+
}
|
|
1080
|
+
const limited = limitedResponseBody(response.body, options.maxTransportBytes, options.teardownTimeoutMs, signal, scope.dispose);
|
|
1081
|
+
return new Response(limited, {
|
|
1082
|
+
status: response.status,
|
|
1083
|
+
statusText: response.statusText,
|
|
1084
|
+
headers: response.headers
|
|
1085
|
+
});
|
|
1086
|
+
})();
|
|
1087
|
+
return raceAbort(pending, scope.signal).catch((error) => {
|
|
1088
|
+
scope.dispose();
|
|
1089
|
+
throw error;
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
function limitedResponseBody(body, maxBytes, teardownTimeoutMs, signal, dispose) {
|
|
1094
|
+
const reader = body.getReader();
|
|
1095
|
+
let received = 0;
|
|
1096
|
+
let settled = false;
|
|
1097
|
+
let controller;
|
|
1098
|
+
const settle = () => {
|
|
1099
|
+
if (settled) return;
|
|
1100
|
+
settled = true;
|
|
1101
|
+
signal.removeEventListener("abort", abort);
|
|
1102
|
+
dispose();
|
|
1103
|
+
};
|
|
1104
|
+
const abort = () => {
|
|
1105
|
+
if (settled) return;
|
|
1106
|
+
const reason = signal.reason ?? /* @__PURE__ */ new Error("MCP HTTP response body was aborted");
|
|
1107
|
+
settle();
|
|
1108
|
+
controller?.error(reason);
|
|
1109
|
+
waitForSettlement(reader.cancel(reason).catch(() => void 0), teardownTimeoutMs);
|
|
1110
|
+
};
|
|
1111
|
+
return new ReadableStream({
|
|
1112
|
+
type: void 0,
|
|
1113
|
+
start(value) {
|
|
1114
|
+
controller = value;
|
|
1115
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1116
|
+
if (signal.aborted) abort();
|
|
1117
|
+
},
|
|
1118
|
+
async pull(value) {
|
|
1119
|
+
if (settled) return;
|
|
1120
|
+
try {
|
|
1121
|
+
const next = await raceAbort(reader.read(), signal);
|
|
1122
|
+
if (next.done) {
|
|
1123
|
+
settle();
|
|
1124
|
+
value.close();
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
received += next.value.byteLength;
|
|
1128
|
+
if (received > maxBytes) {
|
|
1129
|
+
const error = /* @__PURE__ */ new Error(`MCP HTTP response exceeds the ${maxBytes}-byte limit`);
|
|
1130
|
+
settle();
|
|
1131
|
+
value.error(error);
|
|
1132
|
+
await waitForSettlement(reader.cancel(error).catch(() => void 0), teardownTimeoutMs);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
value.enqueue(next.value);
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
if (settled) return;
|
|
1138
|
+
settle();
|
|
1139
|
+
value.error(error);
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
async cancel(reason) {
|
|
1143
|
+
settle();
|
|
1144
|
+
await waitForSettlement(reader.cancel(reason).catch(() => void 0), teardownTimeoutMs);
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
async function validateBeforeFetch(url, options, signal) {
|
|
1149
|
+
signal.throwIfAborted();
|
|
1150
|
+
if (options.validateEndpoint === void 0) return;
|
|
1151
|
+
const pending = Promise.resolve().then(() => {
|
|
1152
|
+
signal.throwIfAborted();
|
|
1153
|
+
return options.validateEndpoint?.(new URL(url), signal);
|
|
1154
|
+
});
|
|
1155
|
+
await raceAbort(pending, signal);
|
|
1156
|
+
signal.throwIfAborted();
|
|
1157
|
+
}
|
|
1158
|
+
function redirectInit(previous, status, crossesOrigin) {
|
|
1159
|
+
const method = (previous.method ?? "GET").toUpperCase();
|
|
1160
|
+
const switchesToGet = status === 303 || (status === 301 || status === 302) && method === "POST";
|
|
1161
|
+
if (!switchesToGet && typeof ReadableStream !== "undefined" && previous.body instanceof ReadableStream) throw new Error("MCP HTTP transport cannot replay a streaming body across a redirect");
|
|
1162
|
+
const headers = crossesOrigin ? new Headers() : new Headers(previous.headers);
|
|
1163
|
+
if (switchesToGet) {
|
|
1164
|
+
headers.delete("content-length");
|
|
1165
|
+
headers.delete("content-type");
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
...previous,
|
|
1169
|
+
redirect: "manual",
|
|
1170
|
+
headers,
|
|
1171
|
+
...switchesToGet ? {
|
|
1172
|
+
method: "GET",
|
|
1173
|
+
body: null
|
|
1174
|
+
} : {}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
async function cancelResponse(response, timeoutMs) {
|
|
1178
|
+
if (response.body === null) return;
|
|
1179
|
+
await waitForSettlement(response.body.cancel().catch(() => void 0), timeoutMs);
|
|
1180
|
+
}
|
|
1181
|
+
function mergeHeaders(base, extra) {
|
|
1182
|
+
const headers = new Headers(base);
|
|
1183
|
+
new Headers(extra).forEach((value, key) => {
|
|
1184
|
+
headers.set(key, value);
|
|
1185
|
+
});
|
|
1186
|
+
return headers;
|
|
1187
|
+
}
|
|
1188
|
+
function isPrivateHostname(value) {
|
|
1189
|
+
const hostname = value.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1190
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".home.arpa") || !hostname.includes(".")) return true;
|
|
1191
|
+
if (hostname.includes(":")) return true;
|
|
1192
|
+
const octets = hostname.split(".").map(Number);
|
|
1193
|
+
if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return false;
|
|
1194
|
+
const [first = 0, second = 0] = octets;
|
|
1195
|
+
return first === 0 || first === 10 || first === 127 || first >= 224 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 198 && (second === 18 || second === 19);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
//#endregion
|
|
1199
|
+
//#region src/client/http-client.ts
|
|
1200
|
+
/** Construct an HTTP client without opening the connection yet. */
|
|
1201
|
+
function createMcpHttpClient(options) {
|
|
1202
|
+
const security = snapshotHttpSecurityOptions(options);
|
|
1203
|
+
const url = validateHttpEndpoint(options.url, security);
|
|
1204
|
+
const { url: _url, fetch: injectedFetch, headers, transport, legacySse, allowedOrigins: _allowedOrigins, requireHttps: _requireHttps, allowPrivateNetwork: _allowPrivateNetwork, allowRedirects: _allowRedirects, validateEndpoint: _validateEndpoint, maxTransportBytes: _maxTransportBytes, ...lifecycle } = options;
|
|
1205
|
+
const legacyOptions = legacySse === false ? void 0 : legacySse;
|
|
1206
|
+
const primaryFactory = () => {
|
|
1207
|
+
const requestInit = { ...transport?.requestInit };
|
|
1208
|
+
if (headers !== void 0) requestInit.headers = mergeHeaders(transport?.requestInit?.headers, headers);
|
|
1209
|
+
const guardedFetch = createGuardedMcpFetch(transport?.fetch ?? injectedFetch ?? globalThis.fetch, security);
|
|
1210
|
+
return new StreamableHTTPClientTransport(url, {
|
|
1211
|
+
...transport,
|
|
1212
|
+
fetch: guardedFetch,
|
|
1213
|
+
...Object.keys(requestInit).length === 0 ? {} : { requestInit }
|
|
1214
|
+
});
|
|
1215
|
+
};
|
|
1216
|
+
const fallbackFactory = legacySse === false ? void 0 : () => {
|
|
1217
|
+
const fallbackUrl = validateHttpEndpoint(legacyOptions?.url ?? url, security);
|
|
1218
|
+
const fallbackOptions = legacyOptions?.transport;
|
|
1219
|
+
const requestInit = { ...fallbackOptions?.requestInit };
|
|
1220
|
+
if (headers !== void 0) requestInit.headers = mergeHeaders(fallbackOptions?.requestInit?.headers, headers);
|
|
1221
|
+
const guardedFetch = createGuardedMcpFetch(fallbackOptions?.fetch ?? transport?.fetch ?? injectedFetch ?? globalThis.fetch, security);
|
|
1222
|
+
return new SSEClientTransport(fallbackUrl, {
|
|
1223
|
+
...fallbackOptions,
|
|
1224
|
+
...fallbackOptions?.authProvider === void 0 && transport?.authProvider !== void 0 ? { authProvider: transport.authProvider } : {},
|
|
1225
|
+
fetch: guardedFetch,
|
|
1226
|
+
...Object.keys(requestInit).length === 0 ? {} : { requestInit }
|
|
1227
|
+
});
|
|
1228
|
+
};
|
|
1229
|
+
return new McpClientConnection(lifecycle, primaryFactory, {
|
|
1230
|
+
authenticationKind: authenticationKindOf(transport?.authProvider, mergeHeaders(transport?.requestInit?.headers, headers)),
|
|
1231
|
+
...fallbackFactory === void 0 ? {} : { fallbackTransportFactory: fallbackFactory },
|
|
1232
|
+
integrationFamily: "mcp-http-client"
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
/** Construct and fully initialize an HTTP client. */
|
|
1236
|
+
async function connectMcpHttp(options) {
|
|
1237
|
+
const connection = createMcpHttpClient(options);
|
|
1238
|
+
try {
|
|
1239
|
+
await connection.connect();
|
|
1240
|
+
return connection;
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
const stage = connectionFailureStage(connection.state.status);
|
|
1243
|
+
const cleanup = await connection.closeWithReport();
|
|
1244
|
+
throw new McpConnectionError(stage, mcpSupportError("MCP_CONNECT_FAILED", stage, "MCP connection startup failed"), cleanup, error);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
//#endregion
|
|
1249
|
+
export { McpConnectionError as a, resolveMcpReconnectOptions as i, createMcpHttpClient as n, McpRemoteToolError as o, McpClientConnection as r, connectMcpHttp as t };
|
|
1250
|
+
//# sourceMappingURL=client-D7Th3S7z.js.map
|