@plurnk/plurnk-mcp 1.6.1 → 1.8.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 (65) hide show
  1. package/.env.defaults +31 -4
  2. package/README.md +161 -42
  3. package/SPEC.md +486 -28
  4. package/dist/McpExecutor.d.ts +28 -5
  5. package/dist/McpExecutor.d.ts.map +1 -1
  6. package/dist/McpExecutor.js +144 -32
  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 +857 -36
  15. package/dist/Module.js.map +1 -1
  16. package/dist/ToolPresentation.d.ts +7 -0
  17. package/dist/ToolPresentation.d.ts.map +1 -0
  18. package/dist/ToolPresentation.js +224 -0
  19. package/dist/ToolPresentation.js.map +1 -0
  20. package/dist/capabilityMatrix.d.ts +23 -0
  21. package/dist/capabilityMatrix.d.ts.map +1 -0
  22. package/dist/capabilityMatrix.js +403 -0
  23. package/dist/capabilityMatrix.js.map +1 -0
  24. package/dist/client.d.ts +27 -8
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +537 -91
  27. package/dist/client.js.map +1 -1
  28. package/dist/config.d.ts +14 -13
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +257 -51
  31. package/dist/config.js.map +1 -1
  32. package/dist/extensionChannel.d.ts +25 -0
  33. package/dist/extensionChannel.d.ts.map +1 -0
  34. package/dist/extensionChannel.js +195 -0
  35. package/dist/extensionChannel.js.map +1 -0
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -2
  39. package/dist/index.js.map +1 -1
  40. package/dist/inputRequired.d.ts +36 -0
  41. package/dist/inputRequired.d.ts.map +1 -0
  42. package/dist/inputRequired.js +171 -0
  43. package/dist/inputRequired.js.map +1 -0
  44. package/dist/mcp-watchdog.mjs +106 -0
  45. package/dist/oauth.d.ts +28 -0
  46. package/dist/oauth.d.ts.map +1 -0
  47. package/dist/oauth.js +149 -0
  48. package/dist/oauth.js.map +1 -0
  49. package/dist/protocol.d.ts +8 -0
  50. package/dist/protocol.d.ts.map +1 -0
  51. package/dist/protocol.js +8 -0
  52. package/dist/protocol.js.map +1 -0
  53. package/dist/protocolHeaders.d.ts +4 -0
  54. package/dist/protocolHeaders.d.ts.map +1 -0
  55. package/dist/protocolHeaders.js +87 -0
  56. package/dist/protocolHeaders.js.map +1 -0
  57. package/dist/subscriptions.d.ts +15 -0
  58. package/dist/subscriptions.d.ts.map +1 -0
  59. package/dist/subscriptions.js +188 -0
  60. package/dist/subscriptions.js.map +1 -0
  61. package/dist/tasks.d.ts +19 -0
  62. package/dist/tasks.d.ts.map +1 -0
  63. package/dist/tasks.js +334 -0
  64. package/dist/tasks.js.map +1 -0
  65. package/package.json +12 -7
package/dist/client.js CHANGED
@@ -1,71 +1,363 @@
1
- import { Client, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
1
+ import { OAuthClientFlowError, OAuthError, UnauthorizedError, 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_OAUTH_CLIENT_CREDENTIALS_EXTENSION_ID, MCP_PROTOCOL_VERSION, } from "./protocol.js";
11
+ import { staticClientCapabilities } from "./capabilityMatrix.js";
12
+ import Subscriptions from "./subscriptions.js";
13
+ import { callToolWithTasks, serverSupportsTasks, } from "./tasks.js";
14
+ // {§mcp-authority} — capability source: the modern discover result when the
15
+ // server offered it, else the legacy initialize result the SDK already folded.
16
+ const serverCapabilities = (client) => client.getDiscoverResult()?.capabilities
17
+ ?? client.getServerCapabilities();
18
+ const message = (error) => error instanceof Error ? error.message : String(error);
19
+ const errorsOf = (error) => error instanceof AggregateError ? [...error.errors] : [error];
20
+ // {§oauth-client-credentials} — a rejected client-credentials grant is an
21
+ // authorization fact, never a generic unavailability.
22
+ export const isClientCredentialsRejection = (error) => errorsOf(error).some((candidate) => {
23
+ let current = candidate;
24
+ while (current !== undefined && current !== null) {
25
+ if (OAuthError.isInstance(current) ||
26
+ UnauthorizedError.isInstance(current) ||
27
+ OAuthClientFlowError.isInstance(current)) {
28
+ return true;
29
+ }
30
+ current = current instanceof Error ? current.cause : undefined;
31
+ }
32
+ return false;
33
+ });
34
+ const expandedRecord = (source, environ, field) => source === undefined
35
+ ? undefined
36
+ : Object.fromEntries(Object.entries(source).map(([key, value]) => [
37
+ key,
38
+ expandReferences(value, environ, `${field}.${key}`),
39
+ ]));
40
+ const requireString = (value, field) => {
41
+ if (typeof value !== "string" || value.length === 0) {
42
+ throw new Error(`${field} must be a non-empty string.`);
43
+ }
44
+ return value;
45
+ };
46
+ const resolveDefinition = (source, environ) => {
47
+ const definition = Validator.assertMcpServerDefinition(source);
48
+ if (definition.transport === "stdio") {
49
+ return {
50
+ transport: "stdio",
51
+ command: expandReferences(requireString(definition.command, `${definition.name}.command`), environ, `${definition.name}.command`),
52
+ args: (definition.args ?? []).map((argument, index) => expandReferences(argument, environ, `${definition.name}.args[${index}]`)),
53
+ ...(definition.cwd === undefined
54
+ ? {}
55
+ : { cwd: expandReferences(definition.cwd, environ, `${definition.name}.cwd`) }),
56
+ ...(definition.env === undefined
57
+ ? {}
58
+ : { env: expandedRecord(definition.env, environ, `${definition.name}.env`) }),
59
+ };
60
+ }
61
+ const url = requireString(definition.url, `${definition.name}.url`);
62
+ const headers = expandedRecord(definition.headers, environ, `${definition.name}.headers`);
63
+ const authorizationHeader = Object.keys(headers ?? {}).find((name) => name.toLowerCase() === "authorization");
64
+ if (definition.authorization !== undefined && authorizationHeader !== undefined) {
65
+ throw new Error(`${definition.name}.authorization conflicts with the Authorization header.`);
66
+ }
67
+ if (definition.authorization === undefined) {
68
+ return {
69
+ transport: "http",
70
+ url,
71
+ ...(headers === undefined ? {} : { headers }),
72
+ cachePartition: "anonymous",
73
+ };
11
74
  }
75
+ if (definition.authorization.type === "bearer") {
76
+ const token = expandReferences(definition.authorization.token, environ, `${definition.name}.authorization.token`);
77
+ if (token.length === 0)
78
+ throw new Error(`${definition.name}.authorization.token resolved empty.`);
79
+ return {
80
+ transport: "http",
81
+ url,
82
+ ...(headers === undefined ? {} : { headers }),
83
+ authProvider: { token: async () => token },
84
+ cachePartition: `bearer:${definition.authorization.token}`,
85
+ };
86
+ }
87
+ if (definition.authorization.type === "client-credentials") {
88
+ const secret = expandReferences(definition.authorization.clientSecret, environ, `${definition.name}.authorization.clientSecret`);
89
+ if (secret.length === 0) {
90
+ throw new Error(`${definition.name}.authorization.clientSecret resolved empty.`);
91
+ }
92
+ return {
93
+ transport: "http",
94
+ url,
95
+ ...(headers === undefined ? {} : { headers }),
96
+ clientCredentials: true,
97
+ authProvider: new ClientCredentialsProvider({
98
+ clientId: definition.authorization.clientId,
99
+ clientSecret: secret,
100
+ // {§oauth-client-credentials} — a declared issuer binds the static
101
+ // credential to that authorization server (SEP-2352); absent, the
102
+ // SDK's legacy no-binding behaviour applies.
103
+ ...(definition.authorization.issuer === undefined
104
+ ? {}
105
+ : { expectedIssuer: definition.authorization.issuer }),
106
+ ...(definition.authorization.scope === undefined
107
+ ? {}
108
+ : { scope: definition.authorization.scope }),
109
+ }),
110
+ cachePartition: `client:${definition.authorization.clientId}`,
111
+ };
112
+ }
113
+ const oauthAuthorization = definition.authorization;
114
+ // {§oauth-lifetime} — registration data, tokens, PKCE verifier, and
115
+ // callback state remain process-memory in this provider; the durable
116
+ // definition stays unexpanded.
117
+ const oauthProvider = new InteractiveOAuthProvider({
118
+ redirectUrl: oauthAuthorization.redirectUrl,
119
+ ...(oauthAuthorization.scope === undefined ? {} : { scope: oauthAuthorization.scope }),
120
+ ...("clientMetadataUrl" in oauthAuthorization
121
+ ? { clientMetadataUrl: oauthAuthorization.clientMetadataUrl }
122
+ : {}),
123
+ ...("clientId" in oauthAuthorization
124
+ ? {
125
+ clientId: oauthAuthorization.clientId,
126
+ clientSecret: expandReferences(oauthAuthorization.clientSecret, environ, `${definition.name}.authorization.clientSecret`),
127
+ }
128
+ : {}),
129
+ });
130
+ const cachePartition = "clientMetadataUrl" in oauthAuthorization
131
+ ? `oauth:cimd:${oauthAuthorization.clientMetadataUrl}`
132
+ : "clientId" in oauthAuthorization
133
+ ? `oauth:client:${oauthAuthorization.clientId}`
134
+ : "oauth:dynamic";
135
+ return {
136
+ transport: "http",
137
+ url,
138
+ ...(headers === undefined ? {} : { headers }),
139
+ authProvider: oauthProvider,
140
+ oauthProvider,
141
+ cachePartition,
142
+ };
143
+ };
144
+ const openTransport = (definition) => {
145
+ if (definition.transport === "http") {
146
+ return new StreamableHTTPClientTransport(new URL(definition.url), {
147
+ ...(definition.headers === undefined
148
+ ? {}
149
+ : { requestInit: { headers: definition.headers } }),
150
+ ...(definition.authProvider === undefined
151
+ ? {}
152
+ : { authProvider: definition.authProvider }),
153
+ fetch: async (url, init) => {
154
+ const body = typeof init?.body === "string"
155
+ ? (() => {
156
+ try {
157
+ return JSON.parse(init.body);
158
+ }
159
+ catch {
160
+ return undefined;
161
+ }
162
+ })()
163
+ : undefined;
164
+ const request = body !== null && typeof body === "object" && !Array.isArray(body)
165
+ ? body
166
+ : undefined;
167
+ if (["tasks/get", "tasks/update", "tasks/cancel"].includes(String(request?.method))
168
+ && typeof request?.params?.taskId === "string") {
169
+ const headers = new Headers(init?.headers);
170
+ headers.set("Mcp-Name", mcpRoutingHeaderValue(request.params.taskId));
171
+ return fetch(url, { ...init, headers });
172
+ }
173
+ return fetch(url, init);
174
+ },
175
+ });
176
+ }
177
+ // {§mcp-stdio-process-ownership} — stdio servers spawn through the
178
+ // parent-death watchdog wrapper: the real server runs detached (own
179
+ // process group) and the wrapper group-kills it (grandchildren included)
180
+ // when the daemon dies by ANY path, including SIGKILL where close()
181
+ // never runs. Protocol bytes are forwarded verbatim.
12
182
  return new StdioClientTransport({
13
- command: config.command,
14
- args: config.args,
15
- cwd: config.cwd,
183
+ command: process.execPath,
184
+ args: [
185
+ new URL("./mcp-watchdog.mjs", import.meta.url).pathname,
186
+ String(process.pid),
187
+ "--",
188
+ definition.command,
189
+ ...definition.args,
190
+ ],
191
+ cwd: definition.cwd,
16
192
  env: {
17
193
  ...getDefaultEnvironment(),
18
- ...config.env,
194
+ ...definition.env,
19
195
  },
20
196
  });
21
197
  };
22
- const openClient = async (config, environ) => {
23
- const client = new Client({
198
+ export class AuthorizationRequiredError extends Error {
199
+ authorizationUrl;
200
+ constructor(authorizationUrl, cause) {
201
+ super("MCP server requires interactive OAuth authorization.", { cause });
202
+ this.name = "AuthorizationRequiredError";
203
+ this.authorizationUrl = authorizationUrl;
204
+ }
205
+ }
206
+ const openClient = async (definition, environ, options, transport) => {
207
+ const changed = (error) => options.onCatalogChanged?.(error ?? null);
208
+ const clientInfo = {
24
209
  name: packageJson.name,
25
210
  version: packageJson.version,
26
- }, {
211
+ };
212
+ // {§mcp-capability-matrix} — static advertisement derives from the
213
+ // capability matrix by construction; the conditional client-credentials
214
+ // extension ({§oauth-client-credentials}) is added only on connections
215
+ // that actually hold a client-credentials definition.
216
+ const matrixCapabilities = staticClientCapabilities();
217
+ const clientCapabilities = {
218
+ ...matrixCapabilities,
219
+ extensions: {
220
+ ...matrixCapabilities.extensions,
221
+ ...(definition.transport === "http" && definition.clientCredentials === true
222
+ ? { [MCP_OAUTH_CLIENT_CREDENTIALS_EXTENSION_ID]: {} }
223
+ : {}),
224
+ },
225
+ };
226
+ const client = new Client(clientInfo, {
227
+ capabilities: clientCapabilities,
27
228
  versionNegotiation: {
28
- mode: { pin: MCP_PROTOCOL_VERSION },
229
+ mode: "auto",
29
230
  },
30
231
  inputRequired: {
31
232
  autoFulfill: false,
233
+ maxRounds: INPUT_REQUIRED_MAX_ROUNDS,
234
+ },
235
+ listChanged: {
236
+ tools: {
237
+ autoRefresh: false,
238
+ onChanged: (error) => changed(error ?? undefined),
239
+ },
240
+ resources: {
241
+ autoRefresh: false,
242
+ onChanged: (error) => changed(error ?? undefined),
243
+ },
244
+ prompts: {
245
+ autoRefresh: false,
246
+ onChanged: (error) => changed(error ?? undefined),
247
+ },
32
248
  },
249
+ ...(definition.transport === "http"
250
+ ? { cachePartition: definition.cachePartition }
251
+ : {}),
33
252
  });
253
+ client.onerror = (error) => options.onInfrastructureError?.(error);
34
254
  try {
35
- await client.connect(openTransport(config), {
255
+ await client.connect(transport, {
36
256
  timeout: connectTimeoutMs(environ),
37
257
  });
38
258
  }
39
259
  catch (cause) {
260
+ const authorizationUrl = definition.transport === "http"
261
+ ? definition.oauthProvider?.takeAuthorizationUrl()
262
+ : undefined;
263
+ let closeFailure;
40
264
  try {
41
265
  await client.close();
42
266
  }
43
- catch (closeCause) {
44
- throw new AggregateError([cause, closeCause], `MCP ${MCP_PROTOCOL_VERSION} connection and cleanup failed.`);
267
+ catch (error) {
268
+ closeFailure = error;
269
+ }
270
+ if (authorizationUrl !== undefined) {
271
+ throw new AuthorizationRequiredError(authorizationUrl.href, closeFailure === undefined ? cause : new AggregateError([cause, closeFailure]));
272
+ }
273
+ if (closeFailure !== undefined) {
274
+ throw new AggregateError([cause, closeFailure], `MCP ${MCP_PROTOCOL_VERSION} connection and cleanup failed.`);
45
275
  }
46
276
  throw new Error(`MCP ${MCP_PROTOCOL_VERSION} connection failed.`, { cause });
47
277
  }
48
- if (client.getProtocolEra() !== "modern"
49
- || client.getNegotiatedProtocolVersion() !== MCP_PROTOCOL_VERSION
50
- || client.getDiscoverResult() === undefined) {
51
- await client.close();
52
- throw new Error(`MCP server did not negotiate required revision ${MCP_PROTOCOL_VERSION}.`);
53
- }
54
- return client;
278
+ // {§mcp-authority} — negotiate-and-degrade. The pinned revision is the host's
279
+ // own wire authority: a modern pinned server with a discover result gets the
280
+ // complete extension wire. Anything the SDK negotiated below it is an
281
+ // ordinary MCP server — standard tool/resource/prompt surface from its
282
+ // initialize result, no discover identity, no tasks, no extension channel.
283
+ const era = client.getProtocolEra();
284
+ const negotiated = client.getNegotiatedProtocolVersion() ?? "";
285
+ const discover = client.getDiscoverResult();
286
+ const modern = era === "modern" && negotiated === MCP_PROTOCOL_VERSION && discover !== undefined;
287
+ const extensions = modern
288
+ ? new ExtensionChannel(transport, {
289
+ protocolVersion: MCP_PROTOCOL_VERSION,
290
+ clientInfo,
291
+ clientCapabilities,
292
+ cancelRequest: async (requestId) => client.notification({
293
+ method: "notifications/cancelled",
294
+ params: { requestId },
295
+ }),
296
+ onError: options.onInfrastructureError,
297
+ })
298
+ : null;
299
+ return {
300
+ client,
301
+ transport,
302
+ extensions,
303
+ protocolVersion: negotiated,
304
+ subscriptions: new Subscriptions(client, {
305
+ timeout: requestTimeoutMs(environ),
306
+ tasks: serverSupportsTasks(discover?.capabilities ?? client.getServerCapabilities()),
307
+ onError: options.onInfrastructureError,
308
+ }),
309
+ };
55
310
  };
56
311
  export default class ServerConnection {
57
- #config;
312
+ #definition;
313
+ #resolved;
58
314
  #environ;
315
+ #options;
59
316
  #client;
60
- #toolHints = new Map();
61
- constructor(config, environ = process.env) {
62
- this.#config = config;
317
+ #pendingAuthorization;
318
+ #activeRequests = 0;
319
+ #closed = false;
320
+ constructor(definition, environ = process.env, options = {}) {
321
+ this.#definition = structuredClone(Validator.assertMcpServerDefinition(definition));
322
+ this.#resolved = resolveDefinition(this.#definition, environ);
63
323
  this.#environ = environ;
324
+ this.#options = options;
325
+ }
326
+ get definition() {
327
+ return structuredClone(this.#definition);
328
+ }
329
+ get activeRequests() {
330
+ return this.#activeRequests;
64
331
  }
65
- connect() {
332
+ get authorizationUrl() {
333
+ return this.#pendingAuthorization?.authorizationUrl ?? null;
334
+ }
335
+ assertReplaceable() {
336
+ if (this.#activeRequests !== 0) {
337
+ throw new Error(`MCP server '${this.#definition.name}' has ${this.#activeRequests} active user request(s).`);
338
+ }
339
+ }
340
+ async #open() {
341
+ if (this.#closed)
342
+ throw new Error(`MCP server '${this.#definition.name}' connection is closed.`);
343
+ if (this.#pendingAuthorization !== undefined) {
344
+ throw new AuthorizationRequiredError(this.#pendingAuthorization.authorizationUrl);
345
+ }
66
346
  if (this.#client !== undefined)
67
347
  return this.#client;
68
- const pending = openClient(this.#config, this.#environ).catch((cause) => {
348
+ const transport = openTransport(this.#resolved);
349
+ const pending = openClient(this.#resolved, this.#environ, this.#options, transport).catch((cause) => {
350
+ if (cause instanceof AuthorizationRequiredError
351
+ && this.#resolved.transport === "http"
352
+ && this.#resolved.oauthProvider !== undefined
353
+ && transport instanceof StreamableHTTPClientTransport) {
354
+ this.#pendingAuthorization = {
355
+ transport,
356
+ provider: this.#resolved.oauthProvider,
357
+ authorizationUrl: cause.authorizationUrl,
358
+ standaloneTransport: true,
359
+ };
360
+ }
69
361
  if (this.#client === pending)
70
362
  this.#client = undefined;
71
363
  throw cause;
@@ -73,81 +365,235 @@ export default class ServerConnection {
73
365
  this.#client = pending;
74
366
  return pending;
75
367
  }
76
- readOnly(tool) {
77
- return this.#toolHints.get(tool) === true;
368
+ async connect() {
369
+ return (await this.#open()).client;
78
370
  }
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;
371
+ async finishAuthorization(callbackUrl) {
372
+ const pending = this.#pendingAuthorization;
373
+ if (pending === undefined) {
374
+ throw new Error(`MCP server '${this.#definition.name}' has no pending OAuth authorization.`);
375
+ }
376
+ const callback = new URL(callbackUrl);
377
+ const expected = new URL(pending.provider.redirectUrl);
378
+ if (callback.protocol !== expected.protocol
379
+ || callback.host !== expected.host
380
+ || callback.pathname !== expected.pathname) {
381
+ throw new Error("OAuth callback URL does not match the configured redirect URL.");
382
+ }
383
+ pending.provider.assertCallbackState(callback);
384
+ await pending.transport.finishAuth(callback.searchParams);
385
+ this.#pendingAuthorization = undefined;
386
+ await this.connect();
84
387
  }
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,
388
+ async #request(run) {
389
+ this.#activeRequests += 1;
390
+ try {
391
+ const opened = await this.#open();
392
+ try {
393
+ return await run(opened.client, opened.subscriptions, opened.extensions);
394
+ }
395
+ catch (cause) {
396
+ const authorization = this.#takeAuthorization(opened, cause);
397
+ if (authorization !== null)
398
+ throw authorization;
399
+ throw cause;
400
+ }
401
+ }
402
+ finally {
403
+ this.#activeRequests -= 1;
404
+ }
405
+ }
406
+ #takeAuthorization(opened, cause) {
407
+ if (this.#resolved.transport !== "http"
408
+ || this.#resolved.oauthProvider === undefined
409
+ || !(opened.transport instanceof StreamableHTTPClientTransport)) {
410
+ return null;
411
+ }
412
+ const authorizationUrl = this.#resolved.oauthProvider.takeAuthorizationUrl();
413
+ if (authorizationUrl === undefined)
414
+ return null;
415
+ this.#pendingAuthorization = {
416
+ transport: opened.transport,
417
+ provider: this.#resolved.oauthProvider,
418
+ authorizationUrl: authorizationUrl.href,
419
+ standaloneTransport: false,
112
420
  };
421
+ return new AuthorizationRequiredError(authorizationUrl.href, cause);
113
422
  }
114
- async resources(signal) {
115
- const client = await this.connect();
116
- const discover = client.getDiscoverResult();
117
- if (discover?.capabilities.resources === undefined) {
423
+ async tools(signal) {
424
+ return this.#request(async (client) => {
425
+ if (serverCapabilities(client)?.tools === undefined)
426
+ return [];
427
+ const { tools } = await client.listTools(undefined, this.#requestOptions(signal));
428
+ return tools;
429
+ });
430
+ }
431
+ async catalog(signal) {
432
+ return this.#request(async (client) => {
433
+ // {§mcp-authority} — the discover result is the modern identity and
434
+ // capability source; a legacy server's initialize result supplies the
435
+ // same facts at its negotiated revision.
436
+ const capabilities = serverCapabilities(client) ?? {};
437
+ const [tools, resources, resourceTemplates, prompts] = await Promise.all([
438
+ capabilities.tools === undefined
439
+ ? Promise.resolve([])
440
+ : client.listTools(undefined, this.#requestOptions(signal)).then((result) => result.tools),
441
+ capabilities.resources === undefined
442
+ ? Promise.resolve([])
443
+ : client.listResources(undefined, this.#requestOptions(signal)).then((result) => result.resources),
444
+ capabilities.resources === undefined
445
+ ? Promise.resolve([])
446
+ : client.listResourceTemplates(undefined, this.#requestOptions(signal))
447
+ .then((result) => result.resourceTemplates),
448
+ capabilities.prompts === undefined
449
+ ? Promise.resolve([])
450
+ : client.listPrompts(undefined, this.#requestOptions(signal)).then((result) => result.prompts),
451
+ ]);
118
452
  return {
119
- resources: [],
120
- resourceTemplates: [],
453
+ protocolVersion: client.getNegotiatedProtocolVersion() ?? "",
454
+ server: client.getServerVersion(),
455
+ instructions: client.getInstructions(),
456
+ capabilities,
457
+ tools,
458
+ resources,
459
+ resourceTemplates,
460
+ prompts,
121
461
  };
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
- };
462
+ });
133
463
  }
134
- async callTool(name, args, signal) {
135
- return (await this.connect()).callTool({ name, arguments: args }, this.#requestOptions(signal));
464
+ async resources(signal) {
465
+ return this.#request(async (client) => {
466
+ if (serverCapabilities(client)?.resources === undefined) {
467
+ return { resources: [], resourceTemplates: [] };
468
+ }
469
+ const [resources, resourceTemplates] = await Promise.all([
470
+ client.listResources(undefined, this.#requestOptions(signal))
471
+ .then((result) => result.resources),
472
+ client.listResourceTemplates(undefined, this.#requestOptions(signal))
473
+ .then((result) => result.resourceTemplates),
474
+ ]);
475
+ return { resources, resourceTemplates };
476
+ });
136
477
  }
137
- async readResource(uri, signal) {
138
- return (await this.connect()).readResource({ uri }, this.#requestOptions(signal));
478
+ async prompts(signal) {
479
+ return this.#request(async (client) => {
480
+ if (serverCapabilities(client)?.prompts === undefined)
481
+ return [];
482
+ return (await client.listPrompts(undefined, this.#requestOptions(signal))).prompts;
483
+ });
139
484
  }
140
- #requestOptions(signal) {
141
- return {
485
+ async callTool(name, args, signal, onProgress, interact, toolDefinition) {
486
+ return this.#request(async (client, subscriptions, extensions) => {
487
+ const timeout = requestTimeoutMs(this.#environ);
488
+ if (serverSupportsTasks(serverCapabilities(client))) {
489
+ const tool = toolDefinition ?? (await client.listTools(undefined, this.#requestOptions(signal))).tools.find((candidate) => candidate.name === name);
490
+ if (tool === undefined) {
491
+ throw new Error(`MCP server '${this.#definition.name}' did not list tool '${name}'.`);
492
+ }
493
+ return callToolWithTasks({
494
+ server: this.#definition.name,
495
+ name,
496
+ args,
497
+ tool,
498
+ signal,
499
+ onProgress,
500
+ interact,
501
+ timeout,
502
+ channel: extensions ?? (() => { throw new Error("MCP tasks channel absent on a non-modern connection."); })(),
503
+ subscriptions,
504
+ });
505
+ }
506
+ return runInputRequiredRequest({
507
+ server: this.#definition.name,
508
+ operation: "tools/call",
509
+ originalParams: { name, arguments: args },
510
+ signal,
511
+ interact,
512
+ onProgress,
513
+ timeout,
514
+ requestLeg: (params, options) => client.callTool(params, {
515
+ ...options,
516
+ ...(toolDefinition === undefined ? {} : { toolDefinition }),
517
+ }),
518
+ });
519
+ });
520
+ }
521
+ async readResource(uri, signal, interact) {
522
+ return this.#request(async (client, subscriptions) => {
523
+ await subscriptions.selectResource(uri);
524
+ return runInputRequiredRequest({
525
+ server: this.#definition.name,
526
+ operation: "resources/read",
527
+ originalParams: { uri },
528
+ signal,
529
+ interact,
530
+ timeout: requestTimeoutMs(this.#environ),
531
+ requestLeg: (params, options, retry) => client.readResource(params, retry ? { ...options, cacheMode: "refresh" } : options),
532
+ });
533
+ });
534
+ }
535
+ async getPrompt(name, args, signal, interact) {
536
+ return this.#request(async (client) => runInputRequiredRequest({
537
+ server: this.#definition.name,
538
+ operation: "prompts/get",
539
+ originalParams: { name, ...(args === undefined ? {} : { arguments: args }) },
142
540
  signal,
541
+ interact,
143
542
  timeout: requestTimeoutMs(this.#environ),
543
+ requestLeg: (params, options) => client.getPrompt(params, options),
544
+ }));
545
+ }
546
+ async complete(params, signal) {
547
+ return this.#request(async (client) => client.complete(params, this.#requestOptions(signal)));
548
+ }
549
+ #requestOptions(signal, onProgress) {
550
+ const timeout = requestTimeoutMs(this.#environ);
551
+ return {
552
+ signal,
553
+ timeout,
554
+ maxTotalTimeout: timeout,
555
+ ...(onProgress === undefined ? {} : { onprogress: onProgress }),
144
556
  };
145
557
  }
146
558
  async close() {
559
+ if (this.#closed)
560
+ return;
561
+ this.#closed = true;
147
562
  const client = this.#client;
148
563
  this.#client = undefined;
149
- if (client !== undefined)
150
- await (await client).close();
564
+ const pending = this.#pendingAuthorization;
565
+ this.#pendingAuthorization = undefined;
566
+ const closures = [];
567
+ if (client !== undefined) {
568
+ closures.push(client.then(async ({ client: connected, extensions, subscriptions }) => {
569
+ const failures = [];
570
+ extensions?.close();
571
+ // A full connection close terminates its listen request. Retiring first
572
+ // avoids a redundant cancellation racing the SDK's removed listen ID.
573
+ const settled = await Promise.allSettled([
574
+ subscriptions.retire(),
575
+ connected.close(),
576
+ ]);
577
+ failures.push(...settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
578
+ if (failures.length === 1)
579
+ throw failures[0];
580
+ if (failures.length > 1) {
581
+ throw new AggregateError(failures, `MCP server '${this.#definition.name}' connection shutdown failed.`);
582
+ }
583
+ }));
584
+ }
585
+ if (pending?.standaloneTransport === true)
586
+ closures.push(pending.transport.close());
587
+ const settled = await Promise.allSettled(closures);
588
+ const failures = settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
589
+ if (failures.length === 1)
590
+ throw failures[0];
591
+ if (failures.length > 1) {
592
+ throw new AggregateError(failures, `MCP server '${this.#definition.name}' shutdown failed.`);
593
+ }
594
+ }
595
+ describeError(error) {
596
+ return message(error);
151
597
  }
152
598
  }
153
599
  //# sourceMappingURL=client.js.map