@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.
@@ -40,12 +40,16 @@ var init_cjs_shims = __esm({
40
40
  // src/cli/config.ts
41
41
  var config_exports = {};
42
42
  __export(config_exports, {
43
+ getConfigDir: () => getConfigDir,
43
44
  getLLMConfig: () => getLLMConfig,
44
45
  getProviderBaseUrl: () => getProviderBaseUrl,
45
46
  isLocalProvider: () => isLocalProvider,
46
47
  loadConfig: () => loadConfig,
47
48
  saveConfig: () => saveConfig
48
49
  });
50
+ function getConfigDir() {
51
+ return CONFIG_DIR;
52
+ }
49
53
  function loadConfig() {
50
54
  let config = {};
51
55
  if ((0, import_fs.existsSync)(CONFIG_FILE)) {
@@ -110,6 +114,7 @@ function getProviderBaseUrl(config) {
110
114
  case "ollama":
111
115
  return config.llm?.baseUrl || "http://localhost:11434/v1";
112
116
  case "local":
117
+ case "custom":
113
118
  return config.llm?.baseUrl || "http://localhost:1234/v1";
114
119
  case "rainfall":
115
120
  default:
@@ -132,6 +137,7 @@ var init_config = __esm({
132
137
  // src/daemon/index.ts
133
138
  var daemon_exports = {};
134
139
  __export(daemon_exports, {
140
+ MCPProxyHub: () => MCPProxyHub,
135
141
  RainfallDaemon: () => RainfallDaemon,
136
142
  getDaemonInstance: () => getDaemonInstance,
137
143
  getDaemonStatus: () => getDaemonStatus,
@@ -140,7 +146,7 @@ __export(daemon_exports, {
140
146
  });
141
147
  module.exports = __toCommonJS(daemon_exports);
142
148
  init_cjs_shims();
143
- var import_ws = require("ws");
149
+ var import_ws2 = require("ws");
144
150
  var import_express = __toESM(require("express"));
145
151
 
146
152
  // src/sdk.ts
@@ -1645,6 +1651,566 @@ var RainfallListenerRegistry = class {
1645
1651
  }
1646
1652
  };
1647
1653
 
1654
+ // src/services/mcp-proxy.ts
1655
+ init_cjs_shims();
1656
+ var import_ws = require("ws");
1657
+ var import_client2 = require("@modelcontextprotocol/sdk/client/index.js");
1658
+ var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
1659
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
1660
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
1661
+ var MCPProxyHub = class {
1662
+ clients = /* @__PURE__ */ new Map();
1663
+ options;
1664
+ refreshTimer;
1665
+ reconnectTimeouts = /* @__PURE__ */ new Map();
1666
+ requestId = 0;
1667
+ constructor(options = {}) {
1668
+ this.options = {
1669
+ debug: options.debug ?? false,
1670
+ autoReconnect: options.autoReconnect ?? true,
1671
+ reconnectDelay: options.reconnectDelay ?? 5e3,
1672
+ toolTimeout: options.toolTimeout ?? 3e4,
1673
+ refreshInterval: options.refreshInterval ?? 3e4
1674
+ };
1675
+ }
1676
+ /**
1677
+ * Initialize the MCP proxy hub
1678
+ */
1679
+ async initialize() {
1680
+ this.log("\u{1F50C} Initializing MCP Proxy Hub...");
1681
+ this.startRefreshTimer();
1682
+ this.log("\u2705 MCP Proxy Hub initialized");
1683
+ }
1684
+ /**
1685
+ * Shutdown the MCP proxy hub and disconnect all clients
1686
+ */
1687
+ async shutdown() {
1688
+ this.log("\u{1F6D1} Shutting down MCP Proxy Hub...");
1689
+ if (this.refreshTimer) {
1690
+ clearInterval(this.refreshTimer);
1691
+ this.refreshTimer = void 0;
1692
+ }
1693
+ for (const timeout of this.reconnectTimeouts.values()) {
1694
+ clearTimeout(timeout);
1695
+ }
1696
+ this.reconnectTimeouts.clear();
1697
+ const disconnectPromises = Array.from(this.clients.entries()).map(
1698
+ async ([name, client]) => {
1699
+ try {
1700
+ await this.disconnectClient(name);
1701
+ } catch (error) {
1702
+ this.log(`Error disconnecting ${name}:`, error);
1703
+ }
1704
+ }
1705
+ );
1706
+ await Promise.allSettled(disconnectPromises);
1707
+ this.clients.clear();
1708
+ this.log("\u{1F44B} MCP Proxy Hub shut down");
1709
+ }
1710
+ /**
1711
+ * Connect to an MCP server
1712
+ */
1713
+ async connectClient(config) {
1714
+ const { name, transport } = config;
1715
+ if (this.clients.has(name)) {
1716
+ this.log(`Reconnecting client: ${name}`);
1717
+ await this.disconnectClient(name);
1718
+ }
1719
+ this.log(`Connecting to MCP server: ${name} (${transport})...`);
1720
+ try {
1721
+ const client = new import_client2.Client(
1722
+ {
1723
+ name: `rainfall-daemon-${name}`,
1724
+ version: "0.2.0"
1725
+ },
1726
+ {
1727
+ capabilities: {}
1728
+ }
1729
+ );
1730
+ let lastErrorTime = 0;
1731
+ client.onerror = (error) => {
1732
+ const now = Date.now();
1733
+ if (now - lastErrorTime > 5e3) {
1734
+ this.log(`MCP Server Error (${name}):`, error.message);
1735
+ lastErrorTime = now;
1736
+ }
1737
+ if (this.options.autoReconnect) {
1738
+ this.scheduleReconnect(name, config);
1739
+ }
1740
+ };
1741
+ let transportInstance;
1742
+ if (transport === "stdio" && config.command) {
1743
+ const env = {};
1744
+ for (const [key, value] of Object.entries({ ...process.env, ...config.env })) {
1745
+ if (value !== void 0) {
1746
+ env[key] = value;
1747
+ }
1748
+ }
1749
+ transportInstance = new import_stdio.StdioClientTransport({
1750
+ command: config.command,
1751
+ args: config.args,
1752
+ env
1753
+ });
1754
+ } else if (transport === "http" && config.url) {
1755
+ transportInstance = new import_streamableHttp.StreamableHTTPClientTransport(
1756
+ new URL(config.url),
1757
+ {
1758
+ requestInit: {
1759
+ headers: config.headers
1760
+ }
1761
+ }
1762
+ );
1763
+ } else if (transport === "websocket" && config.url) {
1764
+ transportInstance = new import_ws.WebSocket(config.url);
1765
+ await new Promise((resolve, reject) => {
1766
+ transportInstance.on("open", () => resolve());
1767
+ transportInstance.on("error", reject);
1768
+ setTimeout(() => reject(new Error("WebSocket connection timeout")), 1e4);
1769
+ });
1770
+ } else {
1771
+ throw new Error(`Invalid transport configuration for ${name}`);
1772
+ }
1773
+ await client.connect(transportInstance);
1774
+ const toolsResult = await client.request(
1775
+ {
1776
+ method: "tools/list",
1777
+ params: {}
1778
+ },
1779
+ import_types.ListToolsResultSchema
1780
+ );
1781
+ const tools = toolsResult.tools.map((tool) => ({
1782
+ name: tool.name,
1783
+ description: tool.description || "",
1784
+ inputSchema: tool.inputSchema,
1785
+ serverName: name
1786
+ }));
1787
+ const clientInfo = {
1788
+ name,
1789
+ client,
1790
+ transport: transportInstance,
1791
+ transportType: transport,
1792
+ tools,
1793
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1794
+ lastUsed: (/* @__PURE__ */ new Date()).toISOString(),
1795
+ config,
1796
+ status: "connected"
1797
+ };
1798
+ this.clients.set(name, clientInfo);
1799
+ this.log(`\u2705 Connected to ${name} (${tools.length} tools)`);
1800
+ this.printAvailableTools(name, tools);
1801
+ return name;
1802
+ } catch (error) {
1803
+ const errorMessage = error instanceof Error ? error.message : String(error);
1804
+ this.log(`\u274C Failed to connect to ${name}:`, errorMessage);
1805
+ if (this.options.autoReconnect) {
1806
+ this.scheduleReconnect(name, config);
1807
+ }
1808
+ throw error;
1809
+ }
1810
+ }
1811
+ /**
1812
+ * Disconnect a specific MCP client
1813
+ */
1814
+ async disconnectClient(name) {
1815
+ const client = this.clients.get(name);
1816
+ if (!client) return;
1817
+ const timeout = this.reconnectTimeouts.get(name);
1818
+ if (timeout) {
1819
+ clearTimeout(timeout);
1820
+ this.reconnectTimeouts.delete(name);
1821
+ }
1822
+ try {
1823
+ await client.client.close();
1824
+ if ("close" in client.transport && typeof client.transport.close === "function") {
1825
+ await client.transport.close();
1826
+ }
1827
+ } catch (error) {
1828
+ this.log(`Error closing client ${name}:`, error);
1829
+ }
1830
+ this.clients.delete(name);
1831
+ this.log(`Disconnected from ${name}`);
1832
+ }
1833
+ /**
1834
+ * Schedule a reconnection attempt
1835
+ */
1836
+ scheduleReconnect(name, config) {
1837
+ if (this.reconnectTimeouts.has(name)) return;
1838
+ const timeout = setTimeout(async () => {
1839
+ this.reconnectTimeouts.delete(name);
1840
+ this.log(`Attempting to reconnect to ${name}...`);
1841
+ try {
1842
+ await this.connectClient(config);
1843
+ } catch (error) {
1844
+ this.log(`Reconnection failed for ${name}`);
1845
+ }
1846
+ }, this.options.reconnectDelay);
1847
+ this.reconnectTimeouts.set(name, timeout);
1848
+ }
1849
+ /**
1850
+ * Call a tool on the appropriate MCP client
1851
+ */
1852
+ async callTool(toolName, args, options = {}) {
1853
+ const timeout = options.timeout ?? this.options.toolTimeout;
1854
+ let clientInfo;
1855
+ let actualToolName = toolName;
1856
+ if (options.namespace) {
1857
+ clientInfo = this.clients.get(options.namespace);
1858
+ if (!clientInfo) {
1859
+ throw new Error(`Namespace '${options.namespace}' not found`);
1860
+ }
1861
+ const prefix = `${options.namespace}-`;
1862
+ if (actualToolName.startsWith(prefix)) {
1863
+ actualToolName = actualToolName.slice(prefix.length);
1864
+ }
1865
+ if (!clientInfo.tools.some((t) => t.name === actualToolName)) {
1866
+ throw new Error(`Tool '${actualToolName}' not found in namespace '${options.namespace}'`);
1867
+ }
1868
+ } else {
1869
+ for (const [, info] of this.clients) {
1870
+ const tool = info.tools.find((t) => t.name === toolName);
1871
+ if (tool) {
1872
+ clientInfo = info;
1873
+ break;
1874
+ }
1875
+ }
1876
+ }
1877
+ if (!clientInfo) {
1878
+ throw new Error(`Tool '${toolName}' not found on any connected MCP server`);
1879
+ }
1880
+ const requestId = `req_${++this.requestId}`;
1881
+ clientInfo.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
1882
+ try {
1883
+ this.log(`[${requestId}] Calling '${actualToolName}' on '${clientInfo.name}'`);
1884
+ const result = await Promise.race([
1885
+ clientInfo.client.request(
1886
+ {
1887
+ method: "tools/call",
1888
+ params: {
1889
+ name: actualToolName,
1890
+ arguments: args
1891
+ }
1892
+ },
1893
+ import_types.CallToolResultSchema
1894
+ ),
1895
+ new Promise(
1896
+ (_, reject) => setTimeout(() => reject(new Error(`Tool call timeout after ${timeout}ms`)), timeout)
1897
+ )
1898
+ ]);
1899
+ this.log(`[${requestId}] Completed successfully`);
1900
+ return this.formatToolResult(result);
1901
+ } catch (error) {
1902
+ this.log(`[${requestId}] Failed:`, error instanceof Error ? error.message : error);
1903
+ if (error instanceof import_types.McpError) {
1904
+ throw new Error(`MCP Error (${toolName}): ${error.message} (code: ${error.code})`);
1905
+ }
1906
+ throw error;
1907
+ }
1908
+ }
1909
+ /**
1910
+ * Format MCP tool result for consistent output
1911
+ */
1912
+ formatToolResult(result) {
1913
+ if (!result || !result.content) {
1914
+ return "";
1915
+ }
1916
+ return result.content.map((item) => {
1917
+ if (item.type === "text") {
1918
+ return item.text || "";
1919
+ } else if (item.type === "resource") {
1920
+ return `[Resource: ${item.resource?.uri || "unknown"}]`;
1921
+ } else if (item.type === "image") {
1922
+ return `[Image: ${item.mimeType || "unknown"}]`;
1923
+ } else if (item.type === "audio") {
1924
+ return `[Audio: ${item.mimeType || "unknown"}]`;
1925
+ } else {
1926
+ return JSON.stringify(item);
1927
+ }
1928
+ }).join("\n");
1929
+ }
1930
+ /**
1931
+ * Get all tools from all connected MCP clients
1932
+ * Optionally with namespace prefix
1933
+ */
1934
+ getAllTools(options = {}) {
1935
+ const allTools = [];
1936
+ for (const [clientName, client] of this.clients) {
1937
+ for (const tool of client.tools) {
1938
+ if (options.namespacePrefix) {
1939
+ allTools.push({
1940
+ ...tool,
1941
+ name: `${clientName}-${tool.name}`
1942
+ });
1943
+ } else {
1944
+ allTools.push(tool);
1945
+ }
1946
+ }
1947
+ }
1948
+ return allTools;
1949
+ }
1950
+ /**
1951
+ * Get tools from a specific client
1952
+ */
1953
+ getClientTools(clientName) {
1954
+ const client = this.clients.get(clientName);
1955
+ return client?.tools || [];
1956
+ }
1957
+ /**
1958
+ * Get list of connected MCP clients
1959
+ */
1960
+ listClients() {
1961
+ return Array.from(this.clients.entries()).map(([name, info]) => ({
1962
+ name,
1963
+ status: info.status,
1964
+ toolCount: info.tools.length,
1965
+ connectedAt: info.connectedAt,
1966
+ lastUsed: info.lastUsed,
1967
+ transportType: info.transportType
1968
+ }));
1969
+ }
1970
+ /**
1971
+ * Get client info by name
1972
+ */
1973
+ getClient(name) {
1974
+ return this.clients.get(name);
1975
+ }
1976
+ /**
1977
+ * Refresh tool lists from all connected clients
1978
+ */
1979
+ async refreshTools() {
1980
+ for (const [name, client] of this.clients) {
1981
+ try {
1982
+ const toolsResult = await client.client.request(
1983
+ {
1984
+ method: "tools/list",
1985
+ params: {}
1986
+ },
1987
+ import_types.ListToolsResultSchema
1988
+ );
1989
+ client.tools = toolsResult.tools.map((tool) => ({
1990
+ name: tool.name,
1991
+ description: tool.description || "",
1992
+ inputSchema: tool.inputSchema,
1993
+ serverName: name
1994
+ }));
1995
+ this.log(`Refreshed ${name}: ${client.tools.length} tools`);
1996
+ } catch (error) {
1997
+ this.log(`Failed to refresh tools for ${name}:`, error);
1998
+ client.status = "error";
1999
+ client.error = error instanceof Error ? error.message : String(error);
2000
+ }
2001
+ }
2002
+ }
2003
+ /**
2004
+ * List resources from a specific client or all clients
2005
+ */
2006
+ async listResources(clientName) {
2007
+ const results = [];
2008
+ const clients = clientName ? [clientName] : Array.from(this.clients.keys());
2009
+ for (const name of clients) {
2010
+ const client = this.clients.get(name);
2011
+ if (!client) continue;
2012
+ try {
2013
+ const result = await client.client.request(
2014
+ {
2015
+ method: "resources/list",
2016
+ params: {}
2017
+ },
2018
+ import_types.ListResourcesResultSchema
2019
+ );
2020
+ results.push({
2021
+ clientName: name,
2022
+ resources: result.resources
2023
+ });
2024
+ } catch (error) {
2025
+ this.log(`Failed to list resources for ${name}:`, error);
2026
+ }
2027
+ }
2028
+ return results;
2029
+ }
2030
+ /**
2031
+ * Read a resource from a specific client
2032
+ */
2033
+ async readResource(uri, clientName) {
2034
+ if (clientName) {
2035
+ const client = this.clients.get(clientName);
2036
+ if (!client) {
2037
+ throw new Error(`Client '${clientName}' not found`);
2038
+ }
2039
+ const result = await client.client.request(
2040
+ {
2041
+ method: "resources/read",
2042
+ params: { uri }
2043
+ },
2044
+ import_types.ReadResourceResultSchema
2045
+ );
2046
+ return result;
2047
+ } else {
2048
+ for (const [name, client] of this.clients) {
2049
+ try {
2050
+ const result = await client.client.request(
2051
+ {
2052
+ method: "resources/read",
2053
+ params: { uri }
2054
+ },
2055
+ import_types.ReadResourceResultSchema
2056
+ );
2057
+ return { clientName: name, ...result };
2058
+ } catch {
2059
+ }
2060
+ }
2061
+ throw new Error(`Resource '${uri}' not found on any client`);
2062
+ }
2063
+ }
2064
+ /**
2065
+ * List prompts from a specific client or all clients
2066
+ */
2067
+ async listPrompts(clientName) {
2068
+ const results = [];
2069
+ const clients = clientName ? [clientName] : Array.from(this.clients.keys());
2070
+ for (const name of clients) {
2071
+ const client = this.clients.get(name);
2072
+ if (!client) continue;
2073
+ try {
2074
+ const result = await client.client.request(
2075
+ {
2076
+ method: "prompts/list",
2077
+ params: {}
2078
+ },
2079
+ import_types.ListPromptsResultSchema
2080
+ );
2081
+ results.push({
2082
+ clientName: name,
2083
+ prompts: result.prompts
2084
+ });
2085
+ } catch (error) {
2086
+ this.log(`Failed to list prompts for ${name}:`, error);
2087
+ }
2088
+ }
2089
+ return results;
2090
+ }
2091
+ /**
2092
+ * Get a prompt from a specific client
2093
+ */
2094
+ async getPrompt(name, args, clientName) {
2095
+ if (clientName) {
2096
+ const client = this.clients.get(clientName);
2097
+ if (!client) {
2098
+ throw new Error(`Client '${clientName}' not found`);
2099
+ }
2100
+ const result = await client.client.request(
2101
+ {
2102
+ method: "prompts/get",
2103
+ params: { name, arguments: args }
2104
+ },
2105
+ import_types.GetPromptResultSchema
2106
+ );
2107
+ return result;
2108
+ } else {
2109
+ for (const [cName, client] of this.clients) {
2110
+ try {
2111
+ const result = await client.client.request(
2112
+ {
2113
+ method: "prompts/get",
2114
+ params: { name, arguments: args }
2115
+ },
2116
+ import_types.GetPromptResultSchema
2117
+ );
2118
+ return { clientName: cName, ...result };
2119
+ } catch {
2120
+ }
2121
+ }
2122
+ throw new Error(`Prompt '${name}' not found on any client`);
2123
+ }
2124
+ }
2125
+ /**
2126
+ * Health check for all connected clients
2127
+ */
2128
+ async healthCheck() {
2129
+ const results = /* @__PURE__ */ new Map();
2130
+ for (const [name, client] of this.clients) {
2131
+ try {
2132
+ const startTime = Date.now();
2133
+ await client.client.request(
2134
+ {
2135
+ method: "tools/list",
2136
+ params: {}
2137
+ },
2138
+ import_types.ListToolsResultSchema
2139
+ );
2140
+ results.set(name, {
2141
+ status: "healthy",
2142
+ responseTime: Date.now() - startTime
2143
+ });
2144
+ } catch (error) {
2145
+ results.set(name, {
2146
+ status: "unhealthy",
2147
+ responseTime: 0,
2148
+ error: error instanceof Error ? error.message : String(error)
2149
+ });
2150
+ if (this.options.autoReconnect) {
2151
+ this.scheduleReconnect(name, client.config);
2152
+ }
2153
+ }
2154
+ }
2155
+ return results;
2156
+ }
2157
+ /**
2158
+ * Start the automatic refresh timer
2159
+ */
2160
+ startRefreshTimer() {
2161
+ if (this.refreshTimer) {
2162
+ clearInterval(this.refreshTimer);
2163
+ }
2164
+ if (this.options.refreshInterval > 0) {
2165
+ this.refreshTimer = setInterval(async () => {
2166
+ try {
2167
+ await this.refreshTools();
2168
+ } catch (error) {
2169
+ this.log("Auto-refresh failed:", error);
2170
+ }
2171
+ }, this.options.refreshInterval);
2172
+ }
2173
+ }
2174
+ /**
2175
+ * Print available tools for a client
2176
+ */
2177
+ printAvailableTools(clientName, tools) {
2178
+ if (tools.length === 0) {
2179
+ this.log(` No tools available from ${clientName}`);
2180
+ return;
2181
+ }
2182
+ this.log(`
2183
+ --- ${clientName} Tools (${tools.length}) ---`);
2184
+ for (const tool of tools) {
2185
+ this.log(` \u2022 ${tool.name}: ${tool.description.slice(0, 60)}${tool.description.length > 60 ? "..." : ""}`);
2186
+ }
2187
+ }
2188
+ /**
2189
+ * Debug logging
2190
+ */
2191
+ log(...args) {
2192
+ if (this.options.debug) {
2193
+ console.log("[MCP-Proxy]", ...args);
2194
+ }
2195
+ }
2196
+ /**
2197
+ * Get statistics about the MCP proxy hub
2198
+ */
2199
+ getStats() {
2200
+ const clients = Array.from(this.clients.entries()).map(([name, info]) => ({
2201
+ name,
2202
+ toolCount: info.tools.length,
2203
+ status: info.status,
2204
+ transportType: info.transportType
2205
+ }));
2206
+ return {
2207
+ totalClients: this.clients.size,
2208
+ totalTools: clients.reduce((sum, c) => sum + c.toolCount, 0),
2209
+ clients
2210
+ };
2211
+ }
2212
+ };
2213
+
1648
2214
  // src/daemon/index.ts
1649
2215
  var RainfallDaemon = class {
1650
2216
  wss;
@@ -1661,11 +2227,16 @@ var RainfallDaemon = class {
1661
2227
  networkedExecutor;
1662
2228
  context;
1663
2229
  listeners;
2230
+ mcpProxy;
2231
+ enableMcpProxy;
2232
+ mcpNamespacePrefix;
1664
2233
  constructor(config = {}) {
1665
2234
  this.port = config.port || 8765;
1666
2235
  this.openaiPort = config.openaiPort || 8787;
1667
2236
  this.rainfallConfig = config.rainfallConfig;
1668
2237
  this.debug = config.debug || false;
2238
+ this.enableMcpProxy = config.enableMcpProxy ?? true;
2239
+ this.mcpNamespacePrefix = config.mcpNamespacePrefix ?? true;
1669
2240
  this.openaiApp = (0, import_express.default)();
1670
2241
  this.openaiApp.use(import_express.default.json());
1671
2242
  }
@@ -1701,6 +2272,19 @@ var RainfallDaemon = class {
1701
2272
  this.networkedExecutor
1702
2273
  );
1703
2274
  await this.loadTools();
2275
+ if (this.enableMcpProxy) {
2276
+ this.mcpProxy = new MCPProxyHub({ debug: this.debug });
2277
+ await this.mcpProxy.initialize();
2278
+ if (this.rainfallConfig?.mcpClients) {
2279
+ for (const clientConfig of this.rainfallConfig.mcpClients) {
2280
+ try {
2281
+ await this.mcpProxy.connectClient(clientConfig);
2282
+ } catch (error) {
2283
+ this.log(`Failed to connect MCP client ${clientConfig.name}:`, error);
2284
+ }
2285
+ }
2286
+ }
2287
+ }
1704
2288
  await this.startWebSocketServer();
1705
2289
  await this.startOpenAIProxy();
1706
2290
  console.log(`\u{1F680} Rainfall daemon running`);
@@ -1721,6 +2305,10 @@ var RainfallDaemon = class {
1721
2305
  if (this.networkedExecutor) {
1722
2306
  await this.networkedExecutor.unregisterEdgeNode();
1723
2307
  }
2308
+ if (this.mcpProxy) {
2309
+ await this.mcpProxy.shutdown();
2310
+ this.mcpProxy = void 0;
2311
+ }
1724
2312
  for (const client of this.clients) {
1725
2313
  client.close();
1726
2314
  }
@@ -1749,6 +2337,30 @@ var RainfallDaemon = class {
1749
2337
  getListenerRegistry() {
1750
2338
  return this.listeners;
1751
2339
  }
2340
+ /**
2341
+ * Get the MCP Proxy Hub for managing external MCP clients
2342
+ */
2343
+ getMCPProxy() {
2344
+ return this.mcpProxy;
2345
+ }
2346
+ /**
2347
+ * Connect an MCP client dynamically
2348
+ */
2349
+ async connectMCPClient(config) {
2350
+ if (!this.mcpProxy) {
2351
+ throw new Error("MCP Proxy Hub is not enabled");
2352
+ }
2353
+ return this.mcpProxy.connectClient(config);
2354
+ }
2355
+ /**
2356
+ * Disconnect an MCP client
2357
+ */
2358
+ async disconnectMCPClient(name) {
2359
+ if (!this.mcpProxy) {
2360
+ throw new Error("MCP Proxy Hub is not enabled");
2361
+ }
2362
+ return this.mcpProxy.disconnectClient(name);
2363
+ }
1752
2364
  async initializeRainfall() {
1753
2365
  if (this.rainfallConfig?.apiKey) {
1754
2366
  this.rainfall = new Rainfall(this.rainfallConfig);
@@ -1789,7 +2401,7 @@ var RainfallDaemon = class {
1789
2401
  }
1790
2402
  }
1791
2403
  async startWebSocketServer() {
1792
- this.wss = new import_ws.WebSocketServer({ port: this.port });
2404
+ this.wss = new import_ws2.WebSocketServer({ port: this.port });
1793
2405
  this.wss.on("connection", (ws) => {
1794
2406
  this.log("\u{1F7E2} MCP client connected");
1795
2407
  this.clients.add(ws);
@@ -1851,7 +2463,7 @@ var RainfallDaemon = class {
1851
2463
  const toolParams = params?.arguments;
1852
2464
  try {
1853
2465
  const startTime = Date.now();
1854
- const result = await this.executeTool(toolName, toolParams);
2466
+ const result = await this.executeToolWithMCP(toolName, toolParams);
1855
2467
  const duration = Date.now() - startTime;
1856
2468
  if (this.context) {
1857
2469
  this.context.recordExecution(toolName, toolParams || {}, result, { duration });
@@ -1916,6 +2528,16 @@ var RainfallDaemon = class {
1916
2528
  });
1917
2529
  }
1918
2530
  }
2531
+ if (this.mcpProxy) {
2532
+ const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
2533
+ for (const tool of proxyTools) {
2534
+ mcpTools.push({
2535
+ name: this.mcpNamespacePrefix ? `${tool.serverName}-${tool.name}` : tool.name,
2536
+ description: tool.description,
2537
+ inputSchema: tool.inputSchema || { type: "object", properties: {} }
2538
+ });
2539
+ }
2540
+ }
1919
2541
  return mcpTools;
1920
2542
  }
1921
2543
  async executeTool(toolId, params) {
@@ -1924,6 +2546,30 @@ var RainfallDaemon = class {
1924
2546
  }
1925
2547
  return this.rainfall.executeTool(toolId, params);
1926
2548
  }
2549
+ /**
2550
+ * Execute a tool, trying MCP proxy first, then falling back to Rainfall tools
2551
+ */
2552
+ async executeToolWithMCP(toolName, params) {
2553
+ if (this.mcpProxy) {
2554
+ try {
2555
+ if (this.mcpNamespacePrefix && toolName.includes("-")) {
2556
+ const namespace = toolName.split("-")[0];
2557
+ const actualToolName = toolName.slice(namespace.length + 1);
2558
+ if (this.mcpProxy.getClient(namespace)) {
2559
+ return await this.mcpProxy.callTool(toolName, params || {}, {
2560
+ namespace
2561
+ });
2562
+ }
2563
+ }
2564
+ return await this.mcpProxy.callTool(toolName, params || {});
2565
+ } catch (error) {
2566
+ if (error instanceof Error && !error.message.includes("not found")) {
2567
+ throw error;
2568
+ }
2569
+ }
2570
+ }
2571
+ return this.executeTool(toolName, params);
2572
+ }
1927
2573
  async startOpenAIProxy() {
1928
2574
  this.openaiApp.get("/v1/models", async (_req, res) => {
1929
2575
  try {
@@ -2039,6 +2685,10 @@ var RainfallDaemon = class {
2039
2685
  this.log(` \u2192 Executing locally`);
2040
2686
  const args = JSON.parse(toolArgsStr);
2041
2687
  toolResult = await this.executeLocalTool(localTool.id, args);
2688
+ } else if (this.mcpProxy) {
2689
+ this.log(` \u2192 Trying MCP proxy`);
2690
+ const args = JSON.parse(toolArgsStr);
2691
+ toolResult = await this.executeToolWithMCP(toolName.replace(/_/g, "-"), args);
2042
2692
  } else {
2043
2693
  const shouldExecuteLocal = body.tool_priority === "local" || body.tool_priority === "stacked";
2044
2694
  if (shouldExecuteLocal) {
@@ -2088,15 +2738,52 @@ var RainfallDaemon = class {
2088
2738
  }
2089
2739
  });
2090
2740
  this.openaiApp.get("/health", (_req, res) => {
2741
+ const mcpStats = this.mcpProxy?.getStats();
2091
2742
  res.json({
2092
2743
  status: "ok",
2093
2744
  daemon: "rainfall",
2094
- version: "0.1.0",
2745
+ version: "0.2.0",
2095
2746
  tools_loaded: this.tools.length,
2747
+ mcp_clients: mcpStats?.totalClients || 0,
2748
+ mcp_tools: mcpStats?.totalTools || 0,
2096
2749
  edge_node_id: this.networkedExecutor?.getEdgeNodeId(),
2097
2750
  clients_connected: this.clients.size
2098
2751
  });
2099
2752
  });
2753
+ this.openaiApp.get("/v1/mcp/clients", (_req, res) => {
2754
+ if (!this.mcpProxy) {
2755
+ res.status(503).json({ error: "MCP proxy not enabled" });
2756
+ return;
2757
+ }
2758
+ res.json(this.mcpProxy.listClients());
2759
+ });
2760
+ this.openaiApp.post("/v1/mcp/connect", async (req, res) => {
2761
+ if (!this.mcpProxy) {
2762
+ res.status(503).json({ error: "MCP proxy not enabled" });
2763
+ return;
2764
+ }
2765
+ try {
2766
+ const name = await this.mcpProxy.connectClient(req.body);
2767
+ res.json({ success: true, client: name });
2768
+ } catch (error) {
2769
+ res.status(500).json({
2770
+ error: error instanceof Error ? error.message : "Failed to connect MCP client"
2771
+ });
2772
+ }
2773
+ });
2774
+ this.openaiApp.post("/v1/mcp/disconnect", async (req, res) => {
2775
+ if (!this.mcpProxy) {
2776
+ res.status(503).json({ error: "MCP proxy not enabled" });
2777
+ return;
2778
+ }
2779
+ const { name } = req.body;
2780
+ if (!name) {
2781
+ res.status(400).json({ error: "Missing required field: name" });
2782
+ return;
2783
+ }
2784
+ await this.mcpProxy.disconnectClient(name);
2785
+ res.json({ success: true });
2786
+ });
2100
2787
  this.openaiApp.get("/status", (_req, res) => {
2101
2788
  res.json(this.getStatus());
2102
2789
  });
@@ -2231,6 +2918,7 @@ var RainfallDaemon = class {
2231
2918
  switch (provider) {
2232
2919
  case "local":
2233
2920
  case "ollama":
2921
+ case "custom":
2234
2922
  return this.callLocalLLM(params, config);
2235
2923
  case "openai":
2236
2924
  case "anthropic":
@@ -2267,7 +2955,8 @@ var RainfallDaemon = class {
2267
2955
  method: "POST",
2268
2956
  headers: {
2269
2957
  "Content-Type": "application/json",
2270
- "Authorization": `Bearer ${apiKey}`
2958
+ "Authorization": `Bearer ${apiKey}`,
2959
+ "User-Agent": "Rainfall-DevKit/1.0"
2271
2960
  },
2272
2961
  body: JSON.stringify({
2273
2962
  model,
@@ -2298,7 +2987,8 @@ var RainfallDaemon = class {
2298
2987
  method: "POST",
2299
2988
  headers: {
2300
2989
  "Content-Type": "application/json",
2301
- "Authorization": `Bearer ${apiKey}`
2990
+ "Authorization": `Bearer ${apiKey}`,
2991
+ "User-Agent": "Rainfall-DevKit/1.0"
2302
2992
  },
2303
2993
  body: JSON.stringify({
2304
2994
  model,
@@ -2379,7 +3069,7 @@ var RainfallDaemon = class {
2379
3069
  }
2380
3070
  async getOpenAITools() {
2381
3071
  const tools = [];
2382
- for (const tool of this.tools.slice(0, 128)) {
3072
+ for (const tool of this.tools.slice(0, 100)) {
2383
3073
  const schema = await this.getToolSchema(tool.id);
2384
3074
  if (schema) {
2385
3075
  const toolSchema = schema;
@@ -2403,6 +3093,24 @@ var RainfallDaemon = class {
2403
3093
  });
2404
3094
  }
2405
3095
  }
3096
+ if (this.mcpProxy) {
3097
+ const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
3098
+ for (const tool of proxyTools.slice(0, 28)) {
3099
+ const inputSchema = tool.inputSchema || {};
3100
+ tools.push({
3101
+ type: "function",
3102
+ function: {
3103
+ name: this.mcpNamespacePrefix ? `${tool.serverName}_${tool.name}`.replace(/-/g, "_") : tool.name.replace(/-/g, "_"),
3104
+ description: `[${tool.serverName}] ${tool.description}`,
3105
+ parameters: {
3106
+ type: "object",
3107
+ properties: inputSchema.properties || {},
3108
+ required: inputSchema.required || []
3109
+ }
3110
+ }
3111
+ });
3112
+ }
3113
+ }
2406
3114
  return tools;
2407
3115
  }
2408
3116
  buildResponseContent() {
@@ -2465,6 +3173,7 @@ function getDaemonInstance() {
2465
3173
  }
2466
3174
  // Annotate the CommonJS export names for ESM import in node:
2467
3175
  0 && (module.exports = {
3176
+ MCPProxyHub,
2468
3177
  RainfallDaemon,
2469
3178
  getDaemonInstance,
2470
3179
  getDaemonStatus,