@brain-mcps/setup 0.1.0

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/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @brain-mcps/setup
2
+
3
+ Configure AI assistant clients to connect to [Brain4AI](https://brain4ai.app) (Cerebro Externo) and [Mentor4AI](https://mentor4ai.app) (Forge Mentor) MCP servers.
4
+
5
+ One command writes the correct config entry for any supported client, handling format differences (Gemini's `httpUrl`, Windsurf's `serverUrl`, Copilot's nested `mcp.servers` path, etc.).
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx @brain-mcps/setup gemini # Configure Gemini CLI for Cerebro Externo
11
+ npx @brain-mcps/setup gemini mentor # Configure Gemini CLI for Forge Mentor
12
+ npx @brain-mcps/setup --detect # Auto-detect and configure all installed clients
13
+ ```
14
+
15
+ After running, open the configured client. It will prompt you to authenticate via OAuth (magic link to your email).
16
+
17
+ ## Supported Clients
18
+
19
+ | Client | Config Format | Key Used |
20
+ | -------------- | ------------------------- | ---------------------------------- |
21
+ | Claude Code | `~/.claude/settings.json` | `url` |
22
+ | Claude Desktop | OS-specific config | `url` |
23
+ | Gemini CLI | `~/.gemini/settings.json` | `httpUrl` |
24
+ | Codex | `~/.codex/config.toml` | `url` (TOML) |
25
+ | Cursor | `~/.cursor/mcp.json` | `url` |
26
+ | Windsurf | `~/.windsurf/mcp.json` | `serverUrl` |
27
+ | GitHub Copilot | `~/.vscode/mcp.json` | `url` (nested under `mcp.servers`) |
28
+
29
+ ## Usage
30
+
31
+ ```bash
32
+ # Configure a specific client
33
+ npx @brain-mcps/setup <client> [product]
34
+
35
+ # Auto-detect installed clients and configure all
36
+ npx @brain-mcps/setup --detect [product]
37
+
38
+ # Preview changes without writing files
39
+ npx @brain-mcps/setup gemini --dry-run
40
+
41
+ # List all supported clients and their config paths
42
+ npx @brain-mcps/setup --list
43
+ ```
44
+
45
+ ### Products
46
+
47
+ - `brain` (default): Cerebro Externo at `https://mcp.brain4ai.app/mcp`
48
+ - `mentor`: Forge Mentor at `https://mcp.mentor4ai.app/mcp`
49
+
50
+ ### Clients
51
+
52
+ Use any of these as the `<client>` argument:
53
+
54
+ `claude-code`, `claude-desktop`, `gemini`, `codex`, `cursor`, `windsurf`, `copilot`
55
+
56
+ ## How It Works
57
+
58
+ 1. Reads the target client's JSON config file (or creates it if missing)
59
+ 2. Merges in the MCP server entry with the correct key format for that client
60
+ 3. Writes the file back, preserving all existing entries
61
+
62
+ If the server is already configured with the same URL, it reports `already_configured` and makes no changes.
63
+
64
+ ## Authentication
65
+
66
+ After configuring a client, the first time you use it the client will trigger an OAuth flow:
67
+
68
+ 1. A browser opens for sign-in
69
+ 2. Enter the email from your alpha invite
70
+ 3. Check your inbox for a magic link
71
+ 4. Click the link to complete authentication
72
+
73
+ ## Troubleshooting
74
+
75
+ ### Config file not found
76
+
77
+ Run `npx @brain-mcps/setup --list` to see the expected config path for each client. The CLI creates the file and parent directories automatically.
78
+
79
+ ### OAuth fails in the browser
80
+
81
+ - Make sure your email matches the alpha invite
82
+ - Magic links expire after 10 minutes; request a new one
83
+ - Try clearing browser cookies for the authentication domain
84
+
85
+ ### Client doesn't pick up changes
86
+
87
+ Some clients require a restart after config changes. Close and reopen the client.
88
+
89
+ ## License
90
+
91
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("./index.js");
5
+ // ── Constants ────────────────────────────────────────────────────────────
6
+ const VALID_PRODUCTS = new Set(["brain", "mentor"]);
7
+ const VALID_CLIENTS = new Set(Object.keys(index_js_1.CLIENT_DEFS));
8
+ // ── Helpers ──────────────────────────────────────────────────────────────
9
+ function printUsage() {
10
+ console.log(`
11
+ brain-mcp-setup: Configure AI clients to connect to Brain4AI / Mentor4AI MCP servers.
12
+
13
+ Usage:
14
+ npx @brain-mcps/setup <client> [product]
15
+ npx @brain-mcps/setup --detect [product]
16
+ npx @brain-mcps/setup --list
17
+
18
+ Arguments:
19
+ client Client to configure: ${[...VALID_CLIENTS].join(", ")}
20
+ product Which product: brain (Cerebro Externo) or mentor (Forge Mentor)
21
+ Default: brain
22
+
23
+ Options:
24
+ --detect Auto-detect installed clients and configure all of them
25
+ --list List all supported clients and their config file paths
26
+ --dry-run Show what would be changed without writing any files
27
+ --help Show this help message
28
+
29
+ Examples:
30
+ npx @brain-mcps/setup gemini # Configure Gemini CLI for Cerebro Externo
31
+ npx @brain-mcps/setup gemini mentor # Configure Gemini CLI for Forge Mentor
32
+ npx @brain-mcps/setup claude-code brain # Configure Claude Code for Cerebro Externo
33
+ npx @brain-mcps/setup --detect # Configure all detected clients
34
+ npx @brain-mcps/setup --list # Show supported clients
35
+ `);
36
+ }
37
+ function printResult(result) {
38
+ const clientName = (0, index_js_1.getClientName)(result.client);
39
+ switch (result.action) {
40
+ case "created":
41
+ console.log(` + ${clientName}: created ${result.configPath}`);
42
+ console.log(` Server "${result.serverName}" added.`);
43
+ break;
44
+ case "updated":
45
+ console.log(` + ${clientName}: updated ${result.configPath}`);
46
+ console.log(` Server "${result.serverName}" added.`);
47
+ break;
48
+ case "already_configured":
49
+ console.log(` = ${clientName}: already configured (${result.serverName})`);
50
+ break;
51
+ }
52
+ }
53
+ // ── Main ─────────────────────────────────────────────────────────────────
54
+ function main() {
55
+ const args = process.argv.slice(2);
56
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
57
+ printUsage();
58
+ process.exit(0);
59
+ }
60
+ if (args.includes("--list")) {
61
+ console.log("\nSupported clients:\n");
62
+ const supported = (0, index_js_1.listClients)().filter((c) => (0, index_js_1.isClientSupportedOnPlatform)(c.id));
63
+ for (const client of supported) {
64
+ console.log(` ${client.id.padEnd(16)} ${client.name.padEnd(24)} ${client.configPath}`);
65
+ }
66
+ const skipped = (0, index_js_1.listClients)().filter((c) => !(0, index_js_1.isClientSupportedOnPlatform)(c.id));
67
+ if (skipped.length > 0) {
68
+ console.log("\nNot supported on this platform:");
69
+ for (const client of skipped) {
70
+ console.log(` ${client.id.padEnd(16)} ${client.name.padEnd(24)} (${client.supportedPlatforms.join(", ")} only)`);
71
+ }
72
+ }
73
+ console.log("");
74
+ return;
75
+ }
76
+ // Parse product from args (defaults to "brain"; errors on unknown values)
77
+ const nonFlagArgs = args.filter((a) => !a.startsWith("--"));
78
+ let product = "brain";
79
+ if (nonFlagArgs.length >= 2) {
80
+ const productArg = nonFlagArgs[1];
81
+ if (!VALID_PRODUCTS.has(productArg)) {
82
+ console.error(`Unknown product: "${productArg}". Valid products: ${[...VALID_PRODUCTS].join(", ")}`);
83
+ process.exit(1);
84
+ }
85
+ product = productArg;
86
+ }
87
+ const dryRun = args.includes("--dry-run");
88
+ const setupOpts = { dryRun };
89
+ if (dryRun) {
90
+ console.log("[dry-run] No files will be modified.\n");
91
+ }
92
+ if (args.includes("--detect")) {
93
+ const detected = (0, index_js_1.detectInstalledClients)();
94
+ if (detected.length === 0) {
95
+ console.log("No supported AI clients detected on this system.");
96
+ console.log("Run with --list to see supported clients.");
97
+ process.exit(1);
98
+ }
99
+ console.log(`\nDetected ${detected.length} client(s). Configuring for ${product}:\n`);
100
+ for (const clientId of detected) {
101
+ try {
102
+ const result = (0, index_js_1.setupClient)(clientId, product, setupOpts);
103
+ printResult(result);
104
+ }
105
+ catch (err) {
106
+ const msg = err instanceof Error ? err.message : String(err);
107
+ console.error(` ! ${(0, index_js_1.getClientName)(clientId)}: ${msg}`);
108
+ }
109
+ }
110
+ if (!dryRun) {
111
+ console.log("\nDone. Open each client and complete the OAuth sign-in when prompted.");
112
+ }
113
+ return;
114
+ }
115
+ // Single client mode
116
+ const clientArg = nonFlagArgs[0];
117
+ if (!clientArg || !VALID_CLIENTS.has(clientArg)) {
118
+ console.error(`Unknown client: "${clientArg}". Valid clients: ${[...VALID_CLIENTS].join(", ")}`);
119
+ process.exit(1);
120
+ }
121
+ const clientId = clientArg;
122
+ if (!(0, index_js_1.isClientSupportedOnPlatform)(clientId)) {
123
+ const def = index_js_1.CLIENT_DEFS[clientId];
124
+ console.error(`Error: ${def.name} is not supported on ${process.platform}. ` +
125
+ `Supported platforms: ${def.supportedPlatforms.join(", ")}.`);
126
+ process.exit(1);
127
+ }
128
+ console.log(`\nConfiguring ${(0, index_js_1.getClientName)(clientId)} for ${product}:\n`);
129
+ const result = (0, index_js_1.setupClient)(clientId, product, setupOpts);
130
+ printResult(result);
131
+ if (result.action !== "already_configured") {
132
+ console.log("\nNext steps:");
133
+ console.log(` 1. Open ${(0, index_js_1.getClientName)(clientId)}`);
134
+ console.log(" 2. The client will prompt you to authenticate via OAuth");
135
+ console.log(" 3. Enter the email from your alpha invite");
136
+ console.log(" 4. Click the magic link in your inbox to complete sign-in");
137
+ console.log("");
138
+ }
139
+ }
140
+ main();
@@ -0,0 +1,38 @@
1
+ export type Product = "brain" | "mentor";
2
+ export type ClientId = "claude-code" | "claude-desktop" | "gemini" | "codex" | "cursor" | "windsurf" | "copilot";
3
+ export type ConfigFormat = "json" | "toml";
4
+ export interface ClientDef {
5
+ id: ClientId;
6
+ name: string;
7
+ /** Path relative to home dir (~ prefix), or absolute on Windows. */
8
+ configPathTemplate: string;
9
+ /** Config file format. Defaults to JSON. */
10
+ format?: ConfigFormat;
11
+ /** Path segments to the servers object (e.g. ["mcpServers"], ["mcp_servers"]). */
12
+ serversKey: string[];
13
+ /** Build the server entry for this client. */
14
+ buildEntry: (url: string) => Record<string, unknown>;
15
+ /** If set, only these platforms are supported. Otherwise all platforms. */
16
+ supportedPlatforms?: NodeJS.Platform[];
17
+ }
18
+ export interface ClientConfig extends ClientDef {
19
+ configPath: string;
20
+ }
21
+ export interface SetupResult {
22
+ client: ClientId;
23
+ configPath: string;
24
+ action: "created" | "updated" | "already_configured";
25
+ serverName: string;
26
+ }
27
+ export interface SetupOptions {
28
+ dryRun?: boolean;
29
+ /** Override home directory (for testing). */
30
+ homeDir?: string;
31
+ }
32
+ export declare const CLIENT_DEFS: Record<ClientId, ClientDef>;
33
+ export declare function resolveConfigPath(clientId: ClientId, homeDir?: string): string;
34
+ export declare function isClientSupportedOnPlatform(clientId: ClientId, platform?: NodeJS.Platform): boolean;
35
+ export declare function getClientName(clientId: ClientId): string;
36
+ export declare function setupClient(clientId: ClientId, product: Product, options?: SetupOptions): SetupResult;
37
+ export declare function detectInstalledClients(homeDir?: string): ClientId[];
38
+ export declare function listClients(homeDir?: string): ClientConfig[];
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CLIENT_DEFS = void 0;
37
+ exports.resolveConfigPath = resolveConfigPath;
38
+ exports.isClientSupportedOnPlatform = isClientSupportedOnPlatform;
39
+ exports.getClientName = getClientName;
40
+ exports.setupClient = setupClient;
41
+ exports.detectInstalledClients = detectInstalledClients;
42
+ exports.listClients = listClients;
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const os = __importStar(require("node:os"));
46
+ const smol_toml_1 = require("smol-toml");
47
+ // ── Product URLs ─────────────────────────────────────────────────────────
48
+ const PRODUCT_URLS = {
49
+ brain: "https://mcp.brain4ai.app/mcp",
50
+ mentor: "https://mcp.mentor4ai.app/mcp",
51
+ };
52
+ const PRODUCT_SERVER_NAMES = {
53
+ brain: "cerebro-externo",
54
+ mentor: "forge-mentor",
55
+ };
56
+ // ── Client Definitions ───────────────────────────────────────────────────
57
+ const CLAUDE_DESKTOP_PLATFORMS = ["darwin", "win32"];
58
+ function getClaudeDesktopTemplate() {
59
+ if (process.platform === "win32") {
60
+ return path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
61
+ }
62
+ // Default to macOS path; supportedPlatforms gates use on other OSes.
63
+ return "~/Library/Application Support/Claude/claude_desktop_config.json";
64
+ }
65
+ exports.CLIENT_DEFS = {
66
+ "claude-code": {
67
+ id: "claude-code",
68
+ name: "Claude Code",
69
+ configPathTemplate: "~/.claude/settings.json",
70
+ serversKey: ["mcpServers"],
71
+ buildEntry: (url) => ({ type: "url", url }),
72
+ },
73
+ "claude-desktop": {
74
+ id: "claude-desktop",
75
+ name: "Claude Desktop",
76
+ configPathTemplate: getClaudeDesktopTemplate(),
77
+ serversKey: ["mcpServers"],
78
+ buildEntry: (url) => ({ url }),
79
+ supportedPlatforms: CLAUDE_DESKTOP_PLATFORMS,
80
+ },
81
+ gemini: {
82
+ id: "gemini",
83
+ name: "Gemini CLI",
84
+ configPathTemplate: "~/.gemini/settings.json",
85
+ serversKey: ["mcpServers"],
86
+ buildEntry: (url) => ({ httpUrl: url }),
87
+ },
88
+ codex: {
89
+ id: "codex",
90
+ name: "Codex (OpenAI CLI)",
91
+ configPathTemplate: "~/.codex/config.toml",
92
+ format: "toml",
93
+ serversKey: ["mcp_servers"],
94
+ buildEntry: (url) => ({ url }),
95
+ },
96
+ cursor: {
97
+ id: "cursor",
98
+ name: "Cursor",
99
+ configPathTemplate: "~/.cursor/mcp.json",
100
+ serversKey: ["mcpServers"],
101
+ buildEntry: (url) => ({ url }),
102
+ },
103
+ windsurf: {
104
+ id: "windsurf",
105
+ name: "Windsurf",
106
+ configPathTemplate: "~/.windsurf/mcp.json",
107
+ serversKey: ["mcpServers"],
108
+ buildEntry: (url) => ({ serverUrl: url }),
109
+ },
110
+ copilot: {
111
+ id: "copilot",
112
+ name: "GitHub Copilot (VS Code)",
113
+ configPathTemplate: "~/.vscode/mcp.json",
114
+ serversKey: ["mcp", "servers"],
115
+ buildEntry: (url) => ({ type: "http", url }),
116
+ },
117
+ };
118
+ // ── Path Resolution ──────────────────────────────────────────────────────
119
+ function resolveConfigPath(clientId, homeDir) {
120
+ const home = homeDir ?? os.homedir();
121
+ const template = exports.CLIENT_DEFS[clientId].configPathTemplate;
122
+ return template.replace(/^~/, home);
123
+ }
124
+ function isClientSupportedOnPlatform(clientId, platform = process.platform) {
125
+ const def = exports.CLIENT_DEFS[clientId];
126
+ return !def.supportedPlatforms || def.supportedPlatforms.includes(platform);
127
+ }
128
+ function getClientName(clientId) {
129
+ return exports.CLIENT_DEFS[clientId].name;
130
+ }
131
+ // ── Setup Engine ─────────────────────────────────────────────────────────
132
+ function readConfigFile(filepath, format) {
133
+ if (!fs.existsSync(filepath))
134
+ return {};
135
+ const raw = fs.readFileSync(filepath, "utf-8");
136
+ if (format === "toml") {
137
+ return (0, smol_toml_1.parse)(raw);
138
+ }
139
+ return JSON.parse(raw);
140
+ }
141
+ function writeConfigFile(filepath, data, format) {
142
+ const dir = path.dirname(filepath);
143
+ if (!fs.existsSync(dir)) {
144
+ fs.mkdirSync(dir, { recursive: true });
145
+ }
146
+ const serialized = format === "toml"
147
+ ? (0, smol_toml_1.stringify)(data) + "\n"
148
+ : JSON.stringify(data, null, 2) + "\n";
149
+ fs.writeFileSync(filepath, serialized, "utf-8");
150
+ }
151
+ function getNestedObject(obj, keys) {
152
+ let current = obj;
153
+ for (const key of keys) {
154
+ const existing = current[key];
155
+ if (!(key in current) ||
156
+ existing === null ||
157
+ typeof existing !== "object" ||
158
+ Array.isArray(existing)) {
159
+ current[key] = {};
160
+ }
161
+ current = current[key];
162
+ }
163
+ return current;
164
+ }
165
+ function setupClient(clientId, product, options = {}) {
166
+ const def = exports.CLIENT_DEFS[clientId];
167
+ if (!isClientSupportedOnPlatform(clientId)) {
168
+ throw new Error(`${def.name} is not supported on ${process.platform}. ` +
169
+ `Supported platforms: ${def.supportedPlatforms.join(", ")}.`);
170
+ }
171
+ const format = def.format ?? "json";
172
+ const configPath = resolveConfigPath(clientId, options.homeDir);
173
+ const url = PRODUCT_URLS[product];
174
+ const serverName = PRODUCT_SERVER_NAMES[product];
175
+ const config = readConfigFile(configPath, format);
176
+ const servers = getNestedObject(config, def.serversKey);
177
+ const existing = servers[serverName];
178
+ if (existing) {
179
+ const existingUrl = existing.url ??
180
+ existing.httpUrl ??
181
+ existing.serverUrl;
182
+ if (existingUrl === url) {
183
+ return {
184
+ client: clientId,
185
+ configPath,
186
+ action: "already_configured",
187
+ serverName,
188
+ };
189
+ }
190
+ }
191
+ const action = fs.existsSync(configPath) ? "updated" : "created";
192
+ if (!options.dryRun) {
193
+ servers[serverName] = def.buildEntry(url);
194
+ writeConfigFile(configPath, config, format);
195
+ }
196
+ return { client: clientId, configPath, action, serverName };
197
+ }
198
+ function detectInstalledClients(homeDir) {
199
+ return Object.keys(exports.CLIENT_DEFS).filter((id) => {
200
+ if (!isClientSupportedOnPlatform(id))
201
+ return false;
202
+ const configPath = resolveConfigPath(id, homeDir);
203
+ const configDir = path.dirname(configPath);
204
+ return fs.existsSync(configDir);
205
+ });
206
+ }
207
+ function listClients(homeDir) {
208
+ return Object.keys(exports.CLIENT_DEFS).map((id) => ({
209
+ ...exports.CLIENT_DEFS[id],
210
+ configPath: resolveConfigPath(id, homeDir),
211
+ }));
212
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,302 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const os = __importStar(require("node:os"));
40
+ const smol_toml_1 = require("smol-toml");
41
+ const index_js_1 = require("./index.js");
42
+ /** Temporarily override process.platform for a test. */
43
+ function withPlatform(platform, fn) {
44
+ const original = Object.getOwnPropertyDescriptor(process, "platform");
45
+ Object.defineProperty(process, "platform", {
46
+ value: platform,
47
+ configurable: true,
48
+ });
49
+ try {
50
+ fn();
51
+ }
52
+ finally {
53
+ if (original)
54
+ Object.defineProperty(process, "platform", original);
55
+ }
56
+ }
57
+ // ── Test helpers: use a real temp directory as fake $HOME ────────────────
58
+ let tmpHome;
59
+ (0, vitest_1.beforeEach)(() => {
60
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "brain-mcp-setup-test-"));
61
+ });
62
+ (0, vitest_1.afterEach)(() => {
63
+ fs.rmSync(tmpHome, { recursive: true, force: true });
64
+ });
65
+ /** Create the config directory for a client inside tmpHome. */
66
+ function ensureConfigDir(clientId) {
67
+ const configPath = (0, index_js_1.resolveConfigPath)(clientId, tmpHome);
68
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
69
+ }
70
+ /** Write a JSON config file for a client inside tmpHome. */
71
+ function writeConfig(clientId, data) {
72
+ const configPath = (0, index_js_1.resolveConfigPath)(clientId, tmpHome);
73
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
74
+ fs.writeFileSync(configPath, JSON.stringify(data, null, 2));
75
+ }
76
+ /** Read a JSON config file for a client from tmpHome. */
77
+ function readConfig(clientId) {
78
+ const configPath = (0, index_js_1.resolveConfigPath)(clientId, tmpHome);
79
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"));
80
+ }
81
+ /** Read a TOML config file for a client from tmpHome. */
82
+ function readTomlConfig(clientId) {
83
+ const configPath = (0, index_js_1.resolveConfigPath)(clientId, tmpHome);
84
+ return (0, smol_toml_1.parse)(fs.readFileSync(configPath, "utf-8"));
85
+ }
86
+ /** Write a raw string config file for a client inside tmpHome. */
87
+ function writeRawConfig(clientId, contents) {
88
+ const configPath = (0, index_js_1.resolveConfigPath)(clientId, tmpHome);
89
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
90
+ fs.writeFileSync(configPath, contents);
91
+ }
92
+ // ── Tests ───────────────────────────────────────────────────────────────
93
+ (0, vitest_1.describe)("listClients", () => {
94
+ (0, vitest_1.it)("returns all 7 supported clients", () => {
95
+ const clients = (0, index_js_1.listClients)();
96
+ (0, vitest_1.expect)(clients).toHaveLength(7);
97
+ const ids = clients.map((c) => c.id);
98
+ (0, vitest_1.expect)(ids).toContain("claude-code");
99
+ (0, vitest_1.expect)(ids).toContain("claude-desktop");
100
+ (0, vitest_1.expect)(ids).toContain("gemini");
101
+ (0, vitest_1.expect)(ids).toContain("codex");
102
+ (0, vitest_1.expect)(ids).toContain("cursor");
103
+ (0, vitest_1.expect)(ids).toContain("windsurf");
104
+ (0, vitest_1.expect)(ids).toContain("copilot");
105
+ });
106
+ });
107
+ (0, vitest_1.describe)("resolveConfigPath", () => {
108
+ (0, vitest_1.it)("replaces ~ with the provided home directory", () => {
109
+ const resolved = (0, index_js_1.resolveConfigPath)("gemini", "/fake/home");
110
+ (0, vitest_1.expect)(resolved).toBe("/fake/home/.gemini/settings.json");
111
+ });
112
+ (0, vitest_1.it)("uses os.homedir when no homeDir provided", () => {
113
+ const resolved = (0, index_js_1.resolveConfigPath)("gemini");
114
+ (0, vitest_1.expect)(resolved).toContain(os.homedir());
115
+ });
116
+ });
117
+ (0, vitest_1.describe)("setupClient", () => {
118
+ (0, vitest_1.it)("creates gemini config with httpUrl key", () => {
119
+ ensureConfigDir("gemini");
120
+ const result = (0, index_js_1.setupClient)("gemini", "brain", { homeDir: tmpHome });
121
+ (0, vitest_1.expect)(result.action).toBe("created");
122
+ (0, vitest_1.expect)(result.serverName).toBe("cerebro-externo");
123
+ const config = readConfig("gemini");
124
+ const servers = config.mcpServers;
125
+ (0, vitest_1.expect)(servers["cerebro-externo"].httpUrl).toBe("https://mcp.brain4ai.app/mcp");
126
+ // Gemini uses httpUrl, not url
127
+ (0, vitest_1.expect)(servers["cerebro-externo"].url).toBeUndefined();
128
+ });
129
+ (0, vitest_1.it)("creates claude-code config with type and url keys", () => {
130
+ ensureConfigDir("claude-code");
131
+ const result = (0, index_js_1.setupClient)("claude-code", "mentor", { homeDir: tmpHome });
132
+ (0, vitest_1.expect)(result.action).toBe("created");
133
+ (0, vitest_1.expect)(result.serverName).toBe("forge-mentor");
134
+ const config = readConfig("claude-code");
135
+ const servers = config.mcpServers;
136
+ (0, vitest_1.expect)(servers["forge-mentor"]).toEqual({
137
+ type: "url",
138
+ url: "https://mcp.mentor4ai.app/mcp",
139
+ });
140
+ });
141
+ (0, vitest_1.it)("creates windsurf config with serverUrl key", () => {
142
+ ensureConfigDir("windsurf");
143
+ const result = (0, index_js_1.setupClient)("windsurf", "brain", { homeDir: tmpHome });
144
+ (0, vitest_1.expect)(result.serverName).toBe("cerebro-externo");
145
+ const config = readConfig("windsurf");
146
+ const servers = config.mcpServers;
147
+ (0, vitest_1.expect)(servers["cerebro-externo"].serverUrl).toBe("https://mcp.brain4ai.app/mcp");
148
+ });
149
+ (0, vitest_1.it)("creates copilot config with nested mcp.servers path", () => {
150
+ ensureConfigDir("copilot");
151
+ (0, index_js_1.setupClient)("copilot", "brain", { homeDir: tmpHome });
152
+ const config = readConfig("copilot");
153
+ const mcp = config.mcp;
154
+ (0, vitest_1.expect)(mcp.servers["cerebro-externo"]).toEqual({
155
+ type: "http",
156
+ url: "https://mcp.brain4ai.app/mcp",
157
+ });
158
+ });
159
+ (0, vitest_1.it)("creates codex config as TOML with mcp_servers.<id>.url", () => {
160
+ ensureConfigDir("codex");
161
+ const result = (0, index_js_1.setupClient)("codex", "brain", { homeDir: tmpHome });
162
+ (0, vitest_1.expect)(result.action).toBe("created");
163
+ (0, vitest_1.expect)(result.serverName).toBe("cerebro-externo");
164
+ (0, vitest_1.expect)(result.configPath).toMatch(/config\.toml$/);
165
+ const config = readTomlConfig("codex");
166
+ const servers = config.mcp_servers;
167
+ (0, vitest_1.expect)(servers["cerebro-externo"]).toEqual({
168
+ url: "https://mcp.brain4ai.app/mcp",
169
+ });
170
+ });
171
+ (0, vitest_1.it)("merges codex config into existing TOML without losing other entries", () => {
172
+ writeRawConfig("codex", [
173
+ 'model = "gpt-5"',
174
+ "",
175
+ "[mcp_servers.metalab]",
176
+ 'command = "npx"',
177
+ 'args = ["metalab-mcp-client"]',
178
+ "",
179
+ ].join("\n"));
180
+ const result = (0, index_js_1.setupClient)("codex", "mentor", { homeDir: tmpHome });
181
+ (0, vitest_1.expect)(result.action).toBe("updated");
182
+ const config = readTomlConfig("codex");
183
+ (0, vitest_1.expect)(config.model).toBe("gpt-5");
184
+ const servers = config.mcp_servers;
185
+ (0, vitest_1.expect)(servers.metalab).toMatchObject({
186
+ command: "npx",
187
+ args: ["metalab-mcp-client"],
188
+ });
189
+ (0, vitest_1.expect)(servers["forge-mentor"]).toEqual({
190
+ url: "https://mcp.mentor4ai.app/mcp",
191
+ });
192
+ });
193
+ (0, vitest_1.it)("returns already_configured for codex when TOML URL matches", () => {
194
+ writeRawConfig("codex", [
195
+ "[mcp_servers.cerebro-externo]",
196
+ 'url = "https://mcp.brain4ai.app/mcp"',
197
+ "",
198
+ ].join("\n"));
199
+ const result = (0, index_js_1.setupClient)("codex", "brain", { homeDir: tmpHome });
200
+ (0, vitest_1.expect)(result.action).toBe("already_configured");
201
+ });
202
+ (0, vitest_1.it)("merges into existing config without overwriting other keys", () => {
203
+ writeConfig("gemini", {
204
+ mcpServers: { "other-server": { url: "https://other.example.com" } },
205
+ someOtherKey: "keep-me",
206
+ });
207
+ const result = (0, index_js_1.setupClient)("gemini", "brain", { homeDir: tmpHome });
208
+ (0, vitest_1.expect)(result.action).toBe("updated");
209
+ const config = readConfig("gemini");
210
+ (0, vitest_1.expect)(config.someOtherKey).toBe("keep-me");
211
+ const servers = config.mcpServers;
212
+ (0, vitest_1.expect)(servers["other-server"].url).toBe("https://other.example.com");
213
+ (0, vitest_1.expect)(servers["cerebro-externo"].httpUrl).toBe("https://mcp.brain4ai.app/mcp");
214
+ });
215
+ (0, vitest_1.it)("returns already_configured when URL matches", () => {
216
+ writeConfig("gemini", {
217
+ mcpServers: {
218
+ "cerebro-externo": { httpUrl: "https://mcp.brain4ai.app/mcp" },
219
+ },
220
+ });
221
+ const result = (0, index_js_1.setupClient)("gemini", "brain", { homeDir: tmpHome });
222
+ (0, vitest_1.expect)(result.action).toBe("already_configured");
223
+ });
224
+ (0, vitest_1.it)("dry-run does not write files", () => {
225
+ ensureConfigDir("gemini");
226
+ const configPath = (0, index_js_1.resolveConfigPath)("gemini", tmpHome);
227
+ const result = (0, index_js_1.setupClient)("gemini", "brain", {
228
+ homeDir: tmpHome,
229
+ dryRun: true,
230
+ });
231
+ (0, vitest_1.expect)(result.action).toBe("created");
232
+ (0, vitest_1.expect)(fs.existsSync(configPath)).toBe(false);
233
+ });
234
+ (0, vitest_1.it)("uses brain4ai.app for brain product", () => {
235
+ ensureConfigDir("claude-code");
236
+ (0, index_js_1.setupClient)("claude-code", "brain", { homeDir: tmpHome });
237
+ const config = readConfig("claude-code");
238
+ const servers = config.mcpServers;
239
+ (0, vitest_1.expect)(servers["cerebro-externo"].url).toContain("brain4ai.app");
240
+ });
241
+ (0, vitest_1.it)("uses mentor4ai.app for mentor product", () => {
242
+ ensureConfigDir("claude-code");
243
+ (0, index_js_1.setupClient)("claude-code", "mentor", { homeDir: tmpHome });
244
+ const config = readConfig("claude-code");
245
+ const servers = config.mcpServers;
246
+ (0, vitest_1.expect)(servers["forge-mentor"].url).toContain("mentor4ai.app");
247
+ });
248
+ });
249
+ (0, vitest_1.describe)("detectInstalledClients", () => {
250
+ (0, vitest_1.it)("detects clients whose config directories exist", () => {
251
+ // Create dirs for gemini and claude-code only
252
+ ensureConfigDir("gemini");
253
+ ensureConfigDir("claude-code");
254
+ const detected = (0, index_js_1.detectInstalledClients)(tmpHome);
255
+ (0, vitest_1.expect)(detected).toContain("gemini");
256
+ (0, vitest_1.expect)(detected).toContain("claude-code");
257
+ (0, vitest_1.expect)(detected).not.toContain("codex");
258
+ });
259
+ (0, vitest_1.it)("returns empty array when no clients are installed", () => {
260
+ const detected = (0, index_js_1.detectInstalledClients)(tmpHome);
261
+ (0, vitest_1.expect)(detected).toHaveLength(0);
262
+ });
263
+ (0, vitest_1.it)("excludes claude-desktop on linux even if config dir exists", () => {
264
+ ensureConfigDir("gemini");
265
+ ensureConfigDir("claude-desktop");
266
+ withPlatform("linux", () => {
267
+ const detected = (0, index_js_1.detectInstalledClients)(tmpHome);
268
+ (0, vitest_1.expect)(detected).toContain("gemini");
269
+ (0, vitest_1.expect)(detected).not.toContain("claude-desktop");
270
+ });
271
+ });
272
+ });
273
+ (0, vitest_1.describe)("isClientSupportedOnPlatform", () => {
274
+ (0, vitest_1.it)("returns true for any-platform clients on every OS", () => {
275
+ (0, vitest_1.expect)((0, index_js_1.isClientSupportedOnPlatform)("gemini", "linux")).toBe(true);
276
+ (0, vitest_1.expect)((0, index_js_1.isClientSupportedOnPlatform)("gemini", "darwin")).toBe(true);
277
+ (0, vitest_1.expect)((0, index_js_1.isClientSupportedOnPlatform)("gemini", "win32")).toBe(true);
278
+ (0, vitest_1.expect)((0, index_js_1.isClientSupportedOnPlatform)("codex", "linux")).toBe(true);
279
+ });
280
+ (0, vitest_1.it)("restricts claude-desktop to darwin and win32", () => {
281
+ (0, vitest_1.expect)((0, index_js_1.isClientSupportedOnPlatform)("claude-desktop", "darwin")).toBe(true);
282
+ (0, vitest_1.expect)((0, index_js_1.isClientSupportedOnPlatform)("claude-desktop", "win32")).toBe(true);
283
+ (0, vitest_1.expect)((0, index_js_1.isClientSupportedOnPlatform)("claude-desktop", "linux")).toBe(false);
284
+ (0, vitest_1.expect)((0, index_js_1.isClientSupportedOnPlatform)("claude-desktop", "freebsd")).toBe(false);
285
+ });
286
+ });
287
+ (0, vitest_1.describe)("setupClient platform restrictions", () => {
288
+ (0, vitest_1.it)("throws when configuring claude-desktop on linux", () => {
289
+ withPlatform("linux", () => {
290
+ (0, vitest_1.expect)(() => (0, index_js_1.setupClient)("claude-desktop", "brain", { homeDir: tmpHome })).toThrow(/not supported on linux/);
291
+ });
292
+ });
293
+ (0, vitest_1.it)("succeeds when configuring claude-desktop on darwin", () => {
294
+ ensureConfigDir("claude-desktop");
295
+ withPlatform("darwin", () => {
296
+ const result = (0, index_js_1.setupClient)("claude-desktop", "brain", {
297
+ homeDir: tmpHome,
298
+ });
299
+ (0, vitest_1.expect)(result.action).toBe("created");
300
+ });
301
+ });
302
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@brain-mcps/setup",
3
+ "version": "0.1.0",
4
+ "description": "Configure AI assistant clients to connect to Brain4AI and Mentor4AI MCP servers",
5
+ "bin": {
6
+ "brain-mcp-setup": "dist/cli.js"
7
+ },
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "check-types": "tsc --noEmit",
13
+ "dev": "tsc --watch",
14
+ "test": "vitest run",
15
+ "clean": "rimraf dist"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "ai",
20
+ "setup",
21
+ "brain4ai",
22
+ "mentor4ai",
23
+ "claude",
24
+ "gemini",
25
+ "chatgpt"
26
+ ],
27
+ "author": "SirGrimorum",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "smol-toml": "^1.6.1"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.0.0",
34
+ "rimraf": "^6.0.1",
35
+ "typescript": "^5.9.2"
36
+ },
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "default": "./dist/index.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md"
46
+ ],
47
+ "engines": {
48
+ "node": ">=18"
49
+ }
50
+ }