@rainfall-devkit/sdk 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-EI7SJH5K.mjs +85 -0
- package/dist/chunk-KOCCGNEQ.mjs +269 -0
- package/dist/chunk-NCQVOLS4.mjs +269 -0
- package/dist/chunk-NTTAVKRT.mjs +89 -0
- package/dist/chunk-RVKW5KBT.mjs +269 -0
- package/dist/chunk-XAHJQRBJ.mjs +269 -0
- package/dist/chunk-XEQ6U3JQ.mjs +269 -0
- package/dist/cli/index.js +1088 -9
- package/dist/cli/index.mjs +102 -3
- package/dist/config-7UT7GYSN.mjs +16 -0
- package/dist/config-MD45VGWD.mjs +14 -0
- package/dist/daemon/index.d.mts +35 -3
- package/dist/daemon/index.d.ts +35 -3
- package/dist/daemon/index.js +716 -7
- package/dist/daemon/index.mjs +720 -8
- package/dist/index.d.mts +248 -3
- package/dist/index.d.ts +248 -3
- package/dist/index.js +490 -2
- package/dist/index.mjs +214 -1
- package/dist/listeners-B5Vy9Ao5.d.ts +372 -0
- package/dist/listeners-DRwITBW_.d.mts +372 -0
- package/dist/listeners-DrMrvFT5.d.ts +372 -0
- package/dist/listeners-MNAnpZj-.d.mts +372 -0
- package/dist/listeners-PZI7iT85.d.ts +372 -0
- package/dist/listeners-jLwetUnx.d.mts +372 -0
- package/dist/mcp.d.mts +6 -2
- package/dist/mcp.d.ts +6 -2
- package/dist/sdk-4OvXPr8E.d.mts +1054 -0
- package/dist/sdk-4OvXPr8E.d.ts +1054 -0
- package/dist/sdk-CN1ezZrI.d.mts +1054 -0
- package/dist/sdk-CN1ezZrI.d.ts +1054 -0
- package/dist/sdk-Xw0BjsLd.d.mts +1054 -0
- package/dist/sdk-Xw0BjsLd.d.ts +1054 -0
- package/package.json +4 -1
package/dist/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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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,
|
|
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 import_libsodium_wrappers_sumo = __toESM(require("libsodium-wrappers-sumo"));
|
|
3282
|
+
var EdgeNodeSecurity = class {
|
|
3283
|
+
sodiumReady;
|
|
3284
|
+
backendSecret;
|
|
3285
|
+
keyPair;
|
|
3286
|
+
constructor(options = {}) {
|
|
3287
|
+
this.sodiumReady = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.crypto_box_keypair();
|
|
3433
|
+
const nonce = import_libsodium_wrappers_sumo.default.randombytes_buf(import_libsodium_wrappers_sumo.default.crypto_box_NONCEBYTES);
|
|
3434
|
+
const message = new TextEncoder().encode(plaintext);
|
|
3435
|
+
const ciphertext = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.randombytes_buf(import_libsodium_wrappers_sumo.default.crypto_secretbox_NONCEBYTES);
|
|
3477
|
+
const message = new TextEncoder().encode(plaintext);
|
|
3478
|
+
const ciphertext = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.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 = import_libsodium_wrappers_sumo.default.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 import_libsodium_wrappers_sumo.default.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":
|