sync-project-mcps 1.0.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,234 @@
1
+ # sync-project-mcps
2
+
3
+ **Zero-config project-level MCP synchronization across AI coding assistants.**
4
+
5
+ One command. All your project MCP servers. Every editor.
6
+
7
+ ```bash
8
+ npx sync-project-mcps
9
+ ```
10
+
11
+ ---
12
+
13
+ ## The Problem
14
+
15
+ You use multiple AI coding assistants - Cursor, Claude Code, Windsurf, Cline. Each has its own MCP configuration file in a different location. Adding a new MCP server means updating 3-5 config files manually. Forgetting one means inconsistent tooling across editors.
16
+
17
+ ## The Solution
18
+
19
+ `sync-project-mcps` finds all your project MCP configurations, merges them, and writes the unified config back to all clients. **No setup required.**
20
+
21
+ ```
22
+ ┌─────────────────────────────────────────────────────────────┐
23
+ │ │
24
+ │ .cursor/mcp.json ──┐ │
25
+ │ │ │
26
+ │ .mcp.json ─────────┼──► MERGE ──► Write to ALL clients │
27
+ │ │ │
28
+ │ .windsurf/... ─────┘ │
29
+ │ │
30
+ └─────────────────────────────────────────────────────────────┘
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ### Run Once (npx)
38
+
39
+ ```bash
40
+ npx sync-project-mcps
41
+ ```
42
+
43
+ ### Preview Changes
44
+
45
+ ```bash
46
+ npx sync-project-mcps --dry-run
47
+ ```
48
+
49
+ ### Verbose Output
50
+
51
+ ```bash
52
+ npx sync-project-mcps -v
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Supported Clients
58
+
59
+ | Client | Config Location | Status |
60
+ |--------|-----------------|--------|
61
+ | **Cursor** | `.cursor/mcp.json` | Project |
62
+ | **Claude Code** | `.mcp.json` | Project |
63
+ | **Windsurf** | `.windsurf/mcp.json` | Project |
64
+ | **Cline** | VS Code globalStorage | Global |
65
+ | **Roo Code** | `.roo/mcp.json` | Project |
66
+
67
+ ---
68
+
69
+ ## Installation
70
+
71
+ ### Option 1: Run with npx (Recommended)
72
+
73
+ No installation needed:
74
+
75
+ ```bash
76
+ npx sync-project-mcps
77
+ ```
78
+
79
+ ### Option 2: Install Globally
80
+
81
+ ```bash
82
+ npm install -g sync-project-mcps
83
+ sync-project-mcps
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Adding MCP Servers
89
+
90
+ ### For Cursor
91
+
92
+ Click to install an MCP server directly:
93
+
94
+ [![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=example&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyJleGFtcGxlLW1jcCJdfQ==)
95
+
96
+ Or add manually to `.cursor/mcp.json`:
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "example": {
102
+ "command": "npx",
103
+ "args": ["example-mcp"]
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ### For Claude Code
110
+
111
+ Install globally with the Claude CLI:
112
+
113
+ ```bash
114
+ claude mcp add example -s user -- npx example-mcp
115
+ ```
116
+
117
+ Or add to project-level `.mcp.json`:
118
+
119
+ ```json
120
+ {
121
+ "mcpServers": {
122
+ "example": {
123
+ "command": "npx",
124
+ "args": ["example-mcp"]
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Then Sync
131
+
132
+ ```bash
133
+ npx sync-project-mcps
134
+ ```
135
+
136
+ Your MCP server is now available in **all** your AI coding assistants.
137
+
138
+ ---
139
+
140
+ ## How It Works
141
+
142
+ 1. **Discovers** MCP configs from all supported clients
143
+ 2. **Merges** all `mcpServers` entries (dedupes by name)
144
+ 3. **Writes** the unified config to every client location
145
+
146
+ ```
147
+ $ npx sync-project-mcps
148
+
149
+ Sync MCP Configurations
150
+
151
+ Found configurations:
152
+ + Cursor: 3 server(s)
153
+ + Claude Code: 2 server(s)
154
+
155
+ Merged result: 4 unique server(s)
156
+ - context7
157
+ - filesystem
158
+ - github
159
+ - playwright
160
+
161
+ Syncing to clients...
162
+ [update] Cursor (no changes)
163
+ [update] Claude Code (+1)
164
+ [create] Windsurf (+4)
165
+ [create] Cline (+4)
166
+ [create] Roo Code (+4)
167
+
168
+ Done!
169
+ ```
170
+
171
+ ---
172
+
173
+ ## CLI Options
174
+
175
+ ```
176
+ sync-project-mcps [options]
177
+
178
+ Options:
179
+ --dry-run Show what would be synced without writing files
180
+ -v, --verbose Show detailed information about each server
181
+ -h, --help Show help message
182
+ --version Show version
183
+ ```
184
+
185
+ ---
186
+
187
+ ## FAQ
188
+
189
+ ### Does it delete servers?
190
+
191
+ No. It only adds missing servers. If a server exists in Cursor but not Claude Code, it gets added to Claude Code. Servers are never removed.
192
+
193
+ ### What if the same server has different configs?
194
+
195
+ First occurrence wins. If `github` is configured differently in Cursor vs Claude Code, the Cursor config is used (it's checked first).
196
+
197
+ ### Does it support environment variables?
198
+
199
+ Yes. Environment variables in configs are preserved as-is.
200
+
201
+ ### What about global vs project configs?
202
+
203
+ Currently syncs project-level configs. Global config support is planned.
204
+
205
+ ---
206
+
207
+ ## Comparison
208
+
209
+ | Feature | sync-project-mcps | sync-mcp | mcpm.sh |
210
+ |---------|-------------------|----------|---------|
211
+ | Scope | Project-level | Global/user | Global |
212
+ | Zero config | Yes | No | No |
213
+ | npx support | Yes | Yes | No |
214
+ | Direction | Merge all | One-to-one | Manual |
215
+
216
+ **sync-project-mcps** is for developers who want project MCP configs synced across all editors with zero friction.
217
+
218
+ ---
219
+
220
+ ## Development
221
+
222
+ ```bash
223
+ git clone https://github.com/user/sync-project-mcps
224
+ cd sync-project-mcps
225
+ npm install
226
+ npm run build
227
+ node dist/index.js
228
+ ```
229
+
230
+ ---
231
+
232
+ ## License
233
+
234
+ MIT
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "sync-project-mcps",
3
+ "version": "1.0.0",
4
+ "description": "Sync project-level MCP configurations across AI coding assistants (Cursor, Claude Code, Windsurf, Cline)",
5
+ "type": "module",
6
+ "bin": {
7
+ "sync-project-mcps": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "keywords": ["mcp", "cursor", "claude", "sync", "cli", "windsurf", "cline", "project"],
15
+ "author": "Vlad Tansky",
16
+ "license": "MIT",
17
+ "devDependencies": {
18
+ "@types/node": "^22.0.0",
19
+ "typescript": "^5.7.0"
20
+ }
21
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ClientConfig, McpConfig } from "../types.js";
4
+
5
+ const CONFIG_PATH = ".mcp.json";
6
+
7
+ export function getClaudeCodeConfig(projectRoot: string): ClientConfig {
8
+ const configPath = join(projectRoot, CONFIG_PATH);
9
+ const exists = existsSync(configPath);
10
+
11
+ if (!exists) {
12
+ return {
13
+ name: "Claude Code",
14
+ path: configPath,
15
+ config: null,
16
+ exists: false,
17
+ };
18
+ }
19
+
20
+ try {
21
+ const content = readFileSync(configPath, "utf-8");
22
+ const config = JSON.parse(content) as McpConfig;
23
+ return {
24
+ name: "Claude Code",
25
+ path: configPath,
26
+ config,
27
+ exists: true,
28
+ };
29
+ } catch (error) {
30
+ const msg = error instanceof Error ? error.message : "Unknown error";
31
+ console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
32
+ return {
33
+ name: "Claude Code",
34
+ path: configPath,
35
+ config: null,
36
+ exists: true,
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { ClientConfig, McpConfig } from "../types.js";
5
+
6
+ function getGlobalConfigPath(): string {
7
+ const platform = process.platform;
8
+ if (platform === "darwin") {
9
+ return join(
10
+ homedir(),
11
+ "Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
12
+ );
13
+ } else if (platform === "win32") {
14
+ return join(
15
+ process.env.APPDATA || "",
16
+ "Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
17
+ );
18
+ }
19
+ return join(
20
+ homedir(),
21
+ ".config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
22
+ );
23
+ }
24
+
25
+ export function getClineConfig(projectRoot: string): ClientConfig {
26
+ const projectPath = join(projectRoot, ".cline/mcp.json");
27
+ const globalPath = getGlobalConfigPath();
28
+
29
+ // Prefer project-level config, fall back to global
30
+ const configPath = existsSync(projectPath) ? projectPath : globalPath;
31
+ const exists = existsSync(configPath);
32
+
33
+ if (!exists) {
34
+ return {
35
+ name: "Cline",
36
+ path: projectPath,
37
+ config: null,
38
+ exists: false,
39
+ };
40
+ }
41
+
42
+ try {
43
+ const content = readFileSync(configPath, "utf-8");
44
+ const config = JSON.parse(content) as McpConfig;
45
+ return {
46
+ name: "Cline",
47
+ path: configPath,
48
+ config,
49
+ exists: true,
50
+ };
51
+ } catch (error) {
52
+ const msg = error instanceof Error ? error.message : "Unknown error";
53
+ console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
54
+ return {
55
+ name: "Cline",
56
+ path: configPath,
57
+ config: null,
58
+ exists: true,
59
+ };
60
+ }
61
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ClientConfig, McpConfig } from "../types.js";
4
+
5
+ const CONFIG_PATH = ".cursor/mcp.json";
6
+
7
+ export function getCursorConfig(projectRoot: string): ClientConfig {
8
+ const configPath = join(projectRoot, CONFIG_PATH);
9
+ const exists = existsSync(configPath);
10
+
11
+ if (!exists) {
12
+ return {
13
+ name: "Cursor",
14
+ path: configPath,
15
+ config: null,
16
+ exists: false,
17
+ };
18
+ }
19
+
20
+ try {
21
+ const content = readFileSync(configPath, "utf-8");
22
+ const config = JSON.parse(content) as McpConfig;
23
+ return {
24
+ name: "Cursor",
25
+ path: configPath,
26
+ config,
27
+ exists: true,
28
+ };
29
+ } catch (error) {
30
+ const msg = error instanceof Error ? error.message : "Unknown error";
31
+ console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
32
+ return {
33
+ name: "Cursor",
34
+ path: configPath,
35
+ config: null,
36
+ exists: true,
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,25 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { ClientConfig } from "../types.js";
5
+
6
+ // Goose uses YAML config - disabled to avoid adding yaml dependency
7
+ // TODO: Enable when yaml parsing is added
8
+
9
+ function getConfigPath(): string {
10
+ const platform = process.platform;
11
+ if (platform === "win32") {
12
+ return join(process.env.USERPROFILE || "", ".config/goose/config.yaml");
13
+ }
14
+ return join(homedir(), ".config/goose/config.yaml");
15
+ }
16
+
17
+ export function getGooseConfig(_projectRoot: string): ClientConfig {
18
+ const configPath = getConfigPath();
19
+ return {
20
+ name: "Goose",
21
+ path: configPath,
22
+ config: null,
23
+ exists: existsSync(configPath),
24
+ };
25
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ClientConfig, McpConfig } from "../types.js";
4
+
5
+ const CONFIG_PATH = ".roo/mcp.json";
6
+
7
+ export function getRooCodeConfig(projectRoot: string): ClientConfig {
8
+ const configPath = join(projectRoot, CONFIG_PATH);
9
+ const exists = existsSync(configPath);
10
+
11
+ if (!exists) {
12
+ return {
13
+ name: "Roo Code",
14
+ path: configPath,
15
+ config: null,
16
+ exists: false,
17
+ };
18
+ }
19
+
20
+ try {
21
+ const content = readFileSync(configPath, "utf-8");
22
+ const config = JSON.parse(content) as McpConfig;
23
+ return {
24
+ name: "Roo Code",
25
+ path: configPath,
26
+ config,
27
+ exists: true,
28
+ };
29
+ } catch (error) {
30
+ const msg = error instanceof Error ? error.message : "Unknown error";
31
+ console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
32
+ return {
33
+ name: "Roo Code",
34
+ path: configPath,
35
+ config: null,
36
+ exists: true,
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,53 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { ClientConfig } from "../types.js";
5
+
6
+ // VS Code MCP support is read-only for now
7
+ // Writing would require merging into settings.json to avoid overwriting other settings
8
+ // TODO: Implement proper merge logic before enabling write support
9
+
10
+ function getConfigPath(): string {
11
+ const platform = process.platform;
12
+ if (platform === "darwin") {
13
+ return join(homedir(), "Library/Application Support/Code/User/settings.json");
14
+ } else if (platform === "win32") {
15
+ return join(process.env.APPDATA || "", "Code/User/settings.json");
16
+ }
17
+ return join(homedir(), ".config/Code/User/settings.json");
18
+ }
19
+
20
+ export function getVSCodeConfig(_projectRoot: string): ClientConfig {
21
+ const configPath = getConfigPath();
22
+ const exists = existsSync(configPath);
23
+
24
+ if (!exists) {
25
+ return {
26
+ name: "VS Code",
27
+ path: configPath,
28
+ config: null,
29
+ exists: false,
30
+ };
31
+ }
32
+
33
+ try {
34
+ const content = readFileSync(configPath, "utf-8");
35
+ const settings = JSON.parse(content);
36
+ const mcpServers = settings["mcp.servers"] || settings.mcpServers || {};
37
+ return {
38
+ name: "VS Code",
39
+ path: configPath,
40
+ config: { mcpServers },
41
+ exists: true,
42
+ };
43
+ } catch (error) {
44
+ const msg = error instanceof Error ? error.message : "Unknown error";
45
+ console.error(`Warning: Failed to parse VS Code settings: ${msg}`);
46
+ return {
47
+ name: "VS Code",
48
+ path: configPath,
49
+ config: null,
50
+ exists: true,
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,46 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { ClientConfig, McpConfig } from "../types.js";
5
+
6
+ function getGlobalConfigPath(): string {
7
+ return join(homedir(), ".codeium/windsurf/mcp_config.json");
8
+ }
9
+
10
+ export function getWindsurfConfig(projectRoot: string): ClientConfig {
11
+ const projectPath = join(projectRoot, ".windsurf/mcp.json");
12
+ const globalPath = getGlobalConfigPath();
13
+
14
+ // Prefer project-level config, fall back to global
15
+ const configPath = existsSync(projectPath) ? projectPath : globalPath;
16
+ const exists = existsSync(configPath);
17
+
18
+ if (!exists) {
19
+ return {
20
+ name: "Windsurf",
21
+ path: projectPath, // Default to project path for creation
22
+ config: null,
23
+ exists: false,
24
+ };
25
+ }
26
+
27
+ try {
28
+ const content = readFileSync(configPath, "utf-8");
29
+ const config = JSON.parse(content) as McpConfig;
30
+ return {
31
+ name: "Windsurf",
32
+ path: configPath,
33
+ config,
34
+ exists: true,
35
+ };
36
+ } catch (error) {
37
+ const msg = error instanceof Error ? error.message : "Unknown error";
38
+ console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
39
+ return {
40
+ name: "Windsurf",
41
+ path: configPath,
42
+ config: null,
43
+ exists: true,
44
+ };
45
+ }
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { dirname } from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import { getCursorConfig } from "./clients/cursor.js";
7
+ import { getClaudeCodeConfig } from "./clients/claude-code.js";
8
+ import { getWindsurfConfig } from "./clients/windsurf.js";
9
+ import { getClineConfig } from "./clients/cline.js";
10
+ import { getRooCodeConfig } from "./clients/roo-code.js";
11
+ import { mergeConfigs, getChanges } from "./merge.js";
12
+ import type { ClientConfig } from "./types.js";
13
+
14
+ const COLORS = {
15
+ reset: "\x1b[0m",
16
+ bold: "\x1b[1m",
17
+ dim: "\x1b[2m",
18
+ green: "\x1b[32m",
19
+ yellow: "\x1b[33m",
20
+ blue: "\x1b[34m",
21
+ cyan: "\x1b[36m",
22
+ red: "\x1b[31m",
23
+ };
24
+
25
+ function c(color: keyof typeof COLORS, text: string): string {
26
+ return `${COLORS[color]}${text}${COLORS.reset}`;
27
+ }
28
+
29
+ const { values: args } = parseArgs({
30
+ options: {
31
+ "dry-run": { type: "boolean", default: false },
32
+ verbose: { type: "boolean", short: "v", default: false },
33
+ help: { type: "boolean", short: "h", default: false },
34
+ version: { type: "boolean", default: false },
35
+ },
36
+ });
37
+
38
+ function printHelp() {
39
+ console.log(`
40
+ ${c("bold", "sync-project-mcps")} - Sync project-level MCP configurations across AI coding assistants
41
+
42
+ ${c("bold", "USAGE")}
43
+ npx sync-project-mcps [options]
44
+
45
+ ${c("bold", "OPTIONS")}
46
+ --dry-run Show what would be synced without writing files
47
+ -v, --verbose Show detailed information
48
+ -h, --help Show this help message
49
+ --version Show version
50
+
51
+ ${c("bold", "SUPPORTED CLIENTS")}
52
+ - Cursor ${c("dim", ".cursor/mcp.json")}
53
+ - Claude Code ${c("dim", ".mcp.json")}
54
+ - Windsurf ${c("dim", ".windsurf/mcp.json")}
55
+ - Cline ${c("dim", ".cline/mcp.json")}
56
+ - Roo Code ${c("dim", ".roo/mcp.json")}
57
+
58
+ ${c("bold", "EXAMPLES")}
59
+ npx sync-project-mcps Sync all MCP configurations
60
+ npx sync-project-mcps --dry-run Preview changes without writing
61
+ `);
62
+ }
63
+
64
+ function run() {
65
+ if (args.help) {
66
+ printHelp();
67
+ process.exit(0);
68
+ }
69
+
70
+ if (args.version) {
71
+ console.log("1.0.0");
72
+ process.exit(0);
73
+ }
74
+
75
+ const projectRoot = process.cwd();
76
+ const dryRun = args["dry-run"];
77
+ const verbose = args.verbose;
78
+
79
+ console.log(c("bold", "\nSync MCP Configurations\n"));
80
+
81
+ if (dryRun) {
82
+ console.log(c("yellow", "DRY RUN - no files will be modified\n"));
83
+ }
84
+
85
+ const clients: ClientConfig[] = [
86
+ getCursorConfig(projectRoot),
87
+ getClaudeCodeConfig(projectRoot),
88
+ getWindsurfConfig(projectRoot),
89
+ getClineConfig(projectRoot),
90
+ getRooCodeConfig(projectRoot),
91
+ ];
92
+
93
+ const existingClients = clients.filter((c) => c.exists && c.config);
94
+ const missingClients = clients.filter((c) => !c.exists);
95
+
96
+ if (existingClients.length === 0) {
97
+ console.log(c("red", "No MCP configurations found.\n"));
98
+ console.log("Expected locations:");
99
+ for (const client of clients) {
100
+ console.log(c("dim", ` ${client.name}: ${client.path}`));
101
+ }
102
+ console.log(
103
+ `\nCreate at least one MCP config file to get started.`
104
+ );
105
+ process.exit(1);
106
+ }
107
+
108
+ console.log(c("cyan", "Found configurations:"));
109
+ for (const client of existingClients) {
110
+ const serverCount = Object.keys(client.config!.mcpServers).length;
111
+ console.log(` ${c("green", "+")} ${client.name}: ${serverCount} server(s)`);
112
+ if (verbose) {
113
+ for (const name of Object.keys(client.config!.mcpServers)) {
114
+ console.log(c("dim", ` - ${name}`));
115
+ }
116
+ }
117
+ }
118
+
119
+ if (missingClients.length > 0 && verbose) {
120
+ console.log(c("dim", "\nNot found (will be created):"));
121
+ for (const client of missingClients) {
122
+ console.log(c("dim", ` - ${client.name}`));
123
+ }
124
+ }
125
+
126
+ const merged = mergeConfigs(existingClients);
127
+ const mergedCount = Object.keys(merged.mcpServers).length;
128
+
129
+ console.log(`\n${c("cyan", "Merged result:")} ${mergedCount} unique server(s)`);
130
+ for (const name of Object.keys(merged.mcpServers).sort()) {
131
+ console.log(` ${c("blue", "-")} ${name}`);
132
+ }
133
+
134
+ console.log(`\n${c("cyan", "Syncing to clients...")}`);
135
+
136
+ for (const client of clients) {
137
+ const changes = getChanges(client, merged);
138
+ const parts: string[] = [];
139
+
140
+ if (changes.added.length > 0) {
141
+ parts.push(c("green", `+${changes.added.length}`));
142
+ }
143
+ if (changes.removed.length > 0) {
144
+ parts.push(c("red", `-${changes.removed.length}`));
145
+ }
146
+
147
+ const changeInfo = parts.length > 0 ? ` (${parts.join(", ")})` : c("dim", " (no changes)");
148
+ const status = client.exists ? c("green", "update") : c("yellow", "create");
149
+
150
+ console.log(` [${status}] ${client.name}${changeInfo}`);
151
+
152
+ if (verbose && changes.added.length > 0) {
153
+ for (const name of changes.added) {
154
+ console.log(c("green", ` + ${name}`));
155
+ }
156
+ }
157
+
158
+ if (!dryRun) {
159
+ const dir = dirname(client.path);
160
+ if (!existsSync(dir)) {
161
+ mkdirSync(dir, { recursive: true });
162
+ }
163
+ writeFileSync(client.path, JSON.stringify(merged, null, 2) + "\n");
164
+ }
165
+ }
166
+
167
+ console.log(`\n${c("green", "Done!")} ${dryRun ? "(dry run)" : ""}\n`);
168
+ }
169
+
170
+ run();
package/src/merge.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { ClientConfig, McpConfig, McpServer } from "./types.js";
2
+
3
+ function serversEqual(a: McpServer, b: McpServer): boolean {
4
+ return (
5
+ a.command === b.command &&
6
+ JSON.stringify(a.args ?? []) === JSON.stringify(b.args ?? []) &&
7
+ JSON.stringify(a.env ?? {}) === JSON.stringify(b.env ?? {})
8
+ );
9
+ }
10
+
11
+ export function mergeConfigs(clients: ClientConfig[]): McpConfig {
12
+ const merged: Record<string, McpServer> = {};
13
+
14
+ for (const client of clients) {
15
+ if (!client.config?.mcpServers) continue;
16
+
17
+ for (const [name, server] of Object.entries(client.config.mcpServers)) {
18
+ if (!merged[name]) {
19
+ merged[name] = { ...server };
20
+ continue;
21
+ }
22
+
23
+ if (!serversEqual(merged[name], server)) {
24
+ console.log(
25
+ ` Warning: "${name}" differs between configs, keeping first occurrence`
26
+ );
27
+ }
28
+ }
29
+ }
30
+
31
+ return { mcpServers: merged };
32
+ }
33
+
34
+ export function getChanges(
35
+ client: ClientConfig,
36
+ merged: McpConfig
37
+ ): { added: string[]; removed: string[] } {
38
+ const currentServers = new Set(
39
+ Object.keys(client.config?.mcpServers ?? {})
40
+ );
41
+ const mergedServers = new Set(Object.keys(merged.mcpServers));
42
+
43
+ const added = [...mergedServers].filter((s) => !currentServers.has(s));
44
+ const removed = [...currentServers].filter((s) => !mergedServers.has(s));
45
+
46
+ return { added, removed };
47
+ }
package/src/types.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type McpServer = {
2
+ command: string;
3
+ args?: string[];
4
+ env?: Record<string, string>;
5
+ disabled?: boolean;
6
+ };
7
+
8
+ export type McpConfig = {
9
+ mcpServers: Record<string, McpServer>;
10
+ };
11
+
12
+ export type ClientConfig = {
13
+ name: string;
14
+ path: string;
15
+ config: McpConfig | null;
16
+ exists: boolean;
17
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }