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 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,2 @@
1
+ import type { ClientConfig } from "../types.js";
2
+ export declare function getClaudeCodeConfig(projectRoot: string): ClientConfig;
@@ -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,2 @@
1
+ import type { ClientConfig } from "../types.js";
2
+ export declare function getClineConfig(projectRoot: string): ClientConfig;
@@ -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,2 @@
1
+ import type { ClientConfig } from "../types.js";
2
+ export declare function getCursorConfig(projectRoot: string): ClientConfig;
@@ -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,2 @@
1
+ import type { ClientConfig } from "../types.js";
2
+ export declare function getGooseConfig(_projectRoot: string): ClientConfig;
@@ -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,2 @@
1
+ import type { ClientConfig } from "../types.js";
2
+ export declare function getRooCodeConfig(projectRoot: string): ClientConfig;
@@ -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,2 @@
1
+ import type { ClientConfig } from "../types.js";
2
+ export declare function getVSCodeConfig(_projectRoot: string): ClientConfig;
@@ -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,2 @@
1
+ import type { ClientConfig } from "../types.js";
2
+ export declare function getWindsurfConfig(projectRoot: string): ClientConfig;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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();
@@ -0,0 +1,6 @@
1
+ import type { ClientConfig, McpConfig } from "./types.js";
2
+ export declare function mergeConfigs(clients: ClientConfig[]): McpConfig;
3
+ export declare function getChanges(client: ClientConfig, merged: McpConfig): {
4
+ added: string[];
5
+ removed: string[];
6
+ };
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
+ }
@@ -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.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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }