|
|
const { safeJsonParse } = require("../../http"); |
|
|
const path = require("path"); |
|
|
const fs = require("fs"); |
|
|
const { Client } = require("@modelcontextprotocol/sdk/client/index.js"); |
|
|
const { |
|
|
StdioClientTransport, |
|
|
} = require("@modelcontextprotocol/sdk/client/stdio.js"); |
|
|
const { |
|
|
SSEClientTransport, |
|
|
} = require("@modelcontextprotocol/sdk/client/sse.js"); |
|
|
const { |
|
|
StreamableHTTPClientTransport, |
|
|
} = require("@modelcontextprotocol/sdk/client/streamableHttp.js"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MCPHypervisor { |
|
|
static _instance; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mcpServerJSONPath; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mcps = {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mcpLoadingResults = {}; |
|
|
|
|
|
constructor() { |
|
|
if (MCPHypervisor._instance) return MCPHypervisor._instance; |
|
|
MCPHypervisor._instance = this; |
|
|
this.log("Initializing MCP Hypervisor - subsequent calls will boot faster"); |
|
|
this.#setupConfigFile(); |
|
|
return this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#setupConfigFile() { |
|
|
this.mcpServerJSONPath = |
|
|
process.env.NODE_ENV === "development" |
|
|
? path.resolve( |
|
|
__dirname, |
|
|
`../../../storage/plugins/anythingllm_mcp_servers.json` |
|
|
) |
|
|
: path.resolve( |
|
|
process.env.STORAGE_DIR ?? |
|
|
path.resolve(__dirname, `../../../storage`), |
|
|
`plugins/anythingllm_mcp_servers.json` |
|
|
); |
|
|
|
|
|
if (!fs.existsSync(this.mcpServerJSONPath)) { |
|
|
fs.mkdirSync(path.dirname(this.mcpServerJSONPath), { recursive: true }); |
|
|
fs.writeFileSync( |
|
|
this.mcpServerJSONPath, |
|
|
JSON.stringify({ mcpServers: {} }, null, 2), |
|
|
{ encoding: "utf8" } |
|
|
); |
|
|
} |
|
|
|
|
|
this.log(`MCP Config File: ${this.mcpServerJSONPath}`); |
|
|
} |
|
|
|
|
|
log(text, ...args) { |
|
|
console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get mcpServerConfigs() { |
|
|
const servers = safeJsonParse( |
|
|
fs.readFileSync(this.mcpServerJSONPath, "utf8"), |
|
|
{ mcpServers: {} } |
|
|
); |
|
|
return Object.entries(servers.mcpServers).map(([name, server]) => ({ |
|
|
name, |
|
|
server, |
|
|
})); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
removeMCPServerFromConfig(name) { |
|
|
const servers = safeJsonParse( |
|
|
fs.readFileSync(this.mcpServerJSONPath, "utf8"), |
|
|
{ mcpServers: {} } |
|
|
); |
|
|
if (!servers.mcpServers[name]) return false; |
|
|
|
|
|
delete servers.mcpServers[name]; |
|
|
fs.writeFileSync( |
|
|
this.mcpServerJSONPath, |
|
|
JSON.stringify(servers, null, 2), |
|
|
"utf8" |
|
|
); |
|
|
this.log(`MCP server ${name} removed from config file`); |
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async reloadMCPServers() { |
|
|
this.pruneMCPServers(); |
|
|
await this.bootMCPServers(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async startMCPServer(name) { |
|
|
if (this.mcps[name]) |
|
|
return { success: false, error: `MCP server ${name} already running` }; |
|
|
const config = this.mcpServerConfigs.find((s) => s.name === name); |
|
|
if (!config) |
|
|
return { |
|
|
success: false, |
|
|
error: `MCP server ${name} not found in config file`, |
|
|
}; |
|
|
|
|
|
try { |
|
|
await this.#startMCPServer(config); |
|
|
this.mcpLoadingResults[name] = { |
|
|
status: "success", |
|
|
message: `Successfully connected to MCP server: ${name}`, |
|
|
}; |
|
|
|
|
|
return { success: true, message: `MCP server ${name} started` }; |
|
|
} catch (e) { |
|
|
this.log(`Failed to start single MCP server: ${name}`, { |
|
|
error: e.message, |
|
|
code: e.code, |
|
|
syscall: e.syscall, |
|
|
path: e.path, |
|
|
stack: e.stack, |
|
|
}); |
|
|
this.mcpLoadingResults[name] = { |
|
|
status: "failed", |
|
|
message: `Failed to start MCP server: ${name} [${e.code || "NO_CODE"}] ${e.message}`, |
|
|
}; |
|
|
|
|
|
|
|
|
if (this.mcps[name]) { |
|
|
this.mcps[name].close(); |
|
|
delete this.mcps[name]; |
|
|
} |
|
|
|
|
|
return { success: false, error: e.message }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pruneMCPServer(name) { |
|
|
if (!name || !this.mcps[name]) return true; |
|
|
|
|
|
this.log(`Pruning MCP server: ${name}`); |
|
|
const mcp = this.mcps[name]; |
|
|
const childProcess = mcp.transport._process; |
|
|
if (childProcess) childProcess.kill(1); |
|
|
mcp.transport.close(); |
|
|
|
|
|
delete this.mcps[name]; |
|
|
this.mcpLoadingResults[name] = { |
|
|
status: "failed", |
|
|
message: `Server was stopped manually by the administrator.`, |
|
|
}; |
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pruneMCPServers() { |
|
|
this.log(`Pruning ${Object.keys(this.mcps).length} MCP servers...`); |
|
|
|
|
|
for (const name of Object.keys(this.mcps)) { |
|
|
if (!this.mcps[name]) continue; |
|
|
const mcp = this.mcps[name]; |
|
|
const childProcess = mcp.transport._process; |
|
|
if (childProcess) |
|
|
this.log(`Killing MCP ${name} (PID: ${childProcess.pid})`, { |
|
|
killed: childProcess.kill(1), |
|
|
}); |
|
|
|
|
|
mcp.transport.close(); |
|
|
mcp.close(); |
|
|
} |
|
|
this.mcps = {}; |
|
|
this.mcpLoadingResults = {}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#buildMCPServerENV(server) { |
|
|
|
|
|
|
|
|
let baseEnv = { |
|
|
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", |
|
|
NODE_PATH: process.env.NODE_PATH || "/usr/local/lib/node_modules", |
|
|
}; |
|
|
|
|
|
|
|
|
if (process.env.ANYTHING_LLM_RUNTIME === "docker") { |
|
|
baseEnv = { |
|
|
|
|
|
NODE_PATH: "/usr/local/lib/node_modules", |
|
|
PATH: "/usr/local/bin:/usr/bin:/bin", |
|
|
...baseEnv, |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
if (!server?.env || Object.keys(server.env).length === 0) { |
|
|
return { env: baseEnv }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
env: { |
|
|
...baseEnv, |
|
|
...server.env, |
|
|
}, |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#parseServerType(server) { |
|
|
if (server.hasOwnProperty("command")) return "stdio"; |
|
|
if (server.hasOwnProperty("url")) return "http"; |
|
|
return "sse"; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#validateServerDefinitionByType(server, type) { |
|
|
if (type === "stdio") { |
|
|
if (server.hasOwnProperty("args") && !Array.isArray(server.args)) |
|
|
throw new Error("MCP server args must be an array"); |
|
|
} |
|
|
|
|
|
if (type === "http") { |
|
|
if (!["sse", "streamable"].includes(server?.type)) |
|
|
throw new Error("MCP server type must have sse or streamable value."); |
|
|
} |
|
|
|
|
|
if (type === "sse") return; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#setupServerTransport(server, type) { |
|
|
|
|
|
if (type !== "stdio") return this.createHttpTransport(server); |
|
|
|
|
|
return new StdioClientTransport({ |
|
|
command: server.command, |
|
|
args: server?.args ?? [], |
|
|
...this.#buildMCPServerENV(server), |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
createHttpTransport(server) { |
|
|
const url = new URL(server.url); |
|
|
|
|
|
|
|
|
switch (server.type) { |
|
|
case "streamable": |
|
|
return new StreamableHTTPClientTransport(url, { |
|
|
requestInit: { |
|
|
headers: server.headers, |
|
|
}, |
|
|
}); |
|
|
default: |
|
|
return new SSEClientTransport(url, { |
|
|
requestInit: { |
|
|
headers: server.headers, |
|
|
}, |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async #startMCPServer({ name, server }) { |
|
|
if (!name) throw new Error("MCP server name is required"); |
|
|
if (!server) throw new Error("MCP server definition is required"); |
|
|
const serverType = this.#parseServerType(server); |
|
|
if (!serverType) throw new Error("MCP server command or url is required"); |
|
|
|
|
|
this.#validateServerDefinitionByType(server, serverType); |
|
|
this.log(`Attempting to start MCP server: ${name}`); |
|
|
const mcp = new Client({ name: name, version: "1.0.0" }); |
|
|
const transport = this.#setupServerTransport(server, serverType); |
|
|
|
|
|
|
|
|
transport.onclose = () => this.log(`${name} - Transport closed`); |
|
|
transport.onerror = (error) => |
|
|
this.log(`${name} - Transport error:`, error); |
|
|
transport.onmessage = (message) => |
|
|
this.log(`${name} - Transport message:`, message); |
|
|
|
|
|
|
|
|
this.mcps[name] = mcp; |
|
|
const connectionPromise = mcp.connect(transport); |
|
|
const timeoutPromise = new Promise((_, reject) => { |
|
|
setTimeout(() => reject(new Error("Connection timeout")), 30_000); |
|
|
}); |
|
|
await Promise.race([connectionPromise, timeoutPromise]); |
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async bootMCPServers() { |
|
|
if (Object.keys(this.mcps).length > 0) { |
|
|
this.log("MCP Servers already running, skipping boot."); |
|
|
return this.mcpLoadingResults; |
|
|
} |
|
|
|
|
|
const serverDefinitions = this.mcpServerConfigs; |
|
|
for (const { name, server } of serverDefinitions) { |
|
|
if ( |
|
|
server.anythingllm?.hasOwnProperty("autoStart") && |
|
|
server.anythingllm.autoStart === false |
|
|
) { |
|
|
this.log( |
|
|
`MCP server ${name} has anythingllm.autoStart property set to false, skipping boot!` |
|
|
); |
|
|
this.mcpLoadingResults[name] = { |
|
|
status: "failed", |
|
|
message: `MCP server ${name} has anythingllm.autoStart property set to false, boot skipped!`, |
|
|
}; |
|
|
continue; |
|
|
} |
|
|
|
|
|
try { |
|
|
await this.#startMCPServer({ name, server }); |
|
|
|
|
|
|
|
|
this.mcpLoadingResults[name] = { |
|
|
status: "success", |
|
|
message: `Successfully connected to MCP server: ${name}`, |
|
|
}; |
|
|
} catch (e) { |
|
|
this.log(`Failed to start MCP server: ${name}`, { |
|
|
error: e.message, |
|
|
code: e.code, |
|
|
syscall: e.syscall, |
|
|
path: e.path, |
|
|
stack: e.stack, |
|
|
}); |
|
|
this.mcpLoadingResults[name] = { |
|
|
status: "failed", |
|
|
message: `Failed to start MCP server: ${name} [${e.code || "NO_CODE"}] ${e.message}`, |
|
|
}; |
|
|
|
|
|
|
|
|
if (this.mcps[name]) { |
|
|
this.mcps[name].close(); |
|
|
delete this.mcps[name]; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const runningServers = Object.keys(this.mcps); |
|
|
this.log( |
|
|
`Successfully started ${runningServers.length} MCP servers:`, |
|
|
runningServers |
|
|
); |
|
|
return this.mcpLoadingResults; |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = MCPHypervisor; |
|
|
|