toolcraft 0.0.23 → 0.0.25

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 (152) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.compile-check.js +1 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +50 -13
  5. package/dist/error-report.js +32 -3
  6. package/dist/human-in-loop/approval-tasks.d.ts +1 -0
  7. package/dist/human-in-loop/approval-tasks.js +7 -5
  8. package/dist/human-in-loop/approvals-commands.js +51 -8
  9. package/dist/human-in-loop/runner.js +24 -19
  10. package/dist/human-in-loop/state-machine.d.ts +3 -3
  11. package/dist/human-in-loop/state-machine.js +13 -5
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.js +6 -1
  14. package/dist/mcp-proxy.js +85 -19
  15. package/dist/mcp.compile-check.js +1 -0
  16. package/dist/mcp.d.ts +1 -0
  17. package/dist/mcp.js +50 -8
  18. package/dist/renderer.js +119 -13
  19. package/dist/sdk.compile-check.js +1 -0
  20. package/dist/sdk.d.ts +1 -0
  21. package/dist/sdk.js +56 -11
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
  24. package/node_modules/@poe-code/agent-defs/package.json +1 -1
  25. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
  26. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
  27. package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
  28. package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
  29. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
  30. package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
  31. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
  32. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
  33. package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
  34. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
  35. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
  38. package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
  42. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
  43. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
  44. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
  45. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
  46. package/node_modules/@poe-code/config-mutations/package.json +1 -1
  47. package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
  48. package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
  49. package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
  50. package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
  51. package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
  52. package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
  53. package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
  54. package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
  55. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
  56. package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
  57. package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
  58. package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
  59. package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
  60. package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
  61. package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
  62. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
  63. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
  64. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
  65. package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
  66. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
  67. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
  68. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
  71. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
  72. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
  73. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
  74. package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
  75. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
  76. package/node_modules/@poe-code/design-system/dist/index.js +2 -1
  77. package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
  78. package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
  79. package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
  80. package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
  81. package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
  82. package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
  83. package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
  84. package/node_modules/@poe-code/design-system/package.json +2 -1
  85. package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
  86. package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
  87. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +377 -130
  88. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +78 -10
  89. package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
  90. package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
  91. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
  92. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +21 -5
  93. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  94. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  95. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
  96. package/node_modules/@poe-code/process-runner/dist/types.d.ts +6 -0
  97. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +61 -0
  98. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +503 -0
  99. package/node_modules/@poe-code/process-runner/package.json +1 -1
  100. package/node_modules/@poe-code/task-list/README.md +0 -2
  101. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
  102. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
  103. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
  104. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
  105. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
  106. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  107. package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
  108. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
  109. package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
  110. package/node_modules/@poe-code/task-list/dist/index.js +2 -0
  111. package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
  112. package/node_modules/@poe-code/task-list/dist/move.js +215 -0
  113. package/node_modules/@poe-code/task-list/dist/open.js +3 -4
  114. package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
  115. package/node_modules/@poe-code/task-list/dist/state.js +9 -0
  116. package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
  117. package/node_modules/@poe-code/task-list/package.json +1 -2
  118. package/node_modules/auth-store/dist/create-secret-store.js +4 -1
  119. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +8 -0
  120. package/node_modules/auth-store/dist/encrypted-file-store.js +104 -8
  121. package/node_modules/auth-store/dist/index.d.ts +1 -1
  122. package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
  123. package/node_modules/auth-store/dist/keychain-store.js +18 -16
  124. package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
  125. package/node_modules/auth-store/dist/provider-store.js +55 -7
  126. package/node_modules/auth-store/dist/types.d.ts +3 -1
  127. package/node_modules/auth-store/package.json +2 -1
  128. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
  129. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
  130. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
  131. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
  132. package/node_modules/mcp-oauth/package.json +1 -0
  133. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
  134. package/node_modules/tiny-mcp-client/dist/internal.d.ts +9 -4
  135. package/node_modules/tiny-mcp-client/dist/internal.js +244 -66
  136. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
  137. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
  138. package/node_modules/tiny-mcp-client/package.json +2 -1
  139. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
  140. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
  141. package/node_modules/tiny-mcp-client/src/internal.ts +287 -76
  142. package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
  143. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
  144. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
  145. package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
  146. package/package.json +10 -12
  147. package/node_modules/@poe-code/file-lock/README.md +0 -52
  148. package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
  149. package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
  150. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
  151. package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
  152. package/node_modules/@poe-code/file-lock/package.json +0 -23
@@ -8,7 +8,7 @@ import {
8
8
  OAuthError,
9
9
  type OAuthClientProvider,
10
10
  type OAuthClientProviderOptions,
11
- } from "../../mcp-oauth/dist/index.js";
11
+ } from "mcp-oauth";
12
12
  import type { Server as TinyStdioMcpServer } from "tiny-stdio-mcp-server";
13
13
  import {
14
14
  OAuthMetadataDiscovery,
@@ -28,7 +28,7 @@ export {
28
28
  export {
29
29
  createAuthStoreSessionStore,
30
30
  createDefaultOAuthClientProvider,
31
- } from "../../mcp-oauth/dist/index.js";
31
+ } from "mcp-oauth";
32
32
  export type {
33
33
  OAuthDiscoveryCache,
34
34
  } from "./oauth-discovery.js";
@@ -39,7 +39,7 @@ export type {
39
39
  OAuthClientProviderOptions,
40
40
  OAuthSessionStore,
41
41
  StoredOAuthSession,
42
- } from "../../mcp-oauth/dist/index.js";
42
+ } from "mcp-oauth";
43
43
 
44
44
  export type RequestId = number | string;
45
45
 
@@ -97,6 +97,7 @@ export interface InitializeResult {
97
97
 
98
98
  export interface McpClientOptions {
99
99
  clientInfo: Implementation;
100
+ requestTimeoutMs?: number;
100
101
  capabilities?: ClientCapabilities;
101
102
  onToolsChanged?: () => void | Promise<void>;
102
103
  onResourcesChanged?: () => void | Promise<void>;
@@ -115,8 +116,11 @@ const MCP_PROTOCOL_VERSION = "2025-03-26";
115
116
  export class McpClient {
116
117
  private currentState: "disconnected" | "initializing" | "ready" | "closed" = "disconnected";
117
118
  private currentServerCapabilities: ServerCapabilities | null = null;
119
+ private currentClientCapabilities: ClientCapabilities | null = null;
118
120
  private currentServerInfo: Implementation | null = null;
119
121
  private currentInstructions: string | undefined;
122
+ private readonly subscribedResourceUris = new Set<string>();
123
+ private readonly activeProgressTokens = new Map<ProgressToken, number>();
120
124
  private readonly options: McpClientOptions;
121
125
  private transport: McpTransport | null = null;
122
126
  private messageLayer: JsonRpcMessageLayer | null = null;
@@ -130,7 +134,9 @@ export class McpClient {
130
134
  }
131
135
 
132
136
  get serverCapabilities(): ServerCapabilities | null {
133
- return this.currentServerCapabilities;
137
+ return this.currentServerCapabilities === null
138
+ ? null
139
+ : structuredClone(this.currentServerCapabilities);
134
140
  }
135
141
 
136
142
  get serverInfo(): Implementation | null {
@@ -162,6 +168,13 @@ export class McpClient {
162
168
  throw new Error("MCP client is already connected");
163
169
  }
164
170
 
171
+ this.currentServerCapabilities = null;
172
+ this.currentClientCapabilities = null;
173
+ this.currentServerInfo = null;
174
+ this.currentInstructions = undefined;
175
+ this.subscribedResourceUris.clear();
176
+ this.activeProgressTokens.clear();
177
+
165
178
  const transportClosedReason = transport.closed
166
179
  .then((closedEvent) => closedEvent.reason)
167
180
  .catch((error: unknown) =>
@@ -170,7 +183,7 @@ export class McpClient {
170
183
  const messageLayer = new JsonRpcMessageLayer(
171
184
  transport.readable,
172
185
  transport.writable,
173
- 30_000,
186
+ this.options.requestTimeoutMs,
174
187
  transportClosedReason
175
188
  );
176
189
  const {
@@ -192,14 +205,8 @@ export class McpClient {
192
205
  );
193
206
  }
194
207
 
195
- if (onRootsList !== undefined) {
196
- messageLayer.onRequest("roots/list", async () => ({
197
- roots: await onRootsList(),
198
- }));
199
- }
200
-
201
208
  messageLayer.onNotification("notifications/tools/list_changed", async () => {
202
- if (onToolsChanged === undefined) {
209
+ if (onToolsChanged === undefined || this.currentServerCapabilities?.tools?.listChanged !== true) {
203
210
  return;
204
211
  }
205
212
 
@@ -222,7 +229,7 @@ export class McpClient {
222
229
  }
223
230
 
224
231
  const { uri } = params as { uri?: unknown };
225
- if (typeof uri !== "string") {
232
+ if (typeof uri !== "string" || !this.subscribedResourceUris.has(uri)) {
226
233
  return;
227
234
  }
228
235
 
@@ -265,7 +272,11 @@ export class McpClient {
265
272
  }
266
273
 
267
274
  const { progressToken, progress } = params;
268
- if (!isRequestId(progressToken) || typeof progress !== "number") {
275
+ if (
276
+ !isRequestId(progressToken) ||
277
+ typeof progress !== "number" ||
278
+ !this.activeProgressTokens.has(progressToken)
279
+ ) {
269
280
  return;
270
281
  }
271
282
 
@@ -297,6 +308,8 @@ export class McpClient {
297
308
  this.transport = transport;
298
309
  this.messageLayer = messageLayer;
299
310
  this.currentState = "initializing";
311
+ this.subscribedResourceUris.clear();
312
+ this.activeProgressTokens.clear();
300
313
  transport.closed
301
314
  .then((closedEvent) => {
302
315
  if (this.transport !== transport) {
@@ -334,26 +347,52 @@ export class McpClient {
334
347
  };
335
348
  }
336
349
 
337
- const initializeResult = (await messageLayer.sendRequest("initialize", {
338
- protocolVersion: MCP_PROTOCOL_VERSION,
339
- clientInfo: this.options.clientInfo,
340
- capabilities,
341
- })) as InitializeResult;
350
+ this.currentClientCapabilities = structuredClone(capabilities);
342
351
 
343
- if (initializeResult.protocolVersion !== MCP_PROTOCOL_VERSION) {
344
- throw new McpError(
345
- ERROR_INVALID_REQUEST,
346
- `Unsupported protocol version: ${initializeResult.protocolVersion}`
347
- );
348
- }
352
+ try {
353
+ const initializeResultValue = await messageLayer.sendRequest("initialize", {
354
+ protocolVersion: MCP_PROTOCOL_VERSION,
355
+ clientInfo: this.options.clientInfo,
356
+ capabilities,
357
+ });
358
+
359
+ if (!isInitializeResult(initializeResultValue)) {
360
+ throw new McpError(ERROR_INVALID_REQUEST, "Invalid initialize result");
361
+ }
362
+
363
+ const initializeResult = initializeResultValue;
364
+
365
+ if (initializeResult.protocolVersion !== MCP_PROTOCOL_VERSION) {
366
+ throw new McpError(
367
+ ERROR_INVALID_REQUEST,
368
+ `Unsupported protocol version: ${initializeResult.protocolVersion}`
369
+ );
370
+ }
349
371
 
350
- this.currentServerCapabilities = initializeResult.capabilities;
351
- this.currentServerInfo = initializeResult.serverInfo;
352
- this.currentInstructions = initializeResult.instructions;
353
- messageLayer.sendNotification("notifications/initialized");
354
- this.currentState = "ready";
372
+ this.currentServerCapabilities = structuredClone(initializeResult.capabilities);
373
+ this.currentServerInfo = { ...initializeResult.serverInfo };
374
+ this.currentInstructions = initializeResult.instructions;
375
+ if (onRootsList !== undefined) {
376
+ messageLayer.onRequest("roots/list", async () => ({
377
+ roots: await onRootsList(),
378
+ }));
379
+ }
380
+ messageLayer.sendNotification("notifications/initialized");
381
+ this.currentState = "ready";
382
+
383
+ return initializeResult;
384
+ } catch (error) {
385
+ if (this.transport === transport) {
386
+ const reason = error instanceof Error ? error : new Error(String(error));
387
+ messageLayer.dispose(reason);
388
+ transport.dispose(reason);
389
+ this.messageLayer = null;
390
+ this.transport = null;
391
+ this.currentState = "disconnected";
392
+ }
355
393
 
356
- return initializeResult;
394
+ throw error;
395
+ }
357
396
  }
358
397
 
359
398
  private getServerCapabilitiesOrThrow(): ServerCapabilities {
@@ -372,10 +411,12 @@ export class McpClient {
372
411
  }
373
412
 
374
413
  const requestParams = params.cursor === undefined ? undefined : { cursor: params.cursor };
375
- return (await messageLayer.sendRequest("tools/list", requestParams)) as {
376
- tools: Tool[];
377
- nextCursor?: string;
378
- };
414
+ const result = await messageLayer.sendRequest("tools/list", requestParams);
415
+ if (!isToolsListResult(result)) {
416
+ throw new McpError(ERROR_INVALID_REQUEST, "Invalid tools/list result");
417
+ }
418
+
419
+ return result;
379
420
  }
380
421
 
381
422
  async callTool(params: CallToolParams, options: CallToolOptions = {}): Promise<CallToolResult> {
@@ -385,6 +426,10 @@ export class McpClient {
385
426
  throw new Error("Server does not support tools");
386
427
  }
387
428
 
429
+ if (options.signal?.aborted) {
430
+ throw options.signal.reason;
431
+ }
432
+
388
433
  const requestParams =
389
434
  options.progressToken === undefined
390
435
  ? params
@@ -395,39 +440,70 @@ export class McpClient {
395
440
  },
396
441
  };
397
442
 
398
- let requestId: RequestId | undefined;
399
- const requestPromise = messageLayer.sendRequest("tools/call", requestParams, {
400
- onRequestId: (nextRequestId) => {
401
- requestId = nextRequestId;
402
- },
403
- }) as Promise<CallToolResult>;
404
- if (options.signal === undefined) {
405
- return await requestPromise;
443
+ if (options.progressToken !== undefined) {
444
+ this.activeProgressTokens.set(
445
+ options.progressToken,
446
+ (this.activeProgressTokens.get(options.progressToken) ?? 0) + 1
447
+ );
406
448
  }
407
- const signal = options.signal;
408
449
 
409
- let abortListener: (() => void) | undefined;
410
- const abortPromise = new Promise<CallToolResult>((_, reject) => {
411
- const rejectWithAbortReason = () => {
412
- if (requestId !== undefined) {
413
- messageLayer.sendNotification("notifications/cancelled", { requestId });
450
+ try {
451
+ let requestId: RequestId | undefined;
452
+ let cancellationSent = false;
453
+ const sendCancellationNotification = () => {
454
+ if (requestId === undefined || cancellationSent) {
455
+ return;
414
456
  }
415
- reject(signal.reason);
457
+ cancellationSent = true;
458
+ messageLayer.sendNotification("notifications/cancelled", { requestId });
416
459
  };
460
+ const requestPromise = messageLayer.sendRequest("tools/call", requestParams, {
461
+ onRequestId: (nextRequestId) => {
462
+ requestId = nextRequestId;
463
+ },
464
+ onTimeout: sendCancellationNotification,
465
+ }).then((result) => {
466
+ if (!isCallToolResult(result)) {
467
+ throw new McpError(ERROR_INVALID_REQUEST, "Invalid tool result");
468
+ }
417
469
 
418
- abortListener = rejectWithAbortReason;
419
- signal.addEventListener("abort", abortListener, { once: true });
420
- if (signal.aborted) {
421
- signal.removeEventListener("abort", abortListener);
422
- rejectWithAbortReason();
470
+ return result;
471
+ });
472
+ if (options.signal === undefined) {
473
+ return await requestPromise;
423
474
  }
424
- });
475
+ const signal = options.signal;
425
476
 
426
- try {
427
- return (await Promise.race([requestPromise, abortPromise])) as CallToolResult;
477
+ let abortListener: (() => void) | undefined;
478
+ const abortPromise = new Promise<CallToolResult>((_, reject) => {
479
+ const rejectWithAbortReason = () => {
480
+ sendCancellationNotification();
481
+ reject(signal.reason);
482
+ };
483
+
484
+ abortListener = rejectWithAbortReason;
485
+ signal.addEventListener("abort", abortListener, { once: true });
486
+ if (signal.aborted) {
487
+ signal.removeEventListener("abort", abortListener);
488
+ rejectWithAbortReason();
489
+ }
490
+ });
491
+
492
+ try {
493
+ return (await Promise.race([requestPromise, abortPromise])) as CallToolResult;
494
+ } finally {
495
+ if (abortListener !== undefined) {
496
+ signal.removeEventListener("abort", abortListener);
497
+ }
498
+ }
428
499
  } finally {
429
- if (abortListener !== undefined) {
430
- signal.removeEventListener("abort", abortListener);
500
+ if (options.progressToken !== undefined) {
501
+ const activeCount = this.activeProgressTokens.get(options.progressToken);
502
+ if (activeCount === 1) {
503
+ this.activeProgressTokens.delete(options.progressToken);
504
+ } else if (activeCount !== undefined) {
505
+ this.activeProgressTokens.set(options.progressToken, activeCount - 1);
506
+ }
431
507
  }
432
508
  }
433
509
  }
@@ -484,6 +560,7 @@ export class McpClient {
484
560
  }
485
561
 
486
562
  await messageLayer.sendRequest("resources/subscribe", { uri });
563
+ this.subscribedResourceUris.add(uri);
487
564
  }
488
565
 
489
566
  async unsubscribe(uri: string): Promise<void> {
@@ -494,6 +571,7 @@ export class McpClient {
494
571
  }
495
572
 
496
573
  await messageLayer.sendRequest("resources/unsubscribe", { uri });
574
+ this.subscribedResourceUris.delete(uri);
497
575
  }
498
576
 
499
577
  async listPrompts(params: PaginatedParams = {}): Promise<{ prompts: Prompt[]; nextCursor?: string }> {
@@ -553,6 +631,10 @@ export class McpClient {
553
631
 
554
632
  async sendRootsChanged(): Promise<void> {
555
633
  const messageLayer = this.getMessageLayerOrThrow();
634
+ if (this.currentClientCapabilities?.roots?.listChanged !== true) {
635
+ throw new Error("Client did not advertise roots list changes");
636
+ }
637
+
556
638
  messageLayer.sendNotification("notifications/roots/list_changed");
557
639
  }
558
640
 
@@ -571,6 +653,12 @@ export class McpClient {
571
653
  this.transport?.dispose(closeError);
572
654
  this.messageLayer = null;
573
655
  this.transport = null;
656
+ this.currentServerCapabilities = null;
657
+ this.currentClientCapabilities = null;
658
+ this.currentServerInfo = null;
659
+ this.currentInstructions = undefined;
660
+ this.subscribedResourceUris.clear();
661
+ this.activeProgressTokens.clear();
574
662
  this.currentState = "closed";
575
663
  }
576
664
  }
@@ -2402,7 +2490,6 @@ export class HttpTransport implements McpTransport {
2402
2490
  this.disposed = true;
2403
2491
  this.abortInFlightFetches();
2404
2492
  this.cancelOpenSseReaders();
2405
- this.terminateSession();
2406
2493
 
2407
2494
  if (!this.writeStream.destroyed && !this.writeStream.writableEnded) {
2408
2495
  this.writeStream.end();
@@ -2412,9 +2499,24 @@ export class HttpTransport implements McpTransport {
2412
2499
  this.readStream.end();
2413
2500
  }
2414
2501
 
2502
+ void this.closeWithSessionTermination(reason);
2503
+ }
2504
+
2505
+ private async closeWithSessionTermination(reason: Error): Promise<void> {
2506
+ let closeReason = reason;
2507
+ if (this.sessionId !== undefined) {
2508
+ const sessionId = this.sessionId;
2509
+ this.sessionId = undefined;
2510
+ try {
2511
+ await this.sendSessionTerminationRequest(sessionId);
2512
+ } catch (error) {
2513
+ closeReason = error instanceof Error ? error : new Error(String(error));
2514
+ }
2515
+ }
2516
+
2415
2517
  const resolveClosed = this.resolveClosed;
2416
2518
  this.resolveClosed = undefined;
2417
- resolveClosed?.({ reason });
2519
+ resolveClosed?.({ reason: closeReason });
2418
2520
  }
2419
2521
 
2420
2522
  private abortInFlightFetches(): void {
@@ -2469,7 +2571,9 @@ export class HttpTransport implements McpTransport {
2469
2571
  await this.throwForPostHttpError(response);
2470
2572
  this.captureSessionId(response);
2471
2573
  this.maybeOpenGetSseStream();
2472
- await this.forwardResponseMessages(response);
2574
+ void this.forwardResponseMessages(response).catch((error) => {
2575
+ this.dispose(error instanceof Error ? error : new Error(String(error)));
2576
+ });
2473
2577
  }
2474
2578
  }
2475
2579
 
@@ -2479,6 +2583,7 @@ export class HttpTransport implements McpTransport {
2479
2583
  headers.set("Content-Type", "application/json");
2480
2584
  if (this.sessionId !== undefined) {
2481
2585
  headers.set("Mcp-Session-Id", this.sessionId);
2586
+ headers.set("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
2482
2587
  }
2483
2588
  return this.authorizeRequestHeaders(headers);
2484
2589
  }
@@ -2488,6 +2593,7 @@ export class HttpTransport implements McpTransport {
2488
2593
  headers.set("Accept", "text/event-stream");
2489
2594
  if (this.sessionId !== undefined) {
2490
2595
  headers.set("Mcp-Session-Id", this.sessionId);
2596
+ headers.set("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
2491
2597
  }
2492
2598
  if (this.lastEventId !== undefined) {
2493
2599
  headers.set("Last-Event-ID", this.lastEventId);
@@ -2498,6 +2604,7 @@ export class HttpTransport implements McpTransport {
2498
2604
  private async createDeleteHeaders(sessionId: string): Promise<Headers> {
2499
2605
  const headers = new Headers(this.headers);
2500
2606
  headers.set("Mcp-Session-Id", sessionId);
2607
+ headers.set("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
2501
2608
  return this.authorizeRequestHeaders(headers);
2502
2609
  }
2503
2610
 
@@ -2516,6 +2623,10 @@ export class HttpTransport implements McpTransport {
2516
2623
  return;
2517
2624
  }
2518
2625
 
2626
+ if (this.sessionId !== undefined && this.sessionId !== sessionId) {
2627
+ throw new Error("HTTP transport response changed active session ID");
2628
+ }
2629
+
2519
2630
  this.sessionId = sessionId;
2520
2631
  }
2521
2632
 
@@ -2534,26 +2645,22 @@ export class HttpTransport implements McpTransport {
2534
2645
  });
2535
2646
  }
2536
2647
 
2537
- private terminateSession(): void {
2538
- if (this.sessionId === undefined) {
2539
- return;
2540
- }
2541
-
2542
- const sessionId = this.sessionId;
2543
- this.sessionId = undefined;
2544
-
2545
- this.sendSessionTerminationRequest(sessionId).catch(() => undefined);
2546
- }
2547
-
2548
2648
  private async sendSessionTerminationRequest(sessionId: string): Promise<void> {
2549
2649
  const response = await this.fetchImpl(this.url, {
2550
2650
  method: "DELETE",
2551
2651
  headers: await this.createDeleteHeaders(sessionId),
2552
2652
  });
2553
2653
 
2554
- if (response.status === 405) {
2654
+ if (response.status === 405 || response.ok) {
2555
2655
  return;
2556
2656
  }
2657
+
2658
+ const responseBody = (await response.text()).trim();
2659
+ const statusDescriptor = `${response.status} ${response.statusText}`.trim();
2660
+ const message = responseBody.length === 0
2661
+ ? `HTTP transport DELETE failed (${statusDescriptor})`
2662
+ : `HTTP transport DELETE failed (${statusDescriptor}): ${responseBody}`;
2663
+ throw new Error(message);
2557
2664
  }
2558
2665
 
2559
2666
  private async consumeGetSseStream(): Promise<void> {
@@ -2566,6 +2673,20 @@ export class HttpTransport implements McpTransport {
2566
2673
  throw new HttpTransportGetSseNotSupportedError();
2567
2674
  }
2568
2675
 
2676
+ if (response.status === 404) {
2677
+ this.sessionId = undefined;
2678
+ throw new Error("HTTP transport session expired (GET 404 response)");
2679
+ }
2680
+
2681
+ if (!response.ok) {
2682
+ const responseBody = (await response.text()).trim();
2683
+ const statusDescriptor = `${response.status} ${response.statusText}`.trim();
2684
+ const message = responseBody.length === 0
2685
+ ? `HTTP transport GET failed (${statusDescriptor})`
2686
+ : `HTTP transport GET failed (${statusDescriptor}): ${responseBody}`;
2687
+ throw new Error(message);
2688
+ }
2689
+
2569
2690
  const contentType = response.headers.get("Content-Type");
2570
2691
  if (contentType === null) {
2571
2692
  return;
@@ -2577,7 +2698,10 @@ export class HttpTransport implements McpTransport {
2577
2698
  if (!this.disposed && this.sessionId !== undefined && this.lastEventId !== undefined) {
2578
2699
  this.maybeOpenGetSseStream();
2579
2700
  }
2701
+ return;
2580
2702
  }
2703
+
2704
+ return;
2581
2705
  }
2582
2706
 
2583
2707
  private async throwForPostHttpError(response: Response): Promise<void> {
@@ -2645,7 +2769,10 @@ export class HttpTransport implements McpTransport {
2645
2769
 
2646
2770
  if (normalizedContentType.includes("application/json")) {
2647
2771
  await this.forwardJsonResponseMessage(response);
2772
+ return;
2648
2773
  }
2774
+
2775
+ throw new Error("HTTP transport POST returned an unsupported response content type");
2649
2776
  }
2650
2777
 
2651
2778
  private async forwardSseResponseMessages(response: Response): Promise<void> {
@@ -2856,9 +2983,13 @@ function normalizeLine(line: string): string {
2856
2983
 
2857
2984
  export async function* readLines(stream: Readable): AsyncGenerator<string> {
2858
2985
  let buffer = "";
2986
+ const decoder = new TextDecoder();
2859
2987
 
2860
2988
  for await (const chunk of stream as AsyncIterable<unknown>) {
2861
- buffer += chunkToString(chunk);
2989
+ buffer +=
2990
+ chunk instanceof Uint8Array
2991
+ ? decoder.decode(chunk, { stream: true })
2992
+ : decoder.decode() + String(chunk);
2862
2993
 
2863
2994
  while (true) {
2864
2995
  const newlineIndex = buffer.indexOf("\n");
@@ -2872,6 +3003,8 @@ export async function* readLines(stream: Readable): AsyncGenerator<string> {
2872
3003
  }
2873
3004
  }
2874
3005
 
3006
+ buffer += decoder.decode();
3007
+
2875
3008
  if (buffer.length > 0) {
2876
3009
  yield normalizeLine(buffer);
2877
3010
  }
@@ -3008,6 +3141,7 @@ interface ActiveIncomingRequest {
3008
3141
  export interface JsonRpcRequestOptions {
3009
3142
  timeoutMs?: number;
3010
3143
  onRequestId?: (requestId: RequestId) => void;
3144
+ onTimeout?: (requestId: RequestId) => void;
3011
3145
  }
3012
3146
 
3013
3147
  interface JsonRpcRequestContext {
@@ -3116,6 +3250,7 @@ export class JsonRpcMessageLayer {
3116
3250
  return new Promise((resolve, reject) => {
3117
3251
  const timeout = setTimeout(() => {
3118
3252
  this.pendingRequests.delete(id);
3253
+ options.onTimeout?.(id);
3119
3254
  reject(new Error(`JSON-RPC request "${method}" timed out after ${timeoutMs}ms`));
3120
3255
  }, timeoutMs);
3121
3256
 
@@ -3383,6 +3518,82 @@ function isObjectRecord(value: unknown): value is Record<string, unknown> {
3383
3518
  return typeof value === "object" && value !== null && !Array.isArray(value);
3384
3519
  }
3385
3520
 
3521
+ function isInitializeResult(value: unknown): value is InitializeResult {
3522
+ if (!isObjectRecord(value) || typeof value.protocolVersion !== "string") {
3523
+ return false;
3524
+ }
3525
+
3526
+ if (!isServerCapabilities(value.capabilities)) {
3527
+ return false;
3528
+ }
3529
+
3530
+ if (
3531
+ !isObjectRecord(value.serverInfo)
3532
+ || typeof value.serverInfo.name !== "string"
3533
+ || value.serverInfo.name.length === 0
3534
+ || typeof value.serverInfo.version !== "string"
3535
+ || value.serverInfo.version.length === 0
3536
+ ) {
3537
+ return false;
3538
+ }
3539
+
3540
+ return value.instructions === undefined || typeof value.instructions === "string";
3541
+ }
3542
+
3543
+ function isServerCapabilities(value: unknown): value is ServerCapabilities {
3544
+ if (!isObjectRecord(value)) {
3545
+ return false;
3546
+ }
3547
+
3548
+ for (const capability of ["prompts", "resources", "tools", "logging", "completions", "experimental"] as const) {
3549
+ if (value[capability] !== undefined && !isObjectRecord(value[capability])) {
3550
+ return false;
3551
+ }
3552
+ }
3553
+
3554
+ return true;
3555
+ }
3556
+
3557
+ function isCallToolResult(value: unknown): value is CallToolResult {
3558
+ if (!isObjectRecord(value) || !Array.isArray(value.content)) {
3559
+ return false;
3560
+ }
3561
+
3562
+ if (value.isError !== undefined && typeof value.isError !== "boolean") {
3563
+ return false;
3564
+ }
3565
+
3566
+ return value.content.every(isContentItem);
3567
+ }
3568
+
3569
+ function isToolsListResult(value: unknown): value is { tools: Tool[]; nextCursor?: string } {
3570
+ return isObjectRecord(value)
3571
+ && Array.isArray(value.tools)
3572
+ && (value.nextCursor === undefined || typeof value.nextCursor === "string");
3573
+ }
3574
+
3575
+ function isContentItem(value: unknown): value is ContentItem {
3576
+ if (!isObjectRecord(value)) {
3577
+ return false;
3578
+ }
3579
+
3580
+ if (value.type === "text") {
3581
+ return typeof value.text === "string";
3582
+ }
3583
+
3584
+ if (value.type === "image" || value.type === "audio") {
3585
+ return typeof value.data === "string" && typeof value.mimeType === "string";
3586
+ }
3587
+
3588
+ if (value.type !== "resource" || !isObjectRecord(value.resource)) {
3589
+ return false;
3590
+ }
3591
+
3592
+ return typeof value.resource.uri === "string"
3593
+ && (value.resource.mimeType === undefined || typeof value.resource.mimeType === "string")
3594
+ && (typeof value.resource.text === "string" || typeof value.resource.blob === "string");
3595
+ }
3596
+
3386
3597
  function hasOwn(
3387
3598
  value: Record<string, unknown>,
3388
3599
  property: string
@@ -293,6 +293,38 @@ describe("McpClient SDK integration callTool", () => {
293
293
  }
294
294
  });
295
295
 
296
+ it("cancels an in-flight slow tool call when the request timeout elapses", async () => {
297
+ const server = await createMockSlowToolServer({ delayMs: 1_000, pollIntervalMs: 5 });
298
+ const { client, cleanup } = await createSdkTestPair(server, () =>
299
+ new McpClient({
300
+ clientInfo: {
301
+ name: "test-client",
302
+ version: "1.0.0",
303
+ },
304
+ requestTimeoutMs: 100,
305
+ })
306
+ );
307
+
308
+ try {
309
+ const callPromise = client.callTool({
310
+ name: "slow",
311
+ arguments: {
312
+ delayMs: 500,
313
+ },
314
+ });
315
+
316
+ await waitFor(() => server.wasStarted(), "Timed out waiting for slow tool to start");
317
+
318
+ await expect(callPromise).rejects.toThrow(
319
+ 'JSON-RPC request "tools/call" timed out after 100ms'
320
+ );
321
+ await waitFor(() => server.wasCancelled(), "Timed out waiting for slow tool cancellation");
322
+ expect(server.getCancelledRequestIds()).toEqual(server.getStartedRequestIds());
323
+ } finally {
324
+ await cleanup();
325
+ }
326
+ });
327
+
296
328
  it("rejects with JSON-RPC error code and message for unknown tool names", async () => {
297
329
  const server = await createMockErrorServer();
298
330
  const { client, cleanup } = await createSdkTestPair(server, () =>
@@ -17,7 +17,7 @@ describe("McpClient integration tools with tiny-stdio-mcp-test-server", () => {
17
17
  try {
18
18
  expect(client.serverInfo).toEqual({
19
19
  name: "tiny-stdio-mcp-test-server",
20
- version: "0.0.1",
20
+ version: "0.1.0",
21
21
  });
22
22
 
23
23
  const { tools } = await client.listTools();