sync-project-mcps 1.0.2 → 1.2.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 +20 -2
- package/dist/clients/codex.d.ts +3 -0
- package/dist/clients/codex.js +66 -0
- package/dist/clients/gemini.d.ts +2 -0
- package/dist/clients/gemini.js +38 -0
- package/dist/clients/opencode.d.ts +3 -0
- package/dist/clients/opencode.js +72 -0
- package/dist/index.js +79 -15
- package/dist/parsers/jsonc.d.ts +1 -0
- package/dist/parsers/jsonc.js +35 -0
- package/dist/parsers/toml.d.ts +5 -0
- package/dist/parsers/toml.js +100 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -61,8 +61,11 @@ npx -y sync-project-mcps@latest -v
|
|
|
61
61
|
| **Cursor** | `.cursor/mcp.json` | Project |
|
|
62
62
|
| **Claude Code** | `.mcp.json` | Project |
|
|
63
63
|
| **Windsurf** | `.windsurf/mcp.json` | Project |
|
|
64
|
-
| **Cline** |
|
|
64
|
+
| **Cline** | `.cline/mcp.json` | Project |
|
|
65
65
|
| **Roo Code** | `.roo/mcp.json` | Project |
|
|
66
|
+
| **Gemini CLI** | `.gemini/settings.json` | Project |
|
|
67
|
+
| **Codex** | `.codex/config.toml` | Project |
|
|
68
|
+
| **OpenCode** | `.opencode/opencode.jsonc` | Project |
|
|
66
69
|
|
|
67
70
|
---
|
|
68
71
|
|
|
@@ -176,19 +179,34 @@ Done!
|
|
|
176
179
|
sync-project-mcps [options]
|
|
177
180
|
|
|
178
181
|
Options:
|
|
182
|
+
-s, --source Use specific client as source of truth (cursor, claude, windsurf, cline, roo, gemini, codex, opencode)
|
|
179
183
|
--dry-run Show what would be synced without writing files
|
|
180
184
|
-v, --verbose Show detailed information about each server
|
|
181
185
|
-h, --help Show help message
|
|
182
186
|
--version Show version
|
|
183
187
|
```
|
|
184
188
|
|
|
189
|
+
### Source Mode
|
|
190
|
+
|
|
191
|
+
By default, the tool **merges** all configs (add-only). Use `--source` to pick one client as the source of truth:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Use Cursor's config as the canonical source
|
|
195
|
+
npx -y sync-project-mcps@latest -s cursor
|
|
196
|
+
|
|
197
|
+
# Preview what would be removed
|
|
198
|
+
npx -y sync-project-mcps@latest -s cursor --dry-run -v
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This will sync **only** the servers from the source client to all others, removing servers that don't exist in the source.
|
|
202
|
+
|
|
185
203
|
---
|
|
186
204
|
|
|
187
205
|
## FAQ
|
|
188
206
|
|
|
189
207
|
### Does it delete servers?
|
|
190
208
|
|
|
191
|
-
|
|
209
|
+
By default, no. It only adds missing servers. Use `--source` to sync from a specific client, which will also remove servers that don't exist in the source.
|
|
192
210
|
|
|
193
211
|
### What if the same server has different configs?
|
|
194
212
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseToml, stringifyToml } from "../parsers/toml.js";
|
|
4
|
+
const CONFIG_PATH = ".codex/config.toml";
|
|
5
|
+
const MCP_PREFIX = "mcp_servers.";
|
|
6
|
+
export function getCodexConfig(projectRoot) {
|
|
7
|
+
const configPath = join(projectRoot, CONFIG_PATH);
|
|
8
|
+
const exists = existsSync(configPath);
|
|
9
|
+
if (!exists) {
|
|
10
|
+
return {
|
|
11
|
+
name: "Codex",
|
|
12
|
+
path: configPath,
|
|
13
|
+
config: null,
|
|
14
|
+
exists: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const content = readFileSync(configPath, "utf-8");
|
|
19
|
+
const parsed = parseToml(content);
|
|
20
|
+
const mcpServers = {};
|
|
21
|
+
for (const [section, values] of Object.entries(parsed)) {
|
|
22
|
+
if (!section.startsWith(MCP_PREFIX))
|
|
23
|
+
continue;
|
|
24
|
+
const serverName = section.slice(MCP_PREFIX.length);
|
|
25
|
+
const command = values.command;
|
|
26
|
+
const args = values.args;
|
|
27
|
+
const env = values.env;
|
|
28
|
+
if (command) {
|
|
29
|
+
mcpServers[serverName] = { command, args, env };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
name: "Codex",
|
|
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 ${configPath}: ${msg}`);
|
|
42
|
+
return {
|
|
43
|
+
name: "Codex",
|
|
44
|
+
path: configPath,
|
|
45
|
+
config: null,
|
|
46
|
+
exists: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function serializeCodexConfig(config, existingContent) {
|
|
51
|
+
const existing = parseToml(existingContent);
|
|
52
|
+
for (const key of Object.keys(existing)) {
|
|
53
|
+
if (key.startsWith(MCP_PREFIX)) {
|
|
54
|
+
delete existing[key];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const [name, server] of Object.entries(config.mcpServers)) {
|
|
58
|
+
const section = `${MCP_PREFIX}${name}`;
|
|
59
|
+
existing[section] = {
|
|
60
|
+
command: server.command,
|
|
61
|
+
...(server.args && { args: server.args }),
|
|
62
|
+
...(server.env && { env: server.env }),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return stringifyToml(existing);
|
|
66
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const CONFIG_PATH = ".gemini/settings.json";
|
|
4
|
+
export function getGeminiConfig(projectRoot) {
|
|
5
|
+
const configPath = join(projectRoot, CONFIG_PATH);
|
|
6
|
+
const exists = existsSync(configPath);
|
|
7
|
+
if (!exists) {
|
|
8
|
+
return {
|
|
9
|
+
name: "Gemini CLI",
|
|
10
|
+
path: configPath,
|
|
11
|
+
config: null,
|
|
12
|
+
exists: false,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const content = readFileSync(configPath, "utf-8");
|
|
17
|
+
const parsed = JSON.parse(content);
|
|
18
|
+
const config = {
|
|
19
|
+
mcpServers: (parsed.mcpServers ?? {}),
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
name: "Gemini CLI",
|
|
23
|
+
path: configPath,
|
|
24
|
+
config,
|
|
25
|
+
exists: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
30
|
+
console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
|
|
31
|
+
return {
|
|
32
|
+
name: "Gemini CLI",
|
|
33
|
+
path: configPath,
|
|
34
|
+
config: null,
|
|
35
|
+
exists: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseJsonc } from "../parsers/jsonc.js";
|
|
4
|
+
const CONFIG_PATH = ".opencode/opencode.jsonc";
|
|
5
|
+
export function getOpenCodeConfig(projectRoot) {
|
|
6
|
+
const configPath = join(projectRoot, CONFIG_PATH);
|
|
7
|
+
const exists = existsSync(configPath);
|
|
8
|
+
if (!exists) {
|
|
9
|
+
return {
|
|
10
|
+
name: "OpenCode",
|
|
11
|
+
path: configPath,
|
|
12
|
+
config: null,
|
|
13
|
+
exists: false,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const content = readFileSync(configPath, "utf-8");
|
|
18
|
+
const parsed = parseJsonc(content);
|
|
19
|
+
const mcpServers = {};
|
|
20
|
+
if (parsed.mcp) {
|
|
21
|
+
for (const [name, server] of Object.entries(parsed.mcp)) {
|
|
22
|
+
if (server.type !== "local")
|
|
23
|
+
continue;
|
|
24
|
+
if (server.enabled === false)
|
|
25
|
+
continue;
|
|
26
|
+
const [command, ...args] = server.command;
|
|
27
|
+
mcpServers[name] = {
|
|
28
|
+
command,
|
|
29
|
+
...(args.length > 0 && { args }),
|
|
30
|
+
...(server.environment && { env: server.environment }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
name: "OpenCode",
|
|
36
|
+
path: configPath,
|
|
37
|
+
config: { mcpServers },
|
|
38
|
+
exists: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
43
|
+
console.error(`Warning: Failed to parse ${configPath}: ${msg}`);
|
|
44
|
+
return {
|
|
45
|
+
name: "OpenCode",
|
|
46
|
+
path: configPath,
|
|
47
|
+
config: null,
|
|
48
|
+
exists: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function serializeOpenCodeConfig(config, existingContent) {
|
|
53
|
+
const existing = parseJsonc(existingContent);
|
|
54
|
+
const remoteMcp = {};
|
|
55
|
+
if (existing.mcp) {
|
|
56
|
+
for (const [name, server] of Object.entries(existing.mcp)) {
|
|
57
|
+
if (server.type === "remote") {
|
|
58
|
+
remoteMcp[name] = server;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const newMcp = { ...remoteMcp };
|
|
63
|
+
for (const [name, server] of Object.entries(config.mcpServers)) {
|
|
64
|
+
const command = [server.command, ...(server.args ?? [])];
|
|
65
|
+
newMcp[name] = {
|
|
66
|
+
type: "local",
|
|
67
|
+
command,
|
|
68
|
+
...(server.env && { environment: server.env }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return JSON.stringify({ ...existing, mcp: newMcp }, null, 2) + "\n";
|
|
72
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { dirname } from "node:path";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { parseArgs } from "node:util";
|
|
5
4
|
import { getCursorConfig } from "./clients/cursor.js";
|
|
6
5
|
import { getClaudeCodeConfig } from "./clients/claude-code.js";
|
|
7
6
|
import { getWindsurfConfig } from "./clients/windsurf.js";
|
|
8
7
|
import { getClineConfig } from "./clients/cline.js";
|
|
9
8
|
import { getRooCodeConfig } from "./clients/roo-code.js";
|
|
9
|
+
import { getGeminiConfig } from "./clients/gemini.js";
|
|
10
|
+
import { getCodexConfig, serializeCodexConfig } from "./clients/codex.js";
|
|
11
|
+
import { getOpenCodeConfig, serializeOpenCodeConfig } from "./clients/opencode.js";
|
|
10
12
|
import { mergeConfigs, getChanges } from "./merge.js";
|
|
11
13
|
const COLORS = {
|
|
12
14
|
reset: "\x1b[0m",
|
|
@@ -21,12 +23,26 @@ const COLORS = {
|
|
|
21
23
|
function c(color, text) {
|
|
22
24
|
return `${COLORS[color]}${text}${COLORS.reset}`;
|
|
23
25
|
}
|
|
26
|
+
const CLIENT_ALIASES = {
|
|
27
|
+
cursor: "Cursor",
|
|
28
|
+
claude: "Claude Code",
|
|
29
|
+
"claude-code": "Claude Code",
|
|
30
|
+
windsurf: "Windsurf",
|
|
31
|
+
cline: "Cline",
|
|
32
|
+
roo: "Roo Code",
|
|
33
|
+
"roo-code": "Roo Code",
|
|
34
|
+
gemini: "Gemini CLI",
|
|
35
|
+
"gemini-cli": "Gemini CLI",
|
|
36
|
+
codex: "Codex",
|
|
37
|
+
opencode: "OpenCode",
|
|
38
|
+
};
|
|
24
39
|
const { values: args } = parseArgs({
|
|
25
40
|
options: {
|
|
26
41
|
"dry-run": { type: "boolean", default: false },
|
|
27
42
|
verbose: { type: "boolean", short: "v", default: false },
|
|
28
43
|
help: { type: "boolean", short: "h", default: false },
|
|
29
44
|
version: { type: "boolean", default: false },
|
|
45
|
+
source: { type: "string", short: "s" },
|
|
30
46
|
},
|
|
31
47
|
});
|
|
32
48
|
function printHelp() {
|
|
@@ -37,6 +53,7 @@ ${c("bold", "USAGE")}
|
|
|
37
53
|
npx sync-project-mcps [options]
|
|
38
54
|
|
|
39
55
|
${c("bold", "OPTIONS")}
|
|
56
|
+
-s, --source Use specific client as source of truth (cursor, claude, windsurf, cline, roo, gemini, codex, opencode)
|
|
40
57
|
--dry-run Show what would be synced without writing files
|
|
41
58
|
-v, --verbose Show detailed information
|
|
42
59
|
-h, --help Show this help message
|
|
@@ -48,10 +65,14 @@ ${c("bold", "SUPPORTED CLIENTS")}
|
|
|
48
65
|
- Windsurf ${c("dim", ".windsurf/mcp.json")}
|
|
49
66
|
- Cline ${c("dim", ".cline/mcp.json")}
|
|
50
67
|
- Roo Code ${c("dim", ".roo/mcp.json")}
|
|
68
|
+
- Gemini CLI ${c("dim", ".gemini/settings.json")}
|
|
69
|
+
- Codex ${c("dim", ".codex/config.toml")}
|
|
70
|
+
- OpenCode ${c("dim", ".opencode/opencode.jsonc")}
|
|
51
71
|
|
|
52
72
|
${c("bold", "EXAMPLES")}
|
|
53
|
-
npx sync-project-mcps
|
|
54
|
-
npx sync-project-mcps
|
|
73
|
+
npx sync-project-mcps Merge all configs (add-only)
|
|
74
|
+
npx sync-project-mcps -s cursor Use Cursor as source of truth
|
|
75
|
+
npx sync-project-mcps -s cursor --dry-run Preview changes
|
|
55
76
|
`);
|
|
56
77
|
}
|
|
57
78
|
function run() {
|
|
@@ -66,7 +87,17 @@ function run() {
|
|
|
66
87
|
const projectRoot = process.cwd();
|
|
67
88
|
const dryRun = args["dry-run"];
|
|
68
89
|
const verbose = args.verbose;
|
|
90
|
+
const sourceArg = args.source?.toLowerCase();
|
|
91
|
+
const sourceName = sourceArg ? CLIENT_ALIASES[sourceArg] : null;
|
|
92
|
+
if (sourceArg && !sourceName) {
|
|
93
|
+
console.error(c("red", `Unknown source: ${sourceArg}`));
|
|
94
|
+
console.error(`Valid sources: ${Object.keys(CLIENT_ALIASES).join(", ")}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
69
97
|
console.log(c("bold", "\nSync MCP Configurations\n"));
|
|
98
|
+
if (sourceName) {
|
|
99
|
+
console.log(c("cyan", `Source: ${sourceName}\n`));
|
|
100
|
+
}
|
|
70
101
|
if (dryRun) {
|
|
71
102
|
console.log(c("yellow", "DRY RUN - no files will be modified\n"));
|
|
72
103
|
}
|
|
@@ -76,6 +107,9 @@ function run() {
|
|
|
76
107
|
getWindsurfConfig(projectRoot),
|
|
77
108
|
getClineConfig(projectRoot),
|
|
78
109
|
getRooCodeConfig(projectRoot),
|
|
110
|
+
getGeminiConfig(projectRoot),
|
|
111
|
+
getCodexConfig(projectRoot),
|
|
112
|
+
getOpenCodeConfig(projectRoot),
|
|
79
113
|
];
|
|
80
114
|
const existingClients = clients.filter((c) => c.exists && c.config);
|
|
81
115
|
const missingClients = clients.filter((c) => !c.exists);
|
|
@@ -99,19 +133,34 @@ function run() {
|
|
|
99
133
|
}
|
|
100
134
|
}
|
|
101
135
|
if (missingClients.length > 0 && verbose) {
|
|
102
|
-
console.log(c("dim", "\nNot found (
|
|
136
|
+
console.log(c("dim", "\nNot found (skipped):"));
|
|
103
137
|
for (const client of missingClients) {
|
|
104
138
|
console.log(c("dim", ` - ${client.name}`));
|
|
105
139
|
}
|
|
106
140
|
}
|
|
107
|
-
|
|
141
|
+
let merged;
|
|
142
|
+
if (sourceName) {
|
|
143
|
+
const sourceClient = existingClients.find((cl) => cl.name === sourceName);
|
|
144
|
+
if (!sourceClient) {
|
|
145
|
+
console.error(c("red", `\nSource "${sourceName}" not found in project.`));
|
|
146
|
+
console.error("Available configs:");
|
|
147
|
+
for (const client of existingClients) {
|
|
148
|
+
console.error(c("dim", ` - ${client.name}`));
|
|
149
|
+
}
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
merged = { mcpServers: { ...sourceClient.config.mcpServers } };
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
merged = mergeConfigs(existingClients);
|
|
156
|
+
}
|
|
108
157
|
const mergedCount = Object.keys(merged.mcpServers).length;
|
|
109
158
|
console.log(`\n${c("cyan", "Merged result:")} ${mergedCount} unique server(s)`);
|
|
110
159
|
for (const name of Object.keys(merged.mcpServers).sort()) {
|
|
111
160
|
console.log(` ${c("blue", "-")} ${name}`);
|
|
112
161
|
}
|
|
113
162
|
console.log(`\n${c("cyan", "Syncing to clients...")}`);
|
|
114
|
-
for (const client of
|
|
163
|
+
for (const client of existingClients) {
|
|
115
164
|
const changes = getChanges(client, merged);
|
|
116
165
|
const parts = [];
|
|
117
166
|
if (changes.added.length > 0) {
|
|
@@ -120,20 +169,35 @@ function run() {
|
|
|
120
169
|
if (changes.removed.length > 0) {
|
|
121
170
|
parts.push(c("red", `-${changes.removed.length}`));
|
|
122
171
|
}
|
|
123
|
-
const
|
|
124
|
-
const
|
|
172
|
+
const hasChanges = parts.length > 0;
|
|
173
|
+
const changeInfo = hasChanges ? ` (${parts.join(", ")})` : "";
|
|
174
|
+
const status = hasChanges ? c("green", "sync") : c("dim", "skip");
|
|
125
175
|
console.log(` [${status}] ${client.name}${changeInfo}`);
|
|
126
|
-
if (verbose
|
|
176
|
+
if (verbose) {
|
|
127
177
|
for (const name of changes.added) {
|
|
128
178
|
console.log(c("green", ` + ${name}`));
|
|
129
179
|
}
|
|
180
|
+
for (const name of changes.removed) {
|
|
181
|
+
console.log(c("red", ` - ${name}`));
|
|
182
|
+
}
|
|
130
183
|
}
|
|
131
|
-
if (!dryRun) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
184
|
+
if (!dryRun && hasChanges) {
|
|
185
|
+
const existingContent = readFileSync(client.path, "utf-8");
|
|
186
|
+
let output;
|
|
187
|
+
if (client.name === "Gemini CLI") {
|
|
188
|
+
const existing = JSON.parse(existingContent);
|
|
189
|
+
output = JSON.stringify({ ...existing, mcpServers: merged.mcpServers }, null, 2) + "\n";
|
|
190
|
+
}
|
|
191
|
+
else if (client.name === "Codex") {
|
|
192
|
+
output = serializeCodexConfig(merged, existingContent);
|
|
193
|
+
}
|
|
194
|
+
else if (client.name === "OpenCode") {
|
|
195
|
+
output = serializeOpenCodeConfig(merged, existingContent);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
output = JSON.stringify(merged, null, 2) + "\n";
|
|
135
199
|
}
|
|
136
|
-
writeFileSync(client.path,
|
|
200
|
+
writeFileSync(client.path, output);
|
|
137
201
|
}
|
|
138
202
|
}
|
|
139
203
|
console.log(`\n${c("green", "Done!")} ${dryRun ? "(dry run)" : ""}\n`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseJsonc<T>(content: string): T;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function parseJsonc(content) {
|
|
2
|
+
let result = "";
|
|
3
|
+
let inString = false;
|
|
4
|
+
let i = 0;
|
|
5
|
+
while (i < content.length) {
|
|
6
|
+
const char = content[i];
|
|
7
|
+
const next = content[i + 1];
|
|
8
|
+
if (char === '"' && (i === 0 || content[i - 1] !== "\\")) {
|
|
9
|
+
inString = !inString;
|
|
10
|
+
result += char;
|
|
11
|
+
i++;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (!inString) {
|
|
15
|
+
if (char === "/" && next === "/") {
|
|
16
|
+
while (i < content.length && content[i] !== "\n" && content[i] !== "\r") {
|
|
17
|
+
i++;
|
|
18
|
+
}
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (char === "/" && next === "*") {
|
|
22
|
+
i += 2;
|
|
23
|
+
while (i < content.length - 1 && !(content[i] === "*" && content[i + 1] === "/")) {
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
i += 2;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
result += char;
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
const cleaned = result.replace(/,(\s*[}\]])/g, "$1");
|
|
34
|
+
return JSON.parse(cleaned);
|
|
35
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type TomlValue = string | string[] | Record<string, string>;
|
|
2
|
+
type TomlSection = Record<string, TomlValue>;
|
|
3
|
+
export declare function parseToml(content: string): Record<string, TomlSection>;
|
|
4
|
+
export declare function stringifyToml(data: Record<string, TomlSection>): string;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export function parseToml(content) {
|
|
2
|
+
const result = {};
|
|
3
|
+
let currentSection = "";
|
|
4
|
+
const lines = content.split("\n");
|
|
5
|
+
for (const rawLine of lines) {
|
|
6
|
+
const line = rawLine.trim();
|
|
7
|
+
if (!line || line.startsWith("#"))
|
|
8
|
+
continue;
|
|
9
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
10
|
+
if (sectionMatch) {
|
|
11
|
+
currentSection = sectionMatch[1];
|
|
12
|
+
result[currentSection] = {};
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (!currentSection)
|
|
16
|
+
continue;
|
|
17
|
+
const keyValueMatch = line.match(/^(\w+)\s*=\s*(.+)$/);
|
|
18
|
+
if (!keyValueMatch)
|
|
19
|
+
continue;
|
|
20
|
+
const [, key, rawValue] = keyValueMatch;
|
|
21
|
+
result[currentSection][key] = parseValue(rawValue.trim());
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
function parseValue(value) {
|
|
26
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
27
|
+
return value.slice(1, -1);
|
|
28
|
+
}
|
|
29
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
30
|
+
return parseArray(value);
|
|
31
|
+
}
|
|
32
|
+
if (value.startsWith("{") && value.endsWith("}")) {
|
|
33
|
+
return parseInlineTable(value);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function parseArray(value) {
|
|
38
|
+
const inner = value.slice(1, -1).trim();
|
|
39
|
+
if (!inner)
|
|
40
|
+
return [];
|
|
41
|
+
const result = [];
|
|
42
|
+
let current = "";
|
|
43
|
+
let inString = false;
|
|
44
|
+
for (let i = 0; i < inner.length; i++) {
|
|
45
|
+
const char = inner[i];
|
|
46
|
+
if (char === '"' && inner[i - 1] !== "\\") {
|
|
47
|
+
inString = !inString;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char === "," && !inString) {
|
|
51
|
+
if (current.trim())
|
|
52
|
+
result.push(current.trim());
|
|
53
|
+
current = "";
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!inString && (char === " " || char === "\t"))
|
|
57
|
+
continue;
|
|
58
|
+
current += char;
|
|
59
|
+
}
|
|
60
|
+
if (current.trim())
|
|
61
|
+
result.push(current.trim());
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function parseInlineTable(value) {
|
|
65
|
+
const inner = value.slice(1, -1).trim();
|
|
66
|
+
if (!inner)
|
|
67
|
+
return {};
|
|
68
|
+
const result = {};
|
|
69
|
+
const pairs = inner.split(",");
|
|
70
|
+
for (const pair of pairs) {
|
|
71
|
+
const match = pair.trim().match(/^"?(\w+)"?\s*=\s*"([^"]*)"$/);
|
|
72
|
+
if (match) {
|
|
73
|
+
result[match[1]] = match[2];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
export function stringifyToml(data) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
for (const [section, values] of Object.entries(data)) {
|
|
81
|
+
lines.push(`[${section}]`);
|
|
82
|
+
for (const [key, value] of Object.entries(values)) {
|
|
83
|
+
lines.push(`${key} = ${stringifyValue(value)}`);
|
|
84
|
+
}
|
|
85
|
+
lines.push("");
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
function stringifyValue(value) {
|
|
90
|
+
if (typeof value === "string") {
|
|
91
|
+
return `"${value}"`;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
return `[${value.map((v) => `"${v}"`).join(", ")}]`;
|
|
95
|
+
}
|
|
96
|
+
const pairs = Object.entries(value)
|
|
97
|
+
.map(([k, v]) => `"${k}" = "${v}"`)
|
|
98
|
+
.join(", ");
|
|
99
|
+
return `{ ${pairs} }`;
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sync-project-mcps",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Sync project-level MCP configurations across AI coding assistants (Cursor, Claude Code, Windsurf, Cline)",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Sync project-level MCP configurations across AI coding assistants (Cursor, Claude Code, Windsurf, Cline, Gemini CLI, Codex, OpenCode)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sync-project-mcps": "./dist/index.js"
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"
|
|
13
|
+
"clean": "rm -rf dist",
|
|
14
|
+
"build": "npm run clean && tsc",
|
|
14
15
|
"dev": "tsc --watch",
|
|
15
16
|
"start": "node dist/index.js",
|
|
16
|
-
"
|
|
17
|
+
"test": "node --test --experimental-strip-types src/**/*.test.ts",
|
|
18
|
+
"prepublishOnly": "npm run build && npm test"
|
|
17
19
|
},
|
|
18
20
|
"keywords": ["mcp", "cursor", "claude", "sync", "cli", "windsurf", "cline", "project"],
|
|
19
21
|
"author": "Vlad Tansky",
|