@plurnk/plurnk-mcp 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.env.defaults +6 -4
  2. package/README.md +110 -45
  3. package/SPEC.md +274 -30
  4. package/dist/McpExecutor.d.ts +25 -4
  5. package/dist/McpExecutor.d.ts.map +1 -1
  6. package/dist/McpExecutor.js +105 -31
  7. package/dist/McpExecutor.js.map +1 -1
  8. package/dist/McpResources.d.ts +2 -2
  9. package/dist/McpResources.d.ts.map +1 -1
  10. package/dist/McpResources.js +114 -24
  11. package/dist/McpResources.js.map +1 -1
  12. package/dist/Module.d.ts +30 -5
  13. package/dist/Module.d.ts.map +1 -1
  14. package/dist/Module.js +649 -35
  15. package/dist/Module.js.map +1 -1
  16. package/dist/ToolPresentation.d.ts +5 -0
  17. package/dist/ToolPresentation.d.ts.map +1 -0
  18. package/dist/ToolPresentation.js +134 -0
  19. package/dist/ToolPresentation.js.map +1 -0
  20. package/dist/client.d.ts +25 -7
  21. package/dist/client.d.ts.map +1 -1
  22. package/dist/client.js +478 -85
  23. package/dist/client.js.map +1 -1
  24. package/dist/config.d.ts +7 -13
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +114 -45
  27. package/dist/config.js.map +1 -1
  28. package/dist/extensionChannel.d.ts +25 -0
  29. package/dist/extensionChannel.d.ts.map +1 -0
  30. package/dist/extensionChannel.js +195 -0
  31. package/dist/extensionChannel.js.map +1 -0
  32. package/dist/index.d.ts +3 -3
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -2
  35. package/dist/index.js.map +1 -1
  36. package/dist/inputRequired.d.ts +36 -0
  37. package/dist/inputRequired.d.ts.map +1 -0
  38. package/dist/inputRequired.js +171 -0
  39. package/dist/inputRequired.js.map +1 -0
  40. package/dist/oauth.d.ts +28 -0
  41. package/dist/oauth.d.ts.map +1 -0
  42. package/dist/oauth.js +149 -0
  43. package/dist/oauth.js.map +1 -0
  44. package/dist/protocol.d.ts +7 -0
  45. package/dist/protocol.d.ts.map +1 -0
  46. package/dist/protocol.js +7 -0
  47. package/dist/protocol.js.map +1 -0
  48. package/dist/protocolHeaders.d.ts +4 -0
  49. package/dist/protocolHeaders.d.ts.map +1 -0
  50. package/dist/protocolHeaders.js +87 -0
  51. package/dist/protocolHeaders.js.map +1 -0
  52. package/dist/subscriptions.d.ts +15 -0
  53. package/dist/subscriptions.d.ts.map +1 -0
  54. package/dist/subscriptions.js +188 -0
  55. package/dist/subscriptions.js.map +1 -0
  56. package/dist/tasks.d.ts +19 -0
  57. package/dist/tasks.d.ts.map +1 -0
  58. package/dist/tasks.js +334 -0
  59. package/dist/tasks.js.map +1 -0
  60. package/package.json +10 -6
package/dist/client.js CHANGED
@@ -1,71 +1,312 @@
1
- import { Client, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
1
+ import { Client, ClientCredentialsProvider, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
2
2
  import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/client/stdio";
3
+ import { Validator, } from "@plurnk/plurnk-contracts";
3
4
  import packageJson from "../package.json" with { type: "json" };
4
- import { connectTimeoutMs, requestTimeoutMs, } from "./config.js";
5
- export const MCP_PROTOCOL_VERSION = "2026-07-28";
6
- const openTransport = (config) => {
7
- if (config.transport === "http") {
8
- return new StreamableHTTPClientTransport(new URL(config.url), config.headers === undefined
9
- ? undefined
10
- : { requestInit: { headers: config.headers } });
5
+ import { connectTimeoutMs, expandReferences, requestTimeoutMs, } from "./config.js";
6
+ import { INPUT_REQUIRED_MAX_ROUNDS, runInputRequiredRequest, } from "./inputRequired.js";
7
+ import InteractiveOAuthProvider from "./oauth.js";
8
+ import ExtensionChannel from "./extensionChannel.js";
9
+ import { mcpRoutingHeaderValue } from "./protocolHeaders.js";
10
+ import { MCP_PROTOCOL_VERSION, MCP_TASKS_EXTENSION_ID, } from "./protocol.js";
11
+ import Subscriptions from "./subscriptions.js";
12
+ import { callToolWithTasks, serverSupportsTasks, } from "./tasks.js";
13
+ const message = (error) => error instanceof Error ? error.message : String(error);
14
+ const expandedRecord = (source, environ, field) => source === undefined
15
+ ? undefined
16
+ : Object.fromEntries(Object.entries(source).map(([key, value]) => [
17
+ key,
18
+ expandReferences(value, environ, `${field}.${key}`),
19
+ ]));
20
+ const requireString = (value, field) => {
21
+ if (typeof value !== "string" || value.length === 0) {
22
+ throw new Error(`${field} must be a non-empty string.`);
23
+ }
24
+ return value;
25
+ };
26
+ const resolveDefinition = (source, environ) => {
27
+ const definition = Validator.assertMcpServerDefinition(source);
28
+ if (definition.transport === "stdio") {
29
+ return {
30
+ transport: "stdio",
31
+ command: expandReferences(requireString(definition.command, `${definition.name}.command`), environ, `${definition.name}.command`),
32
+ args: (definition.args ?? []).map((argument, index) => expandReferences(argument, environ, `${definition.name}.args[${index}]`)),
33
+ ...(definition.cwd === undefined
34
+ ? {}
35
+ : { cwd: expandReferences(definition.cwd, environ, `${definition.name}.cwd`) }),
36
+ ...(definition.env === undefined
37
+ ? {}
38
+ : { env: expandedRecord(definition.env, environ, `${definition.name}.env`) }),
39
+ };
40
+ }
41
+ const url = requireString(definition.url, `${definition.name}.url`);
42
+ const headers = expandedRecord(definition.headers, environ, `${definition.name}.headers`);
43
+ const authorizationHeader = Object.keys(headers ?? {}).find((name) => name.toLowerCase() === "authorization");
44
+ if (definition.authorization !== undefined && authorizationHeader !== undefined) {
45
+ throw new Error(`${definition.name}.authorization conflicts with the Authorization header.`);
46
+ }
47
+ if (definition.authorization === undefined) {
48
+ return {
49
+ transport: "http",
50
+ url,
51
+ ...(headers === undefined ? {} : { headers }),
52
+ cachePartition: "anonymous",
53
+ };
54
+ }
55
+ if (definition.authorization.type === "bearer") {
56
+ const token = expandReferences(definition.authorization.token, environ, `${definition.name}.authorization.token`);
57
+ if (token.length === 0)
58
+ throw new Error(`${definition.name}.authorization.token resolved empty.`);
59
+ return {
60
+ transport: "http",
61
+ url,
62
+ ...(headers === undefined ? {} : { headers }),
63
+ authProvider: { token: async () => token },
64
+ cachePartition: `bearer:${definition.authorization.token}`,
65
+ };
66
+ }
67
+ if (definition.authorization.type === "client-credentials") {
68
+ const secret = expandReferences(definition.authorization.clientSecret, environ, `${definition.name}.authorization.clientSecret`);
69
+ if (secret.length === 0) {
70
+ throw new Error(`${definition.name}.authorization.clientSecret resolved empty.`);
71
+ }
72
+ return {
73
+ transport: "http",
74
+ url,
75
+ ...(headers === undefined ? {} : { headers }),
76
+ authProvider: new ClientCredentialsProvider({
77
+ clientId: definition.authorization.clientId,
78
+ clientSecret: secret,
79
+ ...(definition.authorization.scope === undefined
80
+ ? {}
81
+ : { scope: definition.authorization.scope }),
82
+ }),
83
+ cachePartition: `client:${definition.authorization.clientId}`,
84
+ };
85
+ }
86
+ const oauthAuthorization = definition.authorization;
87
+ const oauthProvider = new InteractiveOAuthProvider({
88
+ redirectUrl: oauthAuthorization.redirectUrl,
89
+ ...(oauthAuthorization.scope === undefined ? {} : { scope: oauthAuthorization.scope }),
90
+ ...("clientMetadataUrl" in oauthAuthorization
91
+ ? { clientMetadataUrl: oauthAuthorization.clientMetadataUrl }
92
+ : {}),
93
+ ...("clientId" in oauthAuthorization
94
+ ? {
95
+ clientId: oauthAuthorization.clientId,
96
+ clientSecret: expandReferences(oauthAuthorization.clientSecret, environ, `${definition.name}.authorization.clientSecret`),
97
+ }
98
+ : {}),
99
+ });
100
+ const cachePartition = "clientMetadataUrl" in oauthAuthorization
101
+ ? `oauth:cimd:${oauthAuthorization.clientMetadataUrl}`
102
+ : "clientId" in oauthAuthorization
103
+ ? `oauth:client:${oauthAuthorization.clientId}`
104
+ : "oauth:dynamic";
105
+ return {
106
+ transport: "http",
107
+ url,
108
+ ...(headers === undefined ? {} : { headers }),
109
+ authProvider: oauthProvider,
110
+ oauthProvider,
111
+ cachePartition,
112
+ };
113
+ };
114
+ const openTransport = (definition) => {
115
+ if (definition.transport === "http") {
116
+ return new StreamableHTTPClientTransport(new URL(definition.url), {
117
+ ...(definition.headers === undefined
118
+ ? {}
119
+ : { requestInit: { headers: definition.headers } }),
120
+ ...(definition.authProvider === undefined
121
+ ? {}
122
+ : { authProvider: definition.authProvider }),
123
+ fetch: async (url, init) => {
124
+ const body = typeof init?.body === "string"
125
+ ? (() => {
126
+ try {
127
+ return JSON.parse(init.body);
128
+ }
129
+ catch {
130
+ return undefined;
131
+ }
132
+ })()
133
+ : undefined;
134
+ const request = body !== null && typeof body === "object" && !Array.isArray(body)
135
+ ? body
136
+ : undefined;
137
+ if (["tasks/get", "tasks/update", "tasks/cancel"].includes(String(request?.method))
138
+ && typeof request?.params?.taskId === "string") {
139
+ const headers = new Headers(init?.headers);
140
+ headers.set("Mcp-Name", mcpRoutingHeaderValue(request.params.taskId));
141
+ return fetch(url, { ...init, headers });
142
+ }
143
+ return fetch(url, init);
144
+ },
145
+ });
11
146
  }
12
147
  return new StdioClientTransport({
13
- command: config.command,
14
- args: config.args,
15
- cwd: config.cwd,
148
+ command: definition.command,
149
+ args: definition.args,
150
+ cwd: definition.cwd,
16
151
  env: {
17
152
  ...getDefaultEnvironment(),
18
- ...config.env,
153
+ ...definition.env,
19
154
  },
20
155
  });
21
156
  };
22
- const openClient = async (config, environ) => {
23
- const client = new Client({
157
+ export class AuthorizationRequiredError extends Error {
158
+ authorizationUrl;
159
+ constructor(authorizationUrl, cause) {
160
+ super("MCP server requires interactive OAuth authorization.", { cause });
161
+ this.name = "AuthorizationRequiredError";
162
+ this.authorizationUrl = authorizationUrl;
163
+ }
164
+ }
165
+ const openClient = async (definition, environ, options, transport) => {
166
+ const changed = (error) => options.onCatalogChanged?.(error ?? null);
167
+ const clientInfo = {
24
168
  name: packageJson.name,
25
169
  version: packageJson.version,
26
- }, {
170
+ };
171
+ const clientCapabilities = {
172
+ elicitation: {
173
+ form: {},
174
+ url: {},
175
+ },
176
+ extensions: {
177
+ [MCP_TASKS_EXTENSION_ID]: {},
178
+ },
179
+ };
180
+ const client = new Client(clientInfo, {
181
+ capabilities: clientCapabilities,
27
182
  versionNegotiation: {
28
183
  mode: { pin: MCP_PROTOCOL_VERSION },
29
184
  },
30
185
  inputRequired: {
31
186
  autoFulfill: false,
187
+ maxRounds: INPUT_REQUIRED_MAX_ROUNDS,
188
+ },
189
+ listChanged: {
190
+ tools: {
191
+ autoRefresh: false,
192
+ onChanged: (error) => changed(error ?? undefined),
193
+ },
194
+ resources: {
195
+ autoRefresh: false,
196
+ onChanged: (error) => changed(error ?? undefined),
197
+ },
198
+ prompts: {
199
+ autoRefresh: false,
200
+ onChanged: (error) => changed(error ?? undefined),
201
+ },
32
202
  },
203
+ ...(definition.transport === "http"
204
+ ? { cachePartition: definition.cachePartition }
205
+ : {}),
33
206
  });
207
+ client.onerror = (error) => options.onInfrastructureError?.(error);
34
208
  try {
35
- await client.connect(openTransport(config), {
209
+ await client.connect(transport, {
36
210
  timeout: connectTimeoutMs(environ),
37
211
  });
38
212
  }
39
213
  catch (cause) {
214
+ const authorizationUrl = definition.transport === "http"
215
+ ? definition.oauthProvider?.takeAuthorizationUrl()
216
+ : undefined;
217
+ let closeFailure;
40
218
  try {
41
219
  await client.close();
42
220
  }
43
- catch (closeCause) {
44
- throw new AggregateError([cause, closeCause], `MCP ${MCP_PROTOCOL_VERSION} connection and cleanup failed.`);
221
+ catch (error) {
222
+ closeFailure = error;
223
+ }
224
+ if (authorizationUrl !== undefined) {
225
+ throw new AuthorizationRequiredError(authorizationUrl.href, closeFailure === undefined ? cause : new AggregateError([cause, closeFailure]));
226
+ }
227
+ if (closeFailure !== undefined) {
228
+ throw new AggregateError([cause, closeFailure], `MCP ${MCP_PROTOCOL_VERSION} connection and cleanup failed.`);
45
229
  }
46
230
  throw new Error(`MCP ${MCP_PROTOCOL_VERSION} connection failed.`, { cause });
47
231
  }
232
+ const discover = client.getDiscoverResult();
48
233
  if (client.getProtocolEra() !== "modern"
49
234
  || client.getNegotiatedProtocolVersion() !== MCP_PROTOCOL_VERSION
50
- || client.getDiscoverResult() === undefined) {
235
+ || discover === undefined) {
51
236
  await client.close();
52
237
  throw new Error(`MCP server did not negotiate required revision ${MCP_PROTOCOL_VERSION}.`);
53
238
  }
54
- return client;
239
+ const extensions = new ExtensionChannel(transport, {
240
+ protocolVersion: MCP_PROTOCOL_VERSION,
241
+ clientInfo,
242
+ clientCapabilities,
243
+ cancelRequest: async (requestId) => client.notification({
244
+ method: "notifications/cancelled",
245
+ params: { requestId },
246
+ }),
247
+ onError: options.onInfrastructureError,
248
+ });
249
+ return {
250
+ client,
251
+ transport,
252
+ extensions,
253
+ subscriptions: new Subscriptions(client, {
254
+ timeout: requestTimeoutMs(environ),
255
+ tasks: serverSupportsTasks(discover.capabilities),
256
+ onError: options.onInfrastructureError,
257
+ }),
258
+ };
55
259
  };
56
260
  export default class ServerConnection {
57
- #config;
261
+ #definition;
262
+ #resolved;
58
263
  #environ;
264
+ #options;
59
265
  #client;
60
- #toolHints = new Map();
61
- constructor(config, environ = process.env) {
62
- this.#config = config;
266
+ #pendingAuthorization;
267
+ #activeRequests = 0;
268
+ #closed = false;
269
+ constructor(definition, environ = process.env, options = {}) {
270
+ this.#definition = structuredClone(Validator.assertMcpServerDefinition(definition));
271
+ this.#resolved = resolveDefinition(this.#definition, environ);
63
272
  this.#environ = environ;
273
+ this.#options = options;
274
+ }
275
+ get definition() {
276
+ return structuredClone(this.#definition);
277
+ }
278
+ get activeRequests() {
279
+ return this.#activeRequests;
280
+ }
281
+ get authorizationUrl() {
282
+ return this.#pendingAuthorization?.authorizationUrl ?? null;
64
283
  }
65
- connect() {
284
+ assertReplaceable() {
285
+ if (this.#activeRequests !== 0) {
286
+ throw new Error(`MCP server '${this.#definition.name}' has ${this.#activeRequests} active user request(s).`);
287
+ }
288
+ }
289
+ async #open() {
290
+ if (this.#closed)
291
+ throw new Error(`MCP server '${this.#definition.name}' connection is closed.`);
292
+ if (this.#pendingAuthorization !== undefined) {
293
+ throw new AuthorizationRequiredError(this.#pendingAuthorization.authorizationUrl);
294
+ }
66
295
  if (this.#client !== undefined)
67
296
  return this.#client;
68
- const pending = openClient(this.#config, this.#environ).catch((cause) => {
297
+ const transport = openTransport(this.#resolved);
298
+ const pending = openClient(this.#resolved, this.#environ, this.#options, transport).catch((cause) => {
299
+ if (cause instanceof AuthorizationRequiredError
300
+ && this.#resolved.transport === "http"
301
+ && this.#resolved.oauthProvider !== undefined
302
+ && transport instanceof StreamableHTTPClientTransport) {
303
+ this.#pendingAuthorization = {
304
+ transport,
305
+ provider: this.#resolved.oauthProvider,
306
+ authorizationUrl: cause.authorizationUrl,
307
+ standaloneTransport: true,
308
+ };
309
+ }
69
310
  if (this.#client === pending)
70
311
  this.#client = undefined;
71
312
  throw cause;
@@ -73,81 +314,233 @@ export default class ServerConnection {
73
314
  this.#client = pending;
74
315
  return pending;
75
316
  }
76
- readOnly(tool) {
77
- return this.#toolHints.get(tool) === true;
317
+ async connect() {
318
+ return (await this.#open()).client;
78
319
  }
79
- async tools(signal) {
80
- const client = await this.connect();
81
- const { tools } = await client.listTools(undefined, this.#requestOptions(signal));
82
- this.#toolHints = new Map(tools.map((tool) => [tool.name, tool.annotations?.readOnlyHint === true]));
83
- return tools;
320
+ async finishAuthorization(callbackUrl) {
321
+ const pending = this.#pendingAuthorization;
322
+ if (pending === undefined) {
323
+ throw new Error(`MCP server '${this.#definition.name}' has no pending OAuth authorization.`);
324
+ }
325
+ const callback = new URL(callbackUrl);
326
+ const expected = new URL(pending.provider.redirectUrl);
327
+ if (callback.protocol !== expected.protocol
328
+ || callback.host !== expected.host
329
+ || callback.pathname !== expected.pathname) {
330
+ throw new Error("OAuth callback URL does not match the configured redirect URL.");
331
+ }
332
+ pending.provider.assertCallbackState(callback);
333
+ await pending.transport.finishAuth(callback.searchParams);
334
+ this.#pendingAuthorization = undefined;
335
+ await this.connect();
84
336
  }
85
- async catalog(signal) {
86
- const client = await this.connect();
87
- const discover = client.getDiscoverResult();
88
- if (discover === undefined)
89
- throw new Error("Modern MCP connection omitted its discovery result.");
90
- const [tools, resources, resourceTemplates, prompts] = await Promise.all([
91
- discover.capabilities.tools === undefined
92
- ? Promise.resolve([])
93
- : this.tools(signal),
94
- discover.capabilities.resources === undefined
95
- ? Promise.resolve([])
96
- : client.listResources(undefined, this.#requestOptions(signal)).then((result) => result.resources),
97
- discover.capabilities.resources === undefined
98
- ? Promise.resolve([])
99
- : client.listResourceTemplates(undefined, this.#requestOptions(signal)).then((result) => result.resourceTemplates),
100
- discover.capabilities.prompts === undefined
101
- ? Promise.resolve([])
102
- : client.listPrompts(undefined, this.#requestOptions(signal)).then((result) => result.prompts),
103
- ]);
104
- return {
105
- protocolVersion: MCP_PROTOCOL_VERSION,
106
- server: client.getServerVersion(),
107
- capabilities: discover.capabilities,
108
- tools,
109
- resources,
110
- resourceTemplates,
111
- prompts,
337
+ async #request(run) {
338
+ this.#activeRequests += 1;
339
+ try {
340
+ const opened = await this.#open();
341
+ try {
342
+ return await run(opened.client, opened.subscriptions, opened.extensions);
343
+ }
344
+ catch (cause) {
345
+ const authorization = this.#takeAuthorization(opened, cause);
346
+ if (authorization !== null)
347
+ throw authorization;
348
+ throw cause;
349
+ }
350
+ }
351
+ finally {
352
+ this.#activeRequests -= 1;
353
+ }
354
+ }
355
+ #takeAuthorization(opened, cause) {
356
+ if (this.#resolved.transport !== "http"
357
+ || this.#resolved.oauthProvider === undefined
358
+ || !(opened.transport instanceof StreamableHTTPClientTransport)) {
359
+ return null;
360
+ }
361
+ const authorizationUrl = this.#resolved.oauthProvider.takeAuthorizationUrl();
362
+ if (authorizationUrl === undefined)
363
+ return null;
364
+ this.#pendingAuthorization = {
365
+ transport: opened.transport,
366
+ provider: this.#resolved.oauthProvider,
367
+ authorizationUrl: authorizationUrl.href,
368
+ standaloneTransport: false,
112
369
  };
370
+ return new AuthorizationRequiredError(authorizationUrl.href, cause);
113
371
  }
114
- async resources(signal) {
115
- const client = await this.connect();
116
- const discover = client.getDiscoverResult();
117
- if (discover?.capabilities.resources === undefined) {
372
+ async tools(signal) {
373
+ return this.#request(async (client) => {
374
+ if (client.getDiscoverResult()?.capabilities.tools === undefined)
375
+ return [];
376
+ const { tools } = await client.listTools(undefined, this.#requestOptions(signal));
377
+ return tools;
378
+ });
379
+ }
380
+ async catalog(signal) {
381
+ return this.#request(async (client) => {
382
+ const discover = client.getDiscoverResult();
383
+ if (discover === undefined)
384
+ throw new Error("Modern MCP connection omitted its discovery result.");
385
+ const [tools, resources, resourceTemplates, prompts] = await Promise.all([
386
+ discover.capabilities.tools === undefined
387
+ ? Promise.resolve([])
388
+ : client.listTools(undefined, this.#requestOptions(signal)).then((result) => result.tools),
389
+ discover.capabilities.resources === undefined
390
+ ? Promise.resolve([])
391
+ : client.listResources(undefined, this.#requestOptions(signal)).then((result) => result.resources),
392
+ discover.capabilities.resources === undefined
393
+ ? Promise.resolve([])
394
+ : client.listResourceTemplates(undefined, this.#requestOptions(signal))
395
+ .then((result) => result.resourceTemplates),
396
+ discover.capabilities.prompts === undefined
397
+ ? Promise.resolve([])
398
+ : client.listPrompts(undefined, this.#requestOptions(signal)).then((result) => result.prompts),
399
+ ]);
118
400
  return {
119
- resources: [],
120
- resourceTemplates: [],
401
+ protocolVersion: MCP_PROTOCOL_VERSION,
402
+ server: client.getServerVersion(),
403
+ capabilities: discover.capabilities,
404
+ tools,
405
+ resources,
406
+ resourceTemplates,
407
+ prompts,
121
408
  };
122
- }
123
- const [resources, resourceTemplates] = await Promise.all([
124
- client.listResources(undefined, this.#requestOptions(signal))
125
- .then((result) => result.resources),
126
- client.listResourceTemplates(undefined, this.#requestOptions(signal))
127
- .then((result) => result.resourceTemplates),
128
- ]);
129
- return {
130
- resources,
131
- resourceTemplates,
132
- };
409
+ });
410
+ }
411
+ async resources(signal) {
412
+ return this.#request(async (client) => {
413
+ if (client.getDiscoverResult()?.capabilities.resources === undefined) {
414
+ return { resources: [], resourceTemplates: [] };
415
+ }
416
+ const [resources, resourceTemplates] = await Promise.all([
417
+ client.listResources(undefined, this.#requestOptions(signal))
418
+ .then((result) => result.resources),
419
+ client.listResourceTemplates(undefined, this.#requestOptions(signal))
420
+ .then((result) => result.resourceTemplates),
421
+ ]);
422
+ return { resources, resourceTemplates };
423
+ });
133
424
  }
134
- async callTool(name, args, signal) {
135
- return (await this.connect()).callTool({ name, arguments: args }, this.#requestOptions(signal));
425
+ async prompts(signal) {
426
+ return this.#request(async (client) => {
427
+ if (client.getDiscoverResult()?.capabilities.prompts === undefined)
428
+ return [];
429
+ return (await client.listPrompts(undefined, this.#requestOptions(signal))).prompts;
430
+ });
136
431
  }
137
- async readResource(uri, signal) {
138
- return (await this.connect()).readResource({ uri }, this.#requestOptions(signal));
432
+ async callTool(name, args, signal, onProgress, interact, toolDefinition) {
433
+ return this.#request(async (client, subscriptions, extensions) => {
434
+ const timeout = requestTimeoutMs(this.#environ);
435
+ if (serverSupportsTasks(client.getDiscoverResult()?.capabilities)) {
436
+ const tool = toolDefinition ?? (await client.listTools(undefined, this.#requestOptions(signal))).tools.find((candidate) => candidate.name === name);
437
+ if (tool === undefined) {
438
+ throw new Error(`MCP server '${this.#definition.name}' did not list tool '${name}'.`);
439
+ }
440
+ return callToolWithTasks({
441
+ server: this.#definition.name,
442
+ name,
443
+ args,
444
+ tool,
445
+ signal,
446
+ onProgress,
447
+ interact,
448
+ timeout,
449
+ channel: extensions,
450
+ subscriptions,
451
+ });
452
+ }
453
+ return runInputRequiredRequest({
454
+ server: this.#definition.name,
455
+ operation: "tools/call",
456
+ originalParams: { name, arguments: args },
457
+ signal,
458
+ interact,
459
+ onProgress,
460
+ timeout,
461
+ requestLeg: (params, options) => client.callTool(params, {
462
+ ...options,
463
+ ...(toolDefinition === undefined ? {} : { toolDefinition }),
464
+ }),
465
+ });
466
+ });
139
467
  }
140
- #requestOptions(signal) {
141
- return {
468
+ async readResource(uri, signal, interact) {
469
+ return this.#request(async (client, subscriptions) => {
470
+ await subscriptions.selectResource(uri);
471
+ return runInputRequiredRequest({
472
+ server: this.#definition.name,
473
+ operation: "resources/read",
474
+ originalParams: { uri },
475
+ signal,
476
+ interact,
477
+ timeout: requestTimeoutMs(this.#environ),
478
+ requestLeg: (params, options, retry) => client.readResource(params, retry ? { ...options, cacheMode: "refresh" } : options),
479
+ });
480
+ });
481
+ }
482
+ async getPrompt(name, args, signal, interact) {
483
+ return this.#request(async (client) => runInputRequiredRequest({
484
+ server: this.#definition.name,
485
+ operation: "prompts/get",
486
+ originalParams: { name, ...(args === undefined ? {} : { arguments: args }) },
142
487
  signal,
488
+ interact,
143
489
  timeout: requestTimeoutMs(this.#environ),
490
+ requestLeg: (params, options) => client.getPrompt(params, options),
491
+ }));
492
+ }
493
+ async complete(params, signal) {
494
+ return this.#request(async (client) => client.complete(params, this.#requestOptions(signal)));
495
+ }
496
+ #requestOptions(signal, onProgress) {
497
+ const timeout = requestTimeoutMs(this.#environ);
498
+ return {
499
+ signal,
500
+ timeout,
501
+ maxTotalTimeout: timeout,
502
+ ...(onProgress === undefined ? {} : { onprogress: onProgress }),
144
503
  };
145
504
  }
146
505
  async close() {
506
+ if (this.#closed)
507
+ return;
508
+ this.#closed = true;
147
509
  const client = this.#client;
148
510
  this.#client = undefined;
149
- if (client !== undefined)
150
- await (await client).close();
511
+ const pending = this.#pendingAuthorization;
512
+ this.#pendingAuthorization = undefined;
513
+ const closures = [];
514
+ if (client !== undefined) {
515
+ closures.push(client.then(async ({ client: connected, extensions, subscriptions }) => {
516
+ const failures = [];
517
+ extensions.close();
518
+ // A full connection close terminates its listen request. Retiring first
519
+ // avoids a redundant cancellation racing the SDK's removed listen ID.
520
+ const settled = await Promise.allSettled([
521
+ subscriptions.retire(),
522
+ connected.close(),
523
+ ]);
524
+ failures.push(...settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
525
+ if (failures.length === 1)
526
+ throw failures[0];
527
+ if (failures.length > 1) {
528
+ throw new AggregateError(failures, `MCP server '${this.#definition.name}' connection shutdown failed.`);
529
+ }
530
+ }));
531
+ }
532
+ if (pending?.standaloneTransport === true)
533
+ closures.push(pending.transport.close());
534
+ const settled = await Promise.allSettled(closures);
535
+ const failures = settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
536
+ if (failures.length === 1)
537
+ throw failures[0];
538
+ if (failures.length > 1) {
539
+ throw new AggregateError(failures, `MCP server '${this.#definition.name}' shutdown failed.`);
540
+ }
541
+ }
542
+ describeError(error) {
543
+ return message(error);
151
544
  }
152
545
  }
153
546
  //# sourceMappingURL=client.js.map