sync-project-mcps 1.0.0 → 1.0.2
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 +7 -7
- package/dist/clients/claude-code.d.ts +2 -0
- package/dist/clients/claude-code.js +35 -0
- package/dist/clients/cline.d.ts +2 -0
- package/dist/clients/cline.js +35 -0
- package/dist/clients/cursor.d.ts +2 -0
- package/dist/clients/cursor.js +35 -0
- package/dist/clients/goose.d.ts +2 -0
- package/dist/clients/goose.js +21 -0
- package/dist/clients/roo-code.d.ts +2 -0
- package/dist/clients/roo-code.js +35 -0
- package/dist/clients/vscode.d.ts +2 -0
- package/dist/clients/vscode.js +49 -0
- package/dist/clients/windsurf.d.ts +2 -0
- package/dist/clients/windsurf.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +141 -0
- package/dist/merge.d.ts +6 -0
- package/dist/merge.js +29 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.js +1 -0
- package/package.json +6 -2
- package/src/clients/claude-code.ts +0 -39
- package/src/clients/cline.ts +0 -61
- package/src/clients/cursor.ts +0 -39
- package/src/clients/goose.ts +0 -25
- package/src/clients/roo-code.ts +0 -39
- package/src/clients/vscode.ts +0 -53
- package/src/clients/windsurf.ts +0 -46
- package/src/index.ts +0 -170
- package/src/merge.ts +0 -47
- package/src/types.ts +0 -17
- package/tsconfig.json +0 -15
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
One command. All your project MCP servers. Every editor.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx sync-project-mcps
|
|
8
|
+
npx -y sync-project-mcps@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
---
|
|
@@ -37,19 +37,19 @@ You use multiple AI coding assistants - Cursor, Claude Code, Windsurf, Cline. Ea
|
|
|
37
37
|
### Run Once (npx)
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
npx sync-project-mcps
|
|
40
|
+
npx -y sync-project-mcps@latest
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
### Preview Changes
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
npx sync-project-mcps --dry-run
|
|
46
|
+
npx -y sync-project-mcps@latest --dry-run
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
### Verbose Output
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
npx sync-project-mcps -v
|
|
52
|
+
npx -y sync-project-mcps@latest -v
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
---
|
|
@@ -73,7 +73,7 @@ npx sync-project-mcps -v
|
|
|
73
73
|
No installation needed:
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
npx sync-project-mcps
|
|
76
|
+
npx -y sync-project-mcps@latest
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
### Option 2: Install Globally
|
|
@@ -130,7 +130,7 @@ Or add to project-level `.mcp.json`:
|
|
|
130
130
|
### Then Sync
|
|
131
131
|
|
|
132
132
|
```bash
|
|
133
|
-
npx sync-project-mcps
|
|
133
|
+
npx -y sync-project-mcps@latest
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
Your MCP server is now available in **all** your AI coding assistants.
|
|
@@ -144,7 +144,7 @@ Your MCP server is now available in **all** your AI coding assistants.
|
|
|
144
144
|
3. **Writes** the unified config to every client location
|
|
145
145
|
|
|
146
146
|
```
|
|
147
|
-
$ npx sync-project-mcps
|
|
147
|
+
$ npx -y sync-project-mcps@latest
|
|
148
148
|
|
|
149
149
|
Sync MCP Configurations
|
|
150
150
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const CONFIG_PATH = ".mcp.json";
|
|
4
|
+
export function getClaudeCodeConfig(projectRoot) {
|
|
5
|
+
const configPath = join(projectRoot, CONFIG_PATH);
|
|
6
|
+
const exists = existsSync(configPath);
|
|
7
|
+
if (!exists) {
|
|
8
|
+
return {
|
|
9
|
+
name: "Claude Code",
|
|
10
|
+
path: configPath,
|
|
11
|
+
config: null,
|
|
12
|
+
exists: false,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(configPath, "utf-8");
|
|
17
|
+
const config = JSON.parse(content);
|
|
18
|
+
return {
|
|
19
|
+
name: "Claude Code",
|
|
20
|
+
path: configPath,
|
|
21
|
+
config,
|
|
22
|
+
exists: true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
27
|
+
console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
|
|
28
|
+
return {
|
|
29
|
+
name: "Claude Code",
|
|
30
|
+
path: configPath,
|
|
31
|
+
config: null,
|
|
32
|
+
exists: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const CONFIG_PATH = ".cline/mcp.json";
|
|
4
|
+
export function getClineConfig(projectRoot) {
|
|
5
|
+
const configPath = join(projectRoot, CONFIG_PATH);
|
|
6
|
+
const exists = existsSync(configPath);
|
|
7
|
+
if (!exists) {
|
|
8
|
+
return {
|
|
9
|
+
name: "Cline",
|
|
10
|
+
path: configPath,
|
|
11
|
+
config: null,
|
|
12
|
+
exists: false,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(configPath, "utf-8");
|
|
17
|
+
const config = JSON.parse(content);
|
|
18
|
+
return {
|
|
19
|
+
name: "Cline",
|
|
20
|
+
path: configPath,
|
|
21
|
+
config,
|
|
22
|
+
exists: true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
27
|
+
console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
|
|
28
|
+
return {
|
|
29
|
+
name: "Cline",
|
|
30
|
+
path: configPath,
|
|
31
|
+
config: null,
|
|
32
|
+
exists: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const CONFIG_PATH = ".cursor/mcp.json";
|
|
4
|
+
export function getCursorConfig(projectRoot) {
|
|
5
|
+
const configPath = join(projectRoot, CONFIG_PATH);
|
|
6
|
+
const exists = existsSync(configPath);
|
|
7
|
+
if (!exists) {
|
|
8
|
+
return {
|
|
9
|
+
name: "Cursor",
|
|
10
|
+
path: configPath,
|
|
11
|
+
config: null,
|
|
12
|
+
exists: false,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(configPath, "utf-8");
|
|
17
|
+
const config = JSON.parse(content);
|
|
18
|
+
return {
|
|
19
|
+
name: "Cursor",
|
|
20
|
+
path: configPath,
|
|
21
|
+
config,
|
|
22
|
+
exists: true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
27
|
+
console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
|
|
28
|
+
return {
|
|
29
|
+
name: "Cursor",
|
|
30
|
+
path: configPath,
|
|
31
|
+
config: null,
|
|
32
|
+
exists: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
// Goose uses YAML config - disabled to avoid adding yaml dependency
|
|
5
|
+
// TODO: Enable when yaml parsing is added
|
|
6
|
+
function getConfigPath() {
|
|
7
|
+
const platform = process.platform;
|
|
8
|
+
if (platform === "win32") {
|
|
9
|
+
return join(process.env.USERPROFILE || "", ".config/goose/config.yaml");
|
|
10
|
+
}
|
|
11
|
+
return join(homedir(), ".config/goose/config.yaml");
|
|
12
|
+
}
|
|
13
|
+
export function getGooseConfig(_projectRoot) {
|
|
14
|
+
const configPath = getConfigPath();
|
|
15
|
+
return {
|
|
16
|
+
name: "Goose",
|
|
17
|
+
path: configPath,
|
|
18
|
+
config: null,
|
|
19
|
+
exists: existsSync(configPath),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const CONFIG_PATH = ".roo/mcp.json";
|
|
4
|
+
export function getRooCodeConfig(projectRoot) {
|
|
5
|
+
const configPath = join(projectRoot, CONFIG_PATH);
|
|
6
|
+
const exists = existsSync(configPath);
|
|
7
|
+
if (!exists) {
|
|
8
|
+
return {
|
|
9
|
+
name: "Roo Code",
|
|
10
|
+
path: configPath,
|
|
11
|
+
config: null,
|
|
12
|
+
exists: false,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(configPath, "utf-8");
|
|
17
|
+
const config = JSON.parse(content);
|
|
18
|
+
return {
|
|
19
|
+
name: "Roo Code",
|
|
20
|
+
path: configPath,
|
|
21
|
+
config,
|
|
22
|
+
exists: true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
27
|
+
console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
|
|
28
|
+
return {
|
|
29
|
+
name: "Roo Code",
|
|
30
|
+
path: configPath,
|
|
31
|
+
config: null,
|
|
32
|
+
exists: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
// VS Code MCP support is read-only for now
|
|
5
|
+
// Writing would require merging into settings.json to avoid overwriting other settings
|
|
6
|
+
// TODO: Implement proper merge logic before enabling write support
|
|
7
|
+
function getConfigPath() {
|
|
8
|
+
const platform = process.platform;
|
|
9
|
+
if (platform === "darwin") {
|
|
10
|
+
return join(homedir(), "Library/Application Support/Code/User/settings.json");
|
|
11
|
+
}
|
|
12
|
+
else if (platform === "win32") {
|
|
13
|
+
return join(process.env.APPDATA || "", "Code/User/settings.json");
|
|
14
|
+
}
|
|
15
|
+
return join(homedir(), ".config/Code/User/settings.json");
|
|
16
|
+
}
|
|
17
|
+
export function getVSCodeConfig(_projectRoot) {
|
|
18
|
+
const configPath = getConfigPath();
|
|
19
|
+
const exists = existsSync(configPath);
|
|
20
|
+
if (!exists) {
|
|
21
|
+
return {
|
|
22
|
+
name: "VS Code",
|
|
23
|
+
path: configPath,
|
|
24
|
+
config: null,
|
|
25
|
+
exists: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(configPath, "utf-8");
|
|
30
|
+
const settings = JSON.parse(content);
|
|
31
|
+
const mcpServers = settings["mcp.servers"] || settings.mcpServers || {};
|
|
32
|
+
return {
|
|
33
|
+
name: "VS Code",
|
|
34
|
+
path: configPath,
|
|
35
|
+
config: { mcpServers },
|
|
36
|
+
exists: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
41
|
+
console.error(`Warning: Failed to parse VS Code settings: ${msg}`);
|
|
42
|
+
return {
|
|
43
|
+
name: "VS Code",
|
|
44
|
+
path: configPath,
|
|
45
|
+
config: null,
|
|
46
|
+
exists: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const CONFIG_PATH = ".windsurf/mcp.json";
|
|
4
|
+
export function getWindsurfConfig(projectRoot) {
|
|
5
|
+
const configPath = join(projectRoot, CONFIG_PATH);
|
|
6
|
+
const exists = existsSync(configPath);
|
|
7
|
+
if (!exists) {
|
|
8
|
+
return {
|
|
9
|
+
name: "Windsurf",
|
|
10
|
+
path: configPath,
|
|
11
|
+
config: null,
|
|
12
|
+
exists: false,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(configPath, "utf-8");
|
|
17
|
+
const config = JSON.parse(content);
|
|
18
|
+
return {
|
|
19
|
+
name: "Windsurf",
|
|
20
|
+
path: configPath,
|
|
21
|
+
config,
|
|
22
|
+
exists: true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
27
|
+
console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
|
|
28
|
+
return {
|
|
29
|
+
name: "Windsurf",
|
|
30
|
+
path: configPath,
|
|
31
|
+
config: null,
|
|
32
|
+
exists: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { getCursorConfig } from "./clients/cursor.js";
|
|
6
|
+
import { getClaudeCodeConfig } from "./clients/claude-code.js";
|
|
7
|
+
import { getWindsurfConfig } from "./clients/windsurf.js";
|
|
8
|
+
import { getClineConfig } from "./clients/cline.js";
|
|
9
|
+
import { getRooCodeConfig } from "./clients/roo-code.js";
|
|
10
|
+
import { mergeConfigs, getChanges } from "./merge.js";
|
|
11
|
+
const COLORS = {
|
|
12
|
+
reset: "\x1b[0m",
|
|
13
|
+
bold: "\x1b[1m",
|
|
14
|
+
dim: "\x1b[2m",
|
|
15
|
+
green: "\x1b[32m",
|
|
16
|
+
yellow: "\x1b[33m",
|
|
17
|
+
blue: "\x1b[34m",
|
|
18
|
+
cyan: "\x1b[36m",
|
|
19
|
+
red: "\x1b[31m",
|
|
20
|
+
};
|
|
21
|
+
function c(color, text) {
|
|
22
|
+
return `${COLORS[color]}${text}${COLORS.reset}`;
|
|
23
|
+
}
|
|
24
|
+
const { values: args } = parseArgs({
|
|
25
|
+
options: {
|
|
26
|
+
"dry-run": { type: "boolean", default: false },
|
|
27
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
28
|
+
help: { type: "boolean", short: "h", default: false },
|
|
29
|
+
version: { type: "boolean", default: false },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
function printHelp() {
|
|
33
|
+
console.log(`
|
|
34
|
+
${c("bold", "sync-project-mcps")} - Sync project-level MCP configurations across AI coding assistants
|
|
35
|
+
|
|
36
|
+
${c("bold", "USAGE")}
|
|
37
|
+
npx sync-project-mcps [options]
|
|
38
|
+
|
|
39
|
+
${c("bold", "OPTIONS")}
|
|
40
|
+
--dry-run Show what would be synced without writing files
|
|
41
|
+
-v, --verbose Show detailed information
|
|
42
|
+
-h, --help Show this help message
|
|
43
|
+
--version Show version
|
|
44
|
+
|
|
45
|
+
${c("bold", "SUPPORTED CLIENTS")}
|
|
46
|
+
- Cursor ${c("dim", ".cursor/mcp.json")}
|
|
47
|
+
- Claude Code ${c("dim", ".mcp.json")}
|
|
48
|
+
- Windsurf ${c("dim", ".windsurf/mcp.json")}
|
|
49
|
+
- Cline ${c("dim", ".cline/mcp.json")}
|
|
50
|
+
- Roo Code ${c("dim", ".roo/mcp.json")}
|
|
51
|
+
|
|
52
|
+
${c("bold", "EXAMPLES")}
|
|
53
|
+
npx sync-project-mcps Sync all MCP configurations
|
|
54
|
+
npx sync-project-mcps --dry-run Preview changes without writing
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
function run() {
|
|
58
|
+
if (args.help) {
|
|
59
|
+
printHelp();
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
if (args.version) {
|
|
63
|
+
console.log("1.0.0");
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
const projectRoot = process.cwd();
|
|
67
|
+
const dryRun = args["dry-run"];
|
|
68
|
+
const verbose = args.verbose;
|
|
69
|
+
console.log(c("bold", "\nSync MCP Configurations\n"));
|
|
70
|
+
if (dryRun) {
|
|
71
|
+
console.log(c("yellow", "DRY RUN - no files will be modified\n"));
|
|
72
|
+
}
|
|
73
|
+
const clients = [
|
|
74
|
+
getCursorConfig(projectRoot),
|
|
75
|
+
getClaudeCodeConfig(projectRoot),
|
|
76
|
+
getWindsurfConfig(projectRoot),
|
|
77
|
+
getClineConfig(projectRoot),
|
|
78
|
+
getRooCodeConfig(projectRoot),
|
|
79
|
+
];
|
|
80
|
+
const existingClients = clients.filter((c) => c.exists && c.config);
|
|
81
|
+
const missingClients = clients.filter((c) => !c.exists);
|
|
82
|
+
if (existingClients.length === 0) {
|
|
83
|
+
console.log(c("red", "No MCP configurations found.\n"));
|
|
84
|
+
console.log("Expected locations:");
|
|
85
|
+
for (const client of clients) {
|
|
86
|
+
console.log(c("dim", ` ${client.name}: ${client.path}`));
|
|
87
|
+
}
|
|
88
|
+
console.log(`\nCreate at least one MCP config file to get started.`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
console.log(c("cyan", "Found configurations:"));
|
|
92
|
+
for (const client of existingClients) {
|
|
93
|
+
const serverCount = Object.keys(client.config.mcpServers).length;
|
|
94
|
+
console.log(` ${c("green", "+")} ${client.name}: ${serverCount} server(s)`);
|
|
95
|
+
if (verbose) {
|
|
96
|
+
for (const name of Object.keys(client.config.mcpServers)) {
|
|
97
|
+
console.log(c("dim", ` - ${name}`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (missingClients.length > 0 && verbose) {
|
|
102
|
+
console.log(c("dim", "\nNot found (will be created):"));
|
|
103
|
+
for (const client of missingClients) {
|
|
104
|
+
console.log(c("dim", ` - ${client.name}`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const merged = mergeConfigs(existingClients);
|
|
108
|
+
const mergedCount = Object.keys(merged.mcpServers).length;
|
|
109
|
+
console.log(`\n${c("cyan", "Merged result:")} ${mergedCount} unique server(s)`);
|
|
110
|
+
for (const name of Object.keys(merged.mcpServers).sort()) {
|
|
111
|
+
console.log(` ${c("blue", "-")} ${name}`);
|
|
112
|
+
}
|
|
113
|
+
console.log(`\n${c("cyan", "Syncing to clients...")}`);
|
|
114
|
+
for (const client of clients) {
|
|
115
|
+
const changes = getChanges(client, merged);
|
|
116
|
+
const parts = [];
|
|
117
|
+
if (changes.added.length > 0) {
|
|
118
|
+
parts.push(c("green", `+${changes.added.length}`));
|
|
119
|
+
}
|
|
120
|
+
if (changes.removed.length > 0) {
|
|
121
|
+
parts.push(c("red", `-${changes.removed.length}`));
|
|
122
|
+
}
|
|
123
|
+
const changeInfo = parts.length > 0 ? ` (${parts.join(", ")})` : c("dim", " (no changes)");
|
|
124
|
+
const status = client.exists ? c("green", "update") : c("yellow", "create");
|
|
125
|
+
console.log(` [${status}] ${client.name}${changeInfo}`);
|
|
126
|
+
if (verbose && changes.added.length > 0) {
|
|
127
|
+
for (const name of changes.added) {
|
|
128
|
+
console.log(c("green", ` + ${name}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!dryRun) {
|
|
132
|
+
const dir = dirname(client.path);
|
|
133
|
+
if (!existsSync(dir)) {
|
|
134
|
+
mkdirSync(dir, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
writeFileSync(client.path, JSON.stringify(merged, null, 2) + "\n");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log(`\n${c("green", "Done!")} ${dryRun ? "(dry run)" : ""}\n`);
|
|
140
|
+
}
|
|
141
|
+
run();
|
package/dist/merge.d.ts
ADDED
package/dist/merge.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function serversEqual(a, b) {
|
|
2
|
+
return (a.command === b.command &&
|
|
3
|
+
JSON.stringify(a.args ?? []) === JSON.stringify(b.args ?? []) &&
|
|
4
|
+
JSON.stringify(a.env ?? {}) === JSON.stringify(b.env ?? {}));
|
|
5
|
+
}
|
|
6
|
+
export function mergeConfigs(clients) {
|
|
7
|
+
const merged = {};
|
|
8
|
+
for (const client of clients) {
|
|
9
|
+
if (!client.config?.mcpServers)
|
|
10
|
+
continue;
|
|
11
|
+
for (const [name, server] of Object.entries(client.config.mcpServers)) {
|
|
12
|
+
if (!merged[name]) {
|
|
13
|
+
merged[name] = { ...server };
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (!serversEqual(merged[name], server)) {
|
|
17
|
+
console.log(` Warning: "${name}" differs between configs, keeping first occurrence`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { mcpServers: merged };
|
|
22
|
+
}
|
|
23
|
+
export function getChanges(client, merged) {
|
|
24
|
+
const currentServers = new Set(Object.keys(client.config?.mcpServers ?? {}));
|
|
25
|
+
const mergedServers = new Set(Object.keys(merged.mcpServers));
|
|
26
|
+
const added = [...mergedServers].filter((s) => !currentServers.has(s));
|
|
27
|
+
const removed = [...currentServers].filter((s) => !mergedServers.has(s));
|
|
28
|
+
return { added, removed };
|
|
29
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type McpServer = {
|
|
2
|
+
command: string;
|
|
3
|
+
args?: string[];
|
|
4
|
+
env?: Record<string, string>;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type McpConfig = {
|
|
8
|
+
mcpServers: Record<string, McpServer>;
|
|
9
|
+
};
|
|
10
|
+
export type ClientConfig = {
|
|
11
|
+
name: string;
|
|
12
|
+
path: string;
|
|
13
|
+
config: McpConfig | null;
|
|
14
|
+
exists: boolean;
|
|
15
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sync-project-mcps",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Sync project-level MCP configurations across AI coding assistants (Cursor, Claude Code, Windsurf, Cline)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sync-project-mcps": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
9
12
|
"scripts": {
|
|
10
13
|
"build": "tsc",
|
|
11
14
|
"dev": "tsc --watch",
|
|
12
|
-
"start": "node dist/index.js"
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
13
17
|
},
|
|
14
18
|
"keywords": ["mcp", "cursor", "claude", "sync", "cli", "windsurf", "cline", "project"],
|
|
15
19
|
"author": "Vlad Tansky",
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
}
|
package/src/clients/cline.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
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
|
-
}
|
package/src/clients/cursor.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
}
|
package/src/clients/goose.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
}
|
package/src/clients/roo-code.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
}
|
package/src/clients/vscode.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
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
|
-
}
|
package/src/clients/windsurf.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
}
|