|
| 1 | +const fs = require("fs"); |
| 2 | +const os = require("os"); |
| 3 | +const path = require("path"); |
| 4 | +const { execFileSync } = require("child_process"); |
| 5 | + |
| 6 | +/** |
| 7 | + * @typedef {Object} ClientConfig |
| 8 | + * @property {Object.<string, any>} mcpServers |
| 9 | + */ |
| 10 | + |
| 11 | +/** |
| 12 | + * @typedef {Object} ClientFileTarget |
| 13 | + * @property {"file"} type |
| 14 | + * @property {string} path |
| 15 | + */ |
| 16 | + |
| 17 | +/** |
| 18 | + * @typedef {Object} ClientCommandTarget |
| 19 | + * @property {"command"} type |
| 20 | + * @property {string} command |
| 21 | + */ |
| 22 | + |
| 23 | +// Initialize platform-specific paths |
| 24 | +const homeDir = os.homedir(); |
| 25 | + |
| 26 | +const platformPaths = { |
| 27 | + win32: { |
| 28 | + baseDir: process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"), |
| 29 | + vscodePath: path.join("Code", "User", "globalStorage"), |
| 30 | + }, |
| 31 | + darwin: { |
| 32 | + baseDir: path.join(homeDir, "Library", "Application Support"), |
| 33 | + vscodePath: path.join("Code", "User", "globalStorage"), |
| 34 | + }, |
| 35 | + linux: { |
| 36 | + baseDir: process.env.XDG_CONFIG_HOME || path.join(homeDir, ".config"), |
| 37 | + vscodePath: path.join("Code/User/globalStorage"), |
| 38 | + }, |
| 39 | +}; |
| 40 | + |
| 41 | +const platform = process.platform; |
| 42 | +const { baseDir, vscodePath } = platformPaths[platform] || platformPaths.linux; |
| 43 | +const defaultClaudePath = path.join( |
| 44 | + baseDir, |
| 45 | + "Claude", |
| 46 | + "claude_desktop_config.json", |
| 47 | +); |
| 48 | + |
| 49 | +// Define client paths using the platform-specific base directories |
| 50 | +const clientPaths = { |
| 51 | + claude: { type: "file", path: defaultClaudePath }, |
| 52 | + cline: { |
| 53 | + type: "file", |
| 54 | + path: path.join( |
| 55 | + baseDir, |
| 56 | + vscodePath, |
| 57 | + "saoudrizwan.claude-dev", |
| 58 | + "settings", |
| 59 | + "cline_mcp_settings.json", |
| 60 | + ), |
| 61 | + }, |
| 62 | + roocode: { |
| 63 | + type: "file", |
| 64 | + path: path.join( |
| 65 | + baseDir, |
| 66 | + vscodePath, |
| 67 | + "rooveterinaryinc.roo-cline", |
| 68 | + "settings", |
| 69 | + "mcp_settings.json", |
| 70 | + ), |
| 71 | + }, |
| 72 | + windsurf: { |
| 73 | + type: "file", |
| 74 | + path: path.join(homeDir, ".codeium", "windsurf", "mcp_config.json"), |
| 75 | + }, |
| 76 | + witsy: { type: "file", path: path.join(baseDir, "Witsy", "settings.json") }, |
| 77 | + enconvo: { |
| 78 | + type: "file", |
| 79 | + path: path.join(homeDir, ".config", "enconvo", "mcp_config.json"), |
| 80 | + }, |
| 81 | + cursor: { type: "file", path: path.join(homeDir, ".cursor", "mcp.json") }, |
| 82 | + vscode: { |
| 83 | + type: "command", |
| 84 | + command: process.platform === "win32" ? "code.cmd" : "code", |
| 85 | + }, |
| 86 | + "vscode-insiders": { |
| 87 | + type: "command", |
| 88 | + command: |
| 89 | + process.platform === "win32" ? "code-insiders.cmd" : "code-insiders", |
| 90 | + }, |
| 91 | + boltai: { type: "file", path: path.join(homeDir, ".boltai", "mcp.json") }, |
| 92 | + "amazon-bedrock": { |
| 93 | + type: "file", |
| 94 | + path: path.join(homeDir, "Amazon Bedrock Client", "mcp_config.json"), |
| 95 | + }, |
| 96 | + amazonq: { |
| 97 | + type: "file", |
| 98 | + path: path.join(homeDir, ".aws", "amazonq", "mcp.json"), |
| 99 | + }, |
| 100 | +}; |
| 101 | + |
| 102 | +/** |
| 103 | + * @param {string} [client] |
| 104 | + * @returns {ClientFileTarget|ClientCommandTarget} |
| 105 | + */ |
| 106 | +function getConfigPath(client) { |
| 107 | + const normalizedClient = client ? client.toLowerCase() : "claude"; |
| 108 | + verbose(`Getting config path for client: ${normalizedClient}`); |
| 109 | + |
| 110 | + const configTarget = clientPaths[normalizedClient] || { |
| 111 | + type: "file", |
| 112 | + path: path.join( |
| 113 | + path.dirname(defaultClaudePath), |
| 114 | + "..", |
| 115 | + client || "claude", |
| 116 | + `${normalizedClient}_config.json`, |
| 117 | + ), |
| 118 | + }; |
| 119 | + |
| 120 | + verbose(`Config path resolved to: ${JSON.stringify(configTarget)}`); |
| 121 | + return configTarget; |
| 122 | +} |
| 123 | + |
| 124 | +/** |
| 125 | + * @param {string} client |
| 126 | + * @returns {ClientConfig} |
| 127 | + */ |
| 128 | +function readConfig(client) { |
| 129 | + verbose(`Reading config for client: ${client}`); |
| 130 | + try { |
| 131 | + const configPath = getConfigPath(client); |
| 132 | + |
| 133 | + // Command-based installers (i.e. VS Code) do not currently support listing servers |
| 134 | + if (configPath.type === "command") { |
| 135 | + return { mcpServers: {} }; |
| 136 | + } |
| 137 | + |
| 138 | + verbose(`Checking if config file exists at: ${configPath.path}`); |
| 139 | + if (!fs.existsSync(configPath.path)) { |
| 140 | + verbose(`Config file not found, returning default empty config`); |
| 141 | + return { mcpServers: {} }; |
| 142 | + } |
| 143 | + |
| 144 | + verbose(`Reading config file content`); |
| 145 | + const rawConfig = JSON.parse(fs.readFileSync(configPath.path, "utf8")); |
| 146 | + verbose( |
| 147 | + `Config loaded successfully: ${JSON.stringify(rawConfig, null, 2)}`, |
| 148 | + ); |
| 149 | + |
| 150 | + return { |
| 151 | + ...rawConfig, |
| 152 | + mcpServers: rawConfig.mcpServers || {}, |
| 153 | + }; |
| 154 | + } catch (error) { |
| 155 | + verbose( |
| 156 | + `Error reading config: ${error instanceof Error ? error.stack : JSON.stringify(error)}`, |
| 157 | + ); |
| 158 | + return { mcpServers: {} }; |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +/** |
| 163 | + * @param {ClientConfig} config |
| 164 | + * @param {string} [client] |
| 165 | + */ |
| 166 | +function writeConfig(configObj, client) { |
| 167 | + verbose(`Writing config for client: ${client || "default"}`); |
| 168 | + verbose(`Config data: ${JSON.stringify(configObj, null, 2)}`); |
| 169 | + |
| 170 | + if (!configObj.mcpServers || typeof configObj.mcpServers !== "object") { |
| 171 | + verbose(`Invalid mcpServers structure in config`); |
| 172 | + throw new Error("Invalid mcpServers structure"); |
| 173 | + } |
| 174 | + |
| 175 | + const configPath = getConfigPath(client); |
| 176 | + if (configPath.type === "command") { |
| 177 | + writeConfigCommand(configObj, configPath); |
| 178 | + } else { |
| 179 | + writeConfigFile(configObj, configPath); |
| 180 | + } |
| 181 | +} |
| 182 | + |
| 183 | +/** |
| 184 | + * @param {ClientConfig} config |
| 185 | + * @param {ClientCommandTarget} target |
| 186 | + */ |
| 187 | +function writeConfigCommand(config, target) { |
| 188 | + const args = []; |
| 189 | + for (const [name, server] of Object.entries(config.mcpServers)) { |
| 190 | + args.push("--add-mcp", JSON.stringify({ ...server, name })); |
| 191 | + } |
| 192 | + |
| 193 | + verbose(`Running command: ${JSON.stringify([target.command, ...args])}`); |
| 194 | + |
| 195 | + try { |
| 196 | + const output = execFileSync(target.command, args); |
| 197 | + verbose(`Executed command successfully: ${output.toString()}`); |
| 198 | + } catch (error) { |
| 199 | + verbose( |
| 200 | + `Error executing command: ${error instanceof Error ? error.message : String(error)}`, |
| 201 | + ); |
| 202 | + |
| 203 | + if (error && error.code === "ENOENT") { |
| 204 | + throw new Error( |
| 205 | + `Command '${target.command}' not found. Make sure ${target.command} is installed and on your PATH`, |
| 206 | + ); |
| 207 | + } |
| 208 | + |
| 209 | + throw error; |
| 210 | + } |
| 211 | +} |
| 212 | + |
| 213 | +/** |
| 214 | + * @param {ClientConfig} config |
| 215 | + * @param {ClientFileTarget} target |
| 216 | + */ |
| 217 | +function writeConfigFile(config, target) { |
| 218 | + const configDir = path.dirname(target.path); |
| 219 | + |
| 220 | + verbose(`Ensuring config directory exists: ${configDir}`); |
| 221 | + if (!fs.existsSync(configDir)) { |
| 222 | + verbose(`Creating directory: ${configDir}`); |
| 223 | + fs.mkdirSync(configDir, { recursive: true }); |
| 224 | + } |
| 225 | + |
| 226 | + let existingConfig = { mcpServers: {} }; |
| 227 | + try { |
| 228 | + if (fs.existsSync(target.path)) { |
| 229 | + verbose(`Reading existing config file for merging`); |
| 230 | + existingConfig = JSON.parse(fs.readFileSync(target.path, "utf8")); |
| 231 | + verbose( |
| 232 | + `Existing config loaded: ${JSON.stringify(existingConfig, null, 2)}`, |
| 233 | + ); |
| 234 | + } |
| 235 | + } catch (error) { |
| 236 | + verbose( |
| 237 | + `Error reading existing config for merge: ${error instanceof Error ? error.message : String(error)}`, |
| 238 | + ); |
| 239 | + // If reading fails, continue with empty existing config |
| 240 | + } |
| 241 | + |
| 242 | + verbose(`Merging configs`); |
| 243 | + const mergedConfig = { |
| 244 | + ...existingConfig, |
| 245 | + ...config, |
| 246 | + }; |
| 247 | + verbose(`Merged config: ${JSON.stringify(mergedConfig, null, 2)}`); |
| 248 | + |
| 249 | + verbose(`Writing config to file: ${target.path}`); |
| 250 | + fs.writeFileSync(target.path, JSON.stringify(mergedConfig, null, 2)); |
| 251 | + verbose(`Config successfully written`); |
| 252 | +} |
| 253 | + |
| 254 | +function verbose(msg) { |
| 255 | + if (process.env.MCP_VERBOSE) { |
| 256 | + console.log(`[config] ${msg}`); |
| 257 | + } |
| 258 | +} |
| 259 | + |
| 260 | +const supportedClients = [ |
| 261 | + { key: "cursor", label: "Cursor" }, |
| 262 | + { key: "claude", label: "Claude" }, |
| 263 | + { key: "vscode", label: "VS Code" }, |
| 264 | + { key: "insiders", label: "VS Code Insiders" }, |
| 265 | + { key: "windsurf", label: "Windsurf" }, |
| 266 | + { key: "cline", label: "Cline" }, |
| 267 | + { key: "roocode", label: "RooCode" }, |
| 268 | + { key: "witsy", label: "Witsy" }, |
| 269 | + { key: "enconvo", label: "Enconvo" }, |
| 270 | + { key: "boltai", label: "BoltAI" }, |
| 271 | + { key: "amazon-bedrock", label: "Amazon Bedrock" }, |
| 272 | + { key: "amazonq", label: "Amazon Q" }, |
| 273 | +]; |
| 274 | + |
| 275 | +module.exports = { |
| 276 | + getConfigPath, |
| 277 | + readConfig, |
| 278 | + writeConfig, |
| 279 | + supportedClients, |
| 280 | +}; |
0 commit comments