@simplysf/simply-gitlab-mcp 0.6.2 → 0.7.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.
Files changed (40) hide show
  1. package/README.md +4 -0
  2. package/docs/guides/mcp-server.md +34 -0
  3. package/lib/commands/gitlab/mcp/install.d.ts +33 -0
  4. package/lib/commands/gitlab/mcp/install.js +123 -0
  5. package/lib/commands/gitlab/mcp/install.js.map +1 -0
  6. package/lib/mcpClients/apply.d.ts +10 -0
  7. package/lib/mcpClients/apply.js +209 -0
  8. package/lib/mcpClients/apply.js.map +1 -0
  9. package/lib/mcpClients/clients.d.ts +36 -0
  10. package/lib/mcpClients/clients.js +111 -0
  11. package/lib/mcpClients/clients.js.map +1 -0
  12. package/lib/mcpClients/existingEntry.d.ts +16 -0
  13. package/lib/mcpClients/existingEntry.js +140 -0
  14. package/lib/mcpClients/existingEntry.js.map +1 -0
  15. package/lib/mcpClients/index.d.ts +8 -0
  16. package/lib/mcpClients/index.js +29 -0
  17. package/lib/mcpClients/index.js.map +1 -0
  18. package/lib/mcpClients/install.d.ts +14 -0
  19. package/lib/mcpClients/install.js +55 -0
  20. package/lib/mcpClients/install.js.map +1 -0
  21. package/lib/mcpClients/jsonEdit.d.ts +12 -0
  22. package/lib/mcpClients/jsonEdit.js +123 -0
  23. package/lib/mcpClients/jsonEdit.js.map +1 -0
  24. package/lib/mcpClients/launch.d.ts +36 -0
  25. package/lib/mcpClients/launch.js +75 -0
  26. package/lib/mcpClients/launch.js.map +1 -0
  27. package/lib/mcpClients/plan.d.ts +14 -0
  28. package/lib/mcpClients/plan.js +95 -0
  29. package/lib/mcpClients/plan.js.map +1 -0
  30. package/lib/mcpClients/report.d.ts +12 -0
  31. package/lib/mcpClients/report.js +63 -0
  32. package/lib/mcpClients/report.js.map +1 -0
  33. package/lib/mcpClients/runner.d.ts +9 -0
  34. package/lib/mcpClients/runner.js +38 -0
  35. package/lib/mcpClients/runner.js.map +1 -0
  36. package/lib/mcpClients/types.d.ts +98 -0
  37. package/lib/mcpClients/types.js +18 -0
  38. package/lib/mcpClients/types.js.map +1 -0
  39. package/oclif.manifest.json +124 -1
  40. package/package.json +6 -3
package/README.md CHANGED
@@ -47,6 +47,10 @@ reads them from a `.env` file instead, and the real environment still wins over
47
47
  For a self-signed or internal CA, add `"NODE_EXTRA_CA_CERTS": "/path/to/ca.pem"` to `env`. This
48
48
  server always verifies certificates.
49
49
 
50
+ With the `simply` CLI, `simply gitlab mcp install --client <id> --env-file ~/gitlab.env` writes the
51
+ entry into Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, Gemini CLI or Codex for you,
52
+ without touching the rest of that client's configuration. See the MCP server guide for the details.
53
+
50
54
  ## Writes
51
55
 
52
56
  **Only read tools are registered by default.** Add `--allow-writes` to `args` to register the seven
@@ -55,6 +55,40 @@ slashes or doubled backslashes inside JSON: `"C:/Users/me/gitlab.env"`.
55
55
 
56
56
  ## Configure a client
57
57
 
58
+ ### With `simply gitlab mcp install`
59
+
60
+ One command writes the entry for you. Name each client with `--client`:
61
+
62
+ ```sh
63
+ simply gitlab mcp install --client claude-code --client cursor --env-file ~/gitlab.env
64
+ ```
65
+
66
+ Client ids: `claude-code`, `claude-desktop`, `cursor`, `vscode`, `windsurf`, `gemini`, `codex`.
67
+ Running it without `--client` lists them, and marks the ones found on this machine.
68
+
69
+ - **Only the entry changes.** In a JSON file, everything else, comments included, is left as it
70
+ was, and the previous file is kept as `<file>.bak`. Claude Code and Codex are configured through
71
+ their own `claude mcp` and `codex mcp` commands. If one isn't on `PATH`, you get the exact command
72
+ to run yourself.
73
+ - **Credentials are never written.** `--env-file` is resolved to an absolute path, checked to exist,
74
+ and passed to the server. Without it, the entry starts with no settings, because a GUI client
75
+ doesn't see your shell's environment.
76
+ - **Server flags are passed through.** `--allow-writes` reaches the server.
77
+ `--allow-writes` also names the entry `simply-gitlab-write`, so it can sit beside the read-only
78
+ one (see [Let the agent write](#let-the-agent-write)). `--name` overrides the name.
79
+ - **`--scope project`** writes the repository's own file in the current directory instead:
80
+ `.mcp.json` for Claude Code, `.cursor/mcp.json`, `.vscode/mcp.json` or `.gemini/settings.json`.
81
+ The entry then runs `simply` from `PATH`, so it works for everyone who clones the repository.
82
+ - **At user scope the entry starts this Node binary and the `simply` CLI's own script.** That works
83
+ for GUI clients that don't see your `PATH`. If a Node upgrade moves the binary, run the command
84
+ again with `--force`. `--launcher simply` or `--launcher npx` choose the other forms.
85
+ - **Running it twice is safe.** An identical entry is left alone. An entry that differs is reported
86
+ as a conflict (exit 2) and is replaced only with `--force`. `--dry-run` shows what would be
87
+ written, and `--json` returns the per-client results.
88
+
89
+ Restart Claude Desktop or Windsurf afterwards. In Claude Code or Gemini CLI, run `/mcp`. The
90
+ sections below show what the command writes, for editing by hand or for any other client.
91
+
58
92
  ### Claude Desktop
59
93
 
60
94
  Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`; Windows:
@@ -0,0 +1,33 @@
1
+ import { Command } from '@oclif/core';
2
+ import { type InstallResult } from '../../../mcpClients/index.js';
3
+ export type GitlabMcpInstallResult = {
4
+ dryRun: boolean;
5
+ results: InstallResult[];
6
+ };
7
+ /**
8
+ * Writes an entry for `simply gitlab mcp` into named MCP clients. Knowledge of the clients lives
9
+ * in `src/mcpClients`; this command only says what the server is and which of its flags to forward.
10
+ * See docs/design/0010-mcp-install.md.
11
+ *
12
+ * Unlike `gitlab mcp`, this command is not a protocol server, so `--json` is safe here.
13
+ */
14
+ export default class GitlabMcpInstall extends Command {
15
+ static enableJsonFlag: boolean;
16
+ static readonly summary = "Add the GitLab MCP server to MCP clients.";
17
+ static readonly description: string;
18
+ static readonly examples: {
19
+ description: string;
20
+ command: string;
21
+ }[];
22
+ static readonly flags: {
23
+ client: import("@oclif/core/interfaces").OptionFlag<("cursor" | "claude-code" | "claude-desktop" | "vscode" | "windsurf" | "gemini" | "codex")[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ scope: import("@oclif/core/interfaces").OptionFlag<"project" | "user", import("@oclif/core/interfaces").CustomOptions>;
25
+ 'env-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ 'allow-writes': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
+ launcher: import("@oclif/core/interfaces").OptionFlag<"absolute" | "simply" | "npx" | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
30
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
+ };
32
+ run(): Promise<GitlabMcpInstallResult>;
33
+ }
@@ -0,0 +1,123 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import path from 'node:path';
17
+ import { Command, Flags } from '@oclif/core';
18
+ import { buildLaunch, CLIENT_IDS, currentEnvironment, defaultLauncher, detectInstalledClients, exitCodeFor, formatClientChoices, formatInstallReport, installServer, resolveEnvFile, SIMPLY_CLI_PACKAGE, } from '../../../mcpClients/index.js';
19
+ /**
20
+ * Writes an entry for `simply gitlab mcp` into named MCP clients. Knowledge of the clients lives
21
+ * in `src/mcpClients`; this command only says what the server is and which of its flags to forward.
22
+ * See docs/design/0010-mcp-install.md.
23
+ *
24
+ * Unlike `gitlab mcp`, this command is not a protocol server, so `--json` is safe here.
25
+ */
26
+ export default class GitlabMcpInstall extends Command {
27
+ static enableJsonFlag = true;
28
+ static summary = 'Add the GitLab MCP server to MCP clients.';
29
+ static description = 'Writes an entry that launches "simply gitlab mcp" into each client named with --client, ' +
30
+ 'keeping the rest of that client’s configuration as it was. Claude Code and Codex are changed ' +
31
+ 'through their own CLIs; every other client’s JSON file is edited in place, with a .bak copy kept.\n\n' +
32
+ 'Credentials are never written. Give the server its connection settings with --env-file, which is ' +
33
+ 'written as an absolute path and must exist now. A GUI client will not pass on your shell’s ' +
34
+ 'environment, so without --env-file the entry starts with no settings.\n\n' +
35
+ 'Running it again is safe: an identical entry is left alone, and a different one is only replaced ' +
36
+ 'with --force.';
37
+ static examples = [
38
+ {
39
+ description: 'Add the read-only server to Claude Code and Cursor',
40
+ command: '<%= config.bin %> <%= command.id %> --client claude-code --client cursor --env-file ~/gitlab.env',
41
+ },
42
+ {
43
+ description: 'Add a second, write-enabled entry (named simply-gitlab-write) to Claude Desktop',
44
+ command: '<%= config.bin %> <%= command.id %> --client claude-desktop --allow-writes --env-file ~/gitlab-write.env',
45
+ },
46
+ {
47
+ description: 'Commit an entry for this repository’s VS Code users',
48
+ command: '<%= config.bin %> <%= command.id %> --client vscode --scope project',
49
+ },
50
+ {
51
+ description: 'See what would be written, and change nothing',
52
+ command: '<%= config.bin %> <%= command.id %> --client gemini --dry-run',
53
+ },
54
+ ];
55
+ static flags = {
56
+ client: Flags.option({
57
+ options: CLIENT_IDS,
58
+ multiple: true,
59
+ summary: 'Client to add the server to. Repeat for several.',
60
+ })(),
61
+ scope: Flags.option({
62
+ options: ['user', 'project'],
63
+ default: 'user',
64
+ summary: 'Your own configuration, or the repository-level file in the current directory.',
65
+ })(),
66
+ 'env-file': Flags.string({
67
+ char: 'e',
68
+ summary: 'Connection settings file for the server. Written as an absolute path.',
69
+ }),
70
+ 'allow-writes': Flags.boolean({
71
+ summary: 'Register the tools that change data. Also makes the default name simply-gitlab-write.',
72
+ default: false,
73
+ }),
74
+ name: Flags.string({
75
+ summary: 'Entry name. Default simply-gitlab, or simply-gitlab-write with --allow-writes.',
76
+ }),
77
+ launcher: Flags.option({
78
+ options: ['absolute', 'simply', 'npx'],
79
+ summary: 'How the entry starts simply. Default absolute for --scope user, simply for --scope project.',
80
+ description: 'absolute: this Node binary and the simply CLI’s own script, which works for GUI clients that do ' +
81
+ 'not see your PATH (re-run with --force after a Node upgrade moves it). simply: "simply" on PATH, ' +
82
+ 'right for a committed file. npx: "npx -y @simplysf/simply-cli", for machines without a global install.',
83
+ })(),
84
+ force: Flags.boolean({ summary: 'Replace an existing entry of the same name that differs.', default: false }),
85
+ 'dry-run': Flags.boolean({ summary: 'Show what would be written, and change nothing.', default: false }),
86
+ };
87
+ async run() {
88
+ const { flags } = await this.parse(GitlabMcpInstall);
89
+ const environment = currentEnvironment();
90
+ if (!flags.client?.length) {
91
+ this.error(formatClientChoices(await detectInstalledClients(environment)), { exit: 2 });
92
+ }
93
+ const launcher = flags.launcher ?? defaultLauncher(flags.scope);
94
+ if (launcher === 'absolute' && this.config.pjson.name !== SIMPLY_CLI_PACKAGE) {
95
+ this.error(`--launcher absolute starts the simply CLI's own script, but this is running under ${this.config.pjson.name}. ` +
96
+ 'Run it as "simply gitlab mcp install", or pass --launcher simply or --launcher npx.', { exit: 2 });
97
+ }
98
+ const simplyArgs = ['gitlab', 'mcp'];
99
+ if (flags['allow-writes'])
100
+ simplyArgs.push('--allow-writes');
101
+ if (flags['env-file']) {
102
+ const envFile = await resolveEnvFile(flags['env-file'], environment).catch((error) => this.error(error.message, { exit: 2 }));
103
+ simplyArgs.push('--env-file', envFile);
104
+ }
105
+ else {
106
+ this.warn('No --env-file given. The server will start with only the settings the client puts in the ' +
107
+ 'entry’s "env" block, and a GUI client does not pass on your shell’s environment.');
108
+ }
109
+ const launch = buildLaunch(launcher, simplyArgs, {
110
+ nodePath: process.execPath,
111
+ runScript: path.join(this.config.root, 'bin', 'run.js'),
112
+ });
113
+ const name = flags.name ?? (flags['allow-writes'] ? 'simply-gitlab-write' : 'simply-gitlab');
114
+ const results = await installServer({ name, ...launch }, flags.client, flags.scope, environment, {
115
+ force: flags.force,
116
+ dryRun: flags['dry-run'],
117
+ });
118
+ this.log(formatInstallReport(results, launcher));
119
+ process.exitCode = exitCodeFor(results);
120
+ return { dryRun: flags['dry-run'], results };
121
+ }
122
+ }
123
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../../../src/commands/gitlab/mcp/install.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { ApplyOptions, InstallPlan, InstallResult } from './types.js';
2
+ /**
3
+ * Carries out a plan: edits the file or runs the client's CLI. Every failure becomes a `failed`
4
+ * result rather than a rejection, so one client's problem never stops the others.
5
+ *
6
+ * @param plan - From `planInstall`.
7
+ * @param options - `force` to replace a differing entry, `dryRun` to change nothing.
8
+ * @returns What happened, or with `dryRun`, what would.
9
+ */
10
+ export declare function applyPlan(plan: InstallPlan, options?: ApplyOptions): Promise<InstallResult>;
@@ -0,0 +1,209 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import fs from 'node:fs/promises';
17
+ import path from 'node:path';
18
+ import { isDeepStrictEqual } from 'node:util';
19
+ import { parse, printParseErrorCode } from 'jsonc-parser';
20
+ import { CLIENTS } from './clients.js';
21
+ import { compareClaudeEntry, compareCodexEntry } from './existingEntry.js';
22
+ import { setNestedEntry } from './jsonEdit.js';
23
+ import { formatCommandLine, runProcess } from './runner.js';
24
+ /**
25
+ * Carries out a plan: edits the file or runs the client's CLI. Every failure becomes a `failed`
26
+ * result rather than a rejection, so one client's problem never stops the others.
27
+ *
28
+ * @param plan - From `planInstall`.
29
+ * @param options - `force` to replace a differing entry, `dryRun` to change nothing.
30
+ * @returns What happened, or with `dryRun`, what would.
31
+ */
32
+ export async function applyPlan(plan, options = {}) {
33
+ switch (plan.kind) {
34
+ case 'file':
35
+ return applyFilePlan(plan, options);
36
+ case 'cli':
37
+ return applyCliPlan(plan, options);
38
+ case 'unsupported':
39
+ return {
40
+ client: plan.client,
41
+ name: plan.name,
42
+ outcome: 'failed',
43
+ target: CLIENTS[plan.client].displayName,
44
+ dryRun: Boolean(options.dryRun),
45
+ exitCode: 2,
46
+ message: plan.reason,
47
+ preview: '',
48
+ };
49
+ }
50
+ }
51
+ async function applyFilePlan(plan, options) {
52
+ const dryRun = Boolean(options.dryRun);
53
+ const result = (outcome, exitCode, message) => ({
54
+ client: plan.client,
55
+ name: plan.name,
56
+ outcome,
57
+ target: plan.file,
58
+ dryRun,
59
+ exitCode,
60
+ message,
61
+ preview: JSON.stringify({ [plan.key]: { [plan.name]: plan.entry } }, null, 2),
62
+ });
63
+ const config = await readConfigFile(plan);
64
+ if ('failure' in config)
65
+ return result('failed', config.exitCode, config.failure);
66
+ const { text, servers } = config;
67
+ const existing = servers?.[plan.name];
68
+ if (existing !== undefined) {
69
+ if (isDeepStrictEqual(existing, plan.entry))
70
+ return result('unchanged', 0);
71
+ if (!options.force)
72
+ return result('conflict', 2, conflictMessage(plan.name, plan.file));
73
+ }
74
+ const outcome = existing === undefined ? 'added' : 'replaced';
75
+ if (dryRun)
76
+ return result(outcome, 0);
77
+ try {
78
+ const updated = setNestedEntry(text ?? '', plan.key, plan.name, plan.entry);
79
+ await fs.mkdir(path.dirname(plan.file), { recursive: true });
80
+ if (text !== undefined)
81
+ await fs.copyFile(plan.file, `${plan.file}.bak`);
82
+ await writeAtomically(plan.file, updated);
83
+ }
84
+ catch (error) {
85
+ return result('failed', 1, `Could not write ${plan.file}: ${error.message}`);
86
+ }
87
+ return result(outcome, 0);
88
+ }
89
+ /** Reads and checks a client's file: absent is fine, unreadable or not an object is a refusal. */
90
+ async function readConfigFile(plan) {
91
+ let text;
92
+ try {
93
+ text = await fs.readFile(plan.file, 'utf8');
94
+ }
95
+ catch (error) {
96
+ if (!isNotFound(error))
97
+ return { failure: `Could not read ${plan.file}: ${error.message}`, exitCode: 1 };
98
+ }
99
+ if (text === undefined && plan.clientDir && !(await isDirectory(plan.clientDir))) {
100
+ return {
101
+ failure: `${CLIENTS[plan.client].displayName} does not appear to be installed: ${plan.clientDir} does not exist.`,
102
+ exitCode: 2,
103
+ };
104
+ }
105
+ const errors = [];
106
+ const root = text === undefined || text.trim() === '' ? {} : parse(text, errors, { allowTrailingComma: true });
107
+ if (errors.length > 0) {
108
+ const [first] = errors;
109
+ return {
110
+ failure: `${plan.file} is not valid JSON (${printParseErrorCode(first.error)} at offset ${first.offset}). ` +
111
+ 'It was left untouched; fix it and run this again.',
112
+ exitCode: 2,
113
+ };
114
+ }
115
+ if (!isRecord(root) || (root[plan.key] !== undefined && !isRecord(root[plan.key]))) {
116
+ return {
117
+ failure: `${plan.file} does not hold an object${isRecord(root) ? ` under "${plan.key}"` : ''}. It was left untouched.`,
118
+ exitCode: 2,
119
+ };
120
+ }
121
+ return { text, servers: root[plan.key] };
122
+ }
123
+ async function applyCliPlan(plan, options) {
124
+ const dryRun = Boolean(options.dryRun);
125
+ const addCommand = formatCommandLine(plan.executable, plan.addArgs);
126
+ const removeCommand = formatCommandLine(plan.executable, plan.removeArgs);
127
+ const result = (outcome, exitCode, message, target = addCommand) => ({
128
+ client: plan.client,
129
+ name: plan.name,
130
+ outcome,
131
+ target,
132
+ dryRun,
133
+ exitCode,
134
+ message,
135
+ preview: addCommand,
136
+ });
137
+ const existing = await readExistingCliEntry(plan);
138
+ if (existing === 'same')
139
+ return result('unchanged', 0);
140
+ if (existing === 'differs' && !options.force)
141
+ return result('conflict', 2, conflictMessage(plan.name, plan.configFile));
142
+ const outcome = existing === 'absent' ? 'added' : 'replaced';
143
+ const commands = existing === 'absent' ? [plan.addArgs] : [plan.removeArgs, plan.addArgs];
144
+ const target = existing === 'absent' ? addCommand : `${removeCommand} && ${addCommand}`;
145
+ if (dryRun)
146
+ return result(outcome, 0, undefined, target);
147
+ const run = options.runner ?? runProcess;
148
+ for (const args of commands) {
149
+ try {
150
+ // eslint-disable-next-line no-await-in-loop -- the remove must finish before the add, or the add sees the old entry
151
+ const { exitCode, stderr, stdout } = await run(plan.executable, args);
152
+ if (exitCode !== 0) {
153
+ const output = (stderr || stdout).trim();
154
+ return result('failed', 1, `${formatCommandLine(plan.executable, args)} exited with ${exitCode}${output ? `: ${output}` : '.'}`, target);
155
+ }
156
+ }
157
+ catch (error) {
158
+ if (isNotFound(error)) {
159
+ return result('failed', 2, `${plan.executable} is not on PATH, so ${CLIENTS[plan.client].displayName} could not be configured. ` +
160
+ `Run this yourself:\n${target}`, target);
161
+ }
162
+ return result('failed', 1, `Could not run ${plan.executable}: ${error.message}`, target);
163
+ }
164
+ }
165
+ return result(outcome, 0, undefined, target);
166
+ }
167
+ async function readExistingCliEntry(plan) {
168
+ let text;
169
+ try {
170
+ text = await fs.readFile(plan.configFile, 'utf8');
171
+ }
172
+ catch {
173
+ // No file means no entry; any other read problem is the client's to report when it runs.
174
+ return 'absent';
175
+ }
176
+ return plan.configFormat === 'claude-json'
177
+ ? compareClaudeEntry(text, plan.name, plan.launch)
178
+ : compareCodexEntry(text, plan.name, plan.launch);
179
+ }
180
+ /** Writes beside the target and renames over it, so a crash never leaves a half-written file. */
181
+ async function writeAtomically(file, contents) {
182
+ const temporary = `${file}.${process.pid}.tmp`;
183
+ await fs.writeFile(temporary, contents, 'utf8');
184
+ try {
185
+ await fs.rename(temporary, file);
186
+ }
187
+ catch (error) {
188
+ await fs.rm(temporary, { force: true });
189
+ throw error;
190
+ }
191
+ }
192
+ function conflictMessage(name, file) {
193
+ return `An entry named "${name}" already exists in ${file} and is different. Re-run with --force to replace it.`;
194
+ }
195
+ async function isDirectory(dir) {
196
+ try {
197
+ return (await fs.stat(dir)).isDirectory();
198
+ }
199
+ catch {
200
+ return false;
201
+ }
202
+ }
203
+ function isNotFound(error) {
204
+ return error?.code === 'ENOENT';
205
+ }
206
+ function isRecord(value) {
207
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
208
+ }
209
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.js","sourceRoot":"","sources":["../../src/mcpClients/apply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import type { ClientId, InstallEnvironment, ServersKey } from './types.js';
3
+ /** Where a client keeps a JSON configuration file, and what it expects inside it. */
4
+ type FileLocation = {
5
+ file: string;
6
+ key: ServersKey;
7
+ /** Add `"type": "stdio"` to the entry. */
8
+ typed: boolean;
9
+ clientDir?: string;
10
+ };
11
+ type ClientDefinition = {
12
+ displayName: string;
13
+ /** What to do after installing, since nothing here can tell whether the client noticed. */
14
+ nextStep: string;
15
+ /** User scope: a file to edit, or the client's own CLI. */
16
+ user: (environment: InstallEnvironment) => FileLocation | {
17
+ cli: 'claude' | 'codex';
18
+ configFile: string;
19
+ };
20
+ /** Project scope, relative to `cwd`; absent where the client has no repo-level file. */
21
+ project?: {
22
+ file: string;
23
+ key: ServersKey;
24
+ typed: boolean;
25
+ };
26
+ };
27
+ /** `path` for the target platform, so Windows paths are built with `\` even when planned elsewhere. */
28
+ declare function pathFor(environment: InstallEnvironment): typeof path.posix;
29
+ /**
30
+ * The client table. A client that moves its file is a one-row change here, which is the reason this
31
+ * lives in one package rather than in each product.
32
+ */
33
+ export declare const CLIENTS: Readonly<Record<ClientId, ClientDefinition>>;
34
+ /** @returns The directory whose presence means the client is installed for this user. */
35
+ export declare function clientMarker(client: ClientId, environment: InstallEnvironment): string;
36
+ export { pathFor };
@@ -0,0 +1,111 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import path from 'node:path';
17
+ /** `path` for the target platform, so Windows paths are built with `\` even when planned elsewhere. */
18
+ function pathFor(environment) {
19
+ return environment.platform === 'win32' ? path.win32 : path.posix;
20
+ }
21
+ /** The per-user application data directory: `%APPDATA%`, `~/Library/Application Support`, or XDG. */
22
+ function appDataDir(environment) {
23
+ const p = pathFor(environment);
24
+ if (environment.platform === 'win32') {
25
+ return environment.env.APPDATA ?? p.join(environment.homedir, 'AppData', 'Roaming');
26
+ }
27
+ if (environment.platform === 'darwin')
28
+ return p.join(environment.homedir, 'Library', 'Application Support');
29
+ return environment.env.XDG_CONFIG_HOME ?? p.join(environment.homedir, '.config');
30
+ }
31
+ function dotDirFile(environment, dir, file) {
32
+ const p = pathFor(environment);
33
+ const clientDir = p.join(environment.homedir, ...dir);
34
+ return { file: p.join(clientDir, file), key: 'mcpServers', typed: false, clientDir };
35
+ }
36
+ /**
37
+ * The client table. A client that moves its file is a one-row change here, which is the reason this
38
+ * lives in one package rather than in each product.
39
+ */
40
+ export const CLIENTS = {
41
+ 'claude-code': {
42
+ displayName: 'Claude Code',
43
+ nextStep: 'Run /mcp in Claude Code to check the server connects.',
44
+ user: (environment) => ({
45
+ cli: 'claude',
46
+ configFile: pathFor(environment).join(environment.env.CLAUDE_CONFIG_DIR ?? environment.homedir, '.claude.json'),
47
+ }),
48
+ project: { file: '.mcp.json', key: 'mcpServers', typed: true },
49
+ },
50
+ 'claude-desktop': {
51
+ displayName: 'Claude Desktop',
52
+ nextStep: 'Quit and restart Claude Desktop.',
53
+ user: (environment) => {
54
+ const clientDir = pathFor(environment).join(appDataDir(environment), 'Claude');
55
+ return {
56
+ file: pathFor(environment).join(clientDir, 'claude_desktop_config.json'),
57
+ key: 'mcpServers',
58
+ typed: false,
59
+ clientDir,
60
+ };
61
+ },
62
+ },
63
+ cursor: {
64
+ displayName: 'Cursor',
65
+ nextStep: 'Check the server under Cursor Settings > MCP, and reload Cursor if it is not listed.',
66
+ user: (environment) => dotDirFile(environment, ['.cursor'], 'mcp.json'),
67
+ project: { file: path.posix.join('.cursor', 'mcp.json'), key: 'mcpServers', typed: false },
68
+ },
69
+ vscode: {
70
+ displayName: 'VS Code',
71
+ nextStep: 'Run "MCP: List Servers" in VS Code and start the server.',
72
+ user: (environment) => {
73
+ const p = pathFor(environment);
74
+ const clientDir = p.join(appDataDir(environment), 'Code');
75
+ return { file: p.join(clientDir, 'User', 'mcp.json'), key: 'servers', typed: true, clientDir };
76
+ },
77
+ project: { file: path.posix.join('.vscode', 'mcp.json'), key: 'servers', typed: true },
78
+ },
79
+ windsurf: {
80
+ displayName: 'Windsurf',
81
+ nextStep: "Refresh Windsurf's MCP servers, or restart Windsurf.",
82
+ user: (environment) => dotDirFile(environment, ['.codeium', 'windsurf'], 'mcp_config.json'),
83
+ },
84
+ gemini: {
85
+ displayName: 'Gemini CLI',
86
+ nextStep: 'Run /mcp in Gemini CLI to check the server connects.',
87
+ user: (environment) => dotDirFile(environment, ['.gemini'], 'settings.json'),
88
+ project: { file: path.posix.join('.gemini', 'settings.json'), key: 'mcpServers', typed: false },
89
+ },
90
+ codex: {
91
+ displayName: 'Codex CLI',
92
+ nextStep: 'Start a new Codex session.',
93
+ user: (environment) => ({
94
+ cli: 'codex',
95
+ configFile: pathFor(environment).join(environment.env.CODEX_HOME ?? pathFor(environment).join(environment.homedir, '.codex'), 'config.toml'),
96
+ }),
97
+ },
98
+ };
99
+ /** @returns The directory whose presence means the client is installed for this user. */
100
+ export function clientMarker(client, environment) {
101
+ const location = CLIENTS[client].user(environment);
102
+ if ('cli' in location) {
103
+ // Claude Code keeps `~/.claude/` (or `$CLAUDE_CONFIG_DIR`); Codex keeps `config.toml` in its home.
104
+ return location.cli === 'claude'
105
+ ? (environment.env.CLAUDE_CONFIG_DIR ?? pathFor(environment).join(environment.homedir, '.claude'))
106
+ : pathFor(environment).dirname(location.configFile);
107
+ }
108
+ return location.clientDir ?? pathFor(environment).dirname(location.file);
109
+ }
110
+ export { pathFor };
111
+ //# sourceMappingURL=clients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clients.js","sourceRoot":"","sources":["../../src/mcpClients/clients.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What a client already has under an entry name, read from a file this module never writes
3
+ * (`~/.claude.json`, `config.toml`). `differs` covers anything that isn't provably the same launch,
4
+ * including an entry this reader can't make sense of: `--force` is then the way through, which is
5
+ * the safe direction to be wrong in.
6
+ */
7
+ export type ExistingEntry = 'absent' | 'same' | 'differs';
8
+ type Launch = {
9
+ command: string;
10
+ args: readonly string[];
11
+ };
12
+ /** @returns How `launch` compares with the user-scope `mcpServers` entry in Claude Code's `~/.claude.json`. */
13
+ export declare function compareClaudeEntry(text: string, name: string, launch: Launch): ExistingEntry;
14
+ /** @returns How `launch` compares with the `[mcp_servers.<name>]` table in Codex's `config.toml`. */
15
+ export declare function compareCodexEntry(text: string, name: string, launch: Launch): ExistingEntry;
16
+ export {};