summaryrefslogtreecommitdiffhomepage
path: root/packages/mcp/src/rpc.ts
blob: eef8d739374a9784fd43b2e4eb32d11a723a606f (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
/**
 * JSON-RPC 2.0 client over an injected write function.
 *
 * Provides request (correlated by id), notify, and onNotification.
 * The caller feeds decoded JSON messages via `handleMessage`.
 */

import { encode } from "./framing.js";

export type WriteFn = (bytes: Uint8Array) => void;

export interface PendingRequest {
	readonly resolve: (value: unknown) => void;
	readonly reject: (reason: unknown) => void;
}

export type NotificationHandler = (params: unknown) => void;

export interface JsonRpcMessage {
	readonly jsonrpc: "2.0";
	readonly id?: number | string | undefined;
	readonly method?: string | undefined;
	readonly params?: unknown;
	readonly result?: unknown;
	readonly error?:
		| { readonly code: number; readonly message: string; readonly data?: unknown }
		| undefined;
}

export class JsonRpcClient {
	private nextId = 1;
	private pending = new Map<number | string, PendingRequest>();
	private notificationHandlers = new Map<string, NotificationHandler>();
	private write: WriteFn;
	private closed = false;

	constructor(write: WriteFn) {
		this.write = write;
	}

	request(method: string, params?: unknown): Promise<unknown> {
		if (this.closed) {
			return Promise.reject(new Error("Connection closed"));
		}
		const id = this.nextId++;
		const msg: JsonRpcMessage = { jsonrpc: "2.0", id, method, params };
		return new Promise((resolve, reject) => {
			this.pending.set(id, { resolve, reject });
			this.sendMessage(msg);
		});
	}

	notify(method: string, params?: unknown): void {
		if (this.closed) return;
		const msg: JsonRpcMessage = { jsonrpc: "2.0", method, params };
		this.sendMessage(msg);
	}

	onNotification(method: string, handler: NotificationHandler): void {
		this.notificationHandlers.set(method, handler);
	}

	handleMessage(json: string): void {
		const msg = JSON.parse(json) as JsonRpcMessage;
		const { id, method } = msg;

		if (method !== undefined && id === undefined) {
			this.handleIncomingNotification(method, msg.params);
		} else if (id !== undefined) {
			this.handleResponse(id, msg);
		}
	}

	private sendMessage(msg: JsonRpcMessage): void {
		this.write(encode(JSON.stringify(msg)));
	}

	private handleResponse(id: number | string, msg: JsonRpcMessage): void {
		const entry = this.pending.get(id);
		if (!entry) return;
		this.pending.delete(id);
		if (msg.error) {
			entry.reject(new Error(msg.error.message));
		} else {
			entry.resolve(msg.result);
		}
	}

	private handleIncomingNotification(method: string, params: unknown): void {
		const handler = this.notificationHandlers.get(method);
		if (handler) {
			handler(params);
		}
	}

	close(): void {
		this.closed = true;
		for (const entry of this.pending.values()) {
			entry.reject(new Error("Connection closed"));
		}
		this.pending.clear();
	}
}