mcp-config-migrator 0.1.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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/chunk-E2A5N2LX.js +467 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +508 -0
- package/dist/index.d.ts +136 -0
- package/dist/index.js +32 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Industrial Curiosity
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# mcp-config-migrator
|
|
2
|
+
|
|
3
|
+
Interactively migrate and merge MCP (Model Context Protocol) server configurations between VS Code, Cursor, and Claude Code.
|
|
4
|
+
|
|
5
|
+
If you configure MCP servers in one editor and want the same servers available in another — or want to keep two editors' configs in sync — this CLI walks you through picking a source and target, shows you what would change, lets you resolve any conflicts, and writes the result back in the target's native format.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx mcp-config-migrator
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
You'll be asked to:
|
|
14
|
+
|
|
15
|
+
1. Pick a **source** IDE and config scope/path (a sensible default is suggested and pre-filled, but always editable).
|
|
16
|
+
2. Pick a **target** IDE and config scope/path the same way.
|
|
17
|
+
3. Review any entries that exist in both configs with different definitions, and choose for each one: accept the source's version, accept the target's version, or merge — which opens an editor with both versions combined and conflicting fields marked git-style (`<<<<<<<`/`=======`/`>>>>>>>`) so you can resolve them by hand.
|
|
18
|
+
4. Confirm a summary (added / unchanged / conflicts resolved) before anything is written.
|
|
19
|
+
5. After writing, optionally remove any server entries from the target as a cleanup step.
|
|
20
|
+
|
|
21
|
+
Nothing is written to disk until you explicitly confirm. Right before the write, you'll be asked whether to back up the target's *current* MCP server entries (not the rest of the file) to a version history — answer "Yes, always" or "No, never" to stop being asked and remember that choice for future runs, or "Yes"/"No" to decide just this once. Answering "Yes" (either form) also asks where to store the backup, pre-filled with the current default and editable; once a backup is written — including silently, when the preference is "always back up" — its storage location is always displayed. You can cancel at any prompt (Ctrl+C) with no changes made and no backup recorded.
|
|
22
|
+
|
|
23
|
+
If you migrate into a Claude Code project-scope config (`.mcp.json`), Claude Code will ask you to re-approve the affected servers next time you open that project — the CLI tells you which servers and reminds you of `claude mcp reset-project-choices` if you'd rather not be prompted again.
|
|
24
|
+
|
|
25
|
+
## Supported IDEs and config scopes
|
|
26
|
+
|
|
27
|
+
| IDE | Scope | Config key | Default path |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| VS Code | Workspace | `servers` | `<project>/.vscode/mcp.json` |
|
|
30
|
+
| VS Code | User | `servers` | macOS: `~/Library/Application Support/Code/User/mcp.json`; Linux: `$XDG_CONFIG_HOME/Code/User/mcp.json` (falls back to `~/.config/...`); Windows: `%APPDATA%\Code\User\mcp.json` |
|
|
31
|
+
| Cursor | Global | `mcpServers` | `~/.cursor/mcp.json` (Windows: `%USERPROFILE%\.cursor\mcp.json`) |
|
|
32
|
+
| Cursor | Project | `mcpServers` | `<project>/.cursor/mcp.json` |
|
|
33
|
+
| Claude Code | User | `mcpServers` (inside `~/.claude.json`) | `$CLAUDE_CONFIG_DIR/.claude.json` if set, else `~/.claude.json` |
|
|
34
|
+
| Claude Code | Project | `mcpServers` (inside `.mcp.json`) | `<project>/.mcp.json` |
|
|
35
|
+
|
|
36
|
+
Every suggested path is shown as editable text — auto-detection is a convenience, not a requirement.
|
|
37
|
+
|
|
38
|
+
VS Code requires a `type` (`stdio`/`http`/`sse`) on every entry; Cursor and Claude Code make `type` optional for `stdio` entries. Fields specific to one IDE (e.g. VS Code's `sandbox` options) are preserved when round-tripping through that same IDE, but dropped — with a warning — when migrating to a different IDE, since there's no equivalent field to write them into.
|
|
39
|
+
|
|
40
|
+
Writes to `~/.claude.json` only touch the `mcpServers` key; OAuth session data, trust state, and any other content in that file is left untouched.
|
|
41
|
+
|
|
42
|
+
## Backup and restore
|
|
43
|
+
|
|
44
|
+
Backups (if you opt in) are appended to a single version history file, by default `~/mcp-config-migrator.versions.json`, holding every backed-up version across every target you've ever migrated to — nothing is ever removed or overwritten by normal use.
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npx mcp-config-migrator restore # prompts for the version history file, defaulting to the canonical one
|
|
48
|
+
npx mcp-config-migrator restore --file <path> # or -f <path>
|
|
49
|
+
npx mcp-config-migrator config backup # view or change the backup preference and storage location
|
|
50
|
+
npx mcp-config-migrator --help # or -h, /?
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`restore` lists every saved version newest-first, lets you preview a version's server entries before committing, and on confirmation writes that version directly back to its original IDE/scope/path — restoring an older version doesn't remove any other version, so you can always restore forward again later.
|
|
54
|
+
|
|
55
|
+
## Example run
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
┌ mcp-config-migrator
|
|
59
|
+
◆ Migrate MCP servers FROM which IDE?
|
|
60
|
+
│ ● Cursor
|
|
61
|
+
◆ Which Cursor config scope?
|
|
62
|
+
│ ● Global
|
|
63
|
+
◆ Confirm the Cursor config path (Global):
|
|
64
|
+
│ ~/.cursor/mcp.json
|
|
65
|
+
◆ Migrate MCP servers TO which IDE?
|
|
66
|
+
│ ● Claude Code
|
|
67
|
+
◆ Which Claude Code config scope?
|
|
68
|
+
│ ● Project (.mcp.json)
|
|
69
|
+
◆ Confirm the Claude Code config path (Project (.mcp.json)):
|
|
70
|
+
│ ./.mcp.json
|
|
71
|
+
◆ Migration summary
|
|
72
|
+
│ Added (2): fetch, github
|
|
73
|
+
│ Unchanged (1): filesystem
|
|
74
|
+
│ Conflicts resolved (0):
|
|
75
|
+
│ accept target (0)
|
|
76
|
+
│ accept source (0)
|
|
77
|
+
│ merged (0)
|
|
78
|
+
◆ Write merged config to ./.mcp.json?
|
|
79
|
+
│ Yes
|
|
80
|
+
◆ Back up the current MCP servers in ./.mcp.json before writing?
|
|
81
|
+
│ ● Yes, always
|
|
82
|
+
◆ Backup storage location:
|
|
83
|
+
│ ~/mcp-config-migrator.versions.json
|
|
84
|
+
✔ Backed up current MCP servers for ./.mcp.json to ~/mcp-config-migrator.versions.json
|
|
85
|
+
✔ Wrote merged config to ./.mcp.json
|
|
86
|
+
◆ Remove any MCP servers from the target before finishing? (none required)
|
|
87
|
+
│ (none selected)
|
|
88
|
+
└ Done.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
npm install
|
|
95
|
+
npm run build # compile to dist/
|
|
96
|
+
npm run test # run the test suite
|
|
97
|
+
npm run lint # lint
|
|
98
|
+
npm run typecheck # type-check without emitting
|
|
99
|
+
```
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
// src/model/equality.ts
|
|
2
|
+
function arraysEqual(a, b) {
|
|
3
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
4
|
+
if (a.length !== b.length) return false;
|
|
5
|
+
return a.every((value, index) => value === b[index]);
|
|
6
|
+
}
|
|
7
|
+
function recordsEqual(a, b) {
|
|
8
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
9
|
+
const aKeys = Object.keys(a);
|
|
10
|
+
const bKeys = Object.keys(b);
|
|
11
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
12
|
+
return aKeys.every((key) => a[key] === b[key]);
|
|
13
|
+
}
|
|
14
|
+
function areServersEqual(a, b) {
|
|
15
|
+
return a.name === b.name && a.transport === b.transport && a.command === b.command && a.cwd === b.cwd && a.url === b.url && arraysEqual(a.args, b.args) && recordsEqual(a.env, b.env) && recordsEqual(a.headers, b.headers);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/model/versionsStore.ts
|
|
19
|
+
import { readFile, writeFile } from "fs/promises";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
function defaultSettingsPath() {
|
|
23
|
+
return join(homedir(), "mcp-config-migrator.versions.json");
|
|
24
|
+
}
|
|
25
|
+
async function readJsonFile(path) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code === "ENOENT") return {};
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function writeJsonFile(path, data) {
|
|
34
|
+
await writeFile(path, `${JSON.stringify(data, null, 2)}
|
|
35
|
+
`, "utf8");
|
|
36
|
+
}
|
|
37
|
+
async function readVersionsStore(settingsPath) {
|
|
38
|
+
const settings = await readJsonFile(settingsPath);
|
|
39
|
+
const configured = settings.configured ?? "alwaysAsk";
|
|
40
|
+
const backupLocation = settings.backupLocation;
|
|
41
|
+
const versionsPath = backupLocation ?? settingsPath;
|
|
42
|
+
const versions = backupLocation ? (await readJsonFile(versionsPath)).versions ?? [] : settings.versions ?? [];
|
|
43
|
+
return { settingsPath, configured, backupLocation, versionsPath, versions };
|
|
44
|
+
}
|
|
45
|
+
async function setPreference(settingsPath, configured) {
|
|
46
|
+
const settings = await readJsonFile(settingsPath);
|
|
47
|
+
await writeJsonFile(settingsPath, { ...settings, configured });
|
|
48
|
+
}
|
|
49
|
+
async function setBackupLocation(settingsPath, backupLocation) {
|
|
50
|
+
const settings = await readJsonFile(settingsPath);
|
|
51
|
+
const next = { ...settings };
|
|
52
|
+
if (backupLocation) {
|
|
53
|
+
next.backupLocation = backupLocation;
|
|
54
|
+
} else {
|
|
55
|
+
delete next.backupLocation;
|
|
56
|
+
}
|
|
57
|
+
await writeJsonFile(settingsPath, next);
|
|
58
|
+
}
|
|
59
|
+
async function appendVersion(settingsPath, entry) {
|
|
60
|
+
const store = await readVersionsStore(settingsPath);
|
|
61
|
+
const versions = [...store.versions, entry];
|
|
62
|
+
if (store.backupLocation) {
|
|
63
|
+
await writeJsonFile(store.versionsPath, { versions });
|
|
64
|
+
} else {
|
|
65
|
+
const settings = await readJsonFile(settingsPath);
|
|
66
|
+
await writeJsonFile(settingsPath, { ...settings, versions });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/adapters/vscode.ts
|
|
71
|
+
import { parse as parseJsonc, modify, applyEdits } from "jsonc-parser";
|
|
72
|
+
|
|
73
|
+
// src/adapters/fileIO.ts
|
|
74
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir } from "fs/promises";
|
|
75
|
+
import { dirname } from "path";
|
|
76
|
+
async function readTextFile(path) {
|
|
77
|
+
try {
|
|
78
|
+
return await readFile2(path, "utf8");
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err.code === "ENOENT") return "";
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function writeTextFile(path, content) {
|
|
85
|
+
await mkdir(dirname(path), { recursive: true });
|
|
86
|
+
await writeFile2(path, content, "utf8");
|
|
87
|
+
}
|
|
88
|
+
function parseJsonObject(text) {
|
|
89
|
+
if (text.trim() === "") return {};
|
|
90
|
+
const parsed = JSON.parse(text);
|
|
91
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/adapters/entryFields.ts
|
|
95
|
+
var STDIO_FIELDS = ["type", "command", "args", "cwd", "env"];
|
|
96
|
+
var REMOTE_FIELDS = ["type", "url", "headers"];
|
|
97
|
+
function entryToNormalized(name, raw, ideId, defaultTransport = "stdio") {
|
|
98
|
+
const transport = raw.type ?? defaultTransport;
|
|
99
|
+
const knownKeys = transport === "stdio" ? STDIO_FIELDS : REMOTE_FIELDS;
|
|
100
|
+
const extraFields = {};
|
|
101
|
+
for (const key of Object.keys(raw)) {
|
|
102
|
+
if (!knownKeys.includes(key)) extraFields[key] = raw[key];
|
|
103
|
+
}
|
|
104
|
+
const server = { name, transport };
|
|
105
|
+
if (transport === "stdio") {
|
|
106
|
+
if (typeof raw.command === "string") server.command = raw.command;
|
|
107
|
+
if (Array.isArray(raw.args)) server.args = raw.args;
|
|
108
|
+
if (typeof raw.cwd === "string") server.cwd = raw.cwd;
|
|
109
|
+
if (raw.env && typeof raw.env === "object") server.env = raw.env;
|
|
110
|
+
} else {
|
|
111
|
+
if (typeof raw.url === "string") server.url = raw.url;
|
|
112
|
+
if (raw.headers && typeof raw.headers === "object") {
|
|
113
|
+
server.headers = raw.headers;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (Object.keys(extraFields).length > 0) {
|
|
117
|
+
server.extra = { sourceIdeId: ideId, fields: extraFields };
|
|
118
|
+
}
|
|
119
|
+
return server;
|
|
120
|
+
}
|
|
121
|
+
function normalizedToEntry(server, ideId, includeType) {
|
|
122
|
+
const entry = {};
|
|
123
|
+
if (includeType || server.transport !== "stdio") {
|
|
124
|
+
entry.type = server.transport;
|
|
125
|
+
}
|
|
126
|
+
if (server.transport === "stdio") {
|
|
127
|
+
if (server.command !== void 0) entry.command = server.command;
|
|
128
|
+
if (server.args !== void 0) entry.args = server.args;
|
|
129
|
+
if (server.cwd !== void 0) entry.cwd = server.cwd;
|
|
130
|
+
if (server.env !== void 0) entry.env = server.env;
|
|
131
|
+
} else {
|
|
132
|
+
if (server.url !== void 0) entry.url = server.url;
|
|
133
|
+
if (server.headers !== void 0) entry.headers = server.headers;
|
|
134
|
+
}
|
|
135
|
+
let droppedFields = [];
|
|
136
|
+
if (server.extra) {
|
|
137
|
+
if (server.extra.sourceIdeId === ideId) {
|
|
138
|
+
Object.assign(entry, server.extra.fields);
|
|
139
|
+
} else {
|
|
140
|
+
droppedFields = Object.keys(server.extra.fields);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { entry, droppedFields };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/adapters/paths.ts
|
|
147
|
+
import { posix, win32 } from "path";
|
|
148
|
+
function homeDir(env, platform) {
|
|
149
|
+
if (platform === "win32") return env.USERPROFILE ?? env.HOME ?? "";
|
|
150
|
+
return env.HOME ?? "";
|
|
151
|
+
}
|
|
152
|
+
function joinForPlatform(platform, ...segments) {
|
|
153
|
+
return (platform === "win32" ? win32 : posix).join(...segments);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/adapters/vscode.ts
|
|
157
|
+
var IDE_ID = "vscode";
|
|
158
|
+
var SERVERS_KEY = "servers";
|
|
159
|
+
function userConfigDir(env, platform) {
|
|
160
|
+
const home = homeDir(env, platform);
|
|
161
|
+
if (platform === "darwin") {
|
|
162
|
+
return joinForPlatform(platform, home, "Library", "Application Support", "Code", "User");
|
|
163
|
+
}
|
|
164
|
+
if (platform === "win32") {
|
|
165
|
+
return joinForPlatform(
|
|
166
|
+
platform,
|
|
167
|
+
env.APPDATA ?? joinForPlatform(platform, home, "AppData", "Roaming"),
|
|
168
|
+
"Code",
|
|
169
|
+
"User"
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return joinForPlatform(platform, env.XDG_CONFIG_HOME ?? joinForPlatform(platform, home, ".config"), "Code", "User");
|
|
173
|
+
}
|
|
174
|
+
var vscodeAdapter = {
|
|
175
|
+
id: IDE_ID,
|
|
176
|
+
label: "VS Code",
|
|
177
|
+
resolveDefaultPaths(env, platform, cwd) {
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
scopeId: "workspace",
|
|
181
|
+
label: "Workspace (.vscode/mcp.json)",
|
|
182
|
+
path: joinForPlatform(platform, cwd, ".vscode", "mcp.json")
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
scopeId: "user",
|
|
186
|
+
label: "User",
|
|
187
|
+
path: joinForPlatform(platform, userConfigDir(env, platform), "mcp.json")
|
|
188
|
+
}
|
|
189
|
+
];
|
|
190
|
+
},
|
|
191
|
+
async load(path) {
|
|
192
|
+
const text = await readTextFile(path);
|
|
193
|
+
const doc = text.trim() === "" ? {} : parseJsonc(text);
|
|
194
|
+
const serversRaw = doc[SERVERS_KEY] ?? {};
|
|
195
|
+
const servers = Object.entries(serversRaw).map(
|
|
196
|
+
([name, raw]) => entryToNormalized(name, raw, IDE_ID, "stdio")
|
|
197
|
+
);
|
|
198
|
+
return { servers };
|
|
199
|
+
},
|
|
200
|
+
async save(path, normalized) {
|
|
201
|
+
const originalText = await readTextFile(path);
|
|
202
|
+
const serversValue = {};
|
|
203
|
+
const droppedFields = [];
|
|
204
|
+
for (const server of normalized.servers) {
|
|
205
|
+
const { entry, droppedFields: dropped } = normalizedToEntry(server, IDE_ID, true);
|
|
206
|
+
serversValue[server.name] = entry;
|
|
207
|
+
if (dropped.length > 0) {
|
|
208
|
+
droppedFields.push({ serverName: server.name, fields: dropped });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const formattingOptions = { tabSize: 2, insertSpaces: true, eol: "\n" };
|
|
212
|
+
const edits = modify(originalText, [SERVERS_KEY], serversValue, { formattingOptions });
|
|
213
|
+
const newText = applyEdits(originalText, edits);
|
|
214
|
+
await writeTextFile(path, newText);
|
|
215
|
+
return { droppedFields };
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/adapters/cursor.ts
|
|
220
|
+
var IDE_ID2 = "cursor";
|
|
221
|
+
var SERVERS_KEY2 = "mcpServers";
|
|
222
|
+
var cursorAdapter = {
|
|
223
|
+
id: IDE_ID2,
|
|
224
|
+
label: "Cursor",
|
|
225
|
+
resolveDefaultPaths(env, platform, cwd) {
|
|
226
|
+
const home = homeDir(env, platform);
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
scopeId: "global",
|
|
230
|
+
label: "Global",
|
|
231
|
+
path: joinForPlatform(platform, home, ".cursor", "mcp.json")
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
scopeId: "project",
|
|
235
|
+
label: "Project (.cursor/mcp.json)",
|
|
236
|
+
path: joinForPlatform(platform, cwd, ".cursor", "mcp.json")
|
|
237
|
+
}
|
|
238
|
+
];
|
|
239
|
+
},
|
|
240
|
+
async load(path) {
|
|
241
|
+
const text = await readTextFile(path);
|
|
242
|
+
const doc = parseJsonObject(text);
|
|
243
|
+
const serversRaw = doc[SERVERS_KEY2] ?? {};
|
|
244
|
+
const servers = Object.entries(serversRaw).map(
|
|
245
|
+
([name, raw]) => entryToNormalized(name, raw, IDE_ID2, "stdio")
|
|
246
|
+
);
|
|
247
|
+
return { servers };
|
|
248
|
+
},
|
|
249
|
+
async save(path, normalized) {
|
|
250
|
+
const text = await readTextFile(path);
|
|
251
|
+
const doc = parseJsonObject(text);
|
|
252
|
+
const serversValue = {};
|
|
253
|
+
const droppedFields = [];
|
|
254
|
+
for (const server of normalized.servers) {
|
|
255
|
+
const { entry, droppedFields: dropped } = normalizedToEntry(server, IDE_ID2, false);
|
|
256
|
+
serversValue[server.name] = entry;
|
|
257
|
+
if (dropped.length > 0) {
|
|
258
|
+
droppedFields.push({ serverName: server.name, fields: dropped });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
doc[SERVERS_KEY2] = serversValue;
|
|
262
|
+
await writeTextFile(path, `${JSON.stringify(doc, null, 2)}
|
|
263
|
+
`);
|
|
264
|
+
return { droppedFields };
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// src/adapters/claudeCode.ts
|
|
269
|
+
var IDE_ID3 = "claude-code";
|
|
270
|
+
var SERVERS_KEY3 = "mcpServers";
|
|
271
|
+
var claudeCodeAdapter = {
|
|
272
|
+
id: IDE_ID3,
|
|
273
|
+
label: "Claude Code",
|
|
274
|
+
resolveDefaultPaths(env, platform, cwd) {
|
|
275
|
+
const userConfigDir2 = env.CLAUDE_CONFIG_DIR ?? homeDir(env, platform);
|
|
276
|
+
return [
|
|
277
|
+
{
|
|
278
|
+
scopeId: "user",
|
|
279
|
+
label: "User (~/.claude.json)",
|
|
280
|
+
path: joinForPlatform(platform, userConfigDir2, ".claude.json")
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
scopeId: "project",
|
|
284
|
+
label: "Project (.mcp.json)",
|
|
285
|
+
path: joinForPlatform(platform, cwd, ".mcp.json")
|
|
286
|
+
}
|
|
287
|
+
];
|
|
288
|
+
},
|
|
289
|
+
async load(path) {
|
|
290
|
+
const text = await readTextFile(path);
|
|
291
|
+
const doc = parseJsonObject(text);
|
|
292
|
+
const serversRaw = doc[SERVERS_KEY3] ?? {};
|
|
293
|
+
const servers = Object.entries(serversRaw).map(
|
|
294
|
+
([name, raw]) => entryToNormalized(name, raw, IDE_ID3, "stdio")
|
|
295
|
+
);
|
|
296
|
+
return { servers };
|
|
297
|
+
},
|
|
298
|
+
async save(path, normalized) {
|
|
299
|
+
const text = await readTextFile(path);
|
|
300
|
+
const doc = parseJsonObject(text);
|
|
301
|
+
const serversValue = {};
|
|
302
|
+
const droppedFields = [];
|
|
303
|
+
for (const server of normalized.servers) {
|
|
304
|
+
const { entry, droppedFields: dropped } = normalizedToEntry(server, IDE_ID3, false);
|
|
305
|
+
serversValue[server.name] = entry;
|
|
306
|
+
if (dropped.length > 0) {
|
|
307
|
+
droppedFields.push({ serverName: server.name, fields: dropped });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
doc[SERVERS_KEY3] = serversValue;
|
|
311
|
+
await writeTextFile(path, `${JSON.stringify(doc, null, 2)}
|
|
312
|
+
`);
|
|
313
|
+
return { droppedFields };
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// src/adapters/registry.ts
|
|
318
|
+
var adapters = [vscodeAdapter, cursorAdapter, claudeCodeAdapter];
|
|
319
|
+
var adapterById = new Map(adapters.map((adapter) => [adapter.id, adapter]));
|
|
320
|
+
function getAdapter(id) {
|
|
321
|
+
const adapter = adapterById.get(id);
|
|
322
|
+
if (!adapter) {
|
|
323
|
+
throw new Error(`Unknown IDE adapter: ${id}`);
|
|
324
|
+
}
|
|
325
|
+
return adapter;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/engine/classify.ts
|
|
329
|
+
function classify(source, target) {
|
|
330
|
+
const targetByName = new Map(target.servers.map((server) => [server.name, server]));
|
|
331
|
+
return source.servers.map((sourceServer) => {
|
|
332
|
+
const targetServer = targetByName.get(sourceServer.name);
|
|
333
|
+
if (!targetServer) {
|
|
334
|
+
return { name: sourceServer.name, kind: "add", source: sourceServer };
|
|
335
|
+
}
|
|
336
|
+
const kind = areServersEqual(sourceServer, targetServer) ? "unchanged" : "conflict";
|
|
337
|
+
return { name: sourceServer.name, kind, source: sourceServer, target: targetServer };
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/engine/diff.ts
|
|
342
|
+
import { diffLines } from "diff";
|
|
343
|
+
function forDisplay(server) {
|
|
344
|
+
const { name: _name, extra, ...rest } = server;
|
|
345
|
+
const display = { ...rest };
|
|
346
|
+
if (extra) display.extra = extra.fields;
|
|
347
|
+
return display;
|
|
348
|
+
}
|
|
349
|
+
function renderConflictDiff(entry) {
|
|
350
|
+
if (entry.kind !== "conflict" || !entry.target) {
|
|
351
|
+
throw new Error(`renderConflictDiff requires a conflict entry: ${entry.name}`);
|
|
352
|
+
}
|
|
353
|
+
const targetJson = `${JSON.stringify(forDisplay(entry.target), null, 2)}
|
|
354
|
+
`;
|
|
355
|
+
const sourceJson = `${JSON.stringify(forDisplay(entry.source), null, 2)}
|
|
356
|
+
`;
|
|
357
|
+
const changes = diffLines(targetJson, sourceJson);
|
|
358
|
+
return changes.map((change) => {
|
|
359
|
+
const prefix = change.added ? "+" : change.removed ? "-" : " ";
|
|
360
|
+
return change.value.split("\n").filter((line, index, lines) => !(index === lines.length - 1 && line === "")).map((line) => `${prefix} ${line}`).join("\n");
|
|
361
|
+
}).join("\n");
|
|
362
|
+
}
|
|
363
|
+
function renderMergeScaffold(entry) {
|
|
364
|
+
if (entry.kind !== "conflict" || !entry.target) {
|
|
365
|
+
throw new Error(`renderMergeScaffold requires a conflict entry: ${entry.name}`);
|
|
366
|
+
}
|
|
367
|
+
const targetJson = `${JSON.stringify(forDisplay(entry.target), null, 2)}
|
|
368
|
+
`;
|
|
369
|
+
const sourceJson = `${JSON.stringify(forDisplay(entry.source), null, 2)}
|
|
370
|
+
`;
|
|
371
|
+
const changes = diffLines(targetJson, sourceJson);
|
|
372
|
+
const out = [];
|
|
373
|
+
for (let i = 0; i < changes.length; i++) {
|
|
374
|
+
const change = changes[i];
|
|
375
|
+
if (!change.added && !change.removed) {
|
|
376
|
+
out.push(change.value);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (change.removed) {
|
|
380
|
+
const next = changes[i + 1];
|
|
381
|
+
const sourceValue = next?.added ? next.value : "";
|
|
382
|
+
if (next?.added) i++;
|
|
383
|
+
out.push(conflictBlock(change.value, sourceValue));
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
out.push(conflictBlock("", change.value));
|
|
387
|
+
}
|
|
388
|
+
return out.join("");
|
|
389
|
+
}
|
|
390
|
+
function conflictBlock(targetLines, sourceLines) {
|
|
391
|
+
return `<<<<<<< target
|
|
392
|
+
${targetLines}=======
|
|
393
|
+
${sourceLines}>>>>>>> source
|
|
394
|
+
`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/engine/merge.ts
|
|
398
|
+
function applyMerge(target, classifications, resolutions) {
|
|
399
|
+
const resultByName = new Map(target.servers.map((server) => [server.name, server]));
|
|
400
|
+
for (const entry of classifications) {
|
|
401
|
+
if (entry.kind === "add") {
|
|
402
|
+
resultByName.set(entry.name, entry.source);
|
|
403
|
+
} else if (entry.kind === "conflict") {
|
|
404
|
+
const resolution = resolutions[entry.name] ?? { kind: "accept-target" };
|
|
405
|
+
if (resolution.kind === "accept-source") {
|
|
406
|
+
resultByName.set(entry.name, entry.source);
|
|
407
|
+
} else if (resolution.kind === "merge") {
|
|
408
|
+
resultByName.set(entry.name, resolution.merged);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { servers: Array.from(resultByName.values()) };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/engine/summary.ts
|
|
416
|
+
function toCategory(names) {
|
|
417
|
+
return { count: names.length, names };
|
|
418
|
+
}
|
|
419
|
+
function summarize(classifications, resolutions) {
|
|
420
|
+
const added = [];
|
|
421
|
+
const unchanged = [];
|
|
422
|
+
const acceptTarget = [];
|
|
423
|
+
const acceptSource = [];
|
|
424
|
+
const merged = [];
|
|
425
|
+
for (const entry of classifications) {
|
|
426
|
+
if (entry.kind === "add") {
|
|
427
|
+
added.push(entry.name);
|
|
428
|
+
} else if (entry.kind === "unchanged") {
|
|
429
|
+
unchanged.push(entry.name);
|
|
430
|
+
} else {
|
|
431
|
+
const resolution = resolutions[entry.name] ?? { kind: "accept-target" };
|
|
432
|
+
if (resolution.kind === "accept-target") acceptTarget.push(entry.name);
|
|
433
|
+
else if (resolution.kind === "accept-source") acceptSource.push(entry.name);
|
|
434
|
+
else merged.push(entry.name);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
added: toCategory(added),
|
|
439
|
+
unchanged: toCategory(unchanged),
|
|
440
|
+
conflicts: {
|
|
441
|
+
total: acceptTarget.length + acceptSource.length + merged.length,
|
|
442
|
+
acceptTarget: toCategory(acceptTarget),
|
|
443
|
+
acceptSource: toCategory(acceptSource),
|
|
444
|
+
merged: toCategory(merged)
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function isNoOp(classifications) {
|
|
449
|
+
return classifications.every((entry) => entry.kind === "unchanged");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export {
|
|
453
|
+
areServersEqual,
|
|
454
|
+
defaultSettingsPath,
|
|
455
|
+
readVersionsStore,
|
|
456
|
+
setPreference,
|
|
457
|
+
setBackupLocation,
|
|
458
|
+
appendVersion,
|
|
459
|
+
adapters,
|
|
460
|
+
getAdapter,
|
|
461
|
+
classify,
|
|
462
|
+
renderConflictDiff,
|
|
463
|
+
renderMergeScaffold,
|
|
464
|
+
applyMerge,
|
|
465
|
+
summarize,
|
|
466
|
+
isNoOp
|
|
467
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
adapters,
|
|
4
|
+
appendVersion,
|
|
5
|
+
applyMerge,
|
|
6
|
+
classify,
|
|
7
|
+
defaultSettingsPath,
|
|
8
|
+
getAdapter,
|
|
9
|
+
isNoOp,
|
|
10
|
+
readVersionsStore,
|
|
11
|
+
renderConflictDiff,
|
|
12
|
+
renderMergeScaffold,
|
|
13
|
+
setBackupLocation,
|
|
14
|
+
setPreference,
|
|
15
|
+
summarize
|
|
16
|
+
} from "./chunk-E2A5N2LX.js";
|
|
17
|
+
|
|
18
|
+
// src/cli/args.ts
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const [first, ...rest] = argv;
|
|
21
|
+
if (first === void 0) return { kind: "migrate" };
|
|
22
|
+
if (first === "--help" || first === "-h" || first === "/?") return { kind: "help" };
|
|
23
|
+
if (first === "restore") {
|
|
24
|
+
const flagIndex = rest.findIndex((arg) => arg === "--file" || arg === "-f");
|
|
25
|
+
const filePath = flagIndex >= 0 ? rest[flagIndex + 1] : void 0;
|
|
26
|
+
return { kind: "restore", filePath };
|
|
27
|
+
}
|
|
28
|
+
if (first === "config" && rest[0] === "backup") return { kind: "config-backup" };
|
|
29
|
+
return { kind: "help" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/cli/configBackupFlow.ts
|
|
33
|
+
import * as p2 from "@clack/prompts";
|
|
34
|
+
|
|
35
|
+
// src/cli/cancel.ts
|
|
36
|
+
import * as p from "@clack/prompts";
|
|
37
|
+
var CliCancelled = class extends Error {
|
|
38
|
+
};
|
|
39
|
+
function unwrap(value) {
|
|
40
|
+
if (p.isCancel(value)) {
|
|
41
|
+
throw new CliCancelled();
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
async function withCancelHandling(fn) {
|
|
46
|
+
try {
|
|
47
|
+
await fn();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err instanceof CliCancelled) {
|
|
50
|
+
p.cancel("Cancelled.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/cli/configBackupFlow.ts
|
|
58
|
+
var PREFERENCE_LABELS = {
|
|
59
|
+
alwaysAsk: "Always ask",
|
|
60
|
+
alwaysOn: "Always back up",
|
|
61
|
+
alwaysOff: "Never back up"
|
|
62
|
+
};
|
|
63
|
+
async function configBackupFlow(options) {
|
|
64
|
+
const settingsPath = options.settingsPath ?? defaultSettingsPath();
|
|
65
|
+
p2.intro("mcp-config-migrator config backup");
|
|
66
|
+
const store = await readVersionsStore(settingsPath);
|
|
67
|
+
p2.note(
|
|
68
|
+
[`Backup preference: ${PREFERENCE_LABELS[store.configured]}`, `Storage location: ${store.versionsPath}`].join("\n"),
|
|
69
|
+
"Current settings"
|
|
70
|
+
);
|
|
71
|
+
const preference = unwrap(
|
|
72
|
+
await p2.select({
|
|
73
|
+
message: "Backup preference:",
|
|
74
|
+
options: Object.keys(PREFERENCE_LABELS).map((value) => ({
|
|
75
|
+
value,
|
|
76
|
+
label: PREFERENCE_LABELS[value]
|
|
77
|
+
})),
|
|
78
|
+
initialValue: store.configured
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
if (preference !== store.configured) {
|
|
82
|
+
await setPreference(settingsPath, preference);
|
|
83
|
+
}
|
|
84
|
+
const location = unwrap(
|
|
85
|
+
await p2.text({
|
|
86
|
+
message: "Storage location for the version history:",
|
|
87
|
+
initialValue: store.versionsPath
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
const nextLocation = location === settingsPath ? void 0 : location;
|
|
91
|
+
if (nextLocation !== store.backupLocation) {
|
|
92
|
+
await setBackupLocation(settingsPath, nextLocation);
|
|
93
|
+
}
|
|
94
|
+
p2.outro("Backup settings updated.");
|
|
95
|
+
}
|
|
96
|
+
async function runConfigBackup(options = {}) {
|
|
97
|
+
await withCancelHandling(() => configBackupFlow(options));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/cli/flow.ts
|
|
101
|
+
import * as p5 from "@clack/prompts";
|
|
102
|
+
|
|
103
|
+
// src/cli/backupFlow.ts
|
|
104
|
+
import * as p3 from "@clack/prompts";
|
|
105
|
+
async function maybeBackup(settingsPath, target) {
|
|
106
|
+
const store = await readVersionsStore(settingsPath);
|
|
107
|
+
if (store.configured === "alwaysOff") return;
|
|
108
|
+
if (store.configured === "alwaysOn") {
|
|
109
|
+
await backupNow(settingsPath, store.versionsPath, target);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const choice = unwrap(
|
|
113
|
+
await p3.select({
|
|
114
|
+
message: `Back up the current MCP servers in ${target.path} before writing?`,
|
|
115
|
+
options: [
|
|
116
|
+
{ value: "yes", label: "Yes" },
|
|
117
|
+
{ value: "yes-always", label: "Yes, always" },
|
|
118
|
+
{ value: "no", label: "No" },
|
|
119
|
+
{ value: "no-never", label: "No, never" }
|
|
120
|
+
],
|
|
121
|
+
initialValue: "yes-always"
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
if (choice === "yes" || choice === "yes-always") {
|
|
125
|
+
const location = unwrap(
|
|
126
|
+
await p3.text({
|
|
127
|
+
message: "Backup storage location:",
|
|
128
|
+
initialValue: store.versionsPath
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
if (location !== store.versionsPath) {
|
|
132
|
+
await setBackupLocation(settingsPath, location === settingsPath ? void 0 : location);
|
|
133
|
+
}
|
|
134
|
+
await backupNow(settingsPath, location, target);
|
|
135
|
+
}
|
|
136
|
+
if (choice === "yes-always") {
|
|
137
|
+
await setPreference(settingsPath, "alwaysOn");
|
|
138
|
+
} else if (choice === "no-never") {
|
|
139
|
+
await setPreference(settingsPath, "alwaysOff");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function backupNow(settingsPath, versionsPath, target) {
|
|
143
|
+
await appendVersion(settingsPath, {
|
|
144
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
145
|
+
ideId: target.ideId,
|
|
146
|
+
scopeId: target.scopeId,
|
|
147
|
+
path: target.path,
|
|
148
|
+
servers: target.config.servers
|
|
149
|
+
});
|
|
150
|
+
p3.log.success(`Backed up current MCP servers for ${target.path} to ${versionsPath}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/cli/mergeFlow.ts
|
|
154
|
+
import * as p4 from "@clack/prompts";
|
|
155
|
+
|
|
156
|
+
// src/cli/editor.ts
|
|
157
|
+
import { spawnSync } from "child_process";
|
|
158
|
+
import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
159
|
+
import { tmpdir } from "os";
|
|
160
|
+
import { join } from "path";
|
|
161
|
+
function resolveEditorCommand(env, platform) {
|
|
162
|
+
return env.VISUAL || env.EDITOR || (platform === "win32" ? "notepad" : "vi");
|
|
163
|
+
}
|
|
164
|
+
async function editText(text5, env, platform) {
|
|
165
|
+
const dir = await mkdtemp(join(tmpdir(), "mcp-config-migrator-merge-"));
|
|
166
|
+
const filePath = join(dir, "entry.json");
|
|
167
|
+
try {
|
|
168
|
+
await writeFile(filePath, text5, "utf8");
|
|
169
|
+
const command = resolveEditorCommand(env, platform);
|
|
170
|
+
const result = spawnSync(`${command} "${filePath}"`, { shell: true, stdio: "inherit" });
|
|
171
|
+
if (result.error) {
|
|
172
|
+
throw new Error(`Failed to launch editor "${command}": ${result.error.message}`);
|
|
173
|
+
}
|
|
174
|
+
if (result.status !== 0) {
|
|
175
|
+
throw new Error(`Editor "${command}" exited with status ${result.status}`);
|
|
176
|
+
}
|
|
177
|
+
return await readFile(filePath, "utf8");
|
|
178
|
+
} finally {
|
|
179
|
+
await rm(dir, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/engine/mergeParse.ts
|
|
184
|
+
var TRANSPORTS = ["stdio", "http", "sse"];
|
|
185
|
+
var STDIO_KEYS = ["transport", "command", "args", "cwd", "env"];
|
|
186
|
+
var REMOTE_KEYS = ["transport", "url", "headers"];
|
|
187
|
+
var MARKER_PATTERN = /^(<{7}|={7}|>{7})/m;
|
|
188
|
+
var MergeValidationError = class extends Error {
|
|
189
|
+
};
|
|
190
|
+
function hasUnresolvedMarkers(text5) {
|
|
191
|
+
return MARKER_PATTERN.test(text5);
|
|
192
|
+
}
|
|
193
|
+
function isStringRecord(value) {
|
|
194
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
195
|
+
}
|
|
196
|
+
function parseMergedServer(name, text5) {
|
|
197
|
+
if (hasUnresolvedMarkers(text5)) {
|
|
198
|
+
throw new MergeValidationError(
|
|
199
|
+
"Unresolved conflict markers remain. Remove every <<<<<<<, =======, and >>>>>>> line before saving."
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
let parsed;
|
|
203
|
+
try {
|
|
204
|
+
parsed = JSON.parse(text5);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
throw new MergeValidationError(`Invalid JSON: ${err.message}`);
|
|
207
|
+
}
|
|
208
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
209
|
+
throw new MergeValidationError("The merged entry must be a JSON object.");
|
|
210
|
+
}
|
|
211
|
+
const raw = parsed;
|
|
212
|
+
if (!TRANSPORTS.includes(raw.transport)) {
|
|
213
|
+
throw new MergeValidationError(`"transport" must be one of: ${TRANSPORTS.join(", ")}`);
|
|
214
|
+
}
|
|
215
|
+
const transport = raw.transport;
|
|
216
|
+
const allowedKeys = transport === "stdio" ? STDIO_KEYS : REMOTE_KEYS;
|
|
217
|
+
const unknownKeys = Object.keys(raw).filter((key) => !allowedKeys.includes(key));
|
|
218
|
+
if (unknownKeys.length > 0) {
|
|
219
|
+
throw new MergeValidationError(`Unrecognized field(s): ${unknownKeys.join(", ")}`);
|
|
220
|
+
}
|
|
221
|
+
const server = { name, transport };
|
|
222
|
+
if (transport === "stdio") {
|
|
223
|
+
if (raw.command !== void 0) {
|
|
224
|
+
if (typeof raw.command !== "string") throw new MergeValidationError(`"command" must be a string`);
|
|
225
|
+
server.command = raw.command;
|
|
226
|
+
}
|
|
227
|
+
if (raw.args !== void 0) {
|
|
228
|
+
if (!Array.isArray(raw.args) || !raw.args.every((a) => typeof a === "string")) {
|
|
229
|
+
throw new MergeValidationError(`"args" must be an array of strings`);
|
|
230
|
+
}
|
|
231
|
+
server.args = raw.args;
|
|
232
|
+
}
|
|
233
|
+
if (raw.cwd !== void 0) {
|
|
234
|
+
if (typeof raw.cwd !== "string") throw new MergeValidationError(`"cwd" must be a string`);
|
|
235
|
+
server.cwd = raw.cwd;
|
|
236
|
+
}
|
|
237
|
+
if (raw.env !== void 0) {
|
|
238
|
+
if (!isStringRecord(raw.env)) throw new MergeValidationError(`"env" must be an object of string values`);
|
|
239
|
+
server.env = raw.env;
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
if (raw.url !== void 0) {
|
|
243
|
+
if (typeof raw.url !== "string") throw new MergeValidationError(`"url" must be a string`);
|
|
244
|
+
server.url = raw.url;
|
|
245
|
+
}
|
|
246
|
+
if (raw.headers !== void 0) {
|
|
247
|
+
if (!isStringRecord(raw.headers)) {
|
|
248
|
+
throw new MergeValidationError(`"headers" must be an object of string values`);
|
|
249
|
+
}
|
|
250
|
+
server.headers = raw.headers;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return server;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/cli/mergeFlow.ts
|
|
257
|
+
async function resolveMergeConflict(entry, env, platform) {
|
|
258
|
+
const original = renderMergeScaffold(entry);
|
|
259
|
+
let current = original;
|
|
260
|
+
for (; ; ) {
|
|
261
|
+
const edited = await editText(current, env, platform);
|
|
262
|
+
try {
|
|
263
|
+
return parseMergedServer(entry.name, edited);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (!(err instanceof MergeValidationError)) throw err;
|
|
266
|
+
current = edited;
|
|
267
|
+
p4.log.error(err.message);
|
|
268
|
+
const choice = await p4.select({
|
|
269
|
+
message: "Resolve the error:",
|
|
270
|
+
options: [
|
|
271
|
+
{ value: "fix", label: "Fix \u2014 reopen the editor with your edits kept" },
|
|
272
|
+
{ value: "redo", label: "Redo \u2014 reopen the editor reset to the original conflict markers" }
|
|
273
|
+
]
|
|
274
|
+
});
|
|
275
|
+
if (unwrap(choice) === "redo") {
|
|
276
|
+
current = original;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/cli/flow.ts
|
|
283
|
+
async function selectIde(message) {
|
|
284
|
+
const choice = await p5.select({
|
|
285
|
+
message,
|
|
286
|
+
options: adapters.map((adapter) => ({ value: adapter.id, label: adapter.label }))
|
|
287
|
+
});
|
|
288
|
+
return unwrap(choice);
|
|
289
|
+
}
|
|
290
|
+
async function selectScopeAndPath(ideLabel, candidates) {
|
|
291
|
+
let candidate = candidates[0];
|
|
292
|
+
if (candidates.length > 1) {
|
|
293
|
+
const scopeId = await p5.select({
|
|
294
|
+
message: `Which ${ideLabel} config scope?`,
|
|
295
|
+
options: candidates.map((c) => ({ value: c.scopeId, label: c.label, hint: c.path }))
|
|
296
|
+
});
|
|
297
|
+
candidate = candidates.find((c) => c.scopeId === unwrap(scopeId));
|
|
298
|
+
}
|
|
299
|
+
const path = await p5.text({
|
|
300
|
+
message: `Confirm the ${ideLabel} config path (${candidate.label}):`,
|
|
301
|
+
initialValue: candidate.path,
|
|
302
|
+
validate: (value) => !value || value.trim() === "" ? "A path is required" : void 0
|
|
303
|
+
});
|
|
304
|
+
return { scopeId: candidate.scopeId, path: unwrap(path) };
|
|
305
|
+
}
|
|
306
|
+
async function resolveConflicts(conflicts, env, platform) {
|
|
307
|
+
const resolutions = {};
|
|
308
|
+
for (const entry of conflicts) {
|
|
309
|
+
p5.note(renderConflictDiff(entry), `Conflict: ${entry.name}`);
|
|
310
|
+
const choice = await p5.select({
|
|
311
|
+
message: `Resolve "${entry.name}":`,
|
|
312
|
+
options: [
|
|
313
|
+
{ value: "accept-target", label: "Accept target's definition" },
|
|
314
|
+
{ value: "accept-source", label: "Accept source's definition" },
|
|
315
|
+
{ value: "merge", label: "Merge\u2026" }
|
|
316
|
+
]
|
|
317
|
+
});
|
|
318
|
+
const resolved = unwrap(choice);
|
|
319
|
+
if (resolved === "merge") {
|
|
320
|
+
const merged = await resolveMergeConflict(entry, env, platform);
|
|
321
|
+
resolutions[entry.name] = { kind: "merge", merged };
|
|
322
|
+
} else {
|
|
323
|
+
resolutions[entry.name] = { kind: resolved };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return resolutions;
|
|
327
|
+
}
|
|
328
|
+
function changedServerNames(classifications, resolutions) {
|
|
329
|
+
return classifications.filter(
|
|
330
|
+
(entry) => entry.kind === "add" || entry.kind === "conflict" && (resolutions[entry.name]?.kind === "accept-source" || resolutions[entry.name]?.kind === "merge")
|
|
331
|
+
).map((entry) => entry.name);
|
|
332
|
+
}
|
|
333
|
+
async function runFlow(options) {
|
|
334
|
+
const cwd = options.cwd ?? process.cwd();
|
|
335
|
+
const env = options.env ?? process.env;
|
|
336
|
+
const platform = options.platform ?? process.platform;
|
|
337
|
+
const settingsPath = options.settingsPath ?? defaultSettingsPath();
|
|
338
|
+
p5.intro("mcp-config-migrator");
|
|
339
|
+
const sourceIdeId = await selectIde("Migrate MCP servers FROM which IDE?");
|
|
340
|
+
const sourceAdapter = getAdapter(sourceIdeId);
|
|
341
|
+
const source = await selectScopeAndPath(
|
|
342
|
+
sourceAdapter.label,
|
|
343
|
+
sourceAdapter.resolveDefaultPaths(env, platform, cwd)
|
|
344
|
+
);
|
|
345
|
+
const targetIdeId = await selectIde("Migrate MCP servers TO which IDE?");
|
|
346
|
+
const targetAdapter = getAdapter(targetIdeId);
|
|
347
|
+
const target = await selectScopeAndPath(
|
|
348
|
+
targetAdapter.label,
|
|
349
|
+
targetAdapter.resolveDefaultPaths(env, platform, cwd)
|
|
350
|
+
);
|
|
351
|
+
const sourceConfig = await sourceAdapter.load(source.path);
|
|
352
|
+
const targetConfig = await targetAdapter.load(target.path);
|
|
353
|
+
const classifications = classify(sourceConfig, targetConfig);
|
|
354
|
+
if (isNoOp(classifications)) {
|
|
355
|
+
p5.outro("Nothing to migrate \u2014 target already has every source entry.");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const conflicts = classifications.filter((entry) => entry.kind === "conflict");
|
|
359
|
+
const resolutions = await resolveConflicts(conflicts, env, platform);
|
|
360
|
+
const summary = summarize(classifications, resolutions);
|
|
361
|
+
const formatCategory = (label, category) => category.count === 0 ? `${label} (0)` : `${label} (${category.count}): ${category.names.join(", ")}`;
|
|
362
|
+
p5.note(
|
|
363
|
+
[
|
|
364
|
+
formatCategory("Added", summary.added),
|
|
365
|
+
formatCategory("Unchanged", summary.unchanged),
|
|
366
|
+
`Conflicts resolved (${summary.conflicts.total}):`,
|
|
367
|
+
` ${formatCategory("accept target", summary.conflicts.acceptTarget)}`,
|
|
368
|
+
` ${formatCategory("accept source", summary.conflicts.acceptSource)}`,
|
|
369
|
+
` ${formatCategory("merged", summary.conflicts.merged)}`
|
|
370
|
+
].join("\n"),
|
|
371
|
+
"Migration summary"
|
|
372
|
+
);
|
|
373
|
+
const confirmed = unwrap(await p5.confirm({ message: `Write merged config to ${target.path}?` }));
|
|
374
|
+
if (!confirmed) {
|
|
375
|
+
p5.outro("No changes were made.");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const merged = applyMerge(targetConfig, classifications, resolutions);
|
|
379
|
+
if (targetAdapter.id === "claude-code" && target.scopeId === "project") {
|
|
380
|
+
const names = changedServerNames(classifications, resolutions);
|
|
381
|
+
if (names.length > 0) {
|
|
382
|
+
p5.note(
|
|
383
|
+
[
|
|
384
|
+
`Claude Code will ask you to re-approve: ${names.join(", ")}`,
|
|
385
|
+
"If you'd rather not be prompted again, run:",
|
|
386
|
+
" claude mcp reset-project-choices"
|
|
387
|
+
].join("\n"),
|
|
388
|
+
"Heads up"
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
await maybeBackup(settingsPath, {
|
|
393
|
+
ideId: targetAdapter.id,
|
|
394
|
+
scopeId: target.scopeId,
|
|
395
|
+
path: target.path,
|
|
396
|
+
config: targetConfig
|
|
397
|
+
});
|
|
398
|
+
const saveResult = await targetAdapter.save(target.path, merged);
|
|
399
|
+
for (const dropped of saveResult.droppedFields) {
|
|
400
|
+
p5.log.warn(`Dropped fields for "${dropped.serverName}" not supported by ${targetAdapter.label}: ${dropped.fields.join(", ")}`);
|
|
401
|
+
}
|
|
402
|
+
p5.log.success(`Wrote merged config to ${target.path}`);
|
|
403
|
+
await runCleanup(targetAdapter, target.path, merged);
|
|
404
|
+
p5.outro("Done.");
|
|
405
|
+
}
|
|
406
|
+
async function runCleanup(targetAdapter, targetPath, merged) {
|
|
407
|
+
if (merged.servers.length === 0) return;
|
|
408
|
+
const result = await p5.multiselect({
|
|
409
|
+
message: "Remove any MCP servers from the target before finishing? (none required)",
|
|
410
|
+
options: merged.servers.map((server) => ({ value: server.name, label: server.name })),
|
|
411
|
+
required: false
|
|
412
|
+
});
|
|
413
|
+
if (p5.isCancel(result)) {
|
|
414
|
+
p5.log.info("Skipped cleanup.");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const toRemove = result;
|
|
418
|
+
if (toRemove.length === 0) return;
|
|
419
|
+
const remaining = {
|
|
420
|
+
servers: merged.servers.filter((server) => !toRemove.includes(server.name))
|
|
421
|
+
};
|
|
422
|
+
await targetAdapter.save(targetPath, remaining);
|
|
423
|
+
p5.log.success(`Removed: ${toRemove.join(", ")}`);
|
|
424
|
+
}
|
|
425
|
+
async function runCli(options = {}) {
|
|
426
|
+
await withCancelHandling(() => runFlow(options));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/cli/restoreFlow.ts
|
|
430
|
+
import * as p6 from "@clack/prompts";
|
|
431
|
+
function formatVersionLabel(version) {
|
|
432
|
+
const adapter = getAdapter(version.ideId);
|
|
433
|
+
return `${version.timestamp} \u2014 ${adapter.label} (${version.scopeId}) \u2014 ${version.path}`;
|
|
434
|
+
}
|
|
435
|
+
async function restoreFlow(options) {
|
|
436
|
+
p6.intro("mcp-config-migrator restore");
|
|
437
|
+
let settingsPath = options.filePath;
|
|
438
|
+
if (!settingsPath) {
|
|
439
|
+
settingsPath = unwrap(
|
|
440
|
+
await p6.text({
|
|
441
|
+
message: "Path to the versions file to restore from:",
|
|
442
|
+
initialValue: defaultSettingsPath()
|
|
443
|
+
})
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const store = await readVersionsStore(settingsPath);
|
|
447
|
+
if (store.versions.length === 0) {
|
|
448
|
+
p6.outro(`No backed-up versions found in ${store.versionsPath}.`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const sorted = [...store.versions].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
452
|
+
const selectedIndex = unwrap(
|
|
453
|
+
await p6.select({
|
|
454
|
+
message: "Select a version to restore:",
|
|
455
|
+
options: sorted.map((version2, index) => ({ value: index, label: formatVersionLabel(version2) }))
|
|
456
|
+
})
|
|
457
|
+
);
|
|
458
|
+
const version = sorted[selectedIndex];
|
|
459
|
+
p6.note(JSON.stringify(version.servers, null, 2), `Preview: ${formatVersionLabel(version)}`);
|
|
460
|
+
const confirmed = unwrap(await p6.confirm({ message: `Overwrite ${version.path} with this version?` }));
|
|
461
|
+
if (!confirmed) {
|
|
462
|
+
p6.outro("Restore cancelled. No changes were made.");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const adapter = getAdapter(version.ideId);
|
|
466
|
+
await adapter.save(version.path, { servers: version.servers });
|
|
467
|
+
p6.log.success(`Restored ${version.path} from ${version.timestamp}.`);
|
|
468
|
+
p6.outro("Done.");
|
|
469
|
+
}
|
|
470
|
+
async function runRestore(options = {}) {
|
|
471
|
+
await withCancelHandling(() => restoreFlow(options));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/cli/index.ts
|
|
475
|
+
function printHelp() {
|
|
476
|
+
console.log(
|
|
477
|
+
[
|
|
478
|
+
"mcp-config-migrator",
|
|
479
|
+
"",
|
|
480
|
+
"Usage:",
|
|
481
|
+
" mcp-config-migrator Interactively migrate MCP servers between IDEs",
|
|
482
|
+
" mcp-config-migrator restore [--file|-f <path>]",
|
|
483
|
+
" Restore a previously backed-up version",
|
|
484
|
+
" mcp-config-migrator config backup View or change the backup preference and storage location",
|
|
485
|
+
" mcp-config-migrator --help, -h, /? Show this help"
|
|
486
|
+
].join("\n")
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const command = parseArgs(process.argv.slice(2));
|
|
491
|
+
switch (command.kind) {
|
|
492
|
+
case "migrate":
|
|
493
|
+
await runCli();
|
|
494
|
+
break;
|
|
495
|
+
case "restore":
|
|
496
|
+
await runRestore({ filePath: command.filePath });
|
|
497
|
+
break;
|
|
498
|
+
case "config-backup":
|
|
499
|
+
await runConfigBackup();
|
|
500
|
+
break;
|
|
501
|
+
case "help":
|
|
502
|
+
printHelp();
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
console.error(err instanceof Error ? err.message : err);
|
|
507
|
+
process.exitCode = 1;
|
|
508
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
type Transport = "stdio" | "http" | "sse";
|
|
2
|
+
/**
|
|
3
|
+
* Fields outside the common shape, tagged with the IDE that produced them.
|
|
4
|
+
* Only re-emitted when serializing back through the same IDE's adapter;
|
|
5
|
+
* dropped (with a warning) when migrating cross-IDE.
|
|
6
|
+
*/
|
|
7
|
+
interface ExtraFields {
|
|
8
|
+
sourceIdeId: string;
|
|
9
|
+
fields: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
interface NormalizedMcpServer {
|
|
12
|
+
name: string;
|
|
13
|
+
transport: Transport;
|
|
14
|
+
command?: string;
|
|
15
|
+
args?: string[];
|
|
16
|
+
cwd?: string;
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
url?: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
extra?: ExtraFields;
|
|
21
|
+
}
|
|
22
|
+
interface NormalizedConfig {
|
|
23
|
+
servers: NormalizedMcpServer[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compares the common normalized shape only — the adapter-specific `extra`
|
|
28
|
+
* bag is excluded so cross-IDE entries that match on every shared field are
|
|
29
|
+
* still classified as Unchanged.
|
|
30
|
+
*/
|
|
31
|
+
declare function areServersEqual(a: NormalizedMcpServer, b: NormalizedMcpServer): boolean;
|
|
32
|
+
|
|
33
|
+
type BackupPreference = "alwaysAsk" | "alwaysOn" | "alwaysOff";
|
|
34
|
+
interface BackupVersion {
|
|
35
|
+
timestamp: string;
|
|
36
|
+
ideId: string;
|
|
37
|
+
scopeId: string;
|
|
38
|
+
path: string;
|
|
39
|
+
servers: NormalizedMcpServer[];
|
|
40
|
+
}
|
|
41
|
+
interface VersionsStore {
|
|
42
|
+
settingsPath: string;
|
|
43
|
+
configured: BackupPreference;
|
|
44
|
+
backupLocation?: string;
|
|
45
|
+
/** The file `versions` is actually read from/written to: `backupLocation` if set, otherwise `settingsPath`. */
|
|
46
|
+
versionsPath: string;
|
|
47
|
+
versions: BackupVersion[];
|
|
48
|
+
}
|
|
49
|
+
declare function defaultSettingsPath(): string;
|
|
50
|
+
declare function readVersionsStore(settingsPath: string): Promise<VersionsStore>;
|
|
51
|
+
declare function setPreference(settingsPath: string, configured: BackupPreference): Promise<void>;
|
|
52
|
+
declare function setBackupLocation(settingsPath: string, backupLocation: string | undefined): Promise<void>;
|
|
53
|
+
/** Appends `entry` to the resolved store's version history without altering or removing any existing entry. */
|
|
54
|
+
declare function appendVersion(settingsPath: string, entry: BackupVersion): Promise<void>;
|
|
55
|
+
|
|
56
|
+
interface DefaultPathCandidate {
|
|
57
|
+
/** Stable id for the scope this path belongs to, e.g. "user", "project". */
|
|
58
|
+
scopeId: string;
|
|
59
|
+
/** Human-readable label shown in CLI prompts, e.g. "User", "Project (.mcp.json)". */
|
|
60
|
+
label: string;
|
|
61
|
+
path: string;
|
|
62
|
+
}
|
|
63
|
+
interface DroppedExtraFields {
|
|
64
|
+
serverName: string;
|
|
65
|
+
fields: string[];
|
|
66
|
+
}
|
|
67
|
+
interface SaveResult {
|
|
68
|
+
/** Adapter-specific fields that couldn't be re-emitted because they came from a different IDE. */
|
|
69
|
+
droppedFields: DroppedExtraFields[];
|
|
70
|
+
}
|
|
71
|
+
interface IdeAdapter {
|
|
72
|
+
id: string;
|
|
73
|
+
label: string;
|
|
74
|
+
resolveDefaultPaths(env: NodeJS.ProcessEnv, platform: NodeJS.Platform, cwd: string): DefaultPathCandidate[];
|
|
75
|
+
load(path: string): Promise<NormalizedConfig>;
|
|
76
|
+
save(path: string, normalized: NormalizedConfig): Promise<SaveResult>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
declare const adapters: readonly IdeAdapter[];
|
|
80
|
+
declare function getAdapter(id: string): IdeAdapter;
|
|
81
|
+
|
|
82
|
+
type ClassificationKind = "add" | "unchanged" | "conflict";
|
|
83
|
+
interface ClassifiedEntry {
|
|
84
|
+
name: string;
|
|
85
|
+
kind: ClassificationKind;
|
|
86
|
+
source: NormalizedMcpServer;
|
|
87
|
+
target?: NormalizedMcpServer;
|
|
88
|
+
}
|
|
89
|
+
/** Classifies each source entry relative to the target, by server name. */
|
|
90
|
+
declare function classify(source: NormalizedConfig, target: NormalizedConfig): ClassifiedEntry[];
|
|
91
|
+
|
|
92
|
+
/** Renders a +/- line diff between a conflicting entry's target and source definitions. */
|
|
93
|
+
declare function renderConflictDiff(entry: ClassifiedEntry): string;
|
|
94
|
+
/**
|
|
95
|
+
* Renders an editable merge scaffold for a conflicting entry: lines
|
|
96
|
+
* identical between target and source pass through unmarked, differing
|
|
97
|
+
* lines are wrapped in git-style conflict markers for the user to resolve
|
|
98
|
+
* by hand.
|
|
99
|
+
*/
|
|
100
|
+
declare function renderMergeScaffold(entry: ClassifiedEntry): string;
|
|
101
|
+
|
|
102
|
+
type ConflictResolution = {
|
|
103
|
+
kind: "accept-target";
|
|
104
|
+
} | {
|
|
105
|
+
kind: "accept-source";
|
|
106
|
+
} | {
|
|
107
|
+
kind: "merge";
|
|
108
|
+
merged: NormalizedMcpServer;
|
|
109
|
+
};
|
|
110
|
+
type ConflictResolutions = Record<string, ConflictResolution>;
|
|
111
|
+
/**
|
|
112
|
+
* Produces the merged config: every Add entry from source, every Conflict
|
|
113
|
+
* entry per its resolution, every Unchanged entry as-is, and every
|
|
114
|
+
* target-only entry left untouched.
|
|
115
|
+
*/
|
|
116
|
+
declare function applyMerge(target: NormalizedConfig, classifications: ClassifiedEntry[], resolutions: ConflictResolutions): NormalizedConfig;
|
|
117
|
+
|
|
118
|
+
interface CategorySummary {
|
|
119
|
+
count: number;
|
|
120
|
+
names: string[];
|
|
121
|
+
}
|
|
122
|
+
interface MigrationSummary {
|
|
123
|
+
added: CategorySummary;
|
|
124
|
+
unchanged: CategorySummary;
|
|
125
|
+
conflicts: {
|
|
126
|
+
total: number;
|
|
127
|
+
acceptTarget: CategorySummary;
|
|
128
|
+
acceptSource: CategorySummary;
|
|
129
|
+
merged: CategorySummary;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
declare function summarize(classifications: ClassifiedEntry[], resolutions: ConflictResolutions): MigrationSummary;
|
|
133
|
+
/** True when a migration has nothing to add and nothing to resolve. */
|
|
134
|
+
declare function isNoOp(classifications: ClassifiedEntry[]): boolean;
|
|
135
|
+
|
|
136
|
+
export { type BackupPreference, type BackupVersion, type ClassificationKind, type ClassifiedEntry, type ConflictResolution, type ConflictResolutions, type DefaultPathCandidate, type DroppedExtraFields, type ExtraFields, type IdeAdapter, type MigrationSummary, type NormalizedConfig, type NormalizedMcpServer, type SaveResult, type Transport, type VersionsStore, adapters, appendVersion, applyMerge, areServersEqual, classify, defaultSettingsPath, getAdapter, isNoOp, readVersionsStore, renderConflictDiff, renderMergeScaffold, setBackupLocation, setPreference, summarize };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
adapters,
|
|
3
|
+
appendVersion,
|
|
4
|
+
applyMerge,
|
|
5
|
+
areServersEqual,
|
|
6
|
+
classify,
|
|
7
|
+
defaultSettingsPath,
|
|
8
|
+
getAdapter,
|
|
9
|
+
isNoOp,
|
|
10
|
+
readVersionsStore,
|
|
11
|
+
renderConflictDiff,
|
|
12
|
+
renderMergeScaffold,
|
|
13
|
+
setBackupLocation,
|
|
14
|
+
setPreference,
|
|
15
|
+
summarize
|
|
16
|
+
} from "./chunk-E2A5N2LX.js";
|
|
17
|
+
export {
|
|
18
|
+
adapters,
|
|
19
|
+
appendVersion,
|
|
20
|
+
applyMerge,
|
|
21
|
+
areServersEqual,
|
|
22
|
+
classify,
|
|
23
|
+
defaultSettingsPath,
|
|
24
|
+
getAdapter,
|
|
25
|
+
isNoOp,
|
|
26
|
+
readVersionsStore,
|
|
27
|
+
renderConflictDiff,
|
|
28
|
+
renderMergeScaffold,
|
|
29
|
+
setBackupLocation,
|
|
30
|
+
setPreference,
|
|
31
|
+
summarize
|
|
32
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-config-migrator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Interactively migrate and merge MCP server configurations between VS Code, Cursor, and Claude Code.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"cli",
|
|
8
|
+
"vscode",
|
|
9
|
+
"cursor",
|
|
10
|
+
"claude-code"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/industrial-curiosity/mcp-config-migrator#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/industrial-curiosity/mcp-config-migrator/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/industrial-curiosity/mcp-config-migrator.git"
|
|
19
|
+
},
|
|
20
|
+
"author": "Industrial Curiosity",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"mcp-config-migrator": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"local": "npm run build && node dist/cli.js",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"lint": "eslint .",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"prepublishOnly": "npm run build"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@clack/prompts": "^1.5.1",
|
|
51
|
+
"diff": "^9.0.0",
|
|
52
|
+
"jsonc-parser": "^3.3.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/diff": "^7.0.2",
|
|
56
|
+
"@types/node": "^22.10.2",
|
|
57
|
+
"eslint": "^10.5.0",
|
|
58
|
+
"tsup": "^8.5.1",
|
|
59
|
+
"typescript": "^5.7.2",
|
|
60
|
+
"typescript-eslint": "^8.18.2",
|
|
61
|
+
"vitest": "^4.1.9"
|
|
62
|
+
}
|
|
63
|
+
}
|