summaryrefslogtreecommitdiffhomepage
path: root/packages/mcp/src/client.ts
blob: 17463d95eb247672f583ecddc88fd39e56ed3d50 (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
118
119
120
121
122
123
/**
 * McpClient — MCP protocol client.
 *
 * Manages a single MCP server connection: initialize handshake,
 * tool discovery, tool invocation, and list_changed notifications.
 */

import type { Connection } from "./transport.js";
import type {
	McpCallResult,
	McpInitializeResult,
	McpListToolsResult,
	McpServerCapabilities,
	McpToolInfo,
} from "./types.js";

export type McpClientState = "disconnected" | "connecting" | "connected" | "error";

export interface McpClientDeps {
	readonly connection: Connection;
}

export class McpClient {
	private state: McpClientState = "disconnected";
	private capabilities: McpServerCapabilities = {};
	private tools: readonly McpToolInfo[] = [];
	private connection: Connection;
	private toolsChangedHandler: (() => void) | null = null;

	constructor(deps: McpClientDeps) {
		this.connection = deps.connection;
	}

	getState(): McpClientState {
		return this.state;
	}

	getCapabilities(): McpServerCapabilities {
		return this.capabilities;
	}

	getTools(): readonly McpToolInfo[] {
		return this.tools;
	}

	onToolsChanged(handler: () => void): void {
		this.toolsChangedHandler = handler;
	}

	async initialize(): Promise<McpInitializeResult> {
		this.state = "connecting";
		try {
			const result = (await this.connection.send("initialize", {
				protocolVersion: "2025-11-25",
				capabilities: {},
				clientInfo: { name: "dispatch", version: "0.0.0" },
			})) as McpInitializeResult;

			this.capabilities = result.capabilities;
			this.connection.notify("notifications/initialized", {});

			this.connection.onNotification("notifications/tools/list_changed", () => {
				if (this.toolsChangedHandler) {
					this.toolsChangedHandler();
				}
			});

			this.state = "connected";
			return result;
		} catch (err: unknown) {
			this.state = "error";
			throw err;
		}
	}

	async listTools(): Promise<readonly McpToolInfo[]> {
		if (this.state !== "connected") {
			throw new Error("Client not connected");
		}
		const result = (await this.connection.send("tools/list")) as McpListToolsResult;
		this.tools = result.tools;
		return this.tools;
	}

	async callTool(name: string, args: unknown, signal?: AbortSignal): Promise<McpCallResult> {
		if (this.state !== "connected") {
			throw new Error("Client not connected");
		}

		if (signal?.aborted) {
			throw new Error("Aborted");
		}

		const resultPromise = this.connection.send("tools/call", {
			name,
			arguments: args,
		}) as Promise<McpCallResult>;

		if (!signal) {
			return resultPromise;
		}

		return new Promise<McpCallResult>((resolve, reject) => {
			const onAbort = () => reject(new Error("Aborted"));
			signal.addEventListener("abort", onAbort, { once: true });
			resultPromise.then(
				(result) => {
					signal.removeEventListener("abort", onAbort);
					resolve(result);
				},
				(err) => {
					signal.removeEventListener("abort", onAbort);
					reject(err);
				},
			);
		});
	}

	close(): void {
		this.state = "disconnected";
		this.connection.close();
	}
}