summaryrefslogtreecommitdiffhomepage
path: root/packages/mcp/src/rpc.test.ts
blob: 9ffb121d6cda398888a176933ad2033fa4978f30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { describe, expect, it } from "vitest";
import { JsonRpcClient } from "./rpc.js";

function makeClient(): {
	client: JsonRpcClient;
	written: Uint8Array[];
	feedMessage: (msg: unknown) => void;
} {
	const written: Uint8Array[] = [];
	const client = new JsonRpcClient((bytes) => {
		written.push(bytes);
	});
	return {
		client,
		written,
		feedMessage: (msg: unknown) => {
			client.handleMessage(JSON.stringify(msg));
		},
	};
}

describe("JsonRpcClient", () => {
	it("request returns result", async () => {
		const { client, feedMessage } = makeClient();

		const resultPromise = client.request("initialize", { protocolVersion: "2025-11-25" });

		feedMessage({ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2025-11-25" } });

		const result = await resultPromise;
		expect(result).toEqual({ protocolVersion: "2025-11-25" });
	});

	it("request rejects on error response", async () => {
		const { client, feedMessage } = makeClient();

		const resultPromise = client.request("bad-method");

		feedMessage({
			jsonrpc: "2.0",
			id: 1,
			error: { code: -32601, message: "Method not found" },
		});

		await expect(resultPromise).rejects.toThrow("Method not found");
	});

	it("notify sends without expecting response", () => {
		const { client, written } = makeClient();

		client.notify("notifications/initialized", {});

		expect(written.length).toBe(1);
		const sent = new TextDecoder().decode(written[0]);
		expect(sent).toContain('"method":"notifications/initialized"');
		expect(sent).not.toContain('"id"');
	});

	it("onNotification fires for matching method", () => {
		const { client, feedMessage } = makeClient();

		let received: unknown = "unset";
		client.onNotification("notifications/tools/list_changed", (params) => {
			received = params;
		});

		feedMessage({ jsonrpc: "2.0", method: "notifications/tools/list_changed", params: { a: 1 } });

		expect(received).toEqual({ a: 1 });
	});

	it("pending request rejected on close", async () => {
		const { client } = makeClient();

		const resultPromise = client.request("slow-method");

		client.close();

		await expect(resultPromise).rejects.toThrow("Connection closed");
	});

	it("incremental request ids", () => {
		const { client, written } = makeClient();

		client.request("a");
		client.request("b");
		client.request("c");

		expect(written.length).toBe(3);
		// Extract JSON body from Content-Length framed messages
		const parse = (bytes: Uint8Array): { id: number } => {
			const text = new TextDecoder().decode(bytes);
			const bodyStart = text.indexOf("\r\n\r\n") + 4;
			return JSON.parse(text.slice(bodyStart)) as { id: number };
		};
		const msg1 = parse(written[0]);
		const msg2 = parse(written[1]);
		const msg3 = parse(written[2]);
		expect(msg1.id).toBe(1);
		expect(msg2.id).toBe(2);
		expect(msg3.id).toBe(3);
	});

	it("notify after close is silently dropped", () => {
		const { client, written } = makeClient();
		client.close();
		const count = written.length;
		client.notify("test");
		expect(written.length).toBe(count);
	});

	it("request after close rejects immediately", async () => {
		const { client } = makeClient();
		client.close();
		await expect(client.request("test")).rejects.toThrow("Connection closed");
	});
});