toolcraft 0.0.23 → 0.0.24

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 (147) 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/docker-execution-env.js +244 -110
  86. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +16 -4
  87. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
  88. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +16 -1
  89. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  90. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  91. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
  92. package/node_modules/@poe-code/process-runner/dist/types.d.ts +3 -0
  93. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +57 -0
  94. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +484 -0
  95. package/node_modules/@poe-code/process-runner/package.json +1 -1
  96. package/node_modules/@poe-code/task-list/README.md +0 -2
  97. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
  98. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
  99. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
  100. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
  101. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
  102. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  103. package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
  104. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
  105. package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
  106. package/node_modules/@poe-code/task-list/dist/index.js +2 -0
  107. package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
  108. package/node_modules/@poe-code/task-list/dist/move.js +215 -0
  109. package/node_modules/@poe-code/task-list/dist/open.js +3 -4
  110. package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
  111. package/node_modules/@poe-code/task-list/dist/state.js +9 -0
  112. package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
  113. package/node_modules/@poe-code/task-list/package.json +1 -2
  114. package/node_modules/auth-store/dist/create-secret-store.js +4 -1
  115. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +7 -0
  116. package/node_modules/auth-store/dist/encrypted-file-store.js +69 -7
  117. package/node_modules/auth-store/dist/index.d.ts +1 -1
  118. package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
  119. package/node_modules/auth-store/dist/keychain-store.js +18 -16
  120. package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
  121. package/node_modules/auth-store/dist/provider-store.js +55 -7
  122. package/node_modules/auth-store/dist/types.d.ts +3 -1
  123. package/node_modules/auth-store/package.json +2 -1
  124. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
  125. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
  126. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
  127. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
  128. package/node_modules/mcp-oauth/package.json +1 -0
  129. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
  130. package/node_modules/tiny-mcp-client/dist/internal.d.ts +8 -4
  131. package/node_modules/tiny-mcp-client/dist/internal.js +237 -67
  132. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
  133. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
  134. package/node_modules/tiny-mcp-client/package.json +2 -1
  135. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
  136. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
  137. package/node_modules/tiny-mcp-client/src/internal.ts +279 -77
  138. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
  139. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
  140. package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
  141. package/package.json +10 -12
  142. package/node_modules/@poe-code/file-lock/README.md +0 -52
  143. package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
  144. package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
  145. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
  146. package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
  147. package/node_modules/@poe-code/file-lock/package.json +0 -23
@@ -183,6 +183,39 @@ const getMessageLayerOrThrow = (client: McpClient): JsonRpcMessageLayer =>
183
183
  }
184
184
  ).getMessageLayerOrThrow();
185
185
 
186
+ async function startClientHandshake(
187
+ result: unknown,
188
+ options: ConstructorParameters<typeof McpClient>[0] = {
189
+ clientInfo: { name: "tiny-mcp-client", version: "0.1.0" },
190
+ }
191
+ ): Promise<{
192
+ client: McpClient;
193
+ readable: PassThrough;
194
+ writable: PassThrough;
195
+ iterator: AsyncIterator<string>;
196
+ connectPromise: Promise<unknown>;
197
+ }> {
198
+ const readable = new PassThrough();
199
+ const writable = new PassThrough();
200
+ const transport: McpTransport = {
201
+ readable,
202
+ writable,
203
+ closed: new Promise(() => {}),
204
+ dispose: vi.fn(),
205
+ };
206
+ const client = new McpClient(options);
207
+ const connectPromise = client.connect(transport);
208
+ const iterator = readLines(writable)[Symbol.asyncIterator]();
209
+ const initializeLine = await iterator.next();
210
+ if (initializeLine.done) {
211
+ throw new Error("Expected initialize request line to be written");
212
+ }
213
+ const initializeRequest = JSON.parse(initializeLine.value) as { id: number };
214
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", id: initializeRequest.id, result })}\n`);
215
+
216
+ return { client, readable, writable, iterator, connectPromise };
217
+ }
218
+
186
219
  describe("HttpTransport constructor", () => {
187
220
  it("accepts url, headers, and injected fetch", () => {
188
221
  const mockFetch = async (): Promise<Response> => new Response(null, { status: 202 });
@@ -627,6 +660,40 @@ describe("HttpTransport constructor", () => {
627
660
  transport.dispose();
628
661
  });
629
662
 
663
+ it("closes when the GET event stream reports an expired session", async () => {
664
+ const mockFetch = vi.fn(async (_input: string | URL, init?: RequestInit): Promise<Response> => {
665
+ if (init?.method === "GET") {
666
+ return new Response(null, { status: 404 });
667
+ }
668
+
669
+ return new Response(null, { status: 202, headers: { "Mcp-Session-Id": "expired-session" } });
670
+ });
671
+ const transport = new HttpTransport({ url: "https://example.com/mcp", fetch: mockFetch });
672
+
673
+ transport.writable.write('{"jsonrpc":"2.0","id":1,"method":"initialize"}\n');
674
+
675
+ await expect(transport.closed).resolves.toMatchObject({
676
+ reason: expect.objectContaining({ message: expect.stringContaining("session expired") }),
677
+ });
678
+ });
679
+
680
+ it("closes when the GET event stream returns a server error", async () => {
681
+ const mockFetch = vi.fn(async (_input: string | URL, init?: RequestInit): Promise<Response> => {
682
+ if (init?.method === "GET") {
683
+ return new Response("event stream failed", { status: 500 });
684
+ }
685
+
686
+ return new Response(null, { status: 202, headers: { "Mcp-Session-Id": "broken-events" } });
687
+ });
688
+ const transport = new HttpTransport({ url: "https://example.com/mcp", fetch: mockFetch });
689
+
690
+ transport.writable.write('{"jsonrpc":"2.0","id":1,"method":"initialize"}\n');
691
+
692
+ await expect(transport.closed).resolves.toMatchObject({
693
+ reason: expect.objectContaining({ message: expect.stringContaining("GET failed") }),
694
+ });
695
+ });
696
+
630
697
  it("sends DELETE with session ID when disposed after initialization", async () => {
631
698
  const mockFetch = vi.fn(async (_input: string | URL, init?: RequestInit): Promise<Response> => {
632
699
  if (init?.method === "GET") {
@@ -708,6 +775,103 @@ describe("HttpTransport constructor", () => {
708
775
  });
709
776
  });
710
777
 
778
+ it("reports failure when DELETE session termination is rejected", async () => {
779
+ const mockFetch = vi.fn(async (_input: string | URL, init?: RequestInit): Promise<Response> => {
780
+ if (init?.method === "GET") {
781
+ return new Response(null, { status: 405 });
782
+ }
783
+ if (init?.method === "DELETE") {
784
+ return new Response("cleanup refused", { status: 500 });
785
+ }
786
+
787
+ return new Response(null, { status: 202, headers: { "Mcp-Session-Id": "failed-delete" } });
788
+ });
789
+ const transport = new HttpTransport({ url: "https://example.com/mcp", fetch: mockFetch });
790
+
791
+ transport.writable.write('{"jsonrpc":"2.0","id":1,"method":"initialize"}\n');
792
+ await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2));
793
+ transport.dispose();
794
+
795
+ await expect(transport.closed).resolves.toMatchObject({
796
+ reason: expect.objectContaining({ message: expect.stringContaining("DELETE failed") }),
797
+ });
798
+ });
799
+
800
+ it("rejects a changed session id returned mid-session", async () => {
801
+ let postCount = 0;
802
+ const requestSessions: Array<string | null> = [];
803
+ const mockFetch = vi.fn(async (_input: string | URL, init?: RequestInit): Promise<Response> => {
804
+ if (init?.method === "GET") {
805
+ return new Response(null, { status: 405 });
806
+ }
807
+ if (init?.method === "DELETE") {
808
+ return new Response(null, { status: 204 });
809
+ }
810
+ requestSessions.push(new Headers(init?.headers).get("mcp-session-id"));
811
+ postCount += 1;
812
+ return new Response(null, {
813
+ status: 202,
814
+ headers: { "Mcp-Session-Id": postCount === 1 ? "session-original" : "session-replacement" },
815
+ });
816
+ });
817
+ const transport = new HttpTransport({ url: "https://example.com/mcp", fetch: mockFetch });
818
+
819
+ transport.writable.write('{"jsonrpc":"2.0","id":1,"method":"initialize"}\n');
820
+ await vi.waitFor(() => expect(postCount).toBe(1));
821
+ transport.writable.write('{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n');
822
+
823
+ await expect(transport.closed).resolves.toMatchObject({
824
+ reason: expect.objectContaining({ message: expect.stringContaining("session ID") }),
825
+ });
826
+ expect(requestSessions).toEqual([null, "session-original"]);
827
+ });
828
+
829
+ it("closes on a successful unsupported HTTP representation", async () => {
830
+ const transport = new HttpTransport({
831
+ url: "https://example.com/mcp",
832
+ fetch: vi.fn(async () => new Response("not-json", { status: 200, headers: { "Content-Type": "text/plain" } })),
833
+ });
834
+
835
+ transport.writable.write('{"jsonrpc":"2.0","id":1,"method":"ping"}\n');
836
+
837
+ await expect(transport.closed).resolves.toMatchObject({
838
+ reason: expect.objectContaining({ message: expect.stringContaining("unsupported response content type") }),
839
+ });
840
+ });
841
+
842
+ it("does not block a subsequent POST behind an open SSE response", async () => {
843
+ const encoder = new TextEncoder();
844
+ let firstController: ReadableStreamDefaultController<Uint8Array> | undefined;
845
+ let postCount = 0;
846
+ const transport = new HttpTransport({
847
+ url: "https://example.com/mcp",
848
+ fetch: vi.fn(async (_input: string | URL, init?: RequestInit) => {
849
+ if (init?.method !== "POST") {
850
+ return new Response(null, { status: 405 });
851
+ }
852
+ postCount += 1;
853
+ if (postCount === 1) {
854
+ return new Response(new ReadableStream({
855
+ start(controller) {
856
+ firstController = controller;
857
+ controller.enqueue(encoder.encode('data: {"jsonrpc":"2.0","id":1,"result":"first"}\n\n'));
858
+ },
859
+ }), { status: 200, headers: { "Content-Type": "text/event-stream" } });
860
+ }
861
+ return new Response('{"jsonrpc":"2.0","id":2,"result":"second"}', { status: 200, headers: { "Content-Type": "application/json" } });
862
+ }),
863
+ });
864
+ const layer = new JsonRpcMessageLayer(transport.readable, transport.writable, 100, transport.closed.then((event) => event.reason));
865
+
866
+ await expect(layer.sendRequest("first")).resolves.toBe("first");
867
+ await expect(layer.sendRequest("second")).resolves.toBe("second");
868
+ expect(postCount).toBe(2);
869
+
870
+ firstController?.close();
871
+ layer.dispose();
872
+ transport.dispose();
873
+ });
874
+
711
875
  it("aborts in-flight POST fetch when disposed", async () => {
712
876
  let postSignal: AbortSignal | undefined;
713
877
  let postAborted = false;
@@ -1309,6 +1473,37 @@ describe("JsonRpcMessageLayer sendRequest", () => {
1309
1473
  });
1310
1474
  });
1311
1475
 
1476
+ describe("JsonRpcMessageLayer UTF-8 input", () => {
1477
+ it("preserves parameters split across UTF-8 chunks", async () => {
1478
+ const output = new PassThrough();
1479
+ const message = Buffer.from(
1480
+ `${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "echo", params: { text: "🧪" } })}\n`,
1481
+ "utf8"
1482
+ );
1483
+ const markerStart = message.indexOf(Buffer.from("🧪", "utf8"));
1484
+ const input = Readable.from(
1485
+ (async function* () {
1486
+ yield message.subarray(0, markerStart + 2);
1487
+ await Promise.resolve();
1488
+ yield message.subarray(markerStart + 2);
1489
+ })()
1490
+ );
1491
+ trackForCleanup(output);
1492
+ const outputIterator = readLines(output)[Symbol.asyncIterator]();
1493
+ const layer = new JsonRpcMessageLayer(input, output);
1494
+ const handler = vi.fn((params: unknown) => ({ params }));
1495
+ layer.onRequest("echo", handler);
1496
+
1497
+ const responseLine = await outputIterator.next();
1498
+ if (responseLine.done) {
1499
+ throw new Error("Expected JSON-RPC response line to be written");
1500
+ }
1501
+
1502
+ expect(handler).toHaveBeenCalledWith({ text: "🧪" }, expect.anything());
1503
+ expect(JSON.parse(responseLine.value)).toMatchObject({ result: { params: { text: "🧪" } } });
1504
+ });
1505
+ });
1506
+
1312
1507
  describe("JsonRpcMessageLayer sendNotification", () => {
1313
1508
  it("writes notification without id and does not create pending request entry", async () => {
1314
1509
  const input = new PassThrough();
@@ -2614,7 +2809,7 @@ describe("StdioTransport real process smoke test", () => {
2614
2809
  expect(response.result.protocolVersion).toBe("2025-03-26");
2615
2810
  expect(response.result.serverInfo).toEqual({
2616
2811
  name: "tiny-stdio-mcp-test-server",
2617
- version: "0.0.1",
2812
+ version: "0.1.0",
2618
2813
  });
2619
2814
  expect(response.result.capabilities.tools.listChanged).toBe(true);
2620
2815
  } finally {
@@ -2827,6 +3022,51 @@ describe("McpClient state guards", () => {
2827
3022
  });
2828
3023
 
2829
3024
  describe("McpClient connect", () => {
3025
+ it("releases a transport after a rejected initialize response", async () => {
3026
+ const firstReadable = new PassThrough();
3027
+ const firstWritable = new PassThrough();
3028
+ const firstTransport: McpTransport = {
3029
+ readable: firstReadable,
3030
+ writable: firstWritable,
3031
+ closed: new Promise(() => {}),
3032
+ dispose: vi.fn(),
3033
+ };
3034
+ const client = new McpClient({
3035
+ clientInfo: { name: "tiny-mcp-client", version: "0.1.0" },
3036
+ });
3037
+
3038
+ const firstConnect = client.connect(firstTransport);
3039
+ const firstIterator = readLines(firstWritable)[Symbol.asyncIterator]();
3040
+ const firstInitializeLine = await firstIterator.next();
3041
+ if (firstInitializeLine.done) {
3042
+ throw new Error("Expected initialize request line to be written");
3043
+ }
3044
+ const firstInitialize = JSON.parse(firstInitializeLine.value) as { id: number };
3045
+ firstReadable.write(`${JSON.stringify({ jsonrpc: "2.0", id: firstInitialize.id, result: { protocolVersion: "2024-11-05", capabilities: {}, serverInfo: { name: "bad", version: "1" } } })}\n`);
3046
+
3047
+ await expect(firstConnect).rejects.toThrow("Unsupported protocol version: 2024-11-05");
3048
+ expect(firstTransport.dispose).toHaveBeenCalledTimes(1);
3049
+ expect(client.state).toBe("disconnected");
3050
+
3051
+ const secondReadable = new PassThrough();
3052
+ const secondWritable = new PassThrough();
3053
+ const secondConnect = client.connect({
3054
+ readable: secondReadable,
3055
+ writable: secondWritable,
3056
+ closed: new Promise(() => {}),
3057
+ dispose: vi.fn(),
3058
+ });
3059
+ const secondIterator = readLines(secondWritable)[Symbol.asyncIterator]();
3060
+ const secondInitializeLine = await secondIterator.next();
3061
+ if (secondInitializeLine.done) {
3062
+ throw new Error("Expected replacement initialize request line to be written");
3063
+ }
3064
+ const secondInitialize = JSON.parse(secondInitializeLine.value) as { id: number };
3065
+ secondReadable.write(`${JSON.stringify({ jsonrpc: "2.0", id: secondInitialize.id, result: { protocolVersion: "2025-03-26", capabilities: {}, serverInfo: { name: "good", version: "1" } } })}\n`);
3066
+ await expect(secondConnect).resolves.toBeDefined();
3067
+ await client.close();
3068
+ });
3069
+
2830
3070
  it("registers notification handlers for all supported server notifications", async () => {
2831
3071
  const readable = new PassThrough();
2832
3072
  const writable = new PassThrough();
@@ -3336,6 +3576,44 @@ describe("McpClient connect", () => {
3336
3576
  await client.close();
3337
3577
  });
3338
3578
 
3579
+ it("does not disclose roots before initialization completes", async () => {
3580
+ const readable = new PassThrough();
3581
+ const writable = new PassThrough();
3582
+ const transport: McpTransport = {
3583
+ readable,
3584
+ writable,
3585
+ closed: new Promise(() => {}),
3586
+ dispose: vi.fn(),
3587
+ };
3588
+ const onRootsList = vi.fn(async () => [{ uri: "file:///secret", name: "secret" }]);
3589
+ const client = new McpClient({
3590
+ clientInfo: { name: "tiny-mcp-client", version: "0.1.0" },
3591
+ onRootsList,
3592
+ });
3593
+
3594
+ const connectPromise = client.connect(transport);
3595
+ const iterator = readLines(writable)[Symbol.asyncIterator]();
3596
+ await iterator.next();
3597
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", id: 779, method: "roots/list" })}\n`);
3598
+
3599
+ const responseLine = await iterator.next();
3600
+ if (responseLine.done) {
3601
+ throw new Error("Expected roots/list rejection line to be written");
3602
+ }
3603
+ expect(JSON.parse(responseLine.value)).toEqual({
3604
+ jsonrpc: "2.0",
3605
+ id: 779,
3606
+ error: {
3607
+ code: ERROR_METHOD_NOT_FOUND,
3608
+ message: "Method not found: roots/list",
3609
+ },
3610
+ });
3611
+ expect(onRootsList).not.toHaveBeenCalled();
3612
+
3613
+ await client.close();
3614
+ await expect(connectPromise).rejects.toThrow("MCP client closed");
3615
+ });
3616
+
3339
3617
  it("returns method-not-found when server sends roots/list and no roots handler is set", async () => {
3340
3618
  const readable = new PassThrough();
3341
3619
  const writable = new PassThrough();
@@ -3408,6 +3686,26 @@ describe("McpClient connect", () => {
3408
3686
  await client.close();
3409
3687
  });
3410
3688
 
3689
+ it("ignores tools/list_changed when the server did not advertise changes", async () => {
3690
+ const onToolsChanged = vi.fn();
3691
+ const { client, readable, iterator, connectPromise } = await startClientHandshake(
3692
+ {
3693
+ protocolVersion: "2025-03-26",
3694
+ capabilities: { tools: {} },
3695
+ serverInfo: { name: "server", version: "1.0.0" },
3696
+ },
3697
+ { clientInfo: { name: "tiny-mcp-client", version: "0.1.0" }, onToolsChanged }
3698
+ );
3699
+ await connectPromise;
3700
+ await iterator.next();
3701
+
3702
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/tools/list_changed" })}\n`);
3703
+ await new Promise<void>((resolve) => setImmediate(resolve));
3704
+
3705
+ expect(onToolsChanged).not.toHaveBeenCalled();
3706
+ await client.close();
3707
+ });
3708
+
3411
3709
  it("calls onToolsChanged when server sends tools/list_changed notification", async () => {
3412
3710
  const readable = new PassThrough();
3413
3711
  const writable = new PassThrough();
@@ -3450,7 +3748,7 @@ describe("McpClient connect", () => {
3450
3748
  id: initializeRequest.id,
3451
3749
  result: {
3452
3750
  protocolVersion: "2025-03-26",
3453
- capabilities: {},
3751
+ capabilities: { tools: { listChanged: true } },
3454
3752
  serverInfo: {
3455
3753
  name: "server",
3456
3754
  version: "1.0.0",
@@ -3666,7 +3964,7 @@ describe("McpClient connect", () => {
3666
3964
  id: initializeRequest.id,
3667
3965
  result: {
3668
3966
  protocolVersion: "2025-03-26",
3669
- capabilities: {},
3967
+ capabilities: { resources: { subscribe: true } },
3670
3968
  serverInfo: {
3671
3969
  name: "server",
3672
3970
  version: "1.0.0",
@@ -3683,6 +3981,15 @@ describe("McpClient connect", () => {
3683
3981
  }
3684
3982
 
3685
3983
  const updatedUri = "file:///workspace/notes.txt";
3984
+ const subscribePromise = client.subscribe(updatedUri);
3985
+ const subscribeLine = await iterator.next();
3986
+ if (subscribeLine.done) {
3987
+ throw new Error("Expected resources/subscribe request line to be written");
3988
+ }
3989
+ const subscribeRequest = JSON.parse(subscribeLine.value) as { id: number };
3990
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", id: subscribeRequest.id, result: {} })}\n`);
3991
+ await subscribePromise;
3992
+
3686
3993
  readable.write(
3687
3994
  `${JSON.stringify({
3688
3995
  jsonrpc: "2.0",
@@ -3701,6 +4008,26 @@ describe("McpClient connect", () => {
3701
4008
  await client.close();
3702
4009
  });
3703
4010
 
4011
+ it("ignores resource updates for unsubscribed uris", async () => {
4012
+ const onResourceUpdated = vi.fn();
4013
+ const { client, readable, iterator, connectPromise } = await startClientHandshake(
4014
+ {
4015
+ protocolVersion: "2025-03-26",
4016
+ capabilities: { resources: { subscribe: true } },
4017
+ serverInfo: { name: "server", version: "1.0.0" },
4018
+ },
4019
+ { clientInfo: { name: "tiny-mcp-client", version: "0.1.0" }, onResourceUpdated }
4020
+ );
4021
+ await connectPromise;
4022
+ await iterator.next();
4023
+
4024
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/resources/updated", params: { uri: "file:///unsubscribed.txt" } })}\n`);
4025
+ await new Promise<void>((resolve) => setImmediate(resolve));
4026
+
4027
+ expect(onResourceUpdated).not.toHaveBeenCalled();
4028
+ await client.close();
4029
+ });
4030
+
3704
4031
  it("calls onProgress with total, progress, and message when server sends progress notification", async () => {
3705
4032
  const readable = new PassThrough();
3706
4033
  const writable = new PassThrough();
@@ -3743,7 +4070,7 @@ describe("McpClient connect", () => {
3743
4070
  id: initializeRequest.id,
3744
4071
  result: {
3745
4072
  protocolVersion: "2025-03-26",
3746
- capabilities: {},
4073
+ capabilities: { tools: {} },
3747
4074
  serverInfo: {
3748
4075
  name: "server",
3749
4076
  version: "1.0.0",
@@ -3765,6 +4092,12 @@ describe("McpClient connect", () => {
3765
4092
  total: 100,
3766
4093
  message: "Halfway there",
3767
4094
  };
4095
+ const callToolPromise = client.callTool({ name: "work" }, { progressToken: expectedProgress.progressToken });
4096
+ const callToolLine = await iterator.next();
4097
+ if (callToolLine.done) {
4098
+ throw new Error("Expected tools/call request line to be written");
4099
+ }
4100
+ const callToolRequest = JSON.parse(callToolLine.value) as { id: number };
3768
4101
  readable.write(
3769
4102
  `${JSON.stringify({
3770
4103
  jsonrpc: "2.0",
@@ -3777,6 +4110,9 @@ describe("McpClient connect", () => {
3777
4110
  expect(onProgress).toHaveBeenCalledTimes(1);
3778
4111
  expect(onProgress).toHaveBeenCalledWith(expectedProgress);
3779
4112
 
4113
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", id: callToolRequest.id, result: { content: [] } })}\n`);
4114
+ await callToolPromise;
4115
+
3780
4116
  await client.close();
3781
4117
  });
3782
4118
 
@@ -3822,7 +4158,7 @@ describe("McpClient connect", () => {
3822
4158
  id: initializeRequest.id,
3823
4159
  result: {
3824
4160
  protocolVersion: "2025-03-26",
3825
- capabilities: {},
4161
+ capabilities: { tools: {} },
3826
4162
  serverInfo: {
3827
4163
  name: "server",
3828
4164
  version: "1.0.0",
@@ -3843,6 +4179,12 @@ describe("McpClient connect", () => {
3843
4179
  progress: 25,
3844
4180
  message: "Started processing",
3845
4181
  };
4182
+ const callToolPromise = client.callTool({ name: "work" }, { progressToken: expectedProgress.progressToken });
4183
+ const callToolLine = await iterator.next();
4184
+ if (callToolLine.done) {
4185
+ throw new Error("Expected tools/call request line to be written");
4186
+ }
4187
+ const callToolRequest = JSON.parse(callToolLine.value) as { id: number };
3846
4188
  readable.write(
3847
4189
  `${JSON.stringify({
3848
4190
  jsonrpc: "2.0",
@@ -3855,6 +4197,9 @@ describe("McpClient connect", () => {
3855
4197
  expect(onProgress).toHaveBeenCalledTimes(1);
3856
4198
  expect(onProgress).toHaveBeenCalledWith(expectedProgress);
3857
4199
 
4200
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", id: callToolRequest.id, result: { content: [] } })}\n`);
4201
+ await callToolPromise;
4202
+
3858
4203
  await client.close();
3859
4204
  });
3860
4205
 
@@ -3925,7 +4270,7 @@ describe("McpClient connect", () => {
3925
4270
  id: initializeRequest.id,
3926
4271
  result: {
3927
4272
  protocolVersion: "2025-03-26",
3928
- capabilities: {},
4273
+ capabilities: { tools: {} },
3929
4274
  serverInfo: {
3930
4275
  name: "server",
3931
4276
  version: "1.0.0",
@@ -3941,6 +4286,13 @@ describe("McpClient connect", () => {
3941
4286
  throw new Error("Expected initialized notification line to be written");
3942
4287
  }
3943
4288
 
4289
+ const callToolPromise = client.callTool({ name: "work" }, { progressToken: "call-3" });
4290
+ const callToolLine = await iterator.next();
4291
+ if (callToolLine.done) {
4292
+ throw new Error("Expected tools/call request line to be written");
4293
+ }
4294
+ const callToolRequest = JSON.parse(callToolLine.value) as { id: number };
4295
+
3944
4296
  for (const progressUpdate of expectedProgressUpdates) {
3945
4297
  readable.write(
3946
4298
  `${JSON.stringify({
@@ -3957,6 +4309,29 @@ describe("McpClient connect", () => {
3957
4309
  expect(onProgress).toHaveBeenNthCalledWith(2, expectedProgressUpdates[1]);
3958
4310
  expect(onProgress).toHaveBeenNthCalledWith(3, expectedProgressUpdates[2]);
3959
4311
 
4312
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", id: callToolRequest.id, result: { content: [] } })}\n`);
4313
+ await callToolPromise;
4314
+
4315
+ await client.close();
4316
+ });
4317
+
4318
+ it("ignores progress notifications for an unknown token", async () => {
4319
+ const onProgress = vi.fn();
4320
+ const { client, readable, iterator, connectPromise } = await startClientHandshake(
4321
+ {
4322
+ protocolVersion: "2025-03-26",
4323
+ capabilities: {},
4324
+ serverInfo: { name: "server", version: "1.0.0" },
4325
+ },
4326
+ { clientInfo: { name: "tiny-mcp-client", version: "0.1.0" }, onProgress }
4327
+ );
4328
+ await connectPromise;
4329
+ await iterator.next();
4330
+
4331
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/progress", params: { progressToken: "unknown", progress: 50 } })}\n`);
4332
+ await new Promise<void>((resolve) => setImmediate(resolve));
4333
+
4334
+ expect(onProgress).not.toHaveBeenCalled();
3960
4335
  await client.close();
3961
4336
  });
3962
4337
 
@@ -4836,6 +5211,29 @@ describe("McpClient connect", () => {
4836
5211
  expect(client.serverInfo).toBeNull();
4837
5212
  expect(client.instructions).toBeUndefined();
4838
5213
  });
5214
+
5215
+ it("rejects malformed initialize server identity", async () => {
5216
+ const { client, connectPromise } = await startClientHandshake({
5217
+ protocolVersion: "2025-03-26",
5218
+ capabilities: {},
5219
+ serverInfo: { name: "server", version: 7 },
5220
+ });
5221
+
5222
+ await expect(connectPromise).rejects.toThrow("Invalid initialize result");
5223
+ expect(client.state).not.toBe("ready");
5224
+ expect(client.serverInfo).toBeNull();
5225
+ });
5226
+
5227
+ it("rejects null initialize capabilities", async () => {
5228
+ const { client, connectPromise } = await startClientHandshake({
5229
+ protocolVersion: "2025-03-26",
5230
+ capabilities: null,
5231
+ serverInfo: { name: "server", version: "1.0.0" },
5232
+ });
5233
+
5234
+ await expect(connectPromise).rejects.toThrow("Invalid initialize result");
5235
+ expect(client.state).not.toBe("ready");
5236
+ });
4839
5237
  });
4840
5238
 
4841
5239
  describe("McpClient listTools", () => {
@@ -5023,6 +5421,27 @@ describe("McpClient listTools", () => {
5023
5421
  nextCursor: "10",
5024
5422
  });
5025
5423
  });
5424
+
5425
+ it("rejects a numeric nextCursor returned from tools/list", async () => {
5426
+ const { client, readable, iterator, connectPromise } = await startClientHandshake({
5427
+ protocolVersion: "2025-03-26",
5428
+ capabilities: { tools: {} },
5429
+ serverInfo: { name: "server", version: "1.0.0" },
5430
+ });
5431
+ await connectPromise;
5432
+ await iterator.next();
5433
+
5434
+ const requestPromise = client.listTools();
5435
+ const requestLine = await iterator.next();
5436
+ if (requestLine.done) {
5437
+ throw new Error("Expected tools/list request line to be written");
5438
+ }
5439
+ const request = JSON.parse(requestLine.value) as { id: number };
5440
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { tools: [], nextCursor: 7 } })}\n`);
5441
+
5442
+ await expect(requestPromise).rejects.toThrow("Invalid tools/list result");
5443
+ await client.close();
5444
+ });
5026
5445
  });
5027
5446
 
5028
5447
  describe("McpClient listResources", () => {
@@ -6844,6 +7263,46 @@ describe("McpClient callTool", () => {
6844
7263
 
6845
7264
  await client.close();
6846
7265
  });
7266
+
7267
+ it("rejects malformed successful tool results", async () => {
7268
+ const { client, readable, iterator, connectPromise } = await startClientHandshake({
7269
+ protocolVersion: "2025-03-26",
7270
+ capabilities: { tools: {} },
7271
+ serverInfo: { name: "server", version: "1.0.0" },
7272
+ });
7273
+ await connectPromise;
7274
+ await iterator.next();
7275
+
7276
+ const requestPromise = client.callTool({ name: "bad", arguments: {} });
7277
+ const requestLine = await iterator.next();
7278
+ if (requestLine.done) {
7279
+ throw new Error("Expected tools/call request line to be written");
7280
+ }
7281
+ const request = JSON.parse(requestLine.value) as { id: number };
7282
+ readable.write(`${JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text" }] } })}\n`);
7283
+
7284
+ await expect(requestPromise).rejects.toThrow("Invalid tool result");
7285
+ await client.close();
7286
+ });
7287
+
7288
+ it("does not dispatch a pre-aborted tool call", async () => {
7289
+ const { client, writable, iterator, connectPromise } = await startClientHandshake({
7290
+ protocolVersion: "2025-03-26",
7291
+ capabilities: { tools: {} },
7292
+ serverInfo: { name: "server", version: "1.0.0" },
7293
+ });
7294
+ await connectPromise;
7295
+ await iterator.next();
7296
+ const controller = new AbortController();
7297
+ controller.abort("already cancelled");
7298
+
7299
+ await expect(client.callTool({ name: "echo" }, { signal: controller.signal })).rejects.toBe(
7300
+ "already cancelled"
7301
+ );
7302
+ await new Promise<void>((resolve) => setImmediate(resolve));
7303
+ expect(writable.readableLength).toBe(0);
7304
+ await client.close();
7305
+ });
6847
7306
  });
6848
7307
 
6849
7308
  describe("McpClient setLogLevel", () => {
@@ -6941,6 +7400,11 @@ describe("McpClient sendRootsChanged", () => {
6941
7400
  name: "tiny-mcp-client",
6942
7401
  version: "0.1.0",
6943
7402
  },
7403
+ capabilities: {
7404
+ roots: {
7405
+ listChanged: true,
7406
+ },
7407
+ },
6944
7408
  });
6945
7409
 
6946
7410
  const connectPromise = client.connect(transport);
@@ -6985,6 +7449,20 @@ describe("McpClient sendRootsChanged", () => {
6985
7449
  method: "notifications/roots/list_changed",
6986
7450
  });
6987
7451
  });
7452
+
7453
+ it("rejects when roots list changes were not advertised", async () => {
7454
+ const { client, connectPromise } = await startClientHandshake({
7455
+ protocolVersion: "2025-03-26",
7456
+ capabilities: {},
7457
+ serverInfo: { name: "server", version: "1.0.0" },
7458
+ });
7459
+ await connectPromise;
7460
+
7461
+ await expect(client.sendRootsChanged()).rejects.toThrow(
7462
+ "Client did not advertise roots list changes"
7463
+ );
7464
+ await client.close();
7465
+ });
6988
7466
  });
6989
7467
 
6990
7468
  describe("McpClient cancel", () => {
@@ -7521,6 +7999,46 @@ describe("McpClient ping", () => {
7521
7999
  }
7522
8000
  });
7523
8001
 
8002
+ it("uses the configured request timeout for client requests", async () => {
8003
+ vi.useFakeTimers();
8004
+
8005
+ try {
8006
+ const readable = new PassThrough();
8007
+ const writable = new PassThrough();
8008
+ const transport: McpTransport = {
8009
+ readable,
8010
+ writable,
8011
+ closed: new Promise(() => {}),
8012
+ dispose: vi.fn(),
8013
+ };
8014
+ const client = new McpClient({
8015
+ clientInfo: {
8016
+ name: "tiny-mcp-client",
8017
+ version: "0.1.0",
8018
+ },
8019
+ requestTimeoutMs: 42_000,
8020
+ });
8021
+
8022
+ const connectPromise = client.connect(transport);
8023
+ const iterator = readLines(writable)[Symbol.asyncIterator]();
8024
+ const initializeLineResult = await iterator.next();
8025
+ if (initializeLineResult.done) {
8026
+ throw new Error("Expected initialize request line to be written");
8027
+ }
8028
+
8029
+ const timeoutPromise = expect(connectPromise).rejects.toThrow(
8030
+ 'JSON-RPC request "initialize" timed out after 42000ms'
8031
+ );
8032
+
8033
+ await vi.advanceTimersByTimeAsync(42_000);
8034
+
8035
+ await timeoutPromise;
8036
+ await client.close();
8037
+ } finally {
8038
+ vi.useRealTimers();
8039
+ }
8040
+ });
8041
+
7524
8042
  it("responds with an empty object when the server sends a ping request", async () => {
7525
8043
  const readable = new PassThrough();
7526
8044
  const writable = new PassThrough();
@@ -7688,6 +8206,47 @@ describe("McpClient close", () => {
7688
8206
  expect(secondTransport.dispose).toHaveBeenCalledTimes(1);
7689
8207
  });
7690
8208
 
8209
+ it("does not use previous capabilities while reconnecting", async () => {
8210
+ const client = new McpClient({
8211
+ clientInfo: { name: "tiny-mcp-client", version: "0.1.0" },
8212
+ });
8213
+ const firstReadable = new PassThrough();
8214
+ const firstWritable = new PassThrough();
8215
+ const firstConnect = client.connect({
8216
+ readable: firstReadable,
8217
+ writable: firstWritable,
8218
+ closed: new Promise(() => {}),
8219
+ dispose: vi.fn(),
8220
+ });
8221
+ const firstIterator = readLines(firstWritable)[Symbol.asyncIterator]();
8222
+ const firstInitializeLine = await firstIterator.next();
8223
+ if (firstInitializeLine.done) {
8224
+ throw new Error("Expected first initialize request line to be written");
8225
+ }
8226
+ const firstInitialize = JSON.parse(firstInitializeLine.value) as { id: number };
8227
+ firstReadable.write(`${JSON.stringify({ jsonrpc: "2.0", id: firstInitialize.id, result: { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "first", version: "1" } } })}\n`);
8228
+ await firstConnect;
8229
+ await client.close();
8230
+
8231
+ const secondReadable = new PassThrough();
8232
+ const secondWritable = new PassThrough();
8233
+ const secondConnect = client.connect({
8234
+ readable: secondReadable,
8235
+ writable: secondWritable,
8236
+ closed: new Promise(() => {}),
8237
+ dispose: vi.fn(),
8238
+ });
8239
+ const secondIterator = readLines(secondWritable)[Symbol.asyncIterator]();
8240
+ await secondIterator.next();
8241
+
8242
+ expect(client.serverCapabilities).toBeNull();
8243
+ await expect(client.listTools()).rejects.toThrow("MCP client has not completed initialization");
8244
+ expect(secondWritable.readableLength).toBe(0);
8245
+
8246
+ await client.close();
8247
+ await expect(secondConnect).rejects.toThrow("MCP client closed");
8248
+ });
8249
+
7691
8250
  it("rejects connect when closed immediately before initialize handshake completes", async () => {
7692
8251
  const readable = new PassThrough();
7693
8252
  const writable = new PassThrough();
@@ -8058,6 +8617,29 @@ describe("McpClient capability gating", () => {
8058
8617
  }
8059
8618
  });
8060
8619
 
8620
+ it("listTools throws when server advertises null tools capability", async () => {
8621
+ const { connectPromise } = await startClientHandshake({
8622
+ protocolVersion: "2025-03-26",
8623
+ capabilities: { tools: null },
8624
+ serverInfo: { name: "server", version: "1.0.0" },
8625
+ });
8626
+
8627
+ await expect(connectPromise).rejects.toThrow("Invalid initialize result");
8628
+ });
8629
+
8630
+ it("does not authorize subscriptions after exposed capabilities are mutated", async () => {
8631
+ const { client, closeClient } = await createConnectedClient({ resources: {} });
8632
+
8633
+ try {
8634
+ (client.serverCapabilities as { resources: { subscribe?: boolean } }).resources.subscribe = true;
8635
+ await expect(client.subscribe("file:///readme.txt")).rejects.toThrow(
8636
+ "Server does not support resource subscriptions"
8637
+ );
8638
+ } finally {
8639
+ await closeClient();
8640
+ }
8641
+ });
8642
+
8061
8643
  it("listResources throws when server has no resources capability", async () => {
8062
8644
  const { client, closeClient } = await createConnectedClient({});
8063
8645