@rainfall-devkit/sdk 0.2.0 → 0.2.2
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/dist/chunk-EI7SJH5K.mjs +85 -0
- package/dist/chunk-KOCCGNEQ.mjs +269 -0
- package/dist/chunk-NCQVOLS4.mjs +269 -0
- package/dist/chunk-NTTAVKRT.mjs +89 -0
- package/dist/chunk-RVKW5KBT.mjs +269 -0
- package/dist/chunk-XAHJQRBJ.mjs +269 -0
- package/dist/chunk-XEQ6U3JQ.mjs +269 -0
- package/dist/cli/index.js +1088 -9
- package/dist/cli/index.mjs +102 -3
- package/dist/config-7UT7GYSN.mjs +16 -0
- package/dist/config-MD45VGWD.mjs +14 -0
- package/dist/daemon/index.d.mts +35 -3
- package/dist/daemon/index.d.ts +35 -3
- package/dist/daemon/index.js +716 -7
- package/dist/daemon/index.mjs +720 -8
- package/dist/index.d.mts +248 -3
- package/dist/index.d.ts +248 -3
- package/dist/index.js +490 -2
- package/dist/index.mjs +214 -1
- package/dist/listeners-B5Vy9Ao5.d.ts +372 -0
- package/dist/listeners-DRwITBW_.d.mts +372 -0
- package/dist/listeners-DrMrvFT5.d.ts +372 -0
- package/dist/listeners-MNAnpZj-.d.mts +372 -0
- package/dist/listeners-PZI7iT85.d.ts +372 -0
- package/dist/listeners-jLwetUnx.d.mts +372 -0
- package/dist/mcp.d.mts +6 -2
- package/dist/mcp.d.ts +6 -2
- package/dist/sdk-4OvXPr8E.d.mts +1054 -0
- package/dist/sdk-4OvXPr8E.d.ts +1054 -0
- package/dist/sdk-CN1ezZrI.d.mts +1054 -0
- package/dist/sdk-CN1ezZrI.d.ts +1054 -0
- package/dist/sdk-Xw0BjsLd.d.mts +1054 -0
- package/dist/sdk-Xw0BjsLd.d.ts +1054 -0
- package/package.json +4 -1
package/dist/daemon/index.mjs
CHANGED
|
@@ -10,6 +10,575 @@ import {
|
|
|
10
10
|
// src/daemon/index.ts
|
|
11
11
|
import { WebSocketServer } from "ws";
|
|
12
12
|
import express from "express";
|
|
13
|
+
|
|
14
|
+
// src/services/mcp-proxy.ts
|
|
15
|
+
import { WebSocket } from "ws";
|
|
16
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
17
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
18
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
19
|
+
import {
|
|
20
|
+
ListToolsResultSchema,
|
|
21
|
+
CallToolResultSchema,
|
|
22
|
+
ListResourcesResultSchema,
|
|
23
|
+
ReadResourceResultSchema,
|
|
24
|
+
ListPromptsResultSchema,
|
|
25
|
+
GetPromptResultSchema,
|
|
26
|
+
McpError
|
|
27
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
28
|
+
var MCPProxyHub = class {
|
|
29
|
+
clients = /* @__PURE__ */ new Map();
|
|
30
|
+
options;
|
|
31
|
+
refreshTimer;
|
|
32
|
+
reconnectTimeouts = /* @__PURE__ */ new Map();
|
|
33
|
+
requestId = 0;
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
this.options = {
|
|
36
|
+
debug: options.debug ?? false,
|
|
37
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
38
|
+
reconnectDelay: options.reconnectDelay ?? 5e3,
|
|
39
|
+
toolTimeout: options.toolTimeout ?? 3e4,
|
|
40
|
+
refreshInterval: options.refreshInterval ?? 3e4
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the MCP proxy hub
|
|
45
|
+
*/
|
|
46
|
+
async initialize() {
|
|
47
|
+
this.log("\u{1F50C} Initializing MCP Proxy Hub...");
|
|
48
|
+
this.startRefreshTimer();
|
|
49
|
+
this.log("\u2705 MCP Proxy Hub initialized");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Shutdown the MCP proxy hub and disconnect all clients
|
|
53
|
+
*/
|
|
54
|
+
async shutdown() {
|
|
55
|
+
this.log("\u{1F6D1} Shutting down MCP Proxy Hub...");
|
|
56
|
+
if (this.refreshTimer) {
|
|
57
|
+
clearInterval(this.refreshTimer);
|
|
58
|
+
this.refreshTimer = void 0;
|
|
59
|
+
}
|
|
60
|
+
for (const timeout of this.reconnectTimeouts.values()) {
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
}
|
|
63
|
+
this.reconnectTimeouts.clear();
|
|
64
|
+
const disconnectPromises = Array.from(this.clients.entries()).map(
|
|
65
|
+
async ([name, client]) => {
|
|
66
|
+
try {
|
|
67
|
+
await this.disconnectClient(name);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.log(`Error disconnecting ${name}:`, error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
await Promise.allSettled(disconnectPromises);
|
|
74
|
+
this.clients.clear();
|
|
75
|
+
this.log("\u{1F44B} MCP Proxy Hub shut down");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Connect to an MCP server
|
|
79
|
+
*/
|
|
80
|
+
async connectClient(config) {
|
|
81
|
+
const { name, transport } = config;
|
|
82
|
+
if (this.clients.has(name)) {
|
|
83
|
+
this.log(`Reconnecting client: ${name}`);
|
|
84
|
+
await this.disconnectClient(name);
|
|
85
|
+
}
|
|
86
|
+
this.log(`Connecting to MCP server: ${name} (${transport})...`);
|
|
87
|
+
try {
|
|
88
|
+
const client = new Client(
|
|
89
|
+
{
|
|
90
|
+
name: `rainfall-daemon-${name}`,
|
|
91
|
+
version: "0.2.0"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
capabilities: {}
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
let lastErrorTime = 0;
|
|
98
|
+
client.onerror = (error) => {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
if (now - lastErrorTime > 5e3) {
|
|
101
|
+
this.log(`MCP Server Error (${name}):`, error.message);
|
|
102
|
+
lastErrorTime = now;
|
|
103
|
+
}
|
|
104
|
+
if (this.options.autoReconnect) {
|
|
105
|
+
this.scheduleReconnect(name, config);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
let transportInstance;
|
|
109
|
+
if (transport === "stdio" && config.command) {
|
|
110
|
+
const env = {};
|
|
111
|
+
for (const [key, value] of Object.entries({ ...process.env, ...config.env })) {
|
|
112
|
+
if (value !== void 0) {
|
|
113
|
+
env[key] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
transportInstance = new StdioClientTransport({
|
|
117
|
+
command: config.command,
|
|
118
|
+
args: config.args,
|
|
119
|
+
env
|
|
120
|
+
});
|
|
121
|
+
} else if (transport === "http" && config.url) {
|
|
122
|
+
transportInstance = new StreamableHTTPClientTransport(
|
|
123
|
+
new URL(config.url),
|
|
124
|
+
{
|
|
125
|
+
requestInit: {
|
|
126
|
+
headers: config.headers
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
} else if (transport === "websocket" && config.url) {
|
|
131
|
+
transportInstance = new WebSocket(config.url);
|
|
132
|
+
await new Promise((resolve, reject) => {
|
|
133
|
+
transportInstance.on("open", () => resolve());
|
|
134
|
+
transportInstance.on("error", reject);
|
|
135
|
+
setTimeout(() => reject(new Error("WebSocket connection timeout")), 1e4);
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error(`Invalid transport configuration for ${name}`);
|
|
139
|
+
}
|
|
140
|
+
await client.connect(transportInstance);
|
|
141
|
+
const toolsResult = await client.request(
|
|
142
|
+
{
|
|
143
|
+
method: "tools/list",
|
|
144
|
+
params: {}
|
|
145
|
+
},
|
|
146
|
+
ListToolsResultSchema
|
|
147
|
+
);
|
|
148
|
+
const tools = toolsResult.tools.map((tool) => ({
|
|
149
|
+
name: tool.name,
|
|
150
|
+
description: tool.description || "",
|
|
151
|
+
inputSchema: tool.inputSchema,
|
|
152
|
+
serverName: name
|
|
153
|
+
}));
|
|
154
|
+
const clientInfo = {
|
|
155
|
+
name,
|
|
156
|
+
client,
|
|
157
|
+
transport: transportInstance,
|
|
158
|
+
transportType: transport,
|
|
159
|
+
tools,
|
|
160
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
161
|
+
lastUsed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
162
|
+
config,
|
|
163
|
+
status: "connected"
|
|
164
|
+
};
|
|
165
|
+
this.clients.set(name, clientInfo);
|
|
166
|
+
this.log(`\u2705 Connected to ${name} (${tools.length} tools)`);
|
|
167
|
+
this.printAvailableTools(name, tools);
|
|
168
|
+
return name;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
171
|
+
this.log(`\u274C Failed to connect to ${name}:`, errorMessage);
|
|
172
|
+
if (this.options.autoReconnect) {
|
|
173
|
+
this.scheduleReconnect(name, config);
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Disconnect a specific MCP client
|
|
180
|
+
*/
|
|
181
|
+
async disconnectClient(name) {
|
|
182
|
+
const client = this.clients.get(name);
|
|
183
|
+
if (!client) return;
|
|
184
|
+
const timeout = this.reconnectTimeouts.get(name);
|
|
185
|
+
if (timeout) {
|
|
186
|
+
clearTimeout(timeout);
|
|
187
|
+
this.reconnectTimeouts.delete(name);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
await client.client.close();
|
|
191
|
+
if ("close" in client.transport && typeof client.transport.close === "function") {
|
|
192
|
+
await client.transport.close();
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.log(`Error closing client ${name}:`, error);
|
|
196
|
+
}
|
|
197
|
+
this.clients.delete(name);
|
|
198
|
+
this.log(`Disconnected from ${name}`);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Schedule a reconnection attempt
|
|
202
|
+
*/
|
|
203
|
+
scheduleReconnect(name, config) {
|
|
204
|
+
if (this.reconnectTimeouts.has(name)) return;
|
|
205
|
+
const timeout = setTimeout(async () => {
|
|
206
|
+
this.reconnectTimeouts.delete(name);
|
|
207
|
+
this.log(`Attempting to reconnect to ${name}...`);
|
|
208
|
+
try {
|
|
209
|
+
await this.connectClient(config);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
this.log(`Reconnection failed for ${name}`);
|
|
212
|
+
}
|
|
213
|
+
}, this.options.reconnectDelay);
|
|
214
|
+
this.reconnectTimeouts.set(name, timeout);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Call a tool on the appropriate MCP client
|
|
218
|
+
*/
|
|
219
|
+
async callTool(toolName, args, options = {}) {
|
|
220
|
+
const timeout = options.timeout ?? this.options.toolTimeout;
|
|
221
|
+
let clientInfo;
|
|
222
|
+
let actualToolName = toolName;
|
|
223
|
+
if (options.namespace) {
|
|
224
|
+
clientInfo = this.clients.get(options.namespace);
|
|
225
|
+
if (!clientInfo) {
|
|
226
|
+
throw new Error(`Namespace '${options.namespace}' not found`);
|
|
227
|
+
}
|
|
228
|
+
const prefix = `${options.namespace}-`;
|
|
229
|
+
if (actualToolName.startsWith(prefix)) {
|
|
230
|
+
actualToolName = actualToolName.slice(prefix.length);
|
|
231
|
+
}
|
|
232
|
+
if (!clientInfo.tools.some((t) => t.name === actualToolName)) {
|
|
233
|
+
throw new Error(`Tool '${actualToolName}' not found in namespace '${options.namespace}'`);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
for (const [, info] of this.clients) {
|
|
237
|
+
const tool = info.tools.find((t) => t.name === toolName);
|
|
238
|
+
if (tool) {
|
|
239
|
+
clientInfo = info;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!clientInfo) {
|
|
245
|
+
throw new Error(`Tool '${toolName}' not found on any connected MCP server`);
|
|
246
|
+
}
|
|
247
|
+
const requestId = `req_${++this.requestId}`;
|
|
248
|
+
clientInfo.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
249
|
+
try {
|
|
250
|
+
this.log(`[${requestId}] Calling '${actualToolName}' on '${clientInfo.name}'`);
|
|
251
|
+
const result = await Promise.race([
|
|
252
|
+
clientInfo.client.request(
|
|
253
|
+
{
|
|
254
|
+
method: "tools/call",
|
|
255
|
+
params: {
|
|
256
|
+
name: actualToolName,
|
|
257
|
+
arguments: args
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
CallToolResultSchema
|
|
261
|
+
),
|
|
262
|
+
new Promise(
|
|
263
|
+
(_, reject) => setTimeout(() => reject(new Error(`Tool call timeout after ${timeout}ms`)), timeout)
|
|
264
|
+
)
|
|
265
|
+
]);
|
|
266
|
+
this.log(`[${requestId}] Completed successfully`);
|
|
267
|
+
return this.formatToolResult(result);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
this.log(`[${requestId}] Failed:`, error instanceof Error ? error.message : error);
|
|
270
|
+
if (error instanceof McpError) {
|
|
271
|
+
throw new Error(`MCP Error (${toolName}): ${error.message} (code: ${error.code})`);
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Format MCP tool result for consistent output
|
|
278
|
+
*/
|
|
279
|
+
formatToolResult(result) {
|
|
280
|
+
if (!result || !result.content) {
|
|
281
|
+
return "";
|
|
282
|
+
}
|
|
283
|
+
return result.content.map((item) => {
|
|
284
|
+
if (item.type === "text") {
|
|
285
|
+
return item.text || "";
|
|
286
|
+
} else if (item.type === "resource") {
|
|
287
|
+
return `[Resource: ${item.resource?.uri || "unknown"}]`;
|
|
288
|
+
} else if (item.type === "image") {
|
|
289
|
+
return `[Image: ${item.mimeType || "unknown"}]`;
|
|
290
|
+
} else if (item.type === "audio") {
|
|
291
|
+
return `[Audio: ${item.mimeType || "unknown"}]`;
|
|
292
|
+
} else {
|
|
293
|
+
return JSON.stringify(item);
|
|
294
|
+
}
|
|
295
|
+
}).join("\n");
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Get all tools from all connected MCP clients
|
|
299
|
+
* Optionally with namespace prefix
|
|
300
|
+
*/
|
|
301
|
+
getAllTools(options = {}) {
|
|
302
|
+
const allTools = [];
|
|
303
|
+
for (const [clientName, client] of this.clients) {
|
|
304
|
+
for (const tool of client.tools) {
|
|
305
|
+
if (options.namespacePrefix) {
|
|
306
|
+
allTools.push({
|
|
307
|
+
...tool,
|
|
308
|
+
name: `${clientName}-${tool.name}`
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
allTools.push(tool);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return allTools;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get tools from a specific client
|
|
319
|
+
*/
|
|
320
|
+
getClientTools(clientName) {
|
|
321
|
+
const client = this.clients.get(clientName);
|
|
322
|
+
return client?.tools || [];
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get list of connected MCP clients
|
|
326
|
+
*/
|
|
327
|
+
listClients() {
|
|
328
|
+
return Array.from(this.clients.entries()).map(([name, info]) => ({
|
|
329
|
+
name,
|
|
330
|
+
status: info.status,
|
|
331
|
+
toolCount: info.tools.length,
|
|
332
|
+
connectedAt: info.connectedAt,
|
|
333
|
+
lastUsed: info.lastUsed,
|
|
334
|
+
transportType: info.transportType
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get client info by name
|
|
339
|
+
*/
|
|
340
|
+
getClient(name) {
|
|
341
|
+
return this.clients.get(name);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Refresh tool lists from all connected clients
|
|
345
|
+
*/
|
|
346
|
+
async refreshTools() {
|
|
347
|
+
for (const [name, client] of this.clients) {
|
|
348
|
+
try {
|
|
349
|
+
const toolsResult = await client.client.request(
|
|
350
|
+
{
|
|
351
|
+
method: "tools/list",
|
|
352
|
+
params: {}
|
|
353
|
+
},
|
|
354
|
+
ListToolsResultSchema
|
|
355
|
+
);
|
|
356
|
+
client.tools = toolsResult.tools.map((tool) => ({
|
|
357
|
+
name: tool.name,
|
|
358
|
+
description: tool.description || "",
|
|
359
|
+
inputSchema: tool.inputSchema,
|
|
360
|
+
serverName: name
|
|
361
|
+
}));
|
|
362
|
+
this.log(`Refreshed ${name}: ${client.tools.length} tools`);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
this.log(`Failed to refresh tools for ${name}:`, error);
|
|
365
|
+
client.status = "error";
|
|
366
|
+
client.error = error instanceof Error ? error.message : String(error);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* List resources from a specific client or all clients
|
|
372
|
+
*/
|
|
373
|
+
async listResources(clientName) {
|
|
374
|
+
const results = [];
|
|
375
|
+
const clients = clientName ? [clientName] : Array.from(this.clients.keys());
|
|
376
|
+
for (const name of clients) {
|
|
377
|
+
const client = this.clients.get(name);
|
|
378
|
+
if (!client) continue;
|
|
379
|
+
try {
|
|
380
|
+
const result = await client.client.request(
|
|
381
|
+
{
|
|
382
|
+
method: "resources/list",
|
|
383
|
+
params: {}
|
|
384
|
+
},
|
|
385
|
+
ListResourcesResultSchema
|
|
386
|
+
);
|
|
387
|
+
results.push({
|
|
388
|
+
clientName: name,
|
|
389
|
+
resources: result.resources
|
|
390
|
+
});
|
|
391
|
+
} catch (error) {
|
|
392
|
+
this.log(`Failed to list resources for ${name}:`, error);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return results;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Read a resource from a specific client
|
|
399
|
+
*/
|
|
400
|
+
async readResource(uri, clientName) {
|
|
401
|
+
if (clientName) {
|
|
402
|
+
const client = this.clients.get(clientName);
|
|
403
|
+
if (!client) {
|
|
404
|
+
throw new Error(`Client '${clientName}' not found`);
|
|
405
|
+
}
|
|
406
|
+
const result = await client.client.request(
|
|
407
|
+
{
|
|
408
|
+
method: "resources/read",
|
|
409
|
+
params: { uri }
|
|
410
|
+
},
|
|
411
|
+
ReadResourceResultSchema
|
|
412
|
+
);
|
|
413
|
+
return result;
|
|
414
|
+
} else {
|
|
415
|
+
for (const [name, client] of this.clients) {
|
|
416
|
+
try {
|
|
417
|
+
const result = await client.client.request(
|
|
418
|
+
{
|
|
419
|
+
method: "resources/read",
|
|
420
|
+
params: { uri }
|
|
421
|
+
},
|
|
422
|
+
ReadResourceResultSchema
|
|
423
|
+
);
|
|
424
|
+
return { clientName: name, ...result };
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
throw new Error(`Resource '${uri}' not found on any client`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* List prompts from a specific client or all clients
|
|
433
|
+
*/
|
|
434
|
+
async listPrompts(clientName) {
|
|
435
|
+
const results = [];
|
|
436
|
+
const clients = clientName ? [clientName] : Array.from(this.clients.keys());
|
|
437
|
+
for (const name of clients) {
|
|
438
|
+
const client = this.clients.get(name);
|
|
439
|
+
if (!client) continue;
|
|
440
|
+
try {
|
|
441
|
+
const result = await client.client.request(
|
|
442
|
+
{
|
|
443
|
+
method: "prompts/list",
|
|
444
|
+
params: {}
|
|
445
|
+
},
|
|
446
|
+
ListPromptsResultSchema
|
|
447
|
+
);
|
|
448
|
+
results.push({
|
|
449
|
+
clientName: name,
|
|
450
|
+
prompts: result.prompts
|
|
451
|
+
});
|
|
452
|
+
} catch (error) {
|
|
453
|
+
this.log(`Failed to list prompts for ${name}:`, error);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return results;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Get a prompt from a specific client
|
|
460
|
+
*/
|
|
461
|
+
async getPrompt(name, args, clientName) {
|
|
462
|
+
if (clientName) {
|
|
463
|
+
const client = this.clients.get(clientName);
|
|
464
|
+
if (!client) {
|
|
465
|
+
throw new Error(`Client '${clientName}' not found`);
|
|
466
|
+
}
|
|
467
|
+
const result = await client.client.request(
|
|
468
|
+
{
|
|
469
|
+
method: "prompts/get",
|
|
470
|
+
params: { name, arguments: args }
|
|
471
|
+
},
|
|
472
|
+
GetPromptResultSchema
|
|
473
|
+
);
|
|
474
|
+
return result;
|
|
475
|
+
} else {
|
|
476
|
+
for (const [cName, client] of this.clients) {
|
|
477
|
+
try {
|
|
478
|
+
const result = await client.client.request(
|
|
479
|
+
{
|
|
480
|
+
method: "prompts/get",
|
|
481
|
+
params: { name, arguments: args }
|
|
482
|
+
},
|
|
483
|
+
GetPromptResultSchema
|
|
484
|
+
);
|
|
485
|
+
return { clientName: cName, ...result };
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
throw new Error(`Prompt '${name}' not found on any client`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Health check for all connected clients
|
|
494
|
+
*/
|
|
495
|
+
async healthCheck() {
|
|
496
|
+
const results = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const [name, client] of this.clients) {
|
|
498
|
+
try {
|
|
499
|
+
const startTime = Date.now();
|
|
500
|
+
await client.client.request(
|
|
501
|
+
{
|
|
502
|
+
method: "tools/list",
|
|
503
|
+
params: {}
|
|
504
|
+
},
|
|
505
|
+
ListToolsResultSchema
|
|
506
|
+
);
|
|
507
|
+
results.set(name, {
|
|
508
|
+
status: "healthy",
|
|
509
|
+
responseTime: Date.now() - startTime
|
|
510
|
+
});
|
|
511
|
+
} catch (error) {
|
|
512
|
+
results.set(name, {
|
|
513
|
+
status: "unhealthy",
|
|
514
|
+
responseTime: 0,
|
|
515
|
+
error: error instanceof Error ? error.message : String(error)
|
|
516
|
+
});
|
|
517
|
+
if (this.options.autoReconnect) {
|
|
518
|
+
this.scheduleReconnect(name, client.config);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return results;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Start the automatic refresh timer
|
|
526
|
+
*/
|
|
527
|
+
startRefreshTimer() {
|
|
528
|
+
if (this.refreshTimer) {
|
|
529
|
+
clearInterval(this.refreshTimer);
|
|
530
|
+
}
|
|
531
|
+
if (this.options.refreshInterval > 0) {
|
|
532
|
+
this.refreshTimer = setInterval(async () => {
|
|
533
|
+
try {
|
|
534
|
+
await this.refreshTools();
|
|
535
|
+
} catch (error) {
|
|
536
|
+
this.log("Auto-refresh failed:", error);
|
|
537
|
+
}
|
|
538
|
+
}, this.options.refreshInterval);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Print available tools for a client
|
|
543
|
+
*/
|
|
544
|
+
printAvailableTools(clientName, tools) {
|
|
545
|
+
if (tools.length === 0) {
|
|
546
|
+
this.log(` No tools available from ${clientName}`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
this.log(`
|
|
550
|
+
--- ${clientName} Tools (${tools.length}) ---`);
|
|
551
|
+
for (const tool of tools) {
|
|
552
|
+
this.log(` \u2022 ${tool.name}: ${tool.description.slice(0, 60)}${tool.description.length > 60 ? "..." : ""}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Debug logging
|
|
557
|
+
*/
|
|
558
|
+
log(...args) {
|
|
559
|
+
if (this.options.debug) {
|
|
560
|
+
console.log("[MCP-Proxy]", ...args);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get statistics about the MCP proxy hub
|
|
565
|
+
*/
|
|
566
|
+
getStats() {
|
|
567
|
+
const clients = Array.from(this.clients.entries()).map(([name, info]) => ({
|
|
568
|
+
name,
|
|
569
|
+
toolCount: info.tools.length,
|
|
570
|
+
status: info.status,
|
|
571
|
+
transportType: info.transportType
|
|
572
|
+
}));
|
|
573
|
+
return {
|
|
574
|
+
totalClients: this.clients.size,
|
|
575
|
+
totalTools: clients.reduce((sum, c) => sum + c.toolCount, 0),
|
|
576
|
+
clients
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/daemon/index.ts
|
|
13
582
|
var RainfallDaemon = class {
|
|
14
583
|
wss;
|
|
15
584
|
openaiApp;
|
|
@@ -25,11 +594,16 @@ var RainfallDaemon = class {
|
|
|
25
594
|
networkedExecutor;
|
|
26
595
|
context;
|
|
27
596
|
listeners;
|
|
597
|
+
mcpProxy;
|
|
598
|
+
enableMcpProxy;
|
|
599
|
+
mcpNamespacePrefix;
|
|
28
600
|
constructor(config = {}) {
|
|
29
601
|
this.port = config.port || 8765;
|
|
30
602
|
this.openaiPort = config.openaiPort || 8787;
|
|
31
603
|
this.rainfallConfig = config.rainfallConfig;
|
|
32
604
|
this.debug = config.debug || false;
|
|
605
|
+
this.enableMcpProxy = config.enableMcpProxy ?? true;
|
|
606
|
+
this.mcpNamespacePrefix = config.mcpNamespacePrefix ?? true;
|
|
33
607
|
this.openaiApp = express();
|
|
34
608
|
this.openaiApp.use(express.json());
|
|
35
609
|
}
|
|
@@ -65,6 +639,19 @@ var RainfallDaemon = class {
|
|
|
65
639
|
this.networkedExecutor
|
|
66
640
|
);
|
|
67
641
|
await this.loadTools();
|
|
642
|
+
if (this.enableMcpProxy) {
|
|
643
|
+
this.mcpProxy = new MCPProxyHub({ debug: this.debug });
|
|
644
|
+
await this.mcpProxy.initialize();
|
|
645
|
+
if (this.rainfallConfig?.mcpClients) {
|
|
646
|
+
for (const clientConfig of this.rainfallConfig.mcpClients) {
|
|
647
|
+
try {
|
|
648
|
+
await this.mcpProxy.connectClient(clientConfig);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
this.log(`Failed to connect MCP client ${clientConfig.name}:`, error);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
68
655
|
await this.startWebSocketServer();
|
|
69
656
|
await this.startOpenAIProxy();
|
|
70
657
|
console.log(`\u{1F680} Rainfall daemon running`);
|
|
@@ -85,6 +672,10 @@ var RainfallDaemon = class {
|
|
|
85
672
|
if (this.networkedExecutor) {
|
|
86
673
|
await this.networkedExecutor.unregisterEdgeNode();
|
|
87
674
|
}
|
|
675
|
+
if (this.mcpProxy) {
|
|
676
|
+
await this.mcpProxy.shutdown();
|
|
677
|
+
this.mcpProxy = void 0;
|
|
678
|
+
}
|
|
88
679
|
for (const client of this.clients) {
|
|
89
680
|
client.close();
|
|
90
681
|
}
|
|
@@ -113,11 +704,35 @@ var RainfallDaemon = class {
|
|
|
113
704
|
getListenerRegistry() {
|
|
114
705
|
return this.listeners;
|
|
115
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Get the MCP Proxy Hub for managing external MCP clients
|
|
709
|
+
*/
|
|
710
|
+
getMCPProxy() {
|
|
711
|
+
return this.mcpProxy;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Connect an MCP client dynamically
|
|
715
|
+
*/
|
|
716
|
+
async connectMCPClient(config) {
|
|
717
|
+
if (!this.mcpProxy) {
|
|
718
|
+
throw new Error("MCP Proxy Hub is not enabled");
|
|
719
|
+
}
|
|
720
|
+
return this.mcpProxy.connectClient(config);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Disconnect an MCP client
|
|
724
|
+
*/
|
|
725
|
+
async disconnectMCPClient(name) {
|
|
726
|
+
if (!this.mcpProxy) {
|
|
727
|
+
throw new Error("MCP Proxy Hub is not enabled");
|
|
728
|
+
}
|
|
729
|
+
return this.mcpProxy.disconnectClient(name);
|
|
730
|
+
}
|
|
116
731
|
async initializeRainfall() {
|
|
117
732
|
if (this.rainfallConfig?.apiKey) {
|
|
118
733
|
this.rainfall = new Rainfall(this.rainfallConfig);
|
|
119
734
|
} else {
|
|
120
|
-
const { loadConfig } = await import("../config-
|
|
735
|
+
const { loadConfig } = await import("../config-7UT7GYSN.mjs");
|
|
121
736
|
const config = loadConfig();
|
|
122
737
|
if (config.apiKey) {
|
|
123
738
|
this.rainfall = new Rainfall({
|
|
@@ -215,7 +830,7 @@ var RainfallDaemon = class {
|
|
|
215
830
|
const toolParams = params?.arguments;
|
|
216
831
|
try {
|
|
217
832
|
const startTime = Date.now();
|
|
218
|
-
const result = await this.
|
|
833
|
+
const result = await this.executeToolWithMCP(toolName, toolParams);
|
|
219
834
|
const duration = Date.now() - startTime;
|
|
220
835
|
if (this.context) {
|
|
221
836
|
this.context.recordExecution(toolName, toolParams || {}, result, { duration });
|
|
@@ -280,6 +895,16 @@ var RainfallDaemon = class {
|
|
|
280
895
|
});
|
|
281
896
|
}
|
|
282
897
|
}
|
|
898
|
+
if (this.mcpProxy) {
|
|
899
|
+
const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
|
|
900
|
+
for (const tool of proxyTools) {
|
|
901
|
+
mcpTools.push({
|
|
902
|
+
name: this.mcpNamespacePrefix ? `${tool.serverName}-${tool.name}` : tool.name,
|
|
903
|
+
description: tool.description,
|
|
904
|
+
inputSchema: tool.inputSchema || { type: "object", properties: {} }
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
283
908
|
return mcpTools;
|
|
284
909
|
}
|
|
285
910
|
async executeTool(toolId, params) {
|
|
@@ -288,6 +913,30 @@ var RainfallDaemon = class {
|
|
|
288
913
|
}
|
|
289
914
|
return this.rainfall.executeTool(toolId, params);
|
|
290
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* Execute a tool, trying MCP proxy first, then falling back to Rainfall tools
|
|
918
|
+
*/
|
|
919
|
+
async executeToolWithMCP(toolName, params) {
|
|
920
|
+
if (this.mcpProxy) {
|
|
921
|
+
try {
|
|
922
|
+
if (this.mcpNamespacePrefix && toolName.includes("-")) {
|
|
923
|
+
const namespace = toolName.split("-")[0];
|
|
924
|
+
const actualToolName = toolName.slice(namespace.length + 1);
|
|
925
|
+
if (this.mcpProxy.getClient(namespace)) {
|
|
926
|
+
return await this.mcpProxy.callTool(toolName, params || {}, {
|
|
927
|
+
namespace
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return await this.mcpProxy.callTool(toolName, params || {});
|
|
932
|
+
} catch (error) {
|
|
933
|
+
if (error instanceof Error && !error.message.includes("not found")) {
|
|
934
|
+
throw error;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return this.executeTool(toolName, params);
|
|
939
|
+
}
|
|
291
940
|
async startOpenAIProxy() {
|
|
292
941
|
this.openaiApp.get("/v1/models", async (_req, res) => {
|
|
293
942
|
try {
|
|
@@ -403,6 +1052,10 @@ var RainfallDaemon = class {
|
|
|
403
1052
|
this.log(` \u2192 Executing locally`);
|
|
404
1053
|
const args = JSON.parse(toolArgsStr);
|
|
405
1054
|
toolResult = await this.executeLocalTool(localTool.id, args);
|
|
1055
|
+
} else if (this.mcpProxy) {
|
|
1056
|
+
this.log(` \u2192 Trying MCP proxy`);
|
|
1057
|
+
const args = JSON.parse(toolArgsStr);
|
|
1058
|
+
toolResult = await this.executeToolWithMCP(toolName.replace(/_/g, "-"), args);
|
|
406
1059
|
} else {
|
|
407
1060
|
const shouldExecuteLocal = body.tool_priority === "local" || body.tool_priority === "stacked";
|
|
408
1061
|
if (shouldExecuteLocal) {
|
|
@@ -452,15 +1105,52 @@ var RainfallDaemon = class {
|
|
|
452
1105
|
}
|
|
453
1106
|
});
|
|
454
1107
|
this.openaiApp.get("/health", (_req, res) => {
|
|
1108
|
+
const mcpStats = this.mcpProxy?.getStats();
|
|
455
1109
|
res.json({
|
|
456
1110
|
status: "ok",
|
|
457
1111
|
daemon: "rainfall",
|
|
458
|
-
version: "0.
|
|
1112
|
+
version: "0.2.0",
|
|
459
1113
|
tools_loaded: this.tools.length,
|
|
1114
|
+
mcp_clients: mcpStats?.totalClients || 0,
|
|
1115
|
+
mcp_tools: mcpStats?.totalTools || 0,
|
|
460
1116
|
edge_node_id: this.networkedExecutor?.getEdgeNodeId(),
|
|
461
1117
|
clients_connected: this.clients.size
|
|
462
1118
|
});
|
|
463
1119
|
});
|
|
1120
|
+
this.openaiApp.get("/v1/mcp/clients", (_req, res) => {
|
|
1121
|
+
if (!this.mcpProxy) {
|
|
1122
|
+
res.status(503).json({ error: "MCP proxy not enabled" });
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
res.json(this.mcpProxy.listClients());
|
|
1126
|
+
});
|
|
1127
|
+
this.openaiApp.post("/v1/mcp/connect", async (req, res) => {
|
|
1128
|
+
if (!this.mcpProxy) {
|
|
1129
|
+
res.status(503).json({ error: "MCP proxy not enabled" });
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
const name = await this.mcpProxy.connectClient(req.body);
|
|
1134
|
+
res.json({ success: true, client: name });
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
res.status(500).json({
|
|
1137
|
+
error: error instanceof Error ? error.message : "Failed to connect MCP client"
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
this.openaiApp.post("/v1/mcp/disconnect", async (req, res) => {
|
|
1142
|
+
if (!this.mcpProxy) {
|
|
1143
|
+
res.status(503).json({ error: "MCP proxy not enabled" });
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const { name } = req.body;
|
|
1147
|
+
if (!name) {
|
|
1148
|
+
res.status(400).json({ error: "Missing required field: name" });
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
await this.mcpProxy.disconnectClient(name);
|
|
1152
|
+
res.json({ success: true });
|
|
1153
|
+
});
|
|
464
1154
|
this.openaiApp.get("/status", (_req, res) => {
|
|
465
1155
|
res.json(this.getStatus());
|
|
466
1156
|
});
|
|
@@ -589,12 +1279,13 @@ var RainfallDaemon = class {
|
|
|
589
1279
|
if (!this.rainfall) {
|
|
590
1280
|
throw new Error("Rainfall SDK not initialized");
|
|
591
1281
|
}
|
|
592
|
-
const { loadConfig, getProviderBaseUrl } = await import("../config-
|
|
1282
|
+
const { loadConfig, getProviderBaseUrl } = await import("../config-7UT7GYSN.mjs");
|
|
593
1283
|
const config = loadConfig();
|
|
594
1284
|
const provider = config.llm?.provider || "rainfall";
|
|
595
1285
|
switch (provider) {
|
|
596
1286
|
case "local":
|
|
597
1287
|
case "ollama":
|
|
1288
|
+
case "custom":
|
|
598
1289
|
return this.callLocalLLM(params, config);
|
|
599
1290
|
case "openai":
|
|
600
1291
|
case "anthropic":
|
|
@@ -619,7 +1310,7 @@ var RainfallDaemon = class {
|
|
|
619
1310
|
* Call external LLM provider (OpenAI, Anthropic) via their OpenAI-compatible APIs
|
|
620
1311
|
*/
|
|
621
1312
|
async callExternalLLM(params, config, provider) {
|
|
622
|
-
const { getProviderBaseUrl } = await import("../config-
|
|
1313
|
+
const { getProviderBaseUrl } = await import("../config-7UT7GYSN.mjs");
|
|
623
1314
|
const baseUrl = config.llm?.baseUrl || getProviderBaseUrl({ llm: { provider } });
|
|
624
1315
|
const apiKey = config.llm?.apiKey;
|
|
625
1316
|
if (!apiKey) {
|
|
@@ -631,7 +1322,8 @@ var RainfallDaemon = class {
|
|
|
631
1322
|
method: "POST",
|
|
632
1323
|
headers: {
|
|
633
1324
|
"Content-Type": "application/json",
|
|
634
|
-
"Authorization": `Bearer ${apiKey}
|
|
1325
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1326
|
+
"User-Agent": "Rainfall-DevKit/1.0"
|
|
635
1327
|
},
|
|
636
1328
|
body: JSON.stringify({
|
|
637
1329
|
model,
|
|
@@ -662,7 +1354,8 @@ var RainfallDaemon = class {
|
|
|
662
1354
|
method: "POST",
|
|
663
1355
|
headers: {
|
|
664
1356
|
"Content-Type": "application/json",
|
|
665
|
-
"Authorization": `Bearer ${apiKey}
|
|
1357
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1358
|
+
"User-Agent": "Rainfall-DevKit/1.0"
|
|
666
1359
|
},
|
|
667
1360
|
body: JSON.stringify({
|
|
668
1361
|
model,
|
|
@@ -743,7 +1436,7 @@ var RainfallDaemon = class {
|
|
|
743
1436
|
}
|
|
744
1437
|
async getOpenAITools() {
|
|
745
1438
|
const tools = [];
|
|
746
|
-
for (const tool of this.tools.slice(0,
|
|
1439
|
+
for (const tool of this.tools.slice(0, 100)) {
|
|
747
1440
|
const schema = await this.getToolSchema(tool.id);
|
|
748
1441
|
if (schema) {
|
|
749
1442
|
const toolSchema = schema;
|
|
@@ -767,6 +1460,24 @@ var RainfallDaemon = class {
|
|
|
767
1460
|
});
|
|
768
1461
|
}
|
|
769
1462
|
}
|
|
1463
|
+
if (this.mcpProxy) {
|
|
1464
|
+
const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
|
|
1465
|
+
for (const tool of proxyTools.slice(0, 28)) {
|
|
1466
|
+
const inputSchema = tool.inputSchema || {};
|
|
1467
|
+
tools.push({
|
|
1468
|
+
type: "function",
|
|
1469
|
+
function: {
|
|
1470
|
+
name: this.mcpNamespacePrefix ? `${tool.serverName}_${tool.name}`.replace(/-/g, "_") : tool.name.replace(/-/g, "_"),
|
|
1471
|
+
description: `[${tool.serverName}] ${tool.description}`,
|
|
1472
|
+
parameters: {
|
|
1473
|
+
type: "object",
|
|
1474
|
+
properties: inputSchema.properties || {},
|
|
1475
|
+
required: inputSchema.required || []
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
770
1481
|
return tools;
|
|
771
1482
|
}
|
|
772
1483
|
buildResponseContent() {
|
|
@@ -828,6 +1539,7 @@ function getDaemonInstance() {
|
|
|
828
1539
|
return daemonInstance;
|
|
829
1540
|
}
|
|
830
1541
|
export {
|
|
1542
|
+
MCPProxyHub,
|
|
831
1543
|
RainfallDaemon,
|
|
832
1544
|
getDaemonInstance,
|
|
833
1545
|
getDaemonStatus,
|