@rainfall-devkit/sdk 0.2.0 → 0.2.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/dist/cli/index.js CHANGED
@@ -964,12 +964,16 @@ var init_sdk = __esm({
964
964
  // src/cli/config.ts
965
965
  var config_exports = {};
966
966
  __export(config_exports, {
967
+ getConfigDir: () => getConfigDir,
967
968
  getLLMConfig: () => getLLMConfig,
968
969
  getProviderBaseUrl: () => getProviderBaseUrl,
969
970
  isLocalProvider: () => isLocalProvider,
970
971
  loadConfig: () => loadConfig,
971
972
  saveConfig: () => saveConfig
972
973
  });
974
+ function getConfigDir() {
975
+ return CONFIG_DIR;
976
+ }
973
977
  function loadConfig() {
974
978
  let config = {};
975
979
  if ((0, import_fs.existsSync)(CONFIG_FILE)) {
@@ -1034,6 +1038,7 @@ function getProviderBaseUrl(config) {
1034
1038
  case "ollama":
1035
1039
  return config.llm?.baseUrl || "http://localhost:11434/v1";
1036
1040
  case "local":
1041
+ case "custom":
1037
1042
  return config.llm?.baseUrl || "http://localhost:1234/v1";
1038
1043
  case "rainfall":
1039
1044
  default:
@@ -1711,9 +1716,576 @@ var init_listeners = __esm({
1711
1716
  }
1712
1717
  });
1713
1718
 
1719
+ // src/services/mcp-proxy.ts
1720
+ var import_ws, import_client2, import_stdio, import_streamableHttp, import_types, MCPProxyHub;
1721
+ var init_mcp_proxy = __esm({
1722
+ "src/services/mcp-proxy.ts"() {
1723
+ "use strict";
1724
+ init_cjs_shims();
1725
+ import_ws = require("ws");
1726
+ import_client2 = require("@modelcontextprotocol/sdk/client/index.js");
1727
+ import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
1728
+ import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
1729
+ import_types = require("@modelcontextprotocol/sdk/types.js");
1730
+ MCPProxyHub = class {
1731
+ clients = /* @__PURE__ */ new Map();
1732
+ options;
1733
+ refreshTimer;
1734
+ reconnectTimeouts = /* @__PURE__ */ new Map();
1735
+ requestId = 0;
1736
+ constructor(options = {}) {
1737
+ this.options = {
1738
+ debug: options.debug ?? false,
1739
+ autoReconnect: options.autoReconnect ?? true,
1740
+ reconnectDelay: options.reconnectDelay ?? 5e3,
1741
+ toolTimeout: options.toolTimeout ?? 3e4,
1742
+ refreshInterval: options.refreshInterval ?? 3e4
1743
+ };
1744
+ }
1745
+ /**
1746
+ * Initialize the MCP proxy hub
1747
+ */
1748
+ async initialize() {
1749
+ this.log("\u{1F50C} Initializing MCP Proxy Hub...");
1750
+ this.startRefreshTimer();
1751
+ this.log("\u2705 MCP Proxy Hub initialized");
1752
+ }
1753
+ /**
1754
+ * Shutdown the MCP proxy hub and disconnect all clients
1755
+ */
1756
+ async shutdown() {
1757
+ this.log("\u{1F6D1} Shutting down MCP Proxy Hub...");
1758
+ if (this.refreshTimer) {
1759
+ clearInterval(this.refreshTimer);
1760
+ this.refreshTimer = void 0;
1761
+ }
1762
+ for (const timeout of this.reconnectTimeouts.values()) {
1763
+ clearTimeout(timeout);
1764
+ }
1765
+ this.reconnectTimeouts.clear();
1766
+ const disconnectPromises = Array.from(this.clients.entries()).map(
1767
+ async ([name, client]) => {
1768
+ try {
1769
+ await this.disconnectClient(name);
1770
+ } catch (error) {
1771
+ this.log(`Error disconnecting ${name}:`, error);
1772
+ }
1773
+ }
1774
+ );
1775
+ await Promise.allSettled(disconnectPromises);
1776
+ this.clients.clear();
1777
+ this.log("\u{1F44B} MCP Proxy Hub shut down");
1778
+ }
1779
+ /**
1780
+ * Connect to an MCP server
1781
+ */
1782
+ async connectClient(config) {
1783
+ const { name, transport } = config;
1784
+ if (this.clients.has(name)) {
1785
+ this.log(`Reconnecting client: ${name}`);
1786
+ await this.disconnectClient(name);
1787
+ }
1788
+ this.log(`Connecting to MCP server: ${name} (${transport})...`);
1789
+ try {
1790
+ const client = new import_client2.Client(
1791
+ {
1792
+ name: `rainfall-daemon-${name}`,
1793
+ version: "0.2.0"
1794
+ },
1795
+ {
1796
+ capabilities: {}
1797
+ }
1798
+ );
1799
+ let lastErrorTime = 0;
1800
+ client.onerror = (error) => {
1801
+ const now = Date.now();
1802
+ if (now - lastErrorTime > 5e3) {
1803
+ this.log(`MCP Server Error (${name}):`, error.message);
1804
+ lastErrorTime = now;
1805
+ }
1806
+ if (this.options.autoReconnect) {
1807
+ this.scheduleReconnect(name, config);
1808
+ }
1809
+ };
1810
+ let transportInstance;
1811
+ if (transport === "stdio" && config.command) {
1812
+ const env = {};
1813
+ for (const [key, value] of Object.entries({ ...process.env, ...config.env })) {
1814
+ if (value !== void 0) {
1815
+ env[key] = value;
1816
+ }
1817
+ }
1818
+ transportInstance = new import_stdio.StdioClientTransport({
1819
+ command: config.command,
1820
+ args: config.args,
1821
+ env
1822
+ });
1823
+ } else if (transport === "http" && config.url) {
1824
+ transportInstance = new import_streamableHttp.StreamableHTTPClientTransport(
1825
+ new URL(config.url),
1826
+ {
1827
+ requestInit: {
1828
+ headers: config.headers
1829
+ }
1830
+ }
1831
+ );
1832
+ } else if (transport === "websocket" && config.url) {
1833
+ transportInstance = new import_ws.WebSocket(config.url);
1834
+ await new Promise((resolve, reject) => {
1835
+ transportInstance.on("open", () => resolve());
1836
+ transportInstance.on("error", reject);
1837
+ setTimeout(() => reject(new Error("WebSocket connection timeout")), 1e4);
1838
+ });
1839
+ } else {
1840
+ throw new Error(`Invalid transport configuration for ${name}`);
1841
+ }
1842
+ await client.connect(transportInstance);
1843
+ const toolsResult = await client.request(
1844
+ {
1845
+ method: "tools/list",
1846
+ params: {}
1847
+ },
1848
+ import_types.ListToolsResultSchema
1849
+ );
1850
+ const tools = toolsResult.tools.map((tool) => ({
1851
+ name: tool.name,
1852
+ description: tool.description || "",
1853
+ inputSchema: tool.inputSchema,
1854
+ serverName: name
1855
+ }));
1856
+ const clientInfo = {
1857
+ name,
1858
+ client,
1859
+ transport: transportInstance,
1860
+ transportType: transport,
1861
+ tools,
1862
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1863
+ lastUsed: (/* @__PURE__ */ new Date()).toISOString(),
1864
+ config,
1865
+ status: "connected"
1866
+ };
1867
+ this.clients.set(name, clientInfo);
1868
+ this.log(`\u2705 Connected to ${name} (${tools.length} tools)`);
1869
+ this.printAvailableTools(name, tools);
1870
+ return name;
1871
+ } catch (error) {
1872
+ const errorMessage = error instanceof Error ? error.message : String(error);
1873
+ this.log(`\u274C Failed to connect to ${name}:`, errorMessage);
1874
+ if (this.options.autoReconnect) {
1875
+ this.scheduleReconnect(name, config);
1876
+ }
1877
+ throw error;
1878
+ }
1879
+ }
1880
+ /**
1881
+ * Disconnect a specific MCP client
1882
+ */
1883
+ async disconnectClient(name) {
1884
+ const client = this.clients.get(name);
1885
+ if (!client) return;
1886
+ const timeout = this.reconnectTimeouts.get(name);
1887
+ if (timeout) {
1888
+ clearTimeout(timeout);
1889
+ this.reconnectTimeouts.delete(name);
1890
+ }
1891
+ try {
1892
+ await client.client.close();
1893
+ if ("close" in client.transport && typeof client.transport.close === "function") {
1894
+ await client.transport.close();
1895
+ }
1896
+ } catch (error) {
1897
+ this.log(`Error closing client ${name}:`, error);
1898
+ }
1899
+ this.clients.delete(name);
1900
+ this.log(`Disconnected from ${name}`);
1901
+ }
1902
+ /**
1903
+ * Schedule a reconnection attempt
1904
+ */
1905
+ scheduleReconnect(name, config) {
1906
+ if (this.reconnectTimeouts.has(name)) return;
1907
+ const timeout = setTimeout(async () => {
1908
+ this.reconnectTimeouts.delete(name);
1909
+ this.log(`Attempting to reconnect to ${name}...`);
1910
+ try {
1911
+ await this.connectClient(config);
1912
+ } catch (error) {
1913
+ this.log(`Reconnection failed for ${name}`);
1914
+ }
1915
+ }, this.options.reconnectDelay);
1916
+ this.reconnectTimeouts.set(name, timeout);
1917
+ }
1918
+ /**
1919
+ * Call a tool on the appropriate MCP client
1920
+ */
1921
+ async callTool(toolName, args, options = {}) {
1922
+ const timeout = options.timeout ?? this.options.toolTimeout;
1923
+ let clientInfo;
1924
+ let actualToolName = toolName;
1925
+ if (options.namespace) {
1926
+ clientInfo = this.clients.get(options.namespace);
1927
+ if (!clientInfo) {
1928
+ throw new Error(`Namespace '${options.namespace}' not found`);
1929
+ }
1930
+ const prefix = `${options.namespace}-`;
1931
+ if (actualToolName.startsWith(prefix)) {
1932
+ actualToolName = actualToolName.slice(prefix.length);
1933
+ }
1934
+ if (!clientInfo.tools.some((t) => t.name === actualToolName)) {
1935
+ throw new Error(`Tool '${actualToolName}' not found in namespace '${options.namespace}'`);
1936
+ }
1937
+ } else {
1938
+ for (const [, info] of this.clients) {
1939
+ const tool = info.tools.find((t) => t.name === toolName);
1940
+ if (tool) {
1941
+ clientInfo = info;
1942
+ break;
1943
+ }
1944
+ }
1945
+ }
1946
+ if (!clientInfo) {
1947
+ throw new Error(`Tool '${toolName}' not found on any connected MCP server`);
1948
+ }
1949
+ const requestId = `req_${++this.requestId}`;
1950
+ clientInfo.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
1951
+ try {
1952
+ this.log(`[${requestId}] Calling '${actualToolName}' on '${clientInfo.name}'`);
1953
+ const result = await Promise.race([
1954
+ clientInfo.client.request(
1955
+ {
1956
+ method: "tools/call",
1957
+ params: {
1958
+ name: actualToolName,
1959
+ arguments: args
1960
+ }
1961
+ },
1962
+ import_types.CallToolResultSchema
1963
+ ),
1964
+ new Promise(
1965
+ (_, reject) => setTimeout(() => reject(new Error(`Tool call timeout after ${timeout}ms`)), timeout)
1966
+ )
1967
+ ]);
1968
+ this.log(`[${requestId}] Completed successfully`);
1969
+ return this.formatToolResult(result);
1970
+ } catch (error) {
1971
+ this.log(`[${requestId}] Failed:`, error instanceof Error ? error.message : error);
1972
+ if (error instanceof import_types.McpError) {
1973
+ throw new Error(`MCP Error (${toolName}): ${error.message} (code: ${error.code})`);
1974
+ }
1975
+ throw error;
1976
+ }
1977
+ }
1978
+ /**
1979
+ * Format MCP tool result for consistent output
1980
+ */
1981
+ formatToolResult(result) {
1982
+ if (!result || !result.content) {
1983
+ return "";
1984
+ }
1985
+ return result.content.map((item) => {
1986
+ if (item.type === "text") {
1987
+ return item.text || "";
1988
+ } else if (item.type === "resource") {
1989
+ return `[Resource: ${item.resource?.uri || "unknown"}]`;
1990
+ } else if (item.type === "image") {
1991
+ return `[Image: ${item.mimeType || "unknown"}]`;
1992
+ } else if (item.type === "audio") {
1993
+ return `[Audio: ${item.mimeType || "unknown"}]`;
1994
+ } else {
1995
+ return JSON.stringify(item);
1996
+ }
1997
+ }).join("\n");
1998
+ }
1999
+ /**
2000
+ * Get all tools from all connected MCP clients
2001
+ * Optionally with namespace prefix
2002
+ */
2003
+ getAllTools(options = {}) {
2004
+ const allTools = [];
2005
+ for (const [clientName, client] of this.clients) {
2006
+ for (const tool of client.tools) {
2007
+ if (options.namespacePrefix) {
2008
+ allTools.push({
2009
+ ...tool,
2010
+ name: `${clientName}-${tool.name}`
2011
+ });
2012
+ } else {
2013
+ allTools.push(tool);
2014
+ }
2015
+ }
2016
+ }
2017
+ return allTools;
2018
+ }
2019
+ /**
2020
+ * Get tools from a specific client
2021
+ */
2022
+ getClientTools(clientName) {
2023
+ const client = this.clients.get(clientName);
2024
+ return client?.tools || [];
2025
+ }
2026
+ /**
2027
+ * Get list of connected MCP clients
2028
+ */
2029
+ listClients() {
2030
+ return Array.from(this.clients.entries()).map(([name, info]) => ({
2031
+ name,
2032
+ status: info.status,
2033
+ toolCount: info.tools.length,
2034
+ connectedAt: info.connectedAt,
2035
+ lastUsed: info.lastUsed,
2036
+ transportType: info.transportType
2037
+ }));
2038
+ }
2039
+ /**
2040
+ * Get client info by name
2041
+ */
2042
+ getClient(name) {
2043
+ return this.clients.get(name);
2044
+ }
2045
+ /**
2046
+ * Refresh tool lists from all connected clients
2047
+ */
2048
+ async refreshTools() {
2049
+ for (const [name, client] of this.clients) {
2050
+ try {
2051
+ const toolsResult = await client.client.request(
2052
+ {
2053
+ method: "tools/list",
2054
+ params: {}
2055
+ },
2056
+ import_types.ListToolsResultSchema
2057
+ );
2058
+ client.tools = toolsResult.tools.map((tool) => ({
2059
+ name: tool.name,
2060
+ description: tool.description || "",
2061
+ inputSchema: tool.inputSchema,
2062
+ serverName: name
2063
+ }));
2064
+ this.log(`Refreshed ${name}: ${client.tools.length} tools`);
2065
+ } catch (error) {
2066
+ this.log(`Failed to refresh tools for ${name}:`, error);
2067
+ client.status = "error";
2068
+ client.error = error instanceof Error ? error.message : String(error);
2069
+ }
2070
+ }
2071
+ }
2072
+ /**
2073
+ * List resources from a specific client or all clients
2074
+ */
2075
+ async listResources(clientName) {
2076
+ const results = [];
2077
+ const clients = clientName ? [clientName] : Array.from(this.clients.keys());
2078
+ for (const name of clients) {
2079
+ const client = this.clients.get(name);
2080
+ if (!client) continue;
2081
+ try {
2082
+ const result = await client.client.request(
2083
+ {
2084
+ method: "resources/list",
2085
+ params: {}
2086
+ },
2087
+ import_types.ListResourcesResultSchema
2088
+ );
2089
+ results.push({
2090
+ clientName: name,
2091
+ resources: result.resources
2092
+ });
2093
+ } catch (error) {
2094
+ this.log(`Failed to list resources for ${name}:`, error);
2095
+ }
2096
+ }
2097
+ return results;
2098
+ }
2099
+ /**
2100
+ * Read a resource from a specific client
2101
+ */
2102
+ async readResource(uri, clientName) {
2103
+ if (clientName) {
2104
+ const client = this.clients.get(clientName);
2105
+ if (!client) {
2106
+ throw new Error(`Client '${clientName}' not found`);
2107
+ }
2108
+ const result = await client.client.request(
2109
+ {
2110
+ method: "resources/read",
2111
+ params: { uri }
2112
+ },
2113
+ import_types.ReadResourceResultSchema
2114
+ );
2115
+ return result;
2116
+ } else {
2117
+ for (const [name, client] of this.clients) {
2118
+ try {
2119
+ const result = await client.client.request(
2120
+ {
2121
+ method: "resources/read",
2122
+ params: { uri }
2123
+ },
2124
+ import_types.ReadResourceResultSchema
2125
+ );
2126
+ return { clientName: name, ...result };
2127
+ } catch {
2128
+ }
2129
+ }
2130
+ throw new Error(`Resource '${uri}' not found on any client`);
2131
+ }
2132
+ }
2133
+ /**
2134
+ * List prompts from a specific client or all clients
2135
+ */
2136
+ async listPrompts(clientName) {
2137
+ const results = [];
2138
+ const clients = clientName ? [clientName] : Array.from(this.clients.keys());
2139
+ for (const name of clients) {
2140
+ const client = this.clients.get(name);
2141
+ if (!client) continue;
2142
+ try {
2143
+ const result = await client.client.request(
2144
+ {
2145
+ method: "prompts/list",
2146
+ params: {}
2147
+ },
2148
+ import_types.ListPromptsResultSchema
2149
+ );
2150
+ results.push({
2151
+ clientName: name,
2152
+ prompts: result.prompts
2153
+ });
2154
+ } catch (error) {
2155
+ this.log(`Failed to list prompts for ${name}:`, error);
2156
+ }
2157
+ }
2158
+ return results;
2159
+ }
2160
+ /**
2161
+ * Get a prompt from a specific client
2162
+ */
2163
+ async getPrompt(name, args, clientName) {
2164
+ if (clientName) {
2165
+ const client = this.clients.get(clientName);
2166
+ if (!client) {
2167
+ throw new Error(`Client '${clientName}' not found`);
2168
+ }
2169
+ const result = await client.client.request(
2170
+ {
2171
+ method: "prompts/get",
2172
+ params: { name, arguments: args }
2173
+ },
2174
+ import_types.GetPromptResultSchema
2175
+ );
2176
+ return result;
2177
+ } else {
2178
+ for (const [cName, client] of this.clients) {
2179
+ try {
2180
+ const result = await client.client.request(
2181
+ {
2182
+ method: "prompts/get",
2183
+ params: { name, arguments: args }
2184
+ },
2185
+ import_types.GetPromptResultSchema
2186
+ );
2187
+ return { clientName: cName, ...result };
2188
+ } catch {
2189
+ }
2190
+ }
2191
+ throw new Error(`Prompt '${name}' not found on any client`);
2192
+ }
2193
+ }
2194
+ /**
2195
+ * Health check for all connected clients
2196
+ */
2197
+ async healthCheck() {
2198
+ const results = /* @__PURE__ */ new Map();
2199
+ for (const [name, client] of this.clients) {
2200
+ try {
2201
+ const startTime = Date.now();
2202
+ await client.client.request(
2203
+ {
2204
+ method: "tools/list",
2205
+ params: {}
2206
+ },
2207
+ import_types.ListToolsResultSchema
2208
+ );
2209
+ results.set(name, {
2210
+ status: "healthy",
2211
+ responseTime: Date.now() - startTime
2212
+ });
2213
+ } catch (error) {
2214
+ results.set(name, {
2215
+ status: "unhealthy",
2216
+ responseTime: 0,
2217
+ error: error instanceof Error ? error.message : String(error)
2218
+ });
2219
+ if (this.options.autoReconnect) {
2220
+ this.scheduleReconnect(name, client.config);
2221
+ }
2222
+ }
2223
+ }
2224
+ return results;
2225
+ }
2226
+ /**
2227
+ * Start the automatic refresh timer
2228
+ */
2229
+ startRefreshTimer() {
2230
+ if (this.refreshTimer) {
2231
+ clearInterval(this.refreshTimer);
2232
+ }
2233
+ if (this.options.refreshInterval > 0) {
2234
+ this.refreshTimer = setInterval(async () => {
2235
+ try {
2236
+ await this.refreshTools();
2237
+ } catch (error) {
2238
+ this.log("Auto-refresh failed:", error);
2239
+ }
2240
+ }, this.options.refreshInterval);
2241
+ }
2242
+ }
2243
+ /**
2244
+ * Print available tools for a client
2245
+ */
2246
+ printAvailableTools(clientName, tools) {
2247
+ if (tools.length === 0) {
2248
+ this.log(` No tools available from ${clientName}`);
2249
+ return;
2250
+ }
2251
+ this.log(`
2252
+ --- ${clientName} Tools (${tools.length}) ---`);
2253
+ for (const tool of tools) {
2254
+ this.log(` \u2022 ${tool.name}: ${tool.description.slice(0, 60)}${tool.description.length > 60 ? "..." : ""}`);
2255
+ }
2256
+ }
2257
+ /**
2258
+ * Debug logging
2259
+ */
2260
+ log(...args) {
2261
+ if (this.options.debug) {
2262
+ console.log("[MCP-Proxy]", ...args);
2263
+ }
2264
+ }
2265
+ /**
2266
+ * Get statistics about the MCP proxy hub
2267
+ */
2268
+ getStats() {
2269
+ const clients = Array.from(this.clients.entries()).map(([name, info]) => ({
2270
+ name,
2271
+ toolCount: info.tools.length,
2272
+ status: info.status,
2273
+ transportType: info.transportType
2274
+ }));
2275
+ return {
2276
+ totalClients: this.clients.size,
2277
+ totalTools: clients.reduce((sum, c) => sum + c.toolCount, 0),
2278
+ clients
2279
+ };
2280
+ }
2281
+ };
2282
+ }
2283
+ });
2284
+
1714
2285
  // src/daemon/index.ts
1715
2286
  var daemon_exports = {};
1716
2287
  __export(daemon_exports, {
2288
+ MCPProxyHub: () => MCPProxyHub,
1717
2289
  RainfallDaemon: () => RainfallDaemon,
1718
2290
  getDaemonInstance: () => getDaemonInstance,
1719
2291
  getDaemonStatus: () => getDaemonStatus,
@@ -1746,17 +2318,19 @@ function getDaemonStatus() {
1746
2318
  function getDaemonInstance() {
1747
2319
  return daemonInstance;
1748
2320
  }
1749
- var import_ws, import_express, RainfallDaemon, daemonInstance;
2321
+ var import_ws2, import_express, RainfallDaemon, daemonInstance;
1750
2322
  var init_daemon = __esm({
1751
2323
  "src/daemon/index.ts"() {
1752
2324
  "use strict";
1753
2325
  init_cjs_shims();
1754
- import_ws = require("ws");
2326
+ import_ws2 = require("ws");
1755
2327
  import_express = __toESM(require("express"));
1756
2328
  init_sdk();
1757
2329
  init_networked();
1758
2330
  init_context();
1759
2331
  init_listeners();
2332
+ init_mcp_proxy();
2333
+ init_mcp_proxy();
1760
2334
  RainfallDaemon = class {
1761
2335
  wss;
1762
2336
  openaiApp;
@@ -1772,11 +2346,16 @@ var init_daemon = __esm({
1772
2346
  networkedExecutor;
1773
2347
  context;
1774
2348
  listeners;
2349
+ mcpProxy;
2350
+ enableMcpProxy;
2351
+ mcpNamespacePrefix;
1775
2352
  constructor(config = {}) {
1776
2353
  this.port = config.port || 8765;
1777
2354
  this.openaiPort = config.openaiPort || 8787;
1778
2355
  this.rainfallConfig = config.rainfallConfig;
1779
2356
  this.debug = config.debug || false;
2357
+ this.enableMcpProxy = config.enableMcpProxy ?? true;
2358
+ this.mcpNamespacePrefix = config.mcpNamespacePrefix ?? true;
1780
2359
  this.openaiApp = (0, import_express.default)();
1781
2360
  this.openaiApp.use(import_express.default.json());
1782
2361
  }
@@ -1812,6 +2391,19 @@ var init_daemon = __esm({
1812
2391
  this.networkedExecutor
1813
2392
  );
1814
2393
  await this.loadTools();
2394
+ if (this.enableMcpProxy) {
2395
+ this.mcpProxy = new MCPProxyHub({ debug: this.debug });
2396
+ await this.mcpProxy.initialize();
2397
+ if (this.rainfallConfig?.mcpClients) {
2398
+ for (const clientConfig of this.rainfallConfig.mcpClients) {
2399
+ try {
2400
+ await this.mcpProxy.connectClient(clientConfig);
2401
+ } catch (error) {
2402
+ this.log(`Failed to connect MCP client ${clientConfig.name}:`, error);
2403
+ }
2404
+ }
2405
+ }
2406
+ }
1815
2407
  await this.startWebSocketServer();
1816
2408
  await this.startOpenAIProxy();
1817
2409
  console.log(`\u{1F680} Rainfall daemon running`);
@@ -1832,6 +2424,10 @@ var init_daemon = __esm({
1832
2424
  if (this.networkedExecutor) {
1833
2425
  await this.networkedExecutor.unregisterEdgeNode();
1834
2426
  }
2427
+ if (this.mcpProxy) {
2428
+ await this.mcpProxy.shutdown();
2429
+ this.mcpProxy = void 0;
2430
+ }
1835
2431
  for (const client of this.clients) {
1836
2432
  client.close();
1837
2433
  }
@@ -1860,6 +2456,30 @@ var init_daemon = __esm({
1860
2456
  getListenerRegistry() {
1861
2457
  return this.listeners;
1862
2458
  }
2459
+ /**
2460
+ * Get the MCP Proxy Hub for managing external MCP clients
2461
+ */
2462
+ getMCPProxy() {
2463
+ return this.mcpProxy;
2464
+ }
2465
+ /**
2466
+ * Connect an MCP client dynamically
2467
+ */
2468
+ async connectMCPClient(config) {
2469
+ if (!this.mcpProxy) {
2470
+ throw new Error("MCP Proxy Hub is not enabled");
2471
+ }
2472
+ return this.mcpProxy.connectClient(config);
2473
+ }
2474
+ /**
2475
+ * Disconnect an MCP client
2476
+ */
2477
+ async disconnectMCPClient(name) {
2478
+ if (!this.mcpProxy) {
2479
+ throw new Error("MCP Proxy Hub is not enabled");
2480
+ }
2481
+ return this.mcpProxy.disconnectClient(name);
2482
+ }
1863
2483
  async initializeRainfall() {
1864
2484
  if (this.rainfallConfig?.apiKey) {
1865
2485
  this.rainfall = new Rainfall(this.rainfallConfig);
@@ -1900,7 +2520,7 @@ var init_daemon = __esm({
1900
2520
  }
1901
2521
  }
1902
2522
  async startWebSocketServer() {
1903
- this.wss = new import_ws.WebSocketServer({ port: this.port });
2523
+ this.wss = new import_ws2.WebSocketServer({ port: this.port });
1904
2524
  this.wss.on("connection", (ws) => {
1905
2525
  this.log("\u{1F7E2} MCP client connected");
1906
2526
  this.clients.add(ws);
@@ -1962,7 +2582,7 @@ var init_daemon = __esm({
1962
2582
  const toolParams = params?.arguments;
1963
2583
  try {
1964
2584
  const startTime = Date.now();
1965
- const result = await this.executeTool(toolName, toolParams);
2585
+ const result = await this.executeToolWithMCP(toolName, toolParams);
1966
2586
  const duration = Date.now() - startTime;
1967
2587
  if (this.context) {
1968
2588
  this.context.recordExecution(toolName, toolParams || {}, result, { duration });
@@ -2027,6 +2647,16 @@ var init_daemon = __esm({
2027
2647
  });
2028
2648
  }
2029
2649
  }
2650
+ if (this.mcpProxy) {
2651
+ const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
2652
+ for (const tool of proxyTools) {
2653
+ mcpTools.push({
2654
+ name: this.mcpNamespacePrefix ? `${tool.serverName}-${tool.name}` : tool.name,
2655
+ description: tool.description,
2656
+ inputSchema: tool.inputSchema || { type: "object", properties: {} }
2657
+ });
2658
+ }
2659
+ }
2030
2660
  return mcpTools;
2031
2661
  }
2032
2662
  async executeTool(toolId, params) {
@@ -2035,6 +2665,30 @@ var init_daemon = __esm({
2035
2665
  }
2036
2666
  return this.rainfall.executeTool(toolId, params);
2037
2667
  }
2668
+ /**
2669
+ * Execute a tool, trying MCP proxy first, then falling back to Rainfall tools
2670
+ */
2671
+ async executeToolWithMCP(toolName, params) {
2672
+ if (this.mcpProxy) {
2673
+ try {
2674
+ if (this.mcpNamespacePrefix && toolName.includes("-")) {
2675
+ const namespace = toolName.split("-")[0];
2676
+ const actualToolName = toolName.slice(namespace.length + 1);
2677
+ if (this.mcpProxy.getClient(namespace)) {
2678
+ return await this.mcpProxy.callTool(toolName, params || {}, {
2679
+ namespace
2680
+ });
2681
+ }
2682
+ }
2683
+ return await this.mcpProxy.callTool(toolName, params || {});
2684
+ } catch (error) {
2685
+ if (error instanceof Error && !error.message.includes("not found")) {
2686
+ throw error;
2687
+ }
2688
+ }
2689
+ }
2690
+ return this.executeTool(toolName, params);
2691
+ }
2038
2692
  async startOpenAIProxy() {
2039
2693
  this.openaiApp.get("/v1/models", async (_req, res) => {
2040
2694
  try {
@@ -2150,6 +2804,10 @@ var init_daemon = __esm({
2150
2804
  this.log(` \u2192 Executing locally`);
2151
2805
  const args = JSON.parse(toolArgsStr);
2152
2806
  toolResult = await this.executeLocalTool(localTool.id, args);
2807
+ } else if (this.mcpProxy) {
2808
+ this.log(` \u2192 Trying MCP proxy`);
2809
+ const args = JSON.parse(toolArgsStr);
2810
+ toolResult = await this.executeToolWithMCP(toolName.replace(/_/g, "-"), args);
2153
2811
  } else {
2154
2812
  const shouldExecuteLocal = body.tool_priority === "local" || body.tool_priority === "stacked";
2155
2813
  if (shouldExecuteLocal) {
@@ -2199,15 +2857,52 @@ var init_daemon = __esm({
2199
2857
  }
2200
2858
  });
2201
2859
  this.openaiApp.get("/health", (_req, res) => {
2860
+ const mcpStats = this.mcpProxy?.getStats();
2202
2861
  res.json({
2203
2862
  status: "ok",
2204
2863
  daemon: "rainfall",
2205
- version: "0.1.0",
2864
+ version: "0.2.0",
2206
2865
  tools_loaded: this.tools.length,
2866
+ mcp_clients: mcpStats?.totalClients || 0,
2867
+ mcp_tools: mcpStats?.totalTools || 0,
2207
2868
  edge_node_id: this.networkedExecutor?.getEdgeNodeId(),
2208
2869
  clients_connected: this.clients.size
2209
2870
  });
2210
2871
  });
2872
+ this.openaiApp.get("/v1/mcp/clients", (_req, res) => {
2873
+ if (!this.mcpProxy) {
2874
+ res.status(503).json({ error: "MCP proxy not enabled" });
2875
+ return;
2876
+ }
2877
+ res.json(this.mcpProxy.listClients());
2878
+ });
2879
+ this.openaiApp.post("/v1/mcp/connect", async (req, res) => {
2880
+ if (!this.mcpProxy) {
2881
+ res.status(503).json({ error: "MCP proxy not enabled" });
2882
+ return;
2883
+ }
2884
+ try {
2885
+ const name = await this.mcpProxy.connectClient(req.body);
2886
+ res.json({ success: true, client: name });
2887
+ } catch (error) {
2888
+ res.status(500).json({
2889
+ error: error instanceof Error ? error.message : "Failed to connect MCP client"
2890
+ });
2891
+ }
2892
+ });
2893
+ this.openaiApp.post("/v1/mcp/disconnect", async (req, res) => {
2894
+ if (!this.mcpProxy) {
2895
+ res.status(503).json({ error: "MCP proxy not enabled" });
2896
+ return;
2897
+ }
2898
+ const { name } = req.body;
2899
+ if (!name) {
2900
+ res.status(400).json({ error: "Missing required field: name" });
2901
+ return;
2902
+ }
2903
+ await this.mcpProxy.disconnectClient(name);
2904
+ res.json({ success: true });
2905
+ });
2211
2906
  this.openaiApp.get("/status", (_req, res) => {
2212
2907
  res.json(this.getStatus());
2213
2908
  });
@@ -2342,6 +3037,7 @@ var init_daemon = __esm({
2342
3037
  switch (provider) {
2343
3038
  case "local":
2344
3039
  case "ollama":
3040
+ case "custom":
2345
3041
  return this.callLocalLLM(params, config);
2346
3042
  case "openai":
2347
3043
  case "anthropic":
@@ -2378,7 +3074,8 @@ var init_daemon = __esm({
2378
3074
  method: "POST",
2379
3075
  headers: {
2380
3076
  "Content-Type": "application/json",
2381
- "Authorization": `Bearer ${apiKey}`
3077
+ "Authorization": `Bearer ${apiKey}`,
3078
+ "User-Agent": "Rainfall-DevKit/1.0"
2382
3079
  },
2383
3080
  body: JSON.stringify({
2384
3081
  model,
@@ -2409,7 +3106,8 @@ var init_daemon = __esm({
2409
3106
  method: "POST",
2410
3107
  headers: {
2411
3108
  "Content-Type": "application/json",
2412
- "Authorization": `Bearer ${apiKey}`
3109
+ "Authorization": `Bearer ${apiKey}`,
3110
+ "User-Agent": "Rainfall-DevKit/1.0"
2413
3111
  },
2414
3112
  body: JSON.stringify({
2415
3113
  model,
@@ -2490,7 +3188,7 @@ var init_daemon = __esm({
2490
3188
  }
2491
3189
  async getOpenAITools() {
2492
3190
  const tools = [];
2493
- for (const tool of this.tools.slice(0, 128)) {
3191
+ for (const tool of this.tools.slice(0, 100)) {
2494
3192
  const schema = await this.getToolSchema(tool.id);
2495
3193
  if (schema) {
2496
3194
  const toolSchema = schema;
@@ -2514,6 +3212,24 @@ var init_daemon = __esm({
2514
3212
  });
2515
3213
  }
2516
3214
  }
3215
+ if (this.mcpProxy) {
3216
+ const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
3217
+ for (const tool of proxyTools.slice(0, 28)) {
3218
+ const inputSchema = tool.inputSchema || {};
3219
+ tools.push({
3220
+ type: "function",
3221
+ function: {
3222
+ name: this.mcpNamespacePrefix ? `${tool.serverName}_${tool.name}`.replace(/-/g, "_") : tool.name.replace(/-/g, "_"),
3223
+ description: `[${tool.serverName}] ${tool.description}`,
3224
+ parameters: {
3225
+ type: "object",
3226
+ properties: inputSchema.properties || {},
3227
+ required: inputSchema.required || []
3228
+ }
3229
+ }
3230
+ });
3231
+ }
3232
+ }
2517
3233
  return tools;
2518
3234
  }
2519
3235
  buildResponseContent() {
@@ -2559,6 +3275,274 @@ var import_url = require("url");
2559
3275
  init_sdk();
2560
3276
  init_config();
2561
3277
  var import_child_process = require("child_process");
3278
+
3279
+ // src/security/edge-node.ts
3280
+ init_cjs_shims();
3281
+ var sodium = __toESM(require("libsodium-wrappers"));
3282
+ var EdgeNodeSecurity = class {
3283
+ sodiumReady;
3284
+ backendSecret;
3285
+ keyPair;
3286
+ constructor(options = {}) {
3287
+ this.sodiumReady = sodium.ready;
3288
+ this.backendSecret = options.backendSecret;
3289
+ this.keyPair = options.keyPair;
3290
+ }
3291
+ /**
3292
+ * Initialize libsodium
3293
+ */
3294
+ async initialize() {
3295
+ await this.sodiumReady;
3296
+ }
3297
+ // ============================================================================
3298
+ // JWT Token Management
3299
+ // ============================================================================
3300
+ /**
3301
+ * Generate a JWT token for an edge node
3302
+ * Note: In production, this is done by the backend. This is for testing.
3303
+ */
3304
+ generateJWT(edgeNodeId, subscriberId, expiresInDays = 30) {
3305
+ if (!this.backendSecret) {
3306
+ throw new Error("Backend secret not configured");
3307
+ }
3308
+ const now = Math.floor(Date.now() / 1e3);
3309
+ const exp = now + expiresInDays * 24 * 60 * 60;
3310
+ const jti = this.generateTokenId();
3311
+ const payload = {
3312
+ sub: edgeNodeId,
3313
+ iss: "rainfall-backend",
3314
+ iat: now,
3315
+ exp,
3316
+ jti,
3317
+ scope: ["edge:heartbeat", "edge:claim", "edge:submit", "edge:queue"]
3318
+ };
3319
+ const header = { alg: "HS256", typ: "JWT" };
3320
+ const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
3321
+ const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
3322
+ const signature = this.hmacSha256(
3323
+ `${encodedHeader}.${encodedPayload}`,
3324
+ this.backendSecret
3325
+ );
3326
+ const encodedSignature = this.base64UrlEncode(signature);
3327
+ return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
3328
+ }
3329
+ /**
3330
+ * Validate a JWT token
3331
+ */
3332
+ validateJWT(token) {
3333
+ const parts = token.split(".");
3334
+ if (parts.length !== 3) {
3335
+ throw new Error("Invalid JWT format");
3336
+ }
3337
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
3338
+ if (this.backendSecret) {
3339
+ const expectedSignature = this.hmacSha256(
3340
+ `${encodedHeader}.${encodedPayload}`,
3341
+ this.backendSecret
3342
+ );
3343
+ const expectedEncoded = this.base64UrlEncode(expectedSignature);
3344
+ if (!this.timingSafeEqual(encodedSignature, expectedEncoded)) {
3345
+ throw new Error("Invalid JWT signature");
3346
+ }
3347
+ }
3348
+ const payload = JSON.parse(this.base64UrlDecode(encodedPayload));
3349
+ const now = Math.floor(Date.now() / 1e3);
3350
+ if (payload.exp < now) {
3351
+ throw new Error("JWT token expired");
3352
+ }
3353
+ if (payload.iss !== "rainfall-backend") {
3354
+ throw new Error("Invalid JWT issuer");
3355
+ }
3356
+ return {
3357
+ edgeNodeId: payload.sub,
3358
+ subscriberId: payload.sub,
3359
+ // Same as edge node ID for now
3360
+ scopes: payload.scope,
3361
+ expiresAt: payload.exp
3362
+ };
3363
+ }
3364
+ /**
3365
+ * Extract bearer token from Authorization header
3366
+ */
3367
+ extractBearerToken(authHeader) {
3368
+ if (!authHeader) return null;
3369
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
3370
+ return match ? match[1] : null;
3371
+ }
3372
+ // ============================================================================
3373
+ // ACL Enforcement
3374
+ // ============================================================================
3375
+ /**
3376
+ * Check if an edge node is allowed to perform an action on a job
3377
+ * Rule: Edge nodes can only access jobs for their own subscriber
3378
+ */
3379
+ checkACL(check) {
3380
+ if (check.subscriberId !== check.jobSubscriberId) {
3381
+ return {
3382
+ allowed: false,
3383
+ reason: `Edge node ${check.edgeNodeId} cannot access jobs from subscriber ${check.jobSubscriberId}`
3384
+ };
3385
+ }
3386
+ const allowedActions = ["heartbeat", "claim", "submit", "queue"];
3387
+ if (!allowedActions.includes(check.action)) {
3388
+ return {
3389
+ allowed: false,
3390
+ reason: `Unknown action: ${check.action}`
3391
+ };
3392
+ }
3393
+ return { allowed: true };
3394
+ }
3395
+ /**
3396
+ * Middleware-style ACL check for job operations
3397
+ */
3398
+ requireSameSubscriber(edgeNodeSubscriberId, jobSubscriberId, operation) {
3399
+ const result = this.checkACL({
3400
+ edgeNodeId: edgeNodeSubscriberId,
3401
+ subscriberId: edgeNodeSubscriberId,
3402
+ jobSubscriberId,
3403
+ action: operation
3404
+ });
3405
+ if (!result.allowed) {
3406
+ throw new Error(result.reason);
3407
+ }
3408
+ }
3409
+ // ============================================================================
3410
+ // Encryption (Libsodium)
3411
+ // ============================================================================
3412
+ /**
3413
+ * Generate a new Ed25519 key pair for an edge node
3414
+ */
3415
+ async generateKeyPair() {
3416
+ await this.sodiumReady;
3417
+ const keyPair = sodium.crypto_box_keypair();
3418
+ return {
3419
+ publicKey: this.bytesToBase64(keyPair.publicKey),
3420
+ privateKey: this.bytesToBase64(keyPair.privateKey)
3421
+ };
3422
+ }
3423
+ /**
3424
+ * Encrypt job parameters for a target edge node using its public key
3425
+ */
3426
+ async encryptForEdgeNode(plaintext, targetPublicKeyBase64) {
3427
+ await this.sodiumReady;
3428
+ if (!this.keyPair) {
3429
+ throw new Error("Local key pair not configured");
3430
+ }
3431
+ const targetPublicKey = this.base64ToBytes(targetPublicKeyBase64);
3432
+ const ephemeralKeyPair = sodium.crypto_box_keypair();
3433
+ const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
3434
+ const message = new TextEncoder().encode(plaintext);
3435
+ const ciphertext = sodium.crypto_box_easy(
3436
+ message,
3437
+ nonce,
3438
+ targetPublicKey,
3439
+ ephemeralKeyPair.privateKey
3440
+ );
3441
+ return {
3442
+ ciphertext: this.bytesToBase64(ciphertext),
3443
+ nonce: this.bytesToBase64(nonce),
3444
+ ephemeralPublicKey: this.bytesToBase64(ephemeralKeyPair.publicKey)
3445
+ };
3446
+ }
3447
+ /**
3448
+ * Decrypt job parameters received from the backend
3449
+ */
3450
+ async decryptFromBackend(encrypted) {
3451
+ await this.sodiumReady;
3452
+ if (!this.keyPair) {
3453
+ throw new Error("Local key pair not configured");
3454
+ }
3455
+ const privateKey = this.base64ToBytes(this.keyPair.privateKey);
3456
+ const ephemeralPublicKey = this.base64ToBytes(encrypted.ephemeralPublicKey);
3457
+ const nonce = this.base64ToBytes(encrypted.nonce);
3458
+ const ciphertext = this.base64ToBytes(encrypted.ciphertext);
3459
+ const decrypted = sodium.crypto_box_open_easy(
3460
+ ciphertext,
3461
+ nonce,
3462
+ ephemeralPublicKey,
3463
+ privateKey
3464
+ );
3465
+ if (!decrypted) {
3466
+ throw new Error("Decryption failed - invalid ciphertext or keys");
3467
+ }
3468
+ return new TextDecoder().decode(decrypted);
3469
+ }
3470
+ /**
3471
+ * Encrypt job parameters for local storage (using secretbox)
3472
+ */
3473
+ async encryptLocal(plaintext, key) {
3474
+ await this.sodiumReady;
3475
+ const keyBytes = this.deriveKey(key);
3476
+ const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
3477
+ const message = new TextEncoder().encode(plaintext);
3478
+ const ciphertext = sodium.crypto_secretbox_easy(message, nonce, keyBytes);
3479
+ return {
3480
+ ciphertext: this.bytesToBase64(ciphertext),
3481
+ nonce: this.bytesToBase64(nonce)
3482
+ };
3483
+ }
3484
+ /**
3485
+ * Decrypt locally stored job parameters
3486
+ */
3487
+ async decryptLocal(encrypted, key) {
3488
+ await this.sodiumReady;
3489
+ const keyBytes = this.deriveKey(key);
3490
+ const nonce = this.base64ToBytes(encrypted.nonce);
3491
+ const ciphertext = this.base64ToBytes(encrypted.ciphertext);
3492
+ const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, keyBytes);
3493
+ if (!decrypted) {
3494
+ throw new Error("Local decryption failed");
3495
+ }
3496
+ return new TextDecoder().decode(decrypted);
3497
+ }
3498
+ // ============================================================================
3499
+ // Utility Methods
3500
+ // ============================================================================
3501
+ generateTokenId() {
3502
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
3503
+ }
3504
+ base64UrlEncode(str) {
3505
+ return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3506
+ }
3507
+ base64UrlDecode(str) {
3508
+ const padding = "=".repeat((4 - str.length % 4) % 4);
3509
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/") + padding;
3510
+ return atob(base64);
3511
+ }
3512
+ hmacSha256(message, secret) {
3513
+ const key = new TextEncoder().encode(secret);
3514
+ const msg = new TextEncoder().encode(message);
3515
+ const hash = sodium.crypto_auth(msg, key);
3516
+ return this.bytesToBase64(hash);
3517
+ }
3518
+ timingSafeEqual(a, b) {
3519
+ if (a.length !== b.length) return false;
3520
+ let result = 0;
3521
+ for (let i = 0; i < a.length; i++) {
3522
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
3523
+ }
3524
+ return result === 0;
3525
+ }
3526
+ bytesToBase64(bytes) {
3527
+ const binString = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
3528
+ return btoa(binString);
3529
+ }
3530
+ base64ToBytes(base64) {
3531
+ const binString = atob(base64);
3532
+ return Uint8Array.from(binString, (m) => m.charCodeAt(0));
3533
+ }
3534
+ deriveKey(password) {
3535
+ const passwordBytes = new TextEncoder().encode(password);
3536
+ return sodium.crypto_generichash(32, passwordBytes, null);
3537
+ }
3538
+ };
3539
+ async function createEdgeNodeSecurity(options = {}) {
3540
+ const security = new EdgeNodeSecurity(options);
3541
+ await security.initialize();
3542
+ return security;
3543
+ }
3544
+
3545
+ // src/cli/index.ts
2562
3546
  function printHelp() {
2563
3547
  console.log(`
2564
3548
  Rainfall CLI - 200+ tools, one key
@@ -2591,6 +3575,9 @@ Commands:
2591
3575
  config set <key> <value> Set configuration value
2592
3576
  config llm Show LLM configuration
2593
3577
 
3578
+ edge generate-keys Generate key pair for edge node encryption
3579
+ edge status Show edge node security status
3580
+
2594
3581
  version Show version information
2595
3582
  upgrade Upgrade to the latest version
2596
3583
 
@@ -2611,6 +3598,9 @@ Options for 'run':
2611
3598
  Options for 'daemon start':
2612
3599
  --port <port> WebSocket port (default: 8765)
2613
3600
  --openai-port <port> OpenAI API port (default: 8787)
3601
+ --mcp-proxy Enable MCP proxy hub (default: enabled)
3602
+ --no-mcp-proxy Disable MCP proxy hub
3603
+ --secure Enable edge node security (JWT, ACLs, encryption)
2614
3604
  --debug Enable verbose debug logging
2615
3605
 
2616
3606
  Examples:
@@ -2990,10 +3980,13 @@ function configLLM() {
2990
3980
  console.log(" anthropic - Use Anthropic API directly");
2991
3981
  console.log(" ollama - Use local Ollama instance");
2992
3982
  console.log(" local - Use any OpenAI-compatible endpoint (LM Studio, etc.)");
3983
+ console.log(" custom - Use any custom OpenAI-compatible endpoint (RunPod, etc.)");
2993
3984
  console.log();
2994
3985
  console.log("Examples:");
2995
3986
  console.log(" rainfall config set llm.provider local");
2996
3987
  console.log(" rainfall config set llm.baseUrl http://localhost:1234/v1");
3988
+ console.log(" rainfall config set llm.provider custom");
3989
+ console.log(" rainfall config set llm.baseUrl https://your-runpod-endpoint.runpod.net/v1");
2997
3990
  console.log(" rainfall config set llm.provider openai");
2998
3991
  console.log(" rainfall config set llm.apiKey sk-...");
2999
3992
  }
@@ -3051,6 +4044,7 @@ async function daemonStart(args) {
3051
4044
  let port;
3052
4045
  let openaiPort;
3053
4046
  let debug = false;
4047
+ let enableMcpProxy = true;
3054
4048
  for (let i = 0; i < args.length; i++) {
3055
4049
  const arg = args[i];
3056
4050
  if (arg === "--port") {
@@ -3061,11 +4055,15 @@ async function daemonStart(args) {
3061
4055
  if (!isNaN(val)) openaiPort = val;
3062
4056
  } else if (arg === "--debug") {
3063
4057
  debug = true;
4058
+ } else if (arg === "--mcp-proxy") {
4059
+ enableMcpProxy = true;
4060
+ } else if (arg === "--no-mcp-proxy") {
4061
+ enableMcpProxy = false;
3064
4062
  }
3065
4063
  }
3066
4064
  const { startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
3067
4065
  try {
3068
- await startDaemon2({ port, openaiPort, debug });
4066
+ await startDaemon2({ port, openaiPort, debug, enableMcpProxy });
3069
4067
  process.on("SIGINT", async () => {
3070
4068
  console.log("\n");
3071
4069
  const { stopDaemon: stopDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
@@ -3137,6 +4135,8 @@ async function daemonStatus() {
3137
4135
  console.log(` WebSocket port: ${status.port}`);
3138
4136
  console.log(` OpenAI API port: ${status.openaiPort}`);
3139
4137
  console.log(` Tools loaded: ${status.toolsLoaded}`);
4138
+ console.log(` MCP clients: ${status.mcpClients || 0}`);
4139
+ console.log(` MCP tools: ${status.mcpTools || 0}`);
3140
4140
  console.log(` Clients connected: ${status.clientsConnected}`);
3141
4141
  console.log(` Edge Node ID: ${status.edgeNodeId || "local"}`);
3142
4142
  console.log();
@@ -3177,6 +4177,71 @@ async function workflowRun(args) {
3177
4177
  console.log(`\u{1F6A7} Running workflow: ${workflowId}`);
3178
4178
  console.log("Workflow execution coming soon!");
3179
4179
  }
4180
+ async function edgeGenerateKeys() {
4181
+ console.log("\u{1F510} Generating edge node key pair...\n");
4182
+ try {
4183
+ const security = await createEdgeNodeSecurity();
4184
+ const keyPair = await security.generateKeyPair();
4185
+ const configDir = getConfigDir();
4186
+ const keysDir = (0, import_path2.join)(configDir, "keys");
4187
+ if (!(0, import_fs2.existsSync)(keysDir)) {
4188
+ (0, import_fs2.mkdirSync)(keysDir, { recursive: true });
4189
+ }
4190
+ const publicKeyPath = (0, import_path2.join)(keysDir, "edge-node.pub");
4191
+ const privateKeyPath = (0, import_path2.join)(keysDir, "edge-node.key");
4192
+ (0, import_fs2.writeFileSync)(publicKeyPath, keyPair.publicKey, { mode: 420 });
4193
+ (0, import_fs2.writeFileSync)(privateKeyPath, keyPair.privateKey, { mode: 384 });
4194
+ console.log("\u2705 Key pair generated successfully!\n");
4195
+ console.log("Public key:", keyPair.publicKey);
4196
+ console.log("\nKey files saved to:");
4197
+ console.log(" Public:", publicKeyPath);
4198
+ console.log(" Private:", privateKeyPath);
4199
+ console.log("\n\u{1F4CB} To register this edge node:");
4200
+ console.log(" 1. Copy the public key above");
4201
+ console.log(" 2. Register with: rainfall edge register <public-key>");
4202
+ console.log(" 3. The backend will return an edgeNodeSecret (JWT)");
4203
+ console.log(" 4. Store the secret securely - it expires in 30 days");
4204
+ } catch (error) {
4205
+ console.error("\u274C Failed to generate keys:", error instanceof Error ? error.message : error);
4206
+ process.exit(1);
4207
+ }
4208
+ }
4209
+ async function edgeStatus() {
4210
+ const configDir = getConfigDir();
4211
+ const keysDir = (0, import_path2.join)(configDir, "keys");
4212
+ const publicKeyPath = (0, import_path2.join)(keysDir, "edge-node.pub");
4213
+ const privateKeyPath = (0, import_path2.join)(keysDir, "edge-node.key");
4214
+ console.log("\u{1F510} Edge Node Security Status\n");
4215
+ const hasPublicKey = (0, import_fs2.existsSync)(publicKeyPath);
4216
+ const hasPrivateKey = (0, import_fs2.existsSync)(privateKeyPath);
4217
+ console.log("Key Pair:");
4218
+ console.log(" Public key:", hasPublicKey ? "\u2705 Present" : "\u274C Missing");
4219
+ console.log(" Private key:", hasPrivateKey ? "\u2705 Present" : "\u274C Missing");
4220
+ if (hasPublicKey) {
4221
+ const publicKey = (0, import_fs2.readFileSync)(publicKeyPath, "utf-8");
4222
+ console.log("\nPublic Key:");
4223
+ console.log(" " + publicKey.substring(0, 50) + "...");
4224
+ }
4225
+ const config = loadConfig();
4226
+ if (config.edgeNodeId) {
4227
+ console.log("\nRegistration:");
4228
+ console.log(" Edge Node ID:", config.edgeNodeId);
4229
+ }
4230
+ if (config.edgeNodeSecret) {
4231
+ console.log(" JWT Secret: \u2705 Present (expires: check with backend)");
4232
+ } else {
4233
+ console.log(" JWT Secret: \u274C Not configured");
4234
+ }
4235
+ console.log("\n\u{1F4DA} Next steps:");
4236
+ if (!hasPublicKey) {
4237
+ console.log(" 1. Run: rainfall edge generate-keys");
4238
+ } else if (!config.edgeNodeSecret) {
4239
+ console.log(" 1. Register your edge node with the backend");
4240
+ console.log(" 2. Store the returned edgeNodeSecret in config");
4241
+ } else {
4242
+ console.log(" Edge node is configured and ready for secure operation");
4243
+ }
4244
+ }
3180
4245
  async function main() {
3181
4246
  const args = process.argv.slice(2);
3182
4247
  const command = args[0];
@@ -3282,6 +4347,20 @@ async function main() {
3282
4347
  case "upgrade":
3283
4348
  await upgrade();
3284
4349
  break;
4350
+ case "edge":
4351
+ switch (subcommand) {
4352
+ case "generate-keys":
4353
+ await edgeGenerateKeys();
4354
+ break;
4355
+ case "status":
4356
+ await edgeStatus();
4357
+ break;
4358
+ default:
4359
+ console.error("Error: Unknown edge subcommand");
4360
+ console.error("\nUsage: rainfall edge <generate-keys|status>");
4361
+ process.exit(1);
4362
+ }
4363
+ break;
3285
4364
  case "help":
3286
4365
  case "--help":
3287
4366
  case "-h":