@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.
@@ -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-DDTQQBN7.mjs");
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.executeTool(toolName, toolParams);
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.1.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-DDTQQBN7.mjs");
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-DDTQQBN7.mjs");
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, 128)) {
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,