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 +234 -0
- package/package.json +21 -0
- package/src/clients/claude-code.ts +39 -0
- package/src/clients/cline.ts +61 -0
- package/src/clients/cursor.ts +39 -0
- package/src/clients/goose.ts +25 -0
- package/src/clients/roo-code.ts +39 -0
- package/src/clients/vscode.ts +53 -0
- package/src/clients/windsurf.ts +46 -0
- package/src/index.ts +170 -0
- package/src/merge.ts +47 -0
- package/src/types.ts +17 -0
- package/tsconfig.json +15 -0
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
|
+
[](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
|
+
}
|