@mergesafe-io/connect 0.2.0 → 0.4.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 +34 -20
- package/lib/actions.js +88 -0
- package/lib/cli.js +82 -54
- package/lib/clients.js +52 -54
- package/lib/config-file.js +34 -0
- package/lib/detect.js +14 -0
- package/lib/paths.js +29 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
# @mergesafe-io/connect
|
|
2
2
|
|
|
3
|
-
One command
|
|
3
|
+
One command connects every AI client on your machine to the
|
|
4
|
+
[MergeSafe](https://app.mergesafe.io) MCP server — globally, so it works in
|
|
5
|
+
every project. No JSON editing.
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
|
-
npx @mergesafe-io/connect
|
|
8
|
+
npx @mergesafe-io/connect --key msk_your_api_key
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
That's it.
|
|
11
|
+
That's it. The CLI detects which clients are installed (Claude Code, Claude
|
|
12
|
+
Desktop, Cursor, Windsurf, VS Code, …), writes each one's **global** config,
|
|
13
|
+
and backs up every file it touches. Restart your clients and the `mergesafe`
|
|
14
|
+
tools are available everywhere.
|
|
15
|
+
|
|
16
|
+
To uninstall from every detected client:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @mergesafe-io/connect --uninstall
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That removes only the mergesafe entry — all other configs are preserved.
|
|
10
23
|
|
|
11
24
|
## What it does
|
|
12
25
|
|
|
13
|
-
-
|
|
26
|
+
- Detects the AI clients installed on your machine (or targets just one if
|
|
27
|
+
you name it: `npx @mergesafe-io/connect cursor --key msk_…`)
|
|
28
|
+
- Finds the right **global** MCP config file for each client and OS
|
|
14
29
|
- Backs up the existing file next to it (`*.bak-<timestamp>`)
|
|
15
30
|
- Merges in the `mergesafe` server entry — every other setting and server is preserved
|
|
16
31
|
- Refuses to touch a file it can't parse (broken JSON aborts with a clear message)
|
|
@@ -19,43 +34,42 @@ No dependencies, no telemetry, Node 18+.
|
|
|
19
34
|
|
|
20
35
|
## Supported clients
|
|
21
36
|
|
|
22
|
-
| Client |
|
|
37
|
+
| Client | Global config written |
|
|
23
38
|
|---|---|
|
|
24
|
-
| `claude-
|
|
25
|
-
| `
|
|
26
|
-
| `cursor
|
|
39
|
+
| `claude-code` | `~/.claude.json` (user scope — every project and session) |
|
|
40
|
+
| `claude-desktop` | `claude_desktop_config.json` (per-OS path) — bridges through `mcp-remote` |
|
|
41
|
+
| `cursor` | `~/.cursor/mcp.json` (`--project` for `./.cursor/mcp.json`) |
|
|
27
42
|
| `windsurf` | `~/.codeium/windsurf/mcp_config.json` |
|
|
28
|
-
| `vscode` | `./.vscode/mcp.json`
|
|
29
|
-
| `opencode` |
|
|
43
|
+
| `vscode` | user-profile `mcp.json` (`--project` for `./.vscode/mcp.json`) |
|
|
44
|
+
| `opencode` | `~/.config/opencode/config.json` |
|
|
30
45
|
| `cline` | `cline_mcp_settings.json` (stock VS Code global storage, per-OS path) |
|
|
31
|
-
| `roo-code` | `./.roo/mcp.json`
|
|
32
|
-
| `continue` |
|
|
46
|
+
| `roo-code` | `mcp_settings.json` in VS Code global storage (`--project` for `./.roo/mcp.json`) |
|
|
47
|
+
| `continue` | `~/.continue/mcpServers/mergesafe.yaml` (`--project` for the project copy) |
|
|
33
48
|
|
|
34
49
|
Not covered: Zed (its `settings.json` is JSONC with comments — a rewrite would
|
|
35
|
-
destroy them; configure manually).
|
|
36
|
-
|
|
37
|
-
`claude mcp add --transport http mergesafe https://mcp.mergesafe.io/mcp --header "Authorization: Bearer msk_…"`
|
|
50
|
+
destroy them; configure manually). Gemini CLI: use its built-in command:
|
|
51
|
+
`gemini mcp add --transport http mergesafe https://mcp.mergesafe.io/mcp -H "Authorization: Bearer msk_…"`
|
|
38
52
|
|
|
39
53
|
## Options
|
|
40
54
|
|
|
41
55
|
| Flag | Effect |
|
|
42
56
|
|---|---|
|
|
43
57
|
| `--key <msk_…>` | Your MergeSafe API key (from app.mergesafe.io → Settings). Prompted if omitted. |
|
|
44
|
-
| `--project` | Write the project-local config instead of the global one (cursor) |
|
|
45
|
-
| `--dry-run` | Print the target
|
|
58
|
+
| `--project` | Write the project-local config instead of the global one (cursor, vscode, roo-code, continue — needs an explicit client) |
|
|
59
|
+
| `--dry-run` | Print the target paths and resulting files without writing |
|
|
60
|
+
| `--uninstall` | Remove the `mergesafe` entry (no client = every detected client) |
|
|
46
61
|
| `--help` | Usage |
|
|
47
62
|
|
|
48
63
|
## Development
|
|
49
64
|
|
|
50
65
|
```bash
|
|
51
66
|
npm test # node --test, zero deps
|
|
52
|
-
node bin/mergesafe-connect.js
|
|
67
|
+
node bin/mergesafe-connect.js --key msk_x --dry-run # auto-detect
|
|
68
|
+
node bin/mergesafe-connect.js cursor --key msk_x --dry-run # single client
|
|
53
69
|
```
|
|
54
70
|
|
|
55
71
|
## Publishing
|
|
56
72
|
|
|
57
|
-
The `@mergesafe` npm scope must exist (create the org on npmjs.com), then:
|
|
58
|
-
|
|
59
73
|
```bash
|
|
60
74
|
npm publish --access public
|
|
61
75
|
```
|
package/lib/actions.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Install/uninstall actions for one client: back up, merge, write. */
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { SERVER_NAME } from "./clients.js";
|
|
6
|
+
import { applyEntry, removeEntry } from "./config-file.js";
|
|
7
|
+
|
|
8
|
+
export async function handleInstall(client, key, configPath, dryRun) {
|
|
9
|
+
const existingText = await readIfExists(configPath);
|
|
10
|
+
|
|
11
|
+
const newText = client.buildFileContent
|
|
12
|
+
? // Dedicated-file clients (Continue): whole-file write
|
|
13
|
+
client.buildFileContent(key)
|
|
14
|
+
: // Merge via applyEntry — only the mergesafe entry is touched
|
|
15
|
+
applyEntry(existingText ?? "", client.rootKey, SERVER_NAME, client.buildEntry(key));
|
|
16
|
+
|
|
17
|
+
if (dryRun) {
|
|
18
|
+
console.log(`Would write ${configPath}:\n\n${newText}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await backupIfPresent(existingText, configPath);
|
|
23
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
24
|
+
await fs.writeFile(configPath, newText, "utf8");
|
|
25
|
+
|
|
26
|
+
console.log(`✔ ${client.label} is connected to MergeSafe (${configPath})`);
|
|
27
|
+
console.log(`→ ${client.restartHint}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleUninstall(client, configPath, dryRun) {
|
|
31
|
+
const existingText = await readIfExists(configPath);
|
|
32
|
+
|
|
33
|
+
if (existingText === null) {
|
|
34
|
+
console.log(`⚠ No config found at ${configPath} — nothing to remove.`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (client.deleteFile) {
|
|
39
|
+
// Continue: delete the dedicated YAML file entirely
|
|
40
|
+
if (!dryRun) {
|
|
41
|
+
await fs.unlink(configPath);
|
|
42
|
+
}
|
|
43
|
+
console.log(`🗑 ${dryRun ? "Would remove" : "Removed"} ${configPath}`);
|
|
44
|
+
console.log(`→ ${client.restartHint}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!client.deleteKey) {
|
|
49
|
+
throw new Error(`Client "${client.label}" does not support --uninstall`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const newText = removeEntry(existingText, client.rootKey, SERVER_NAME);
|
|
53
|
+
|
|
54
|
+
if (newText === null) {
|
|
55
|
+
console.log(`⚠ No mergesafe entry found in ${configPath} — nothing to remove.`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (dryRun) {
|
|
60
|
+
console.log(`Would write ${configPath}:\n\n${newText}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await backupIfPresent(existingText, configPath);
|
|
65
|
+
await fs.writeFile(configPath, newText, "utf8");
|
|
66
|
+
console.log(`🗑 Removed mergesafe entry from ${configPath}`);
|
|
67
|
+
console.log(`→ ${client.restartHint}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function backupIfPresent(existingText, configPath) {
|
|
71
|
+
if (existingText === null) return;
|
|
72
|
+
const backupPath = `${configPath}.bak-${timestamp()}`;
|
|
73
|
+
await fs.copyFile(configPath, backupPath);
|
|
74
|
+
console.log(`• Backed up existing config to ${backupPath}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function readIfExists(filePath) {
|
|
78
|
+
try {
|
|
79
|
+
return await fs.readFile(filePath, "utf8");
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err.code === "ENOENT") return null;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function timestamp() {
|
|
87
|
+
return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
88
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -1,35 +1,38 @@
|
|
|
1
|
-
/** CLI flow: parse args,
|
|
2
|
-
import fs from "node:fs/promises";
|
|
1
|
+
/** CLI flow: parse args, detect or resolve clients, delegate to actions. */
|
|
3
2
|
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
3
|
import readline from "node:readline/promises";
|
|
6
4
|
import { parseArgs } from "node:util";
|
|
7
5
|
|
|
6
|
+
import { handleInstall, handleUninstall } from "./actions.js";
|
|
8
7
|
import { SERVER_NAME, getClient, listClientIds } from "./clients.js";
|
|
9
|
-
import {
|
|
8
|
+
import { detectClients } from "./detect.js";
|
|
10
9
|
|
|
11
|
-
const USAGE = `Usage: npx @mergesafe-io/connect
|
|
10
|
+
const USAGE = `Usage: npx @mergesafe-io/connect [client] [options]
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
Connects your AI clients to the MergeSafe MCP server — globally, so it works
|
|
13
|
+
in every project. With no <client> argument, every installed client is
|
|
14
|
+
detected and configured in one go. Existing files are backed up first; only
|
|
15
|
+
the "${SERVER_NAME}" entry is touched.
|
|
15
16
|
|
|
16
|
-
Clients:
|
|
17
|
-
claude-
|
|
18
|
-
|
|
19
|
-
cursor
|
|
17
|
+
Clients (all global unless noted):
|
|
18
|
+
claude-code Claude Code — ~/.claude.json (user scope, every project)
|
|
19
|
+
claude-desktop Claude Desktop — claude_desktop_config.json (via mcp-remote bridge)
|
|
20
|
+
cursor Cursor — ~/.cursor/mcp.json (--project for ./.cursor/mcp.json)
|
|
20
21
|
windsurf Windsurf — ~/.codeium/windsurf/mcp_config.json
|
|
21
|
-
vscode VS Code — .vscode/mcp.json
|
|
22
|
-
opencode opencode — opencode.json
|
|
22
|
+
vscode VS Code — user-profile mcp.json (--project for ./.vscode/mcp.json)
|
|
23
|
+
opencode opencode — ~/.config/opencode/config.json
|
|
23
24
|
cline Cline — cline_mcp_settings.json (stock VS Code global storage)
|
|
24
|
-
roo-code Roo Code —
|
|
25
|
-
continue Continue —
|
|
25
|
+
roo-code Roo Code — VS Code global storage (--project for ./.roo/mcp.json)
|
|
26
|
+
continue Continue — ~/.continue/mcpServers/mergesafe.yaml (--project for ./.continue/…)
|
|
26
27
|
|
|
27
28
|
Options:
|
|
28
29
|
--key <msk_...> MergeSafe API key (create one on app.mergesafe.io/dashboard/settings;
|
|
29
30
|
prompted interactively if omitted)
|
|
30
|
-
--project Write the project-local config instead of the global one
|
|
31
|
+
--project Write the project-local config instead of the global one
|
|
32
|
+
(cursor, vscode, roo-code, continue — needs an explicit client)
|
|
31
33
|
--dry-run Print the target path and resulting file without writing
|
|
32
|
-
--
|
|
34
|
+
--uninstall Remove the mergesafe entry (no client = every detected client)
|
|
35
|
+
--help Show usage
|
|
33
36
|
`;
|
|
34
37
|
|
|
35
38
|
export async function main(argv) {
|
|
@@ -40,20 +43,16 @@ export async function main(argv) {
|
|
|
40
43
|
key: { type: "string" },
|
|
41
44
|
project: { type: "boolean", default: false },
|
|
42
45
|
"dry-run": { type: "boolean", default: false },
|
|
46
|
+
uninstall: { type: "boolean", default: false },
|
|
43
47
|
help: { type: "boolean", default: false },
|
|
44
48
|
},
|
|
45
49
|
});
|
|
46
50
|
|
|
47
|
-
if (values.help
|
|
51
|
+
if (values.help) {
|
|
48
52
|
console.log(USAGE);
|
|
49
|
-
if (!values.help) {
|
|
50
|
-
throw new Error(`Missing <client> argument. Supported: ${listClientIds().join(", ")}`);
|
|
51
|
-
}
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
const client = getClient(positionals[0]);
|
|
56
|
-
const key = await resolveKey(values.key);
|
|
57
56
|
const ctx = {
|
|
58
57
|
platform: process.platform,
|
|
59
58
|
home: os.homedir(),
|
|
@@ -62,29 +61,71 @@ export async function main(argv) {
|
|
|
62
61
|
project: values.project,
|
|
63
62
|
};
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const newText = client.buildFileContent
|
|
70
|
-
? client.buildFileContent(key)
|
|
71
|
-
: applyEntry(existingText ?? "", client.rootKey, SERVER_NAME, client.buildEntry(key));
|
|
72
|
-
|
|
73
|
-
if (values["dry-run"]) {
|
|
74
|
-
console.log(`Would write ${configPath}:\n\n${newText}`);
|
|
64
|
+
// --uninstall can come before the client name: "uninstall cursor" or "cursor --uninstall"
|
|
65
|
+
const filtered = positionals.filter((p) => p !== "--uninstall");
|
|
66
|
+
if (filtered.length === 0) {
|
|
67
|
+
await runAllDetected(values, ctx);
|
|
75
68
|
return;
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
const client = getClient(filtered[0]);
|
|
72
|
+
const configPath = client.configPath(ctx);
|
|
73
|
+
if (values.uninstall) {
|
|
74
|
+
await handleUninstall(client, configPath, values["dry-run"]);
|
|
75
|
+
} else {
|
|
76
|
+
const key = await resolveKey(values.key);
|
|
77
|
+
await handleInstall(client, key, configPath, values["dry-run"]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** No-client mode: detect every installed client and configure them globally. */
|
|
82
|
+
async function runAllDetected(values, ctx) {
|
|
83
|
+
if (values.project) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"--project needs an explicit client (e.g. `npx @mergesafe-io/connect cursor --project`).",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const detected = detectClients(ctx);
|
|
90
|
+
if (detected.length === 0) {
|
|
91
|
+
console.log(USAGE);
|
|
92
|
+
throw new Error(
|
|
93
|
+
`No supported AI client detected on this machine. Specify one explicitly: ${listClientIds().join(", ")}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
console.log(`Detected clients: ${detected.map((id) => getClient(id).label).join(", ")}\n`);
|
|
97
|
+
|
|
98
|
+
const key = values.uninstall ? null : await resolveKey(values.key);
|
|
99
|
+
const failures = [];
|
|
100
|
+
for (const id of detected) {
|
|
101
|
+
const client = getClient(id);
|
|
102
|
+
try {
|
|
103
|
+
if (values.uninstall) {
|
|
104
|
+
await handleUninstall(client, client.configPath(ctx), values["dry-run"]);
|
|
105
|
+
} else {
|
|
106
|
+
await handleInstall(client, key, client.configPath(ctx), values["dry-run"]);
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
failures.push(`${client.label}: ${err.message}`);
|
|
110
|
+
}
|
|
111
|
+
console.log("");
|
|
82
112
|
}
|
|
83
|
-
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
84
|
-
await fs.writeFile(configPath, newText, "utf8");
|
|
85
113
|
|
|
86
|
-
|
|
87
|
-
|
|
114
|
+
reportOutcome(detected.length, failures, values.uninstall);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reportOutcome(total, failures, uninstall) {
|
|
118
|
+
for (const failure of failures) {
|
|
119
|
+
console.error(`⚠ ${failure}`);
|
|
120
|
+
}
|
|
121
|
+
if (failures.length === total) {
|
|
122
|
+
throw new Error("Nothing was configured — every detected client failed (see above).");
|
|
123
|
+
}
|
|
124
|
+
if (!uninstall) {
|
|
125
|
+
console.log(
|
|
126
|
+
"✔ MergeSafe is connected globally — restart your clients and the tools work in every project.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
88
129
|
}
|
|
89
130
|
|
|
90
131
|
async function resolveKey(flagValue) {
|
|
@@ -102,16 +143,3 @@ async function resolveKey(flagValue) {
|
|
|
102
143
|
}
|
|
103
144
|
return key;
|
|
104
145
|
}
|
|
105
|
-
|
|
106
|
-
async function readIfExists(filePath) {
|
|
107
|
-
try {
|
|
108
|
-
return await fs.readFile(filePath, "utf8");
|
|
109
|
-
} catch (err) {
|
|
110
|
-
if (err.code === "ENOENT") return null;
|
|
111
|
-
throw err;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function timestamp() {
|
|
116
|
-
return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
117
|
-
}
|
package/lib/clients.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Per-client knowledge: where the MCP config lives and what to write in it. */
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { claudeDesktopConfigPath, vscodeUserDir } from "./paths.js";
|
|
5
|
+
|
|
4
6
|
export const MCP_HTTP_URL = "https://mcp.mergesafe.io/mcp";
|
|
5
7
|
// Legacy endpoint, kept for clients without solid Streamable HTTP support
|
|
6
8
|
// (opencode: SSE-only "remote" type; cline: open transport bugs; continue:
|
|
@@ -11,12 +13,29 @@ export const SERVER_NAME = "mergesafe";
|
|
|
11
13
|
/**
|
|
12
14
|
* ctx: { platform, home, cwd, env, project } — injected by the CLI so
|
|
13
15
|
* every path decision stays testable without touching the real machine.
|
|
16
|
+
*
|
|
17
|
+
* Every client is global (user-wide) by default; `--project` opts the few
|
|
18
|
+
* clients that support it into a project-local config instead.
|
|
19
|
+
* `detectPaths` lists markers whose existence means the client is installed.
|
|
14
20
|
*/
|
|
15
21
|
const CLIENTS = {
|
|
22
|
+
"claude-code": {
|
|
23
|
+
label: "Claude Code",
|
|
24
|
+
rootKey: "mcpServers",
|
|
25
|
+
configPath: (ctx) => path.join(ctx.home, ".claude.json"),
|
|
26
|
+
detectPaths: (ctx) => [path.join(ctx.home, ".claude.json"), path.join(ctx.home, ".claude")],
|
|
27
|
+
deleteKey: true,
|
|
28
|
+
// Same entry `claude mcp add --scope user --transport http` writes:
|
|
29
|
+
// user scope, so every project and session sees the server.
|
|
30
|
+
buildEntry: (key) => ({ type: "http", url: MCP_HTTP_URL, headers: bearer(key) }),
|
|
31
|
+
restartHint: "Restart any open Claude Code sessions — the server is user-wide, every project gets it.",
|
|
32
|
+
},
|
|
16
33
|
"claude-desktop": {
|
|
17
34
|
label: "Claude Desktop",
|
|
18
35
|
rootKey: "mcpServers",
|
|
19
36
|
configPath: (ctx) => claudeDesktopConfigPath(ctx),
|
|
37
|
+
detectPaths: (ctx) => [path.dirname(claudeDesktopConfigPath(ctx))],
|
|
38
|
+
deleteKey: true,
|
|
20
39
|
// Claude Desktop has no native remote + Bearer support; bridge via
|
|
21
40
|
// mcp-remote (http-first: tries Streamable HTTP, falls back to SSE).
|
|
22
41
|
// No space after "Authorization:" and env-expanded token — both
|
|
@@ -35,6 +54,8 @@ const CLIENTS = {
|
|
|
35
54
|
ctx.project
|
|
36
55
|
? path.join(ctx.cwd, ".cursor", "mcp.json")
|
|
37
56
|
: path.join(ctx.home, ".cursor", "mcp.json"),
|
|
57
|
+
detectPaths: (ctx) => [path.join(ctx.home, ".cursor")],
|
|
58
|
+
deleteKey: true,
|
|
38
59
|
// Cursor auto-detects Streamable HTTP from the URL (SSE fallback built in).
|
|
39
60
|
buildEntry: (key) => ({ url: MCP_HTTP_URL, headers: bearer(key) }),
|
|
40
61
|
restartHint: "Restart Cursor or reload the window.",
|
|
@@ -43,60 +64,63 @@ const CLIENTS = {
|
|
|
43
64
|
label: "Windsurf",
|
|
44
65
|
rootKey: "mcpServers",
|
|
45
66
|
configPath: (ctx) => path.join(ctx.home, ".codeium", "windsurf", "mcp_config.json"),
|
|
67
|
+
detectPaths: (ctx) => [path.join(ctx.home, ".codeium", "windsurf")],
|
|
68
|
+
deleteKey: true,
|
|
46
69
|
buildEntry: (key) => ({ serverUrl: MCP_HTTP_URL, headers: bearer(key) }),
|
|
47
70
|
restartHint: "Fully quit and reopen Windsurf.",
|
|
48
71
|
},
|
|
49
72
|
vscode: {
|
|
50
|
-
label: "VS Code
|
|
73
|
+
label: "VS Code",
|
|
51
74
|
rootKey: "servers",
|
|
52
|
-
configPath: (ctx) =>
|
|
75
|
+
configPath: (ctx) =>
|
|
76
|
+
ctx.project
|
|
77
|
+
? path.join(ctx.cwd, ".vscode", "mcp.json")
|
|
78
|
+
: path.join(vscodeUserDir(ctx), "mcp.json"),
|
|
79
|
+
detectPaths: (ctx) => [vscodeUserDir(ctx)],
|
|
80
|
+
deleteKey: true,
|
|
53
81
|
buildEntry: (key) => ({ type: "http", url: MCP_HTTP_URL, headers: bearer(key) }),
|
|
54
|
-
restartHint: "
|
|
82
|
+
restartHint: "Restart VS Code, then check MCP: List Servers in the command palette.",
|
|
55
83
|
},
|
|
56
84
|
opencode: {
|
|
57
|
-
label: "opencode
|
|
85
|
+
label: "opencode",
|
|
58
86
|
rootKey: "mcp",
|
|
59
|
-
configPath: (ctx) => path.join(ctx.
|
|
87
|
+
configPath: (ctx) => path.join(ctx.home, ".config", "opencode", "config.json"),
|
|
88
|
+
detectPaths: (ctx) => [path.join(ctx.home, ".config", "opencode")],
|
|
89
|
+
deleteKey: true,
|
|
60
90
|
buildEntry: (key) => ({ type: "remote", url: MCP_SSE_URL, enabled: true, headers: bearer(key) }),
|
|
61
91
|
restartHint: "Restart opencode for changes to take effect.",
|
|
62
92
|
},
|
|
63
|
-
"cursor-local": {
|
|
64
|
-
label: "Cursor (local server)",
|
|
65
|
-
rootKey: "mcpServers",
|
|
66
|
-
configPath: (ctx) => path.join(ctx.cwd, ".cursor", "mcp.json"),
|
|
67
|
-
// Local stdio server so Cursor can scan the local project — requires the
|
|
68
|
-
// mergesafe-mcp command (Python package) on PATH; Cursor starts it itself.
|
|
69
|
-
buildEntry: (key) => ({
|
|
70
|
-
command: "mergesafe-mcp",
|
|
71
|
-
env: {
|
|
72
|
-
MCP_TRANSPORT: "stdio",
|
|
73
|
-
MERGESAFE_API_URL: "https://api.mergesafe.io",
|
|
74
|
-
MERGESAFE_FRONTEND_URL: "https://app.mergesafe.io",
|
|
75
|
-
MERGESAFE_API_KEY: key,
|
|
76
|
-
},
|
|
77
|
-
}),
|
|
78
|
-
restartHint:
|
|
79
|
-
"Restart Cursor. Requires the mergesafe-mcp command installed (Python 3.11+).",
|
|
80
|
-
},
|
|
81
93
|
cline: {
|
|
82
94
|
label: "Cline",
|
|
83
95
|
rootKey: "mcpServers",
|
|
84
96
|
configPath: (ctx) =>
|
|
85
97
|
path.join(vscodeUserDir(ctx), "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
98
|
+
detectPaths: (ctx) => [path.join(vscodeUserDir(ctx), "globalStorage", "saoudrizwan.claude-dev")],
|
|
99
|
+
deleteKey: true,
|
|
86
100
|
buildEntry: (key) => ({ url: MCP_SSE_URL, headers: bearer(key) }),
|
|
87
101
|
restartHint: "Reload the VS Code window (stock VS Code path — not Insiders/VSCodium).",
|
|
88
102
|
},
|
|
89
103
|
"roo-code": {
|
|
90
|
-
label: "Roo Code
|
|
104
|
+
label: "Roo Code",
|
|
91
105
|
rootKey: "mcpServers",
|
|
92
|
-
configPath: (ctx) =>
|
|
106
|
+
configPath: (ctx) =>
|
|
107
|
+
ctx.project
|
|
108
|
+
? path.join(ctx.cwd, ".roo", "mcp.json")
|
|
109
|
+
: path.join(vscodeUserDir(ctx), "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json"),
|
|
110
|
+
detectPaths: (ctx) => [path.join(vscodeUserDir(ctx), "globalStorage", "rooveterinaryinc.roo-cline")],
|
|
111
|
+
deleteKey: true,
|
|
93
112
|
buildEntry: (key) => ({ type: "sse", url: MCP_SSE_URL, headers: bearer(key), disabled: false }),
|
|
94
|
-
restartHint: "Reload the VS Code window
|
|
113
|
+
restartHint: "Reload the VS Code window.",
|
|
95
114
|
},
|
|
96
115
|
continue: {
|
|
97
|
-
label: "Continue
|
|
98
|
-
configPath: (ctx) =>
|
|
116
|
+
label: "Continue",
|
|
117
|
+
configPath: (ctx) =>
|
|
118
|
+
ctx.project
|
|
119
|
+
? path.join(ctx.cwd, ".continue", "mcpServers", "mergesafe.yaml")
|
|
120
|
+
: path.join(ctx.home, ".continue", "mcpServers", "mergesafe.yaml"),
|
|
121
|
+
detectPaths: (ctx) => [path.join(ctx.home, ".continue")],
|
|
99
122
|
// Continue uses one YAML file per MCP server — whole-file write, no merge.
|
|
123
|
+
deleteFile: true,
|
|
100
124
|
buildFileContent: (key) =>
|
|
101
125
|
[
|
|
102
126
|
"name: Mergesafe MCP",
|
|
@@ -119,32 +143,6 @@ function bearer(key) {
|
|
|
119
143
|
return { Authorization: `Bearer ${key}` };
|
|
120
144
|
}
|
|
121
145
|
|
|
122
|
-
/** Stock VS Code user dir per OS (globalStorage lives under it). */
|
|
123
|
-
function vscodeUserDir(ctx) {
|
|
124
|
-
if (ctx.platform === "darwin") {
|
|
125
|
-
return path.join(ctx.home, "Library", "Application Support", "Code", "User");
|
|
126
|
-
}
|
|
127
|
-
if (ctx.platform === "win32") {
|
|
128
|
-
const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
|
|
129
|
-
return path.join(appData, "Code", "User");
|
|
130
|
-
}
|
|
131
|
-
const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
|
|
132
|
-
return path.join(configHome, "Code", "User");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function claudeDesktopConfigPath(ctx) {
|
|
136
|
-
const file = "claude_desktop_config.json";
|
|
137
|
-
if (ctx.platform === "darwin") {
|
|
138
|
-
return path.join(ctx.home, "Library", "Application Support", "Claude", file);
|
|
139
|
-
}
|
|
140
|
-
if (ctx.platform === "win32") {
|
|
141
|
-
const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
|
|
142
|
-
return path.join(appData, "Claude", file);
|
|
143
|
-
}
|
|
144
|
-
const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
|
|
145
|
-
return path.join(configHome, "Claude", file);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
146
|
export function getClient(id) {
|
|
149
147
|
const client = CLIENTS[id];
|
|
150
148
|
if (!client) {
|
package/lib/config-file.js
CHANGED
|
@@ -14,6 +14,40 @@ export function applyEntry(text, rootKey, serverName, entry) {
|
|
|
14
14
|
return `${JSON.stringify(config, null, 2)}\n`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Remove the `serverName` entry under `rootKey` from the JSON text.
|
|
19
|
+
* Preserves all other entries. If the section becomes empty, removes it too.
|
|
20
|
+
* Returns the new file text (2-space indent, trailing newline).
|
|
21
|
+
*/
|
|
22
|
+
export function removeEntry(text, rootKey, serverName) {
|
|
23
|
+
const config = parseConfig(text);
|
|
24
|
+
const section = config[rootKey];
|
|
25
|
+
|
|
26
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
27
|
+
// Nothing to remove — the config has no such section
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const copy = { ...section };
|
|
32
|
+
delete copy[serverName];
|
|
33
|
+
|
|
34
|
+
if (Object.keys(copy).length === 0) {
|
|
35
|
+
delete config[rootKey];
|
|
36
|
+
} else {
|
|
37
|
+
config[rootKey] = copy;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Remove the mergesafe server entry from a dedicated YAML file (Continue).
|
|
45
|
+
* Returns the new file text (overwrite), or null if the file doesn't exist.
|
|
46
|
+
*/
|
|
47
|
+
export function removeFileContent() {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
17
51
|
function parseConfig(text) {
|
|
18
52
|
if (!text || !text.trim()) {
|
|
19
53
|
return {};
|
package/lib/detect.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Detect which supported AI clients are installed on this machine. */
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
import { getClient, listClientIds } from "./clients.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Return the ids of every client whose marker path exists.
|
|
8
|
+
* `exists` is injectable so detection stays testable without a real machine.
|
|
9
|
+
*/
|
|
10
|
+
export function detectClients(ctx, exists = fs.existsSync) {
|
|
11
|
+
return listClientIds().filter((id) =>
|
|
12
|
+
getClient(id).detectPaths(ctx).some((markerPath) => exists(markerPath)),
|
|
13
|
+
);
|
|
14
|
+
}
|
package/lib/paths.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Per-OS filesystem locations shared by several clients. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Stock VS Code user dir per OS (globalStorage and user mcp.json live under it). */
|
|
5
|
+
export function vscodeUserDir(ctx) {
|
|
6
|
+
if (ctx.platform === "darwin") {
|
|
7
|
+
return path.join(ctx.home, "Library", "Application Support", "Code", "User");
|
|
8
|
+
}
|
|
9
|
+
if (ctx.platform === "win32") {
|
|
10
|
+
const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
|
|
11
|
+
return path.join(appData, "Code", "User");
|
|
12
|
+
}
|
|
13
|
+
const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
|
|
14
|
+
return path.join(configHome, "Code", "User");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Claude Desktop's claude_desktop_config.json per OS. */
|
|
18
|
+
export function claudeDesktopConfigPath(ctx) {
|
|
19
|
+
const file = "claude_desktop_config.json";
|
|
20
|
+
if (ctx.platform === "darwin") {
|
|
21
|
+
return path.join(ctx.home, "Library", "Application Support", "Claude", file);
|
|
22
|
+
}
|
|
23
|
+
if (ctx.platform === "win32") {
|
|
24
|
+
const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
|
|
25
|
+
return path.join(appData, "Claude", file);
|
|
26
|
+
}
|
|
27
|
+
const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
|
|
28
|
+
return path.join(configHome, "Claude", file);
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mergesafe-io/connect",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "One
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "One command connects every AI client on your machine to MergeSafe MCP — Claude Code, Claude Desktop, Cursor, Windsurf, VS Code and more, globally.",
|
|
5
5
|
"keywords": ["mergesafe", "mcp", "model-context-protocol", "installer"],
|
|
6
6
|
"homepage": "https://app.mergesafe.io",
|
|
7
7
|
"license": "MIT",
|