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();
}
}
|