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.
- package/README.md +2 -2
- package/dist/cli.compile-check.js +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +50 -13
- package/dist/error-report.js +32 -3
- package/dist/human-in-loop/approval-tasks.d.ts +1 -0
- package/dist/human-in-loop/approval-tasks.js +7 -5
- package/dist/human-in-loop/approvals-commands.js +51 -8
- package/dist/human-in-loop/runner.js +24 -19
- package/dist/human-in-loop/state-machine.d.ts +3 -3
- package/dist/human-in-loop/state-machine.js +13 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -1
- package/dist/mcp-proxy.js +85 -19
- package/dist/mcp.compile-check.js +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +50 -8
- package/dist/renderer.js +119 -13
- package/dist/sdk.compile-check.js +1 -0
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +56 -11
- package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
- package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
- package/node_modules/@poe-code/agent-defs/package.json +1 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
- package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
- package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
- package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
- package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
- package/node_modules/@poe-code/config-mutations/package.json +1 -1
- package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
- package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
- package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
- package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
- package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
- package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
- package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/design-system/dist/index.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
- package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
- package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
- package/node_modules/@poe-code/design-system/package.json +2 -1
- package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +377 -130
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +78 -10
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +21 -5
- package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
- package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +6 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +61 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +503 -0
- package/node_modules/@poe-code/process-runner/package.json +1 -1
- package/node_modules/@poe-code/task-list/README.md +0 -2
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
- package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
- package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/task-list/dist/index.js +2 -0
- package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/move.js +215 -0
- package/node_modules/@poe-code/task-list/dist/open.js +3 -4
- package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
- package/node_modules/@poe-code/task-list/dist/state.js +9 -0
- package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
- package/node_modules/@poe-code/task-list/package.json +1 -2
- package/node_modules/auth-store/dist/create-secret-store.js +4 -1
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +8 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +104 -8
- package/node_modules/auth-store/dist/index.d.ts +1 -1
- package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
- package/node_modules/auth-store/dist/keychain-store.js +18 -16
- package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
- package/node_modules/auth-store/dist/provider-store.js +55 -7
- package/node_modules/auth-store/dist/types.d.ts +3 -1
- package/node_modules/auth-store/package.json +2 -1
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
- package/node_modules/mcp-oauth/package.json +1 -0
- package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +9 -4
- package/node_modules/tiny-mcp-client/dist/internal.js +244 -66
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
- package/node_modules/tiny-mcp-client/package.json +2 -1
- package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
- package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
- package/node_modules/tiny-mcp-client/src/internal.ts +287 -76
- package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
- package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
- package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
- package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
- package/package.json +10 -12
- package/node_modules/@poe-code/file-lock/README.md +0 -52
- package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
- package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
- package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
- package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
- 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
|
|
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
|
|