@johpaz/hive-mcp 0.1.1
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/package.json +25 -0
- package/src/config.ts +12 -0
- package/src/index.ts +1 -0
- package/src/logger.ts +40 -0
- package/src/manager.ts +325 -0
- package/src/transports/index.ts +51 -0
- package/src/transports/sse.ts +59 -0
- package/src/transports/websocket.ts +70 -0
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@johpaz/hive-mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Hive MCP Client — Model Context Protocol client layer",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "bun test"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "latest",
|
|
15
|
+
"zod": "latest"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "latest",
|
|
19
|
+
"@types/bun": "latest"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./src/index.ts",
|
|
23
|
+
"./transports": "./src/transports/index.ts"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface MCPConfig {
|
|
2
|
+
servers?: Record<string, MCPServerConfig>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface MCPServerConfig {
|
|
6
|
+
transport: "stdio" | "sse" | "websocket";
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
command?: string;
|
|
9
|
+
args?: string[];
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
url?: string;
|
|
12
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./manager.ts";
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
|
|
3
|
+
class Logger {
|
|
4
|
+
private context: string;
|
|
5
|
+
private level: LogLevel = "info";
|
|
6
|
+
|
|
7
|
+
constructor(context: string) {
|
|
8
|
+
this.context = context;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private log(level: LogLevel, message: string, data?: Record<string, unknown>): void {
|
|
12
|
+
const levels: LogLevel[] = ["debug", "info", "warn", "error"];
|
|
13
|
+
if (levels.indexOf(level) < levels.indexOf(this.level)) return;
|
|
14
|
+
|
|
15
|
+
const timestamp = new Date().toISOString();
|
|
16
|
+
const prefix = `[${timestamp}] [${level.toUpperCase().padEnd(5)}] [${this.context}]`;
|
|
17
|
+
|
|
18
|
+
if (data) {
|
|
19
|
+
console.log(`${prefix} ${message}`, JSON.stringify(data));
|
|
20
|
+
} else {
|
|
21
|
+
console.log(`${prefix} ${message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
debug(message: string, data?: Record<string, unknown>): void { this.log("debug", message, data); }
|
|
26
|
+
info(message: string, data?: Record<string, unknown>): void { this.log("info", message, data); }
|
|
27
|
+
warn(message: string, data?: Record<string, unknown>): void { this.log("warn", message, data); }
|
|
28
|
+
error(message: string, data?: Record<string, unknown>): void { this.log("error", message, data); }
|
|
29
|
+
|
|
30
|
+
child(context: string): Logger {
|
|
31
|
+
return new Logger(`${this.context}:${context}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setLevel(level: LogLevel): void {
|
|
35
|
+
this.level = level;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const logger = new Logger("mcp");
|
|
40
|
+
export type { LogLevel };
|
package/src/manager.ts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
import type { MCPConfig, MCPServerConfig } from "./config.ts";
|
|
4
|
+
import { logger } from "./logger.ts";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
createTransport,
|
|
8
|
+
type TransportType,
|
|
9
|
+
} from "./transports/index.ts";
|
|
10
|
+
|
|
11
|
+
export interface MCPTool {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
inputSchema: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MCPResource {
|
|
18
|
+
uri: string;
|
|
19
|
+
name: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
mimeType?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MCPPrompt {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
arguments?: Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
required?: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface MCPServerState {
|
|
35
|
+
name: string;
|
|
36
|
+
config: MCPServerConfig;
|
|
37
|
+
client: Client | null;
|
|
38
|
+
transport: Transport | null;
|
|
39
|
+
status: "connected" | "disconnected" | "error" | "connecting";
|
|
40
|
+
tools: MCPTool[];
|
|
41
|
+
resources: MCPResource[];
|
|
42
|
+
prompts: MCPPrompt[];
|
|
43
|
+
reconnectAttempts: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class MCPClientManager {
|
|
47
|
+
private servers: Map<string, MCPServerState> = new Map();
|
|
48
|
+
private config: MCPConfig;
|
|
49
|
+
private log = logger.child("mcp");
|
|
50
|
+
|
|
51
|
+
constructor(config: MCPConfig) {
|
|
52
|
+
this.config = config;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async initialize(): Promise<void> {
|
|
56
|
+
const servers = this.config.servers ?? {};
|
|
57
|
+
|
|
58
|
+
for (const [name, serverConfig] of Object.entries(servers)) {
|
|
59
|
+
if (serverConfig.enabled !== false) {
|
|
60
|
+
this.servers.set(name, {
|
|
61
|
+
name,
|
|
62
|
+
config: serverConfig as MCPServerConfig,
|
|
63
|
+
client: null,
|
|
64
|
+
transport: null,
|
|
65
|
+
status: "disconnected",
|
|
66
|
+
tools: [],
|
|
67
|
+
resources: [],
|
|
68
|
+
prompts: [],
|
|
69
|
+
reconnectAttempts: 0,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.log.info(`MCP Client initialized with ${this.servers.size} servers`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private expandPath(p: string): string {
|
|
78
|
+
if (p.startsWith("~")) {
|
|
79
|
+
return path.join(process.env.HOME ?? "", p.slice(1));
|
|
80
|
+
}
|
|
81
|
+
return p;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private createTransportForServer(state: MCPServerState): Transport {
|
|
85
|
+
const transportType = state.config.transport as TransportType;
|
|
86
|
+
|
|
87
|
+
switch (transportType) {
|
|
88
|
+
case "stdio": {
|
|
89
|
+
const command = state.config.command ?? "npx";
|
|
90
|
+
const args = state.config.args ?? [];
|
|
91
|
+
|
|
92
|
+
const env: Record<string, string> = {};
|
|
93
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
94
|
+
if (value !== undefined) {
|
|
95
|
+
env[key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (state.config.env) {
|
|
99
|
+
for (const [key, value] of Object.entries(state.config.env)) {
|
|
100
|
+
env[key] = this.expandPath(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return createTransport({
|
|
105
|
+
type: "stdio",
|
|
106
|
+
stdio: { command, args, env },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case "sse": {
|
|
111
|
+
const url = state.config.url;
|
|
112
|
+
if (!url) {
|
|
113
|
+
throw new Error("SSE transport requires 'url' config");
|
|
114
|
+
}
|
|
115
|
+
return createTransport({
|
|
116
|
+
type: "sse",
|
|
117
|
+
sse: { url },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "websocket": {
|
|
122
|
+
const url = state.config.url;
|
|
123
|
+
if (!url) {
|
|
124
|
+
throw new Error("WebSocket transport requires 'url' config");
|
|
125
|
+
}
|
|
126
|
+
return createTransport({
|
|
127
|
+
type: "websocket",
|
|
128
|
+
websocket: { url },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Unknown transport type: ${transportType}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async connectServer(name: string): Promise<void> {
|
|
138
|
+
const state = this.servers.get(name);
|
|
139
|
+
if (!state) {
|
|
140
|
+
throw new Error(`MCP server not found: ${name}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (state.status === "connected") {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
state.status = "connecting";
|
|
148
|
+
this.log.info(`Connecting to MCP server: ${name}`);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const transport = this.createTransportForServer(state);
|
|
152
|
+
|
|
153
|
+
const client = new Client(
|
|
154
|
+
{ name: "hive", version: "0.1.0" },
|
|
155
|
+
{ capabilities: {} }
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
await client.connect(transport);
|
|
159
|
+
|
|
160
|
+
state.client = client;
|
|
161
|
+
state.transport = transport;
|
|
162
|
+
state.status = "connected";
|
|
163
|
+
state.reconnectAttempts = 0;
|
|
164
|
+
|
|
165
|
+
await this.discoverCapabilities(name);
|
|
166
|
+
|
|
167
|
+
this.log.info(`Connected to MCP server: ${name}`, {
|
|
168
|
+
tools: state.tools.length,
|
|
169
|
+
resources: state.resources.length,
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
state.status = "error";
|
|
173
|
+
this.log.error(`Failed to connect to MCP server ${name}`, {
|
|
174
|
+
error: (error as Error).message,
|
|
175
|
+
});
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async discoverCapabilities(name: string): Promise<void> {
|
|
181
|
+
const state = this.servers.get(name);
|
|
182
|
+
if (!state?.client) return;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const toolsResult = await state.client.listTools();
|
|
186
|
+
state.tools = (toolsResult.tools ?? []).map((t) => ({
|
|
187
|
+
name: t.name,
|
|
188
|
+
description: t.description ?? "",
|
|
189
|
+
inputSchema: t.inputSchema as Record<string, unknown>,
|
|
190
|
+
}));
|
|
191
|
+
} catch {
|
|
192
|
+
this.log.debug(`No tools from MCP server: ${name}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const resourcesResult = await state.client.listResources();
|
|
197
|
+
state.resources = (resourcesResult.resources ?? []).map((r) => ({
|
|
198
|
+
uri: r.uri,
|
|
199
|
+
name: r.name,
|
|
200
|
+
description: r.description,
|
|
201
|
+
mimeType: r.mimeType,
|
|
202
|
+
}));
|
|
203
|
+
} catch {
|
|
204
|
+
this.log.debug(`No resources from MCP server: ${name}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const promptsResult = await state.client.listPrompts();
|
|
209
|
+
state.prompts = (promptsResult.prompts ?? []).map((p) => ({
|
|
210
|
+
name: p.name,
|
|
211
|
+
description: p.description,
|
|
212
|
+
arguments: p.arguments,
|
|
213
|
+
}));
|
|
214
|
+
} catch {
|
|
215
|
+
this.log.debug(`No prompts from MCP server: ${name}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async disconnectServer(name: string): Promise<void> {
|
|
220
|
+
const state = this.servers.get(name);
|
|
221
|
+
if (!state) return;
|
|
222
|
+
|
|
223
|
+
if (state.client) {
|
|
224
|
+
try {
|
|
225
|
+
await state.client.close();
|
|
226
|
+
} catch {
|
|
227
|
+
// Ignore close errors
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
state.client = null;
|
|
232
|
+
state.transport = null;
|
|
233
|
+
state.status = "disconnected";
|
|
234
|
+
|
|
235
|
+
this.log.info(`Disconnected from MCP server: ${name}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async callTool(
|
|
239
|
+
serverName: string,
|
|
240
|
+
toolName: string,
|
|
241
|
+
args: Record<string, unknown>
|
|
242
|
+
): Promise<unknown> {
|
|
243
|
+
const state = this.servers.get(serverName);
|
|
244
|
+
if (!state?.client) {
|
|
245
|
+
throw new Error(`MCP server not connected: ${serverName}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.log.debug(`Calling MCP tool: ${serverName}/${toolName}`, { args });
|
|
249
|
+
|
|
250
|
+
const result = await state.client.callTool({
|
|
251
|
+
name: toolName,
|
|
252
|
+
arguments: args,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return result.content;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async readResource(serverName: string, uri: string): Promise<unknown> {
|
|
259
|
+
const state = this.servers.get(serverName);
|
|
260
|
+
if (!state?.client) {
|
|
261
|
+
throw new Error(`MCP server not connected: ${serverName}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const result = await state.client.readResource({ uri });
|
|
265
|
+
return result.contents;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
getServerStatus(name: string): MCPServerState["status"] | undefined {
|
|
269
|
+
return this.servers.get(name)?.status;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
getServerTools(name: string): MCPTool[] {
|
|
273
|
+
return this.servers.get(name)?.tools ?? [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
getServerResources(name: string): MCPResource[] {
|
|
277
|
+
return this.servers.get(name)?.resources ?? [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getAllTools(): Map<string, MCPTool[]> {
|
|
281
|
+
const result = new Map<string, MCPTool[]>();
|
|
282
|
+
for (const [name, state] of this.servers) {
|
|
283
|
+
if (state.status === "connected") {
|
|
284
|
+
result.set(name, state.tools);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
listServers(): Array<{ name: string; status: string; tools: number }> {
|
|
291
|
+
return Array.from(this.servers.values()).map((s) => ({
|
|
292
|
+
name: s.name,
|
|
293
|
+
status: s.status,
|
|
294
|
+
tools: s.tools.length,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async connectAll(): Promise<void> {
|
|
299
|
+
const promises: Promise<void>[] = [];
|
|
300
|
+
|
|
301
|
+
for (const name of this.servers.keys()) {
|
|
302
|
+
promises.push(
|
|
303
|
+
this.connectServer(name).catch((error) => {
|
|
304
|
+
this.log.error(`Failed to connect ${name}: ${error.message}`);
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await Promise.allSettled(promises);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async disconnectAll(): Promise<void> {
|
|
313
|
+
const promises: Promise<void>[] = [];
|
|
314
|
+
|
|
315
|
+
for (const name of this.servers.keys()) {
|
|
316
|
+
promises.push(this.disconnectServer(name));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
await Promise.allSettled(promises);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function createMCPManager(config: MCPConfig): MCPClientManager {
|
|
324
|
+
return new MCPClientManager(config);
|
|
325
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
import { SSETransport, type SSETransportConfig } from "./sse.ts";
|
|
4
|
+
import { WebSocketTransport, type WebSocketTransportConfig } from "./websocket.ts";
|
|
5
|
+
|
|
6
|
+
export { SSETransport, type SSETransportConfig };
|
|
7
|
+
export { WebSocketTransport, type WebSocketTransportConfig };
|
|
8
|
+
|
|
9
|
+
export interface StdioTransportConfig {
|
|
10
|
+
command: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TransportType = "stdio" | "sse" | "websocket";
|
|
16
|
+
|
|
17
|
+
export interface TransportOptions {
|
|
18
|
+
type: TransportType;
|
|
19
|
+
stdio?: StdioTransportConfig;
|
|
20
|
+
sse?: SSETransportConfig;
|
|
21
|
+
websocket?: WebSocketTransportConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createTransport(options: TransportOptions): Transport {
|
|
25
|
+
switch (options.type) {
|
|
26
|
+
case "stdio":
|
|
27
|
+
if (!options.stdio) {
|
|
28
|
+
throw new Error("stdio config required for stdio transport");
|
|
29
|
+
}
|
|
30
|
+
return new StdioClientTransport({
|
|
31
|
+
command: options.stdio.command,
|
|
32
|
+
args: options.stdio.args ?? [],
|
|
33
|
+
env: options.stdio.env ?? (process.env as Record<string, string>),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
case "sse":
|
|
37
|
+
if (!options.sse) {
|
|
38
|
+
throw new Error("sse config required for SSE transport");
|
|
39
|
+
}
|
|
40
|
+
return new SSETransport(options.sse) as unknown as Transport;
|
|
41
|
+
|
|
42
|
+
case "websocket":
|
|
43
|
+
if (!options.websocket) {
|
|
44
|
+
throw new Error("websocket config required for WebSocket transport");
|
|
45
|
+
}
|
|
46
|
+
return new WebSocketTransport(options.websocket) as unknown as Transport;
|
|
47
|
+
|
|
48
|
+
default:
|
|
49
|
+
throw new Error(`Unknown transport type: ${options.type}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
|
|
3
|
+
export interface SSETransportConfig {
|
|
4
|
+
url: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class SSETransport {
|
|
9
|
+
private url: string;
|
|
10
|
+
private headers: Record<string, string>;
|
|
11
|
+
private abortController: AbortController | null = null;
|
|
12
|
+
|
|
13
|
+
onmessage: ((message: unknown) => void) | undefined;
|
|
14
|
+
onerror: ((error: Error) => void) | undefined;
|
|
15
|
+
onclose: (() => void) | undefined;
|
|
16
|
+
|
|
17
|
+
constructor(config: SSETransportConfig) {
|
|
18
|
+
this.url = config.url;
|
|
19
|
+
this.headers = config.headers ?? {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async start(): Promise<void> {
|
|
23
|
+
this.abortController = new AbortController();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async close(): Promise<void> {
|
|
27
|
+
this.abortController?.abort();
|
|
28
|
+
this.abortController = null;
|
|
29
|
+
|
|
30
|
+
if (this.onclose) {
|
|
31
|
+
this.onclose();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async send(message: unknown): Promise<void> {
|
|
36
|
+
const response = await fetch(this.url, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
...this.headers,
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(message),
|
|
43
|
+
signal: this.abortController?.signal,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`SSE request failed: ${response.status}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
if (this.onmessage && data) {
|
|
52
|
+
this.onmessage(data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createSSETransport(config: SSETransportConfig): Transport {
|
|
58
|
+
return new SSETransport(config) as unknown as Transport;
|
|
59
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
|
|
3
|
+
export interface WebSocketTransportConfig {
|
|
4
|
+
url: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class WebSocketTransport {
|
|
9
|
+
private url: string;
|
|
10
|
+
private ws: WebSocket | null = null;
|
|
11
|
+
|
|
12
|
+
onmessage: ((message: unknown) => void) | undefined;
|
|
13
|
+
onerror: ((error: Error) => void) | undefined;
|
|
14
|
+
onclose: (() => void) | undefined;
|
|
15
|
+
|
|
16
|
+
constructor(config: WebSocketTransportConfig) {
|
|
17
|
+
this.url = config.url;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async start(): Promise<void> {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
this.ws = new WebSocket(this.url);
|
|
23
|
+
|
|
24
|
+
this.ws.onopen = () => resolve();
|
|
25
|
+
|
|
26
|
+
this.ws.onmessage = (event) => {
|
|
27
|
+
if (this.onmessage) {
|
|
28
|
+
try {
|
|
29
|
+
const data = JSON.parse(event.data as string);
|
|
30
|
+
this.onmessage(data);
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignore parse errors
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
this.ws.onerror = () => {
|
|
38
|
+
const error = new Error("WebSocket connection failed");
|
|
39
|
+
if (this.onerror) {
|
|
40
|
+
this.onerror(error);
|
|
41
|
+
}
|
|
42
|
+
reject(error);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.ws.onclose = () => {
|
|
46
|
+
if (this.onclose) {
|
|
47
|
+
this.onclose();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async close(): Promise<void> {
|
|
54
|
+
if (this.ws) {
|
|
55
|
+
this.ws.close();
|
|
56
|
+
this.ws = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async send(message: unknown): Promise<void> {
|
|
61
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
62
|
+
throw new Error("WebSocket not connected");
|
|
63
|
+
}
|
|
64
|
+
this.ws.send(JSON.stringify(message));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createWebSocketTransport(config: WebSocketTransportConfig): Transport {
|
|
69
|
+
return new WebSocketTransport(config) as unknown as Transport;
|
|
70
|
+
}
|