@gmickel/gno 1.12.4 → 1.14.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 +126 -59
- package/assets/skill/SKILL.md +8 -1
- package/assets/skill/cli-reference.md +18 -7
- package/assets/skill/mcp-reference.md +22 -3
- package/package.json +3 -1
- package/src/app/constants.ts +43 -10
- package/src/app/index-name.ts +127 -0
- package/src/cli/commands/doctor-activation.ts +151 -0
- package/src/cli/commands/doctor.ts +41 -16
- package/src/cli/commands/get.ts +18 -0
- package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
- package/src/cli/commands/mcp/config-discovery.ts +42 -0
- package/src/cli/commands/mcp/config-editors.ts +432 -0
- package/src/cli/commands/mcp/config.ts +63 -160
- package/src/cli/commands/mcp/install.ts +75 -37
- package/src/cli/commands/mcp/paths.ts +141 -136
- package/src/cli/commands/mcp/server-entry.ts +66 -0
- package/src/cli/commands/mcp/status.ts +189 -57
- package/src/cli/commands/mcp/target-display.ts +30 -0
- package/src/cli/commands/mcp/uninstall.ts +29 -31
- package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
- package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
- package/src/cli/commands/multi-get.ts +31 -6
- package/src/cli/commands/status.ts +107 -11
- package/src/cli/program.ts +66 -20
- package/src/core/activation-connector-health.ts +19 -0
- package/src/core/activation-probe-plan.ts +321 -0
- package/src/core/activation-probe.ts +138 -0
- package/src/core/activation-receipt-store.ts +39 -0
- package/src/core/activation-status.ts +513 -0
- package/src/core/activation-verifier.ts +416 -0
- package/src/core/connector-environment.ts +68 -0
- package/src/core/connector-policy.ts +233 -0
- package/src/core/connector-verification-target.ts +150 -0
- package/src/core/connector-verifier.ts +497 -0
- package/src/core/indexed-reference.ts +33 -8
- package/src/core/runtime-entrypoint.ts +24 -0
- package/src/mcp/activation-verification-mode.ts +4 -0
- package/src/mcp/server.ts +9 -2
- package/src/sdk/client.ts +7 -0
- package/src/sdk/types.ts +1 -0
- package/src/serve/activation-health.ts +91 -0
- package/src/serve/background-runtime.ts +11 -1
- package/src/serve/connectors.ts +164 -19
- package/src/serve/public/components/BootstrapStatus.tsx +94 -1
- package/src/serve/public/components/FirstRunWizard.tsx +13 -51
- package/src/serve/public/components/HealthCenter.tsx +8 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Connectors.tsx +216 -55
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/routes/api.ts +152 -8
- package/src/serve/server.ts +44 -9
- package/src/serve/status-model.ts +4 -0
- package/src/serve/status.ts +79 -35
- package/src/store/activation-receipts.ts +390 -0
- package/src/store/index.ts +8 -0
- package/src/store/migrations/012-activation-receipts.ts +38 -0
- package/src/store/migrations/013-fts-sync-marker.ts +39 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +313 -53
- package/src/store/types.ts +118 -0
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* @module src/cli/commands/mcp/config
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { copyFile,
|
|
9
|
-
import { dirname } from "node:path";
|
|
8
|
+
import { copyFile, lstat, stat } from "node:fs/promises";
|
|
10
9
|
|
|
10
|
+
import type { ConnectorWorkspaceEnvironment } from "../../../core/connector-environment.js";
|
|
11
11
|
import type { McpConfigFormat } from "./paths.js";
|
|
12
12
|
|
|
13
13
|
import { CliError } from "../../errors.js";
|
|
14
|
+
import { writeMcpConfigTextAtomically } from "./atomic-config-write.js";
|
|
14
15
|
|
|
15
16
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
17
|
// Types
|
|
@@ -20,6 +21,7 @@ import { CliError } from "../../errors.js";
|
|
|
20
21
|
export interface StandardMcpEntry {
|
|
21
22
|
command: string;
|
|
22
23
|
args: string[];
|
|
24
|
+
env?: ConnectorWorkspaceEnvironment;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** OpenCode mcp entry (command is array, has type and enabled) */
|
|
@@ -27,6 +29,7 @@ export interface OpenCodeMcpEntry {
|
|
|
27
29
|
type: "local";
|
|
28
30
|
command: string[];
|
|
29
31
|
enabled: boolean;
|
|
32
|
+
environment?: ConnectorWorkspaceEnvironment;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/** Config with standard mcpServers key */
|
|
@@ -68,8 +71,29 @@ async function checkConfigPath(
|
|
|
68
71
|
configPath: string
|
|
69
72
|
): Promise<"file" | "missing"> {
|
|
70
73
|
try {
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
74
|
+
const pathStats = await lstat(configPath);
|
|
75
|
+
if (pathStats.isSymbolicLink()) {
|
|
76
|
+
let targetStats: Awaited<ReturnType<typeof stat>>;
|
|
77
|
+
try {
|
|
78
|
+
targetStats = await stat(configPath);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
81
|
+
throw new CliError(
|
|
82
|
+
"RUNTIME",
|
|
83
|
+
`Config path is a dangling symbolic link: ${configPath}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
if (!targetStats.isFile()) {
|
|
89
|
+
throw new CliError(
|
|
90
|
+
"RUNTIME",
|
|
91
|
+
`Config symbolic link target is not a file: ${configPath}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return "file";
|
|
95
|
+
}
|
|
96
|
+
if (!pathStats.isFile()) {
|
|
73
97
|
throw new CliError(
|
|
74
98
|
"RUNTIME",
|
|
75
99
|
`Config path exists but is not a file: ${configPath}`
|
|
@@ -84,88 +108,49 @@ async function checkConfigPath(
|
|
|
84
108
|
}
|
|
85
109
|
}
|
|
86
110
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
* Returns empty config if file doesn't exist.
|
|
90
|
-
* Returns null if file doesn't exist AND returnNullOnMissing is true.
|
|
91
|
-
* Throws on malformed JSON/YAML or if path is a directory.
|
|
92
|
-
*/
|
|
93
|
-
export async function readMcpConfig(
|
|
111
|
+
/** Read config text without parsing so format-specific editors preserve layout. */
|
|
112
|
+
export async function readMcpConfigText(
|
|
94
113
|
configPath: string,
|
|
95
|
-
options?: { returnNullOnMissing?: boolean
|
|
96
|
-
): Promise<
|
|
114
|
+
options?: { returnNullOnMissing?: boolean }
|
|
115
|
+
): Promise<string | null> {
|
|
97
116
|
const pathStatus = await checkConfigPath(configPath);
|
|
98
|
-
|
|
99
117
|
if (pathStatus === "missing") {
|
|
100
|
-
return options?.returnNullOnMissing ? null :
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const file = Bun.file(configPath);
|
|
104
|
-
const content = await file.text();
|
|
105
|
-
|
|
106
|
-
// Handle empty file
|
|
107
|
-
if (!content.trim()) {
|
|
108
|
-
return {};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
if (options?.yaml) {
|
|
113
|
-
return Bun.YAML.parse(content) as McpConfig;
|
|
114
|
-
}
|
|
115
|
-
return JSON.parse(content) as McpConfig;
|
|
116
|
-
} catch {
|
|
117
|
-
const format = options?.yaml ? "YAML" : "JSON";
|
|
118
|
-
throw new CliError(
|
|
119
|
-
"RUNTIME",
|
|
120
|
-
`Malformed ${format} in ${configPath}. Please fix or backup and delete the file.`
|
|
121
|
-
);
|
|
118
|
+
return options?.returnNullOnMissing ? null : "";
|
|
122
119
|
}
|
|
120
|
+
return Bun.file(configPath).text();
|
|
123
121
|
}
|
|
124
122
|
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
* Creates backup of existing file first.
|
|
128
|
-
*/
|
|
129
|
-
export async function writeMcpConfig(
|
|
123
|
+
/** Write MCP text atomically while preserving the same backup contract. */
|
|
124
|
+
export async function writeMcpConfigText(
|
|
130
125
|
configPath: string,
|
|
131
|
-
|
|
132
|
-
options?: { yaml?: boolean }
|
|
126
|
+
content: string
|
|
133
127
|
): Promise<void> {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
// Ensure directory exists
|
|
137
|
-
await mkdir(dir, { recursive: true });
|
|
138
|
-
|
|
139
|
-
// Create backup of existing file
|
|
128
|
+
const backupPath = `${configPath}.bak`;
|
|
140
129
|
try {
|
|
141
130
|
const stats = await stat(configPath);
|
|
142
131
|
if (stats.isFile()) {
|
|
143
|
-
|
|
132
|
+
try {
|
|
133
|
+
const backupStats = await lstat(backupPath);
|
|
134
|
+
if (!backupStats.isFile() || backupStats.isSymbolicLink()) {
|
|
135
|
+
throw new CliError(
|
|
136
|
+
"RUNTIME",
|
|
137
|
+
`MCP config backup path is not a regular file: ${backupPath}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
await copyFile(configPath, backupPath);
|
|
144
146
|
}
|
|
145
|
-
} catch {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Serialize content
|
|
150
|
-
const content = options?.yaml
|
|
151
|
-
? Bun.YAML.stringify(config)
|
|
152
|
-
: JSON.stringify(config, null, 2);
|
|
153
|
-
|
|
154
|
-
// Write to temp file first
|
|
155
|
-
const tmpPath = `${configPath}.tmp.${Date.now()}.${process.pid}`;
|
|
156
|
-
try {
|
|
157
|
-
await Bun.write(tmpPath, content);
|
|
158
|
-
// Atomic rename
|
|
159
|
-
await rename(tmpPath, configPath);
|
|
160
|
-
} catch (err) {
|
|
161
|
-
// Cleanup temp file on error
|
|
162
|
-
try {
|
|
163
|
-
await unlink(tmpPath);
|
|
164
|
-
} catch {
|
|
165
|
-
// Ignore cleanup errors
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
149
|
+
throw error;
|
|
166
150
|
}
|
|
167
|
-
throw err;
|
|
168
151
|
}
|
|
152
|
+
|
|
153
|
+
await writeMcpConfigTextAtomically(configPath, content);
|
|
169
154
|
}
|
|
170
155
|
|
|
171
156
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -188,6 +173,8 @@ export function getServersKey(
|
|
|
188
173
|
return "mcp";
|
|
189
174
|
case "amp_mcp":
|
|
190
175
|
return "amp.mcpServers";
|
|
176
|
+
case "codex_toml":
|
|
177
|
+
throw new Error("Codex TOML uses dedicated config operations");
|
|
191
178
|
default: {
|
|
192
179
|
const _exhaustive: never = format;
|
|
193
180
|
throw new Error(`Unknown format: ${String(_exhaustive)}`);
|
|
@@ -195,48 +182,14 @@ export function getServersKey(
|
|
|
195
182
|
}
|
|
196
183
|
}
|
|
197
184
|
|
|
198
|
-
/**
|
|
199
|
-
* Check if format uses YAML.
|
|
200
|
-
*/
|
|
201
|
-
export function isYamlFormat(format: McpConfigFormat): boolean {
|
|
202
|
-
return format === "yaml_standard";
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Check if a server entry exists in config for given format.
|
|
207
|
-
*/
|
|
208
|
-
export function hasServerEntry(
|
|
209
|
-
config: AnyMcpConfig,
|
|
210
|
-
serverName: string,
|
|
211
|
-
format: McpConfigFormat
|
|
212
|
-
): boolean {
|
|
213
|
-
const key = getServersKey(format);
|
|
214
|
-
const servers = config[key] as Record<string, unknown> | undefined;
|
|
215
|
-
return !!servers?.[serverName];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Get server entry from config.
|
|
220
|
-
*/
|
|
221
|
-
export function getServerEntry(
|
|
222
|
-
config: AnyMcpConfig,
|
|
223
|
-
serverName: string,
|
|
224
|
-
format: McpConfigFormat
|
|
225
|
-
): StandardMcpEntry | OpenCodeMcpEntry | undefined {
|
|
226
|
-
const key = getServersKey(format);
|
|
227
|
-
const servers = config[key] as
|
|
228
|
-
| Record<string, StandardMcpEntry | OpenCodeMcpEntry>
|
|
229
|
-
| undefined;
|
|
230
|
-
return servers?.[serverName];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
185
|
/**
|
|
234
186
|
* Build entry for a specific format from standard command/args.
|
|
235
187
|
*/
|
|
236
188
|
export function buildEntry(
|
|
237
189
|
command: string,
|
|
238
190
|
args: string[],
|
|
239
|
-
format: McpConfigFormat
|
|
191
|
+
format: McpConfigFormat,
|
|
192
|
+
env?: ConnectorWorkspaceEnvironment
|
|
240
193
|
): StandardMcpEntry | OpenCodeMcpEntry {
|
|
241
194
|
if (format === "mcp") {
|
|
242
195
|
// OpenCode: command is array [command, ...args]
|
|
@@ -244,59 +197,9 @@ export function buildEntry(
|
|
|
244
197
|
type: "local",
|
|
245
198
|
command: [command, ...args],
|
|
246
199
|
enabled: true,
|
|
200
|
+
...(env ? { environment: env } : {}),
|
|
247
201
|
};
|
|
248
202
|
}
|
|
249
203
|
// All others use standard format
|
|
250
|
-
return { command, args };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Add or update server entry in config.
|
|
255
|
-
*/
|
|
256
|
-
export function setServerEntry(
|
|
257
|
-
config: AnyMcpConfig,
|
|
258
|
-
serverName: string,
|
|
259
|
-
entry: StandardMcpEntry | OpenCodeMcpEntry,
|
|
260
|
-
format: McpConfigFormat
|
|
261
|
-
): void {
|
|
262
|
-
const key = getServersKey(format);
|
|
263
|
-
|
|
264
|
-
// Initialize servers record if needed
|
|
265
|
-
if (!config[key]) {
|
|
266
|
-
(config as Record<string, unknown>)[key] = {};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const servers = config[key] as Record<
|
|
270
|
-
string,
|
|
271
|
-
StandardMcpEntry | OpenCodeMcpEntry
|
|
272
|
-
>;
|
|
273
|
-
servers[serverName] = entry;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Remove server entry from config.
|
|
278
|
-
* Returns true if entry was removed, false if not found.
|
|
279
|
-
*/
|
|
280
|
-
export function removeServerEntry(
|
|
281
|
-
config: AnyMcpConfig,
|
|
282
|
-
serverName: string,
|
|
283
|
-
format: McpConfigFormat
|
|
284
|
-
): boolean {
|
|
285
|
-
const key = getServersKey(format);
|
|
286
|
-
const servers = config[key] as
|
|
287
|
-
| Record<string, StandardMcpEntry | OpenCodeMcpEntry>
|
|
288
|
-
| undefined;
|
|
289
|
-
|
|
290
|
-
if (!servers?.[serverName]) {
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
delete servers[serverName];
|
|
295
|
-
|
|
296
|
-
// Clean up empty servers object
|
|
297
|
-
if (Object.keys(servers).length === 0) {
|
|
298
|
-
delete (config as Record<string, unknown>)[key];
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return true;
|
|
204
|
+
return { command, args, ...(env ? { env } : {}) };
|
|
302
205
|
}
|
|
@@ -4,27 +4,36 @@
|
|
|
4
4
|
* @module src/cli/commands/mcp/install
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { DEFAULT_INDEX_NAME } from "../../../app/constants.js";
|
|
8
|
+
import { getConfigPaths, toAbsolutePath } from "../../../config/paths.js";
|
|
7
9
|
import { CliError } from "../../errors.js";
|
|
8
10
|
import { getGlobals } from "../../program.js";
|
|
11
|
+
import { resolveMcpConfigLocation } from "./config-discovery.js";
|
|
12
|
+
import {
|
|
13
|
+
getJsoncServerEntry,
|
|
14
|
+
getTomlServerEntry,
|
|
15
|
+
getYamlServerEntry,
|
|
16
|
+
setJsoncServerEntry,
|
|
17
|
+
setTomlServerEntry,
|
|
18
|
+
setYamlServerEntry,
|
|
19
|
+
} from "./config-editors.js";
|
|
9
20
|
import {
|
|
10
|
-
type AnyMcpConfig,
|
|
11
21
|
buildEntry,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type StandardMcpEntry,
|
|
16
|
-
setServerEntry,
|
|
17
|
-
writeMcpConfig,
|
|
22
|
+
getServersKey,
|
|
23
|
+
readMcpConfigText,
|
|
24
|
+
writeMcpConfigText,
|
|
18
25
|
} from "./config.js";
|
|
19
26
|
import {
|
|
20
27
|
buildMcpServerEntry,
|
|
28
|
+
getDefaultTargetScope,
|
|
29
|
+
getTargetScopes,
|
|
21
30
|
getTargetDisplayName,
|
|
22
|
-
MCP_SERVER_NAME,
|
|
23
31
|
type McpScope,
|
|
32
|
+
type McpServerEntry,
|
|
24
33
|
type McpTarget,
|
|
25
34
|
resolveMcpConfigPath,
|
|
26
|
-
TARGETS_WITH_PROJECT_SCOPE,
|
|
27
35
|
} from "./paths.js";
|
|
36
|
+
import { normalizeMcpServerEntryForInstall } from "./server-entry.js";
|
|
28
37
|
|
|
29
38
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
39
|
// Types
|
|
@@ -36,6 +45,10 @@ export interface InstallOptions {
|
|
|
36
45
|
force?: boolean;
|
|
37
46
|
dryRun?: boolean;
|
|
38
47
|
enableWrite?: boolean;
|
|
48
|
+
/** Index identity persisted in the installed MCP command. */
|
|
49
|
+
indexName?: string;
|
|
50
|
+
/** Active GNO config persisted as a canonical absolute path. */
|
|
51
|
+
configPath?: string;
|
|
39
52
|
/** Override cwd (testing) */
|
|
40
53
|
cwd?: string;
|
|
41
54
|
/** Override home dir (testing) */
|
|
@@ -51,7 +64,7 @@ export interface McpInstallResult {
|
|
|
51
64
|
scope: McpScope;
|
|
52
65
|
configPath: string;
|
|
53
66
|
action: "created" | "updated" | "dry_run_create" | "dry_run_update";
|
|
54
|
-
serverEntry:
|
|
67
|
+
serverEntry: McpServerEntry;
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -64,7 +77,7 @@ export interface McpInstallResult {
|
|
|
64
77
|
export async function installMcpToTarget(
|
|
65
78
|
target: McpTarget,
|
|
66
79
|
scope: McpScope,
|
|
67
|
-
serverEntry:
|
|
80
|
+
serverEntry: McpServerEntry,
|
|
68
81
|
options: {
|
|
69
82
|
force?: boolean;
|
|
70
83
|
dryRun?: boolean;
|
|
@@ -73,23 +86,25 @@ export async function installMcpToTarget(
|
|
|
73
86
|
}
|
|
74
87
|
): Promise<McpInstallResult> {
|
|
75
88
|
const { force = false, dryRun = false, cwd, homeDir } = options;
|
|
76
|
-
|
|
77
|
-
const
|
|
89
|
+
const normalizedEntry = normalizeMcpServerEntryForInstall(serverEntry);
|
|
90
|
+
const resolvedPaths = resolveMcpConfigPath({
|
|
78
91
|
target,
|
|
79
92
|
scope,
|
|
80
93
|
cwd,
|
|
81
94
|
homeDir,
|
|
82
95
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
const configPath = await resolveMcpConfigLocation(resolvedPaths);
|
|
97
|
+
const { configFormat } = resolvedPaths;
|
|
98
|
+
const content = (await readMcpConfigText(configPath)) ?? "";
|
|
99
|
+
const serversKey =
|
|
100
|
+
configFormat === "codex_toml" ? "mcp_servers" : getServersKey(configFormat);
|
|
101
|
+
const parsed =
|
|
102
|
+
configFormat === "codex_toml"
|
|
103
|
+
? getTomlServerEntry(content, configPath)
|
|
104
|
+
: configFormat === "yaml_standard"
|
|
105
|
+
? getYamlServerEntry(content, configPath, serversKey)
|
|
106
|
+
: getJsoncServerEntry(content, configPath, serversKey);
|
|
107
|
+
const alreadyExists = parsed.exists;
|
|
93
108
|
if (alreadyExists && !force) {
|
|
94
109
|
throw new CliError(
|
|
95
110
|
"VALIDATION",
|
|
@@ -99,33 +114,44 @@ export async function installMcpToTarget(
|
|
|
99
114
|
);
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
const
|
|
117
|
+
const entry = buildEntry(
|
|
118
|
+
normalizedEntry.command,
|
|
119
|
+
normalizedEntry.args,
|
|
120
|
+
configFormat,
|
|
121
|
+
normalizedEntry.env
|
|
122
|
+
);
|
|
123
|
+
const standardEntry = {
|
|
124
|
+
command: normalizedEntry.command,
|
|
125
|
+
args: normalizedEntry.args,
|
|
126
|
+
...(normalizedEntry.env ? { env: normalizedEntry.env } : {}),
|
|
127
|
+
};
|
|
128
|
+
const updated =
|
|
129
|
+
configFormat === "codex_toml"
|
|
130
|
+
? setTomlServerEntry(content, configPath, standardEntry)
|
|
131
|
+
: configFormat === "yaml_standard"
|
|
132
|
+
? setYamlServerEntry(content, configPath, serversKey, standardEntry)
|
|
133
|
+
: setJsoncServerEntry(content, configPath, serversKey, entry);
|
|
103
134
|
|
|
135
|
+
const wouldCreate = !alreadyExists;
|
|
104
136
|
if (dryRun) {
|
|
105
137
|
return {
|
|
106
138
|
target,
|
|
107
139
|
scope,
|
|
108
140
|
configPath,
|
|
109
141
|
action: wouldCreate ? "dry_run_create" : "dry_run_update",
|
|
110
|
-
serverEntry,
|
|
142
|
+
serverEntry: normalizedEntry,
|
|
111
143
|
};
|
|
112
144
|
}
|
|
113
145
|
|
|
114
146
|
const action = wouldCreate ? "created" : "updated";
|
|
115
|
-
|
|
116
|
-
// Build format-specific entry and add to config
|
|
117
|
-
const entry = buildEntry(serverEntry.command, serverEntry.args, configFormat);
|
|
118
|
-
setServerEntry(config, MCP_SERVER_NAME, entry, configFormat);
|
|
119
|
-
|
|
120
|
-
// Write atomically
|
|
121
|
-
await writeMcpConfig(configPath, config, { yaml: useYaml });
|
|
147
|
+
await writeMcpConfigText(configPath, updated);
|
|
122
148
|
|
|
123
149
|
return {
|
|
124
150
|
target,
|
|
125
151
|
scope,
|
|
126
152
|
configPath,
|
|
127
153
|
action,
|
|
128
|
-
serverEntry,
|
|
154
|
+
serverEntry: normalizedEntry,
|
|
129
155
|
};
|
|
130
156
|
}
|
|
131
157
|
|
|
@@ -145,24 +171,36 @@ function safeGetGlobals(): { json: boolean; quiet: boolean } {
|
|
|
145
171
|
*/
|
|
146
172
|
export async function installMcp(opts: InstallOptions = {}): Promise<void> {
|
|
147
173
|
const target = opts.target ?? "claude-desktop";
|
|
148
|
-
const scope = opts.scope ??
|
|
174
|
+
const scope = opts.scope ?? getDefaultTargetScope(target);
|
|
149
175
|
const force = opts.force ?? false;
|
|
150
176
|
const dryRun = opts.dryRun ?? false;
|
|
151
177
|
const enableWrite = opts.enableWrite ?? false;
|
|
178
|
+
const indexName = opts.indexName ?? DEFAULT_INDEX_NAME;
|
|
179
|
+
const paths = getConfigPaths();
|
|
180
|
+
const configPath = toAbsolutePath(
|
|
181
|
+
opts.configPath ?? paths.configFile,
|
|
182
|
+
opts.cwd
|
|
183
|
+
);
|
|
152
184
|
const globals = safeGetGlobals();
|
|
153
185
|
const json = opts.json ?? globals.json;
|
|
154
186
|
const quiet = opts.quiet ?? globals.quiet;
|
|
155
187
|
|
|
156
188
|
// Validate scope - only some targets support project scope
|
|
157
|
-
if (
|
|
189
|
+
if (!getTargetScopes(target).includes(scope)) {
|
|
158
190
|
throw new CliError(
|
|
159
191
|
"VALIDATION",
|
|
160
|
-
`${getTargetDisplayName(target)} does not support
|
|
192
|
+
`${getTargetDisplayName(target)} does not support ${scope} scope.`
|
|
161
193
|
);
|
|
162
194
|
}
|
|
163
195
|
|
|
164
196
|
// Build server entry (uses process.execPath, always succeeds)
|
|
165
|
-
const serverEntry = buildMcpServerEntry({
|
|
197
|
+
const serverEntry = buildMcpServerEntry({
|
|
198
|
+
enableWrite,
|
|
199
|
+
indexName,
|
|
200
|
+
configPath,
|
|
201
|
+
dataDir: toAbsolutePath(paths.dataDir, opts.cwd),
|
|
202
|
+
cacheDir: toAbsolutePath(paths.cacheDir, opts.cwd),
|
|
203
|
+
});
|
|
166
204
|
|
|
167
205
|
// Install
|
|
168
206
|
const result = await installMcpToTarget(target, scope, serverEntry, {
|