@mergesafe-io/connect 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/README.md +61 -0
- package/bin/mergesafe-connect.js +7 -0
- package/lib/cli.js +117 -0
- package/lib/clients.js +152 -0
- package/lib/config-file.js +35 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @mergesafe-io/connect
|
|
2
|
+
|
|
3
|
+
One command to connect your AI client to the [MergeSafe](https://app.mergesafe.io) MCP server — no JSON editing.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @mergesafe-io/connect claude-desktop --key msk_your_api_key
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That's it. Restart the client and the `mergesafe` tools are available.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- Finds the right MCP config file for your client and OS
|
|
14
|
+
- Backs up the existing file next to it (`*.bak-<timestamp>`)
|
|
15
|
+
- Merges in the `mergesafe` server entry — every other setting and server is preserved
|
|
16
|
+
- Refuses to touch a file it can't parse (broken JSON aborts with a clear message)
|
|
17
|
+
|
|
18
|
+
No dependencies, no telemetry, Node 18+.
|
|
19
|
+
|
|
20
|
+
## Supported clients
|
|
21
|
+
|
|
22
|
+
| Client | Config written |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `claude-desktop` | `claude_desktop_config.json` (global, per-OS path) — bridges through `mcp-remote` |
|
|
25
|
+
| `cursor` | `~/.cursor/mcp.json` (or `./.cursor/mcp.json` with `--project`) |
|
|
26
|
+
| `cursor-local` | `./.cursor/mcp.json` — local stdio server (needs `mergesafe-mcp` on PATH) |
|
|
27
|
+
| `windsurf` | `~/.codeium/windsurf/mcp_config.json` |
|
|
28
|
+
| `vscode` | `./.vscode/mcp.json` (current project) |
|
|
29
|
+
| `opencode` | `./opencode.json` (current project) |
|
|
30
|
+
| `cline` | `cline_mcp_settings.json` (stock VS Code global storage, per-OS path) |
|
|
31
|
+
| `roo-code` | `./.roo/mcp.json` (current project) |
|
|
32
|
+
| `continue` | `./.continue/mcpServers/mergesafe.yaml` (dedicated file, whole-file write) |
|
|
33
|
+
|
|
34
|
+
Not covered: Zed (its `settings.json` is JSONC with comments — a rewrite would
|
|
35
|
+
destroy them; configure manually). For Claude Code CLI and Gemini CLI, use their
|
|
36
|
+
built-in commands instead:
|
|
37
|
+
`claude mcp add --transport sse mergesafe https://mcp.mergesafe.io/sse --header "Authorization: Bearer msk_…"`
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
| Flag | Effect |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `--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 path and resulting file without writing |
|
|
46
|
+
| `--help` | Usage |
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm test # node --test, zero deps
|
|
52
|
+
node bin/mergesafe-connect.js cursor --key msk_x --dry-run
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Publishing
|
|
56
|
+
|
|
57
|
+
The `@mergesafe` npm scope must exist (create the org on npmjs.com), then:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm publish --access public
|
|
61
|
+
```
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** CLI flow: parse args, resolve client + key, back up, merge, write. */
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline/promises";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
|
|
8
|
+
import { SERVER_NAME, getClient, listClientIds } from "./clients.js";
|
|
9
|
+
import { applyEntry } from "./config-file.js";
|
|
10
|
+
|
|
11
|
+
const USAGE = `Usage: npx @mergesafe-io/connect <client> [options]
|
|
12
|
+
|
|
13
|
+
Writes the MergeSafe MCP server entry into the client's config file.
|
|
14
|
+
The existing file is backed up first; only the "${SERVER_NAME}" entry is touched.
|
|
15
|
+
|
|
16
|
+
Clients:
|
|
17
|
+
claude-desktop Claude Desktop (global config, via mcp-remote bridge)
|
|
18
|
+
cursor Cursor — global ~/.cursor/mcp.json (--project for ./.cursor/mcp.json)
|
|
19
|
+
cursor-local Cursor local stdio server — ./.cursor/mcp.json (needs mergesafe-mcp)
|
|
20
|
+
windsurf Windsurf — ~/.codeium/windsurf/mcp_config.json
|
|
21
|
+
vscode VS Code — .vscode/mcp.json in the current project
|
|
22
|
+
opencode opencode — opencode.json in the current project
|
|
23
|
+
cline Cline — cline_mcp_settings.json (stock VS Code global storage)
|
|
24
|
+
roo-code Roo Code — .roo/mcp.json in the current project
|
|
25
|
+
continue Continue — .continue/mcpServers/mergesafe.yaml in the current project
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--key <msk_...> MergeSafe API key (create one on app.mergesafe.io/dashboard/settings;
|
|
29
|
+
prompted interactively if omitted)
|
|
30
|
+
--project Write the project-local config instead of the global one (cursor)
|
|
31
|
+
--dry-run Print the target path and resulting file without writing
|
|
32
|
+
--help Show this help
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export async function main(argv) {
|
|
36
|
+
const { values, positionals } = parseArgs({
|
|
37
|
+
args: argv,
|
|
38
|
+
allowPositionals: true,
|
|
39
|
+
options: {
|
|
40
|
+
key: { type: "string" },
|
|
41
|
+
project: { type: "boolean", default: false },
|
|
42
|
+
"dry-run": { type: "boolean", default: false },
|
|
43
|
+
help: { type: "boolean", default: false },
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (values.help || positionals.length === 0) {
|
|
48
|
+
console.log(USAGE);
|
|
49
|
+
if (!values.help) {
|
|
50
|
+
throw new Error(`Missing <client> argument. Supported: ${listClientIds().join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const client = getClient(positionals[0]);
|
|
56
|
+
const key = await resolveKey(values.key);
|
|
57
|
+
const ctx = {
|
|
58
|
+
platform: process.platform,
|
|
59
|
+
home: os.homedir(),
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
env: process.env,
|
|
62
|
+
project: values.project,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const configPath = client.configPath(ctx);
|
|
66
|
+
const existingText = await readIfExists(configPath);
|
|
67
|
+
// Dedicated-file clients (Continue) get a whole-file write; the rest are
|
|
68
|
+
// merged into the shared JSON config without touching other entries.
|
|
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}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (existingText !== null) {
|
|
79
|
+
const backupPath = `${configPath}.bak-${timestamp()}`;
|
|
80
|
+
await fs.copyFile(configPath, backupPath);
|
|
81
|
+
console.log(`• Backed up existing config to ${backupPath}`);
|
|
82
|
+
}
|
|
83
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
84
|
+
await fs.writeFile(configPath, newText, "utf8");
|
|
85
|
+
|
|
86
|
+
console.log(`✔ ${client.label} is connected to MergeSafe (${configPath})`);
|
|
87
|
+
console.log(`→ ${client.restartHint}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function resolveKey(flagValue) {
|
|
91
|
+
let key = flagValue;
|
|
92
|
+
if (!key) {
|
|
93
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
94
|
+
key = (await rl.question("Paste your MergeSafe API key (msk_…): ")).trim();
|
|
95
|
+
rl.close();
|
|
96
|
+
}
|
|
97
|
+
if (!key) {
|
|
98
|
+
throw new Error("An API key is required. Create one on app.mergesafe.io/dashboard/settings.");
|
|
99
|
+
}
|
|
100
|
+
if (!key.startsWith("msk_")) {
|
|
101
|
+
console.error("⚠ Key does not start with msk_ — double-check it, continuing anyway.");
|
|
102
|
+
}
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
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
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** Per-client knowledge: where the MCP config lives and what to write in it. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const MCP_SSE_URL = "https://mcp.mergesafe.io/sse";
|
|
5
|
+
export const SERVER_NAME = "mergesafe";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ctx: { platform, home, cwd, env, project } — injected by the CLI so
|
|
9
|
+
* every path decision stays testable without touching the real machine.
|
|
10
|
+
*/
|
|
11
|
+
const CLIENTS = {
|
|
12
|
+
"claude-desktop": {
|
|
13
|
+
label: "Claude Desktop",
|
|
14
|
+
rootKey: "mcpServers",
|
|
15
|
+
configPath: (ctx) => claudeDesktopConfigPath(ctx),
|
|
16
|
+
// Claude Desktop has no native SSE + Bearer support; bridge via
|
|
17
|
+
// mcp-remote. No space after "Authorization:" and env-expanded token —
|
|
18
|
+
// both required by mcp-remote arg parsing.
|
|
19
|
+
buildEntry: (key) => ({
|
|
20
|
+
command: "npx",
|
|
21
|
+
args: ["mcp-remote", MCP_SSE_URL, "--header", "Authorization:Bearer ${AUTH_TOKEN}"],
|
|
22
|
+
env: { AUTH_TOKEN: key },
|
|
23
|
+
}),
|
|
24
|
+
restartHint: "Fully quit and reopen Claude Desktop (needs Node.js 18+ for npx).",
|
|
25
|
+
},
|
|
26
|
+
cursor: {
|
|
27
|
+
label: "Cursor",
|
|
28
|
+
rootKey: "mcpServers",
|
|
29
|
+
configPath: (ctx) =>
|
|
30
|
+
ctx.project
|
|
31
|
+
? path.join(ctx.cwd, ".cursor", "mcp.json")
|
|
32
|
+
: path.join(ctx.home, ".cursor", "mcp.json"),
|
|
33
|
+
buildEntry: (key) => ({ url: MCP_SSE_URL, headers: bearer(key) }),
|
|
34
|
+
restartHint: "Restart Cursor or reload the window.",
|
|
35
|
+
},
|
|
36
|
+
windsurf: {
|
|
37
|
+
label: "Windsurf",
|
|
38
|
+
rootKey: "mcpServers",
|
|
39
|
+
configPath: (ctx) => path.join(ctx.home, ".codeium", "windsurf", "mcp_config.json"),
|
|
40
|
+
buildEntry: (key) => ({ serverUrl: MCP_SSE_URL, headers: bearer(key) }),
|
|
41
|
+
restartHint: "Fully quit and reopen Windsurf.",
|
|
42
|
+
},
|
|
43
|
+
vscode: {
|
|
44
|
+
label: "VS Code (project)",
|
|
45
|
+
rootKey: "servers",
|
|
46
|
+
configPath: (ctx) => path.join(ctx.cwd, ".vscode", "mcp.json"),
|
|
47
|
+
buildEntry: (key) => ({ type: "sse", url: MCP_SSE_URL, headers: bearer(key) }),
|
|
48
|
+
restartHint: "Open mcp.json in VS Code and press the “Start” CodeLens.",
|
|
49
|
+
},
|
|
50
|
+
opencode: {
|
|
51
|
+
label: "opencode (project)",
|
|
52
|
+
rootKey: "mcp",
|
|
53
|
+
configPath: (ctx) => path.join(ctx.cwd, "opencode.json"),
|
|
54
|
+
buildEntry: (key) => ({ type: "remote", url: MCP_SSE_URL, enabled: true, headers: bearer(key) }),
|
|
55
|
+
restartHint: "Restart opencode for changes to take effect.",
|
|
56
|
+
},
|
|
57
|
+
"cursor-local": {
|
|
58
|
+
label: "Cursor (local server)",
|
|
59
|
+
rootKey: "mcpServers",
|
|
60
|
+
configPath: (ctx) => path.join(ctx.cwd, ".cursor", "mcp.json"),
|
|
61
|
+
// Local stdio server so Cursor can scan the local project — requires the
|
|
62
|
+
// mergesafe-mcp command (Python package) on PATH; Cursor starts it itself.
|
|
63
|
+
buildEntry: (key) => ({
|
|
64
|
+
command: "mergesafe-mcp",
|
|
65
|
+
env: {
|
|
66
|
+
MCP_TRANSPORT: "stdio",
|
|
67
|
+
MERGESAFE_API_URL: "https://api.mergesafe.io",
|
|
68
|
+
MERGESAFE_FRONTEND_URL: "https://app.mergesafe.io",
|
|
69
|
+
MERGESAFE_API_KEY: key,
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
restartHint:
|
|
73
|
+
"Restart Cursor. Requires the mergesafe-mcp command installed (Python 3.11+).",
|
|
74
|
+
},
|
|
75
|
+
cline: {
|
|
76
|
+
label: "Cline",
|
|
77
|
+
rootKey: "mcpServers",
|
|
78
|
+
configPath: (ctx) =>
|
|
79
|
+
path.join(vscodeUserDir(ctx), "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
80
|
+
buildEntry: (key) => ({ url: MCP_SSE_URL, headers: bearer(key) }),
|
|
81
|
+
restartHint: "Reload the VS Code window (stock VS Code path — not Insiders/VSCodium).",
|
|
82
|
+
},
|
|
83
|
+
"roo-code": {
|
|
84
|
+
label: "Roo Code (project)",
|
|
85
|
+
rootKey: "mcpServers",
|
|
86
|
+
configPath: (ctx) => path.join(ctx.cwd, ".roo", "mcp.json"),
|
|
87
|
+
buildEntry: (key) => ({ type: "sse", url: MCP_SSE_URL, headers: bearer(key), disabled: false }),
|
|
88
|
+
restartHint: "Reload the VS Code window; project config overrides global settings.",
|
|
89
|
+
},
|
|
90
|
+
continue: {
|
|
91
|
+
label: "Continue (project)",
|
|
92
|
+
configPath: (ctx) => path.join(ctx.cwd, ".continue", "mcpServers", "mergesafe.yaml"),
|
|
93
|
+
// Continue uses one YAML file per MCP server — whole-file write, no merge.
|
|
94
|
+
buildFileContent: (key) =>
|
|
95
|
+
[
|
|
96
|
+
"name: Mergesafe MCP",
|
|
97
|
+
"version: 0.0.1",
|
|
98
|
+
"schema: v1",
|
|
99
|
+
"mcpServers:",
|
|
100
|
+
" - name: mergesafe",
|
|
101
|
+
" type: sse",
|
|
102
|
+
` url: ${MCP_SSE_URL}`,
|
|
103
|
+
" requestOptions:",
|
|
104
|
+
" headers:",
|
|
105
|
+
` Authorization: Bearer ${key}`,
|
|
106
|
+
"",
|
|
107
|
+
].join("\n"),
|
|
108
|
+
restartHint: "Use Continue in agent mode, then check the mergesafe tools appear.",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function bearer(key) {
|
|
113
|
+
return { Authorization: `Bearer ${key}` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Stock VS Code user dir per OS (globalStorage lives under it). */
|
|
117
|
+
function vscodeUserDir(ctx) {
|
|
118
|
+
if (ctx.platform === "darwin") {
|
|
119
|
+
return path.join(ctx.home, "Library", "Application Support", "Code", "User");
|
|
120
|
+
}
|
|
121
|
+
if (ctx.platform === "win32") {
|
|
122
|
+
const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
|
|
123
|
+
return path.join(appData, "Code", "User");
|
|
124
|
+
}
|
|
125
|
+
const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
|
|
126
|
+
return path.join(configHome, "Code", "User");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function claudeDesktopConfigPath(ctx) {
|
|
130
|
+
const file = "claude_desktop_config.json";
|
|
131
|
+
if (ctx.platform === "darwin") {
|
|
132
|
+
return path.join(ctx.home, "Library", "Application Support", "Claude", file);
|
|
133
|
+
}
|
|
134
|
+
if (ctx.platform === "win32") {
|
|
135
|
+
const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
|
|
136
|
+
return path.join(appData, "Claude", file);
|
|
137
|
+
}
|
|
138
|
+
const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
|
|
139
|
+
return path.join(configHome, "Claude", file);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getClient(id) {
|
|
143
|
+
const client = CLIENTS[id];
|
|
144
|
+
if (!client) {
|
|
145
|
+
throw new Error(`Unknown client "${id}". Supported: ${listClientIds().join(", ")}`);
|
|
146
|
+
}
|
|
147
|
+
return client;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function listClientIds() {
|
|
151
|
+
return Object.keys(CLIENTS);
|
|
152
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Non-destructive JSON config merging: only our entry is touched. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merge `entry` under `rootKey`.`serverName` into the JSON text of an MCP
|
|
5
|
+
* config file. Everything else in the file is preserved as parsed.
|
|
6
|
+
* Returns the new file text (2-space indent, trailing newline).
|
|
7
|
+
*/
|
|
8
|
+
export function applyEntry(text, rootKey, serverName, entry) {
|
|
9
|
+
const config = parseConfig(text);
|
|
10
|
+
const section = config[rootKey];
|
|
11
|
+
const existing =
|
|
12
|
+
section && typeof section === "object" && !Array.isArray(section) ? section : {};
|
|
13
|
+
config[rootKey] = { ...existing, [serverName]: entry };
|
|
14
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseConfig(text) {
|
|
18
|
+
if (!text || !text.trim()) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(text);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Existing config is not valid JSON (${err.message}). Nothing was changed — fix or move the file, then re-run.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Existing config's top level is not a JSON object. Nothing was changed — fix or move the file, then re-run.",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mergesafe-io/connect",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-command MergeSafe MCP setup for Claude Desktop, Cursor, Windsurf, VS Code and opencode.",
|
|
5
|
+
"keywords": ["mergesafe", "mcp", "model-context-protocol", "installer"],
|
|
6
|
+
"homepage": "https://app.mergesafe.io",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"mergesafe-connect": "bin/mergesafe-connect.js"
|
|
11
|
+
},
|
|
12
|
+
"files": ["bin", "lib", "README.md"],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.17"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test"
|
|
21
|
+
}
|
|
22
|
+
}
|