@toolprint/hypertool-mcp
Version:
HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers
254 lines • 8.32 kB
JavaScript
/**
* MCP Server Manager - Core functionality for managing MCP server configurations
*/
import { promises as fs } from "fs";
import { join } from "path";
import { homedir } from "os";
export class MCPServerManager {
basePath;
mcpConfigPath;
constructor(basePath = join(homedir(), ".toolprint/hypertool-mcp")) {
this.basePath = basePath;
this.mcpConfigPath = join(basePath, "mcp.json");
}
/**
* Initialize the MCP manager and ensure config exists
*/
async initialize() {
// Ensure directory exists
await fs.mkdir(this.basePath, { recursive: true });
// Create empty config if it doesn't exist
try {
await fs.access(this.mcpConfigPath);
}
catch {
const emptyConfig = {
mcpServers: {},
_metadata: {
sources: {},
},
};
await this.saveConfig(emptyConfig);
}
}
/**
* List all configured MCP servers
*/
async listServers() {
const config = await this.loadConfig();
const servers = [];
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
const metadata = config._metadata?.sources?.[name];
servers.push({
name,
config: serverConfig,
metadata,
});
}
return servers;
}
/**
* Get a specific server configuration
*/
async getServer(name) {
const config = await this.loadConfig();
const serverConfig = config.mcpServers[name];
if (!serverConfig) {
return null;
}
const metadata = config._metadata?.sources?.[name];
return {
name,
config: serverConfig,
metadata,
};
}
/**
* Add a new MCP server
*/
async addServer(name, serverConfig) {
const config = await this.loadConfig();
// Check if server already exists
if (config.mcpServers[name]) {
throw new Error(`Server '${name}' already exists`);
}
// Validate server name
if (!this.isValidServerName(name)) {
throw new Error(`Invalid server name '${name}'. Use alphanumeric characters, hyphens, and underscores only.`);
}
// Validate server configuration
this.validateServerConfig(serverConfig);
// Add server
config.mcpServers[name] = serverConfig;
// Add metadata
if (!config._metadata) {
config._metadata = { sources: {} };
}
if (!config._metadata.sources) {
config._metadata.sources = {};
}
config._metadata.sources[name] = {
app: "manual",
importedAt: new Date().toISOString(),
addedManually: true,
};
// Save configuration
await this.saveConfig(config);
}
/**
* Remove an MCP server
*/
async removeServer(name) {
const config = await this.loadConfig();
// Check if server exists
if (!config.mcpServers[name]) {
throw new Error(`Server '${name}' not found`);
}
// Remove server
delete config.mcpServers[name];
// Remove metadata
if (config._metadata?.sources) {
delete config._metadata.sources[name];
}
// Save configuration
await this.saveConfig(config);
}
/**
* Update an existing server configuration
*/
async updateServer(name, serverConfig) {
const config = await this.loadConfig();
// Check if server exists
if (!config.mcpServers[name]) {
throw new Error(`Server '${name}' not found`);
}
// Validate server configuration
this.validateServerConfig(serverConfig);
// Update server
config.mcpServers[name] = serverConfig;
// Update metadata timestamp
if (config._metadata?.sources?.[name]) {
config._metadata.sources[name].importedAt = new Date().toISOString();
}
// Save configuration
await this.saveConfig(config);
}
/**
* Check if a server name is valid
*/
isValidServerName(name) {
// Allow alphanumeric, hyphens, underscores, and dots
return /^[a-zA-Z0-9\-_.]+$/.test(name);
}
/**
* Validate server configuration
*/
validateServerConfig(config) {
// Validate transport type
const validTypes = ["stdio", "http", "sse", "websocket"];
if (!validTypes.includes(config.type)) {
throw new Error(`Invalid transport type '${config.type}'. Must be one of: ${validTypes.join(", ")}`);
}
// Validate stdio configuration
if (config.type === "stdio") {
if (!config.command) {
throw new Error("Stdio transport requires a command");
}
if (config.url) {
throw new Error("Stdio transport should not have a URL");
}
}
// Validate HTTP/SSE/WebSocket configuration
if (["http", "sse", "websocket"].includes(config.type)) {
if (!config.url) {
throw new Error(`${config.type.toUpperCase()} transport requires a URL`);
}
if (config.command) {
throw new Error(`${config.type.toUpperCase()} transport should not have a command`);
}
// Validate URL format
try {
new URL(config.url);
}
catch {
throw new Error(`Invalid URL: ${config.url}`);
}
}
// Validate environment variables
if (config.env && typeof config.env !== "object") {
throw new Error("Environment variables must be an object");
}
// Validate headers
if (config.headers && typeof config.headers !== "object") {
throw new Error("Headers must be an object");
}
}
/**
* Load the MCP configuration
*/
async loadConfig() {
try {
const content = await fs.readFile(this.mcpConfigPath, "utf-8");
return JSON.parse(content);
}
catch {
// Return empty config if file doesn't exist or is invalid
return {
mcpServers: {},
_metadata: {
sources: {},
},
};
}
}
/**
* Save the MCP configuration
*/
async saveConfig(config) {
// Create backup before saving
await this.createBackup();
// Save configuration
await fs.writeFile(this.mcpConfigPath, JSON.stringify(config, null, 2), "utf-8");
}
/**
* Create a backup of the current configuration
*/
async createBackup() {
try {
const backupDir = join(this.basePath, "backups", "mcp");
await fs.mkdir(backupDir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupPath = join(backupDir, `mcp_${timestamp}.json`);
const content = await fs.readFile(this.mcpConfigPath, "utf-8");
await fs.writeFile(backupPath, content, "utf-8");
}
catch {
// Ignore backup errors - don't fail the operation
}
}
/**
* Parse environment variable string (key=value)
*/
static parseEnvVar(envStr) {
const index = envStr.indexOf("=");
if (index === -1) {
throw new Error(`Invalid environment variable format: ${envStr}. Use KEY=value`);
}
const key = envStr.substring(0, index);
const value = envStr.substring(index + 1);
return [key, value];
}
/**
* Parse header string (key=value)
*/
static parseHeader(headerStr) {
const index = headerStr.indexOf("=");
if (index === -1) {
throw new Error(`Invalid header format: ${headerStr}. Use Header=value`);
}
const key = headerStr.substring(0, index);
const value = headerStr.substring(index + 1);
return [key, value];
}
}
//# sourceMappingURL=index.js.map