@mergesafe-io/connect 0.4.0 → 0.5.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 +43 -6
- package/lib/actions.js +6 -3
- package/lib/cli.js +2 -1
- package/lib/clients.js +15 -1
- package/lib/paths.js +7 -0
- package/lib/toml-config.js +91 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
One command connects every AI client on your machine to the
|
|
4
4
|
[MergeSafe](https://app.mergesafe.io) MCP server — globally, so it works in
|
|
5
|
-
every project. No
|
|
5
|
+
every project. No config editing.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npx @mergesafe-io/connect --key msk_your_api_key
|
|
9
9
|
```
|
|
10
10
|
|
|
11
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,
|
|
12
|
+
Desktop, Codex, Cursor, Windsurf, VS Code, …), writes each one's **global** config,
|
|
13
13
|
and backs up every file it touches. Restart your clients and the `mergesafe`
|
|
14
14
|
tools are available everywhere.
|
|
15
15
|
|
|
@@ -28,9 +28,9 @@ That removes only the mergesafe entry — all other configs are preserved.
|
|
|
28
28
|
- Finds the right **global** MCP config file for each client and OS
|
|
29
29
|
- Backs up the existing file next to it (`*.bak-<timestamp>`)
|
|
30
30
|
- Merges in the `mergesafe` server entry — every other setting and server is preserved
|
|
31
|
-
- Refuses to touch a file it can't parse (broken JSON aborts with a clear message)
|
|
31
|
+
- Refuses to touch a file it can't parse (broken JSON or TOML aborts with a clear message)
|
|
32
32
|
|
|
33
|
-
No
|
|
33
|
+
No telemetry, Node 18.17+. Codex TOML updates use `@decimalturn/toml-patch` to preserve comments and unrelated settings.
|
|
34
34
|
|
|
35
35
|
## Supported clients
|
|
36
36
|
|
|
@@ -38,6 +38,7 @@ No dependencies, no telemetry, Node 18+.
|
|
|
38
38
|
|---|---|
|
|
39
39
|
| `claude-code` | `~/.claude.json` (user scope — every project and session) |
|
|
40
40
|
| `claude-desktop` | `claude_desktop_config.json` (per-OS path) — bridges through `mcp-remote` |
|
|
41
|
+
| `codex` | `~/.codex/config.toml` (or `$CODEX_HOME/config.toml`; `--project` for `./.codex/config.toml`) |
|
|
41
42
|
| `cursor` | `~/.cursor/mcp.json` (`--project` for `./.cursor/mcp.json`) |
|
|
42
43
|
| `windsurf` | `~/.codeium/windsurf/mcp_config.json` |
|
|
43
44
|
| `vscode` | user-profile `mcp.json` (`--project` for `./.vscode/mcp.json`) |
|
|
@@ -50,12 +51,47 @@ Not covered: Zed (its `settings.json` is JSONC with comments — a rewrite would
|
|
|
50
51
|
destroy them; configure manually). Gemini CLI: use its built-in command:
|
|
51
52
|
`gemini mcp add --transport http mergesafe https://mcp.mergesafe.io/mcp -H "Authorization: Bearer msk_…"`
|
|
52
53
|
|
|
54
|
+
## Codex
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx @mergesafe-io/connect@latest codex --key msk_your_api_key
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The [official Codex MCP documentation](https://developers.openai.com/codex/mcp/)
|
|
61
|
+
specifies `~/.codex/config.toml` for the shared user configuration and
|
|
62
|
+
`.codex/config.toml` for trusted projects. If `CODEX_HOME` is set, the user
|
|
63
|
+
configuration is written to `$CODEX_HOME/config.toml`, following the
|
|
64
|
+
[Codex configuration documentation](https://learn.chatgpt.com/docs/config-file/config-advanced).
|
|
65
|
+
The CLI, desktop app, and IDE extension share this configuration.
|
|
66
|
+
|
|
67
|
+
The installer adds a Streamable HTTP connection with your API key:
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[mcp_servers.mergesafe]
|
|
71
|
+
url = "https://mcp.mergesafe.io/mcp"
|
|
72
|
+
http_headers = { Authorization = "Bearer msk_your_api_key" }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Existing Codex settings, comments, and other MCP servers are preserved.
|
|
76
|
+
Restart Codex or reload the IDE extension, then check `/mcp`.
|
|
77
|
+
To install only in a trusted project:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx @mergesafe-io/connect@latest codex --project --key msk_your_api_key
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
To remove only MergeSafe from the Codex user configuration:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @mergesafe-io/connect@latest codex --uninstall
|
|
87
|
+
```
|
|
88
|
+
|
|
53
89
|
## Options
|
|
54
90
|
|
|
55
91
|
| Flag | Effect |
|
|
56
92
|
|---|---|
|
|
57
93
|
| `--key <msk_…>` | Your MergeSafe API key (from app.mergesafe.io → Settings). Prompted if omitted. |
|
|
58
|
-
| `--project` | Write the project-local config instead of the global one (cursor, vscode, roo-code, continue — needs an explicit client) |
|
|
94
|
+
| `--project` | Write the project-local config instead of the global one (codex, cursor, vscode, roo-code, continue — needs an explicit client) |
|
|
59
95
|
| `--dry-run` | Print the target paths and resulting files without writing |
|
|
60
96
|
| `--uninstall` | Remove the `mergesafe` entry (no client = every detected client) |
|
|
61
97
|
| `--help` | Usage |
|
|
@@ -63,7 +99,8 @@ destroy them; configure manually). Gemini CLI: use its built-in command:
|
|
|
63
99
|
## Development
|
|
64
100
|
|
|
65
101
|
```bash
|
|
66
|
-
npm
|
|
102
|
+
npm ci
|
|
103
|
+
npm test # node --test
|
|
67
104
|
node bin/mergesafe-connect.js --key msk_x --dry-run # auto-detect
|
|
68
105
|
node bin/mergesafe-connect.js cursor --key msk_x --dry-run # single client
|
|
69
106
|
```
|
package/lib/actions.js
CHANGED
|
@@ -4,15 +4,17 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { SERVER_NAME } from "./clients.js";
|
|
6
6
|
import { applyEntry, removeEntry } from "./config-file.js";
|
|
7
|
+
import { applyTomlEntry, removeTomlEntry } from "./toml-config.js";
|
|
7
8
|
|
|
8
9
|
export async function handleInstall(client, key, configPath, dryRun) {
|
|
9
10
|
const existingText = await readIfExists(configPath);
|
|
10
11
|
|
|
12
|
+
const mergeEntry = client.configFormat === "toml" ? applyTomlEntry : applyEntry;
|
|
11
13
|
const newText = client.buildFileContent
|
|
12
14
|
? // Dedicated-file clients (Continue): whole-file write
|
|
13
15
|
client.buildFileContent(key)
|
|
14
|
-
: // Merge
|
|
15
|
-
|
|
16
|
+
: // Merge only the mergesafe entry in the client’s config format
|
|
17
|
+
mergeEntry(existingText ?? "", client.rootKey, SERVER_NAME, client.buildEntry(key));
|
|
16
18
|
|
|
17
19
|
if (dryRun) {
|
|
18
20
|
console.log(`Would write ${configPath}:\n\n${newText}`);
|
|
@@ -49,7 +51,8 @@ export async function handleUninstall(client, configPath, dryRun) {
|
|
|
49
51
|
throw new Error(`Client "${client.label}" does not support --uninstall`);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
const
|
|
54
|
+
const deleteEntry = client.configFormat === "toml" ? removeTomlEntry : removeEntry;
|
|
55
|
+
const newText = deleteEntry(existingText, client.rootKey, SERVER_NAME);
|
|
53
56
|
|
|
54
57
|
if (newText === null) {
|
|
55
58
|
console.log(`⚠ No mergesafe entry found in ${configPath} — nothing to remove.`);
|
package/lib/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ the "${SERVER_NAME}" entry is touched.
|
|
|
17
17
|
Clients (all global unless noted):
|
|
18
18
|
claude-code Claude Code — ~/.claude.json (user scope, every project)
|
|
19
19
|
claude-desktop Claude Desktop — claude_desktop_config.json (via mcp-remote bridge)
|
|
20
|
+
codex Codex — ~/.codex/config.toml (or $CODEX_HOME/config.toml; --project for ./.codex/config.toml)
|
|
20
21
|
cursor Cursor — ~/.cursor/mcp.json (--project for ./.cursor/mcp.json)
|
|
21
22
|
windsurf Windsurf — ~/.codeium/windsurf/mcp_config.json
|
|
22
23
|
vscode VS Code — user-profile mcp.json (--project for ./.vscode/mcp.json)
|
|
@@ -29,7 +30,7 @@ Options:
|
|
|
29
30
|
--key <msk_...> MergeSafe API key (create one on app.mergesafe.io/dashboard/settings;
|
|
30
31
|
prompted interactively if omitted)
|
|
31
32
|
--project Write the project-local config instead of the global one
|
|
32
|
-
(cursor, vscode, roo-code, continue — needs an explicit client)
|
|
33
|
+
(codex, cursor, vscode, roo-code, continue — needs an explicit client)
|
|
33
34
|
--dry-run Print the target path and resulting file without writing
|
|
34
35
|
--uninstall Remove the mergesafe entry (no client = every detected client)
|
|
35
36
|
--help Show usage
|
package/lib/clients.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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";
|
|
4
|
+
import { claudeDesktopConfigPath, codexUserDir, vscodeUserDir } from "./paths.js";
|
|
5
5
|
|
|
6
6
|
export const MCP_HTTP_URL = "https://mcp.mergesafe.io/mcp";
|
|
7
7
|
// Legacy endpoint, kept for clients without solid Streamable HTTP support
|
|
@@ -47,6 +47,20 @@ const CLIENTS = {
|
|
|
47
47
|
}),
|
|
48
48
|
restartHint: "Fully quit and reopen Claude Desktop (needs Node.js 18+ for npx).",
|
|
49
49
|
},
|
|
50
|
+
codex: {
|
|
51
|
+
label: "Codex",
|
|
52
|
+
configFormat: "toml",
|
|
53
|
+
rootKey: "mcp_servers",
|
|
54
|
+
// Official configuration: https://developers.openai.com/codex/mcp/
|
|
55
|
+
configPath: (ctx) =>
|
|
56
|
+
ctx.project
|
|
57
|
+
? path.join(ctx.cwd, ".codex", "config.toml")
|
|
58
|
+
: path.join(codexUserDir(ctx), "config.toml"),
|
|
59
|
+
detectPaths: (ctx) => [codexUserDir(ctx)],
|
|
60
|
+
deleteKey: true,
|
|
61
|
+
buildEntry: (key) => ({ url: MCP_HTTP_URL, http_headers: bearer(key) }),
|
|
62
|
+
restartHint: "Restart Codex or reload the IDE extension, then check /mcp. Project configs require a trusted project.",
|
|
63
|
+
},
|
|
50
64
|
cursor: {
|
|
51
65
|
label: "Cursor",
|
|
52
66
|
rootKey: "mcpServers",
|
package/lib/paths.js
CHANGED
|
@@ -27,3 +27,10 @@ export function claudeDesktopConfigPath(ctx) {
|
|
|
27
27
|
const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
|
|
28
28
|
return path.join(configHome, "Claude", file);
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/** Codex's shared user configuration directory (CLI, app, and IDE extension).
|
|
32
|
+
* https://learn.chatgpt.com/docs/config-file/config-advanced
|
|
33
|
+
*/
|
|
34
|
+
export function codexUserDir(ctx) {
|
|
35
|
+
return ctx.env.CODEX_HOME || path.join(ctx.home, ".codex");
|
|
36
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Edit only one MCP entry, preserving other TOML settings and comments. */
|
|
2
|
+
import { parse, patch, TomlDocument } from "@decimalturn/toml-patch";
|
|
3
|
+
|
|
4
|
+
export function applyTomlEntry(text, rootKey, serverName, entry) {
|
|
5
|
+
const config = parseConfig(text);
|
|
6
|
+
const section = config[rootKey];
|
|
7
|
+
if (section !== undefined && !isTable(section)) {
|
|
8
|
+
throw new Error(`Existing config's ${rootKey} is not a TOML table. Nothing was changed.`);
|
|
9
|
+
}
|
|
10
|
+
config[rootKey] = { ...section, [serverName]: entry };
|
|
11
|
+
return patchConfig(text, config, rootKey, serverName, !section || !Object.hasOwn(section, serverName));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function removeTomlEntry(text, rootKey, serverName) {
|
|
15
|
+
const config = parseConfig(text);
|
|
16
|
+
const section = config[rootKey];
|
|
17
|
+
if (!isTable(section) || !Object.hasOwn(section, serverName)) return null;
|
|
18
|
+
|
|
19
|
+
delete section[serverName];
|
|
20
|
+
if (Object.keys(section).length === 0) delete config[rootKey];
|
|
21
|
+
return patchConfig(text, config, rootKey, serverName);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isTable(value) {
|
|
25
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseConfig(text) {
|
|
29
|
+
try {
|
|
30
|
+
return parse(text);
|
|
31
|
+
} catch {
|
|
32
|
+
// Parser errors can contain source lines with API keys; do not print them.
|
|
33
|
+
throw new Error("Existing config is not valid TOML. Nothing was changed — fix or move the file, then re-run.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function patchConfig(text, config, rootKey, serverName, newEntry = false) {
|
|
38
|
+
try {
|
|
39
|
+
const bom = text.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
40
|
+
if (bom) text = text.slice(1);
|
|
41
|
+
// toml-patch attaches comments before a header to the preceding table.
|
|
42
|
+
// Keep other tables' leading comments out of the entry being replaced.
|
|
43
|
+
const comments = tableAnchors(text).filter(({ keys, commentStart, start }) =>
|
|
44
|
+
!(keys[0] === rootKey && (keys[1] === serverName || !Object.hasOwn(config, rootKey))) &&
|
|
45
|
+
commentStart < start,
|
|
46
|
+
).map((anchor) => ({ ...anchor, text: text.slice(anchor.commentStart, anchor.start) }));
|
|
47
|
+
for (const comment of comments.slice().reverse()) {
|
|
48
|
+
text = text.slice(0, comment.commentStart) + text.slice(comment.start);
|
|
49
|
+
}
|
|
50
|
+
let updated = patch(text, config, newEntry ? { inlineTableStart: 2 } : undefined);
|
|
51
|
+
const anchors = new Map(tableAnchors(updated).map((anchor) => [anchor.id, anchor.start]));
|
|
52
|
+
for (const comment of comments.slice().reverse()) {
|
|
53
|
+
const start = anchors.get(comment.id);
|
|
54
|
+
if (start === undefined) throw new Error("An unrelated table was removed");
|
|
55
|
+
updated = updated.slice(0, start) + comment.text + updated.slice(start);
|
|
56
|
+
}
|
|
57
|
+
parse(updated);
|
|
58
|
+
return bom + (updated.endsWith("\n") ? updated : `${updated}\n`);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error("Could not update the TOML config. Nothing was changed — fix or move the file, then re-run.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Locate real TOML headers and their standalone leading comments via the CST. */
|
|
65
|
+
function tableAnchors(text) {
|
|
66
|
+
const nodes = new TomlDocument(text).cst;
|
|
67
|
+
const lines = [0];
|
|
68
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") lines.push(i + 1);
|
|
69
|
+
const occurrences = new Map();
|
|
70
|
+
const anchors = [];
|
|
71
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
72
|
+
const table = nodes[i];
|
|
73
|
+
if (table.type !== "Table" && table.type !== "TableArray") continue;
|
|
74
|
+
const keys = table.key.item.value;
|
|
75
|
+
const key = JSON.stringify([table.type, keys]);
|
|
76
|
+
const occurrence = occurrences.get(key) ?? 0;
|
|
77
|
+
occurrences.set(key, occurrence + 1);
|
|
78
|
+
const start = lines[table.loc.start.line - 1];
|
|
79
|
+
const preceding = nodes[i - 1]?.items ?? nodes.slice(0, i);
|
|
80
|
+
let commentStart = start;
|
|
81
|
+
for (let j = preceding.length - 1; j >= 0; j--) {
|
|
82
|
+
const item = preceding[j];
|
|
83
|
+
if (item.type !== "Comment") break;
|
|
84
|
+
const lineStart = lines[item.loc.start.line - 1];
|
|
85
|
+
if (text.slice(lineStart, lineStart + item.loc.start.column).trim()) break;
|
|
86
|
+
commentStart = lineStart;
|
|
87
|
+
}
|
|
88
|
+
anchors.push({ id: `${key}:${occurrence}`, keys, start, commentStart });
|
|
89
|
+
}
|
|
90
|
+
return anchors;
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mergesafe-io/connect",
|
|
3
|
-
"version": "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.",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "One command connects every AI client on your machine to MergeSafe MCP — Claude Code, Claude Desktop, Codex, 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",
|
|
@@ -18,5 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"test": "node --test"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@decimalturn/toml-patch": "3.1.2"
|
|
21
24
|
}
|
|
22
25
|
}
|