@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,9 +5,17 @@
|
|
|
5
5
|
* @module src/cli/commands/mcp/paths
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
9
8
|
import { homedir, platform } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
import type { ConnectorWorkspaceEnvironment } from "../../../core/connector-environment";
|
|
12
|
+
|
|
13
|
+
import { resolveDirs } from "../../../app/constants";
|
|
14
|
+
import { assertValidIndexName } from "../../../app/index-name";
|
|
15
|
+
import { getCurrentGnoEntrypoint } from "../../../core/runtime-entrypoint";
|
|
16
|
+
import { getTargetDisplayName } from "./target-display.js";
|
|
17
|
+
|
|
18
|
+
export { getTargetDisplayName } from "./target-display.js";
|
|
11
19
|
|
|
12
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
21
|
// Types
|
|
@@ -40,7 +48,8 @@ export type McpConfigFormat =
|
|
|
40
48
|
| "context_servers"
|
|
41
49
|
| "mcp"
|
|
42
50
|
| "amp_mcp"
|
|
43
|
-
| "yaml_standard"
|
|
51
|
+
| "yaml_standard"
|
|
52
|
+
| "codex_toml";
|
|
44
53
|
|
|
45
54
|
export interface McpConfigPaths {
|
|
46
55
|
/** Config file path */
|
|
@@ -49,11 +58,22 @@ export interface McpConfigPaths {
|
|
|
49
58
|
supportsProjectScope: boolean;
|
|
50
59
|
/** Config format for this target */
|
|
51
60
|
configFormat: McpConfigFormat;
|
|
61
|
+
/** Supported alternate filenames, checked without creating duplicates. */
|
|
62
|
+
alternativeConfigPaths?: string[];
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
export interface McpServerEntry {
|
|
55
66
|
command: string;
|
|
56
67
|
args: string[];
|
|
68
|
+
env?: ConnectorWorkspaceEnvironment;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface McpServerEntryOptions {
|
|
72
|
+
enableWrite?: boolean;
|
|
73
|
+
indexName?: string;
|
|
74
|
+
configPath?: string;
|
|
75
|
+
dataDir?: string;
|
|
76
|
+
cacheDir?: string;
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
export interface McpPathOptions {
|
|
@@ -63,6 +83,10 @@ export interface McpPathOptions {
|
|
|
63
83
|
cwd?: string;
|
|
64
84
|
/** Override home dir (testing) */
|
|
65
85
|
homeDir?: string;
|
|
86
|
+
/** Override runtime platform (testing) */
|
|
87
|
+
platform?: NodeJS.Platform;
|
|
88
|
+
/** Override process environment used for platform config directories (testing) */
|
|
89
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -85,18 +109,43 @@ export const MCP_TARGETS: McpTarget[] = [
|
|
|
85
109
|
"librechat",
|
|
86
110
|
];
|
|
87
111
|
|
|
88
|
-
/**
|
|
89
|
-
export const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
]
|
|
112
|
+
/** Canonical target scope support. LibreChat has no user-global config. */
|
|
113
|
+
export const MCP_TARGET_SCOPES: Readonly<
|
|
114
|
+
Record<McpTarget, readonly McpScope[]>
|
|
115
|
+
> = {
|
|
116
|
+
"claude-desktop": ["user"],
|
|
117
|
+
"claude-code": ["user", "project"],
|
|
118
|
+
codex: ["user", "project"],
|
|
119
|
+
cursor: ["user", "project"],
|
|
120
|
+
zed: ["user"],
|
|
121
|
+
windsurf: ["user"],
|
|
122
|
+
opencode: ["user", "project"],
|
|
123
|
+
amp: ["user"],
|
|
124
|
+
lmstudio: ["user"],
|
|
125
|
+
librechat: ["project"],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Targets that support project scope. */
|
|
129
|
+
export const TARGETS_WITH_PROJECT_SCOPE: McpTarget[] = MCP_TARGETS.filter(
|
|
130
|
+
(target) => MCP_TARGET_SCOPES[target].includes("project")
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
/** Return the supported scopes in canonical display/operation order. */
|
|
134
|
+
export function getTargetScopes(target: McpTarget): readonly McpScope[] {
|
|
135
|
+
return MCP_TARGET_SCOPES[target];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Default to user scope when available, otherwise the sole supported scope. */
|
|
139
|
+
export function getDefaultTargetScope(target: McpTarget): McpScope {
|
|
140
|
+
const scopes = getTargetScopes(target);
|
|
141
|
+
return scopes.includes("user") ? "user" : (scopes[0] ?? "user");
|
|
142
|
+
}
|
|
96
143
|
|
|
97
144
|
/** Get config format for a target */
|
|
98
145
|
export function getTargetConfigFormat(target: McpTarget): McpConfigFormat {
|
|
99
146
|
switch (target) {
|
|
147
|
+
case "codex":
|
|
148
|
+
return "codex_toml";
|
|
100
149
|
case "zed":
|
|
101
150
|
return "context_servers";
|
|
102
151
|
case "opencode":
|
|
@@ -110,9 +159,6 @@ export function getTargetConfigFormat(target: McpTarget): McpConfigFormat {
|
|
|
110
159
|
}
|
|
111
160
|
}
|
|
112
161
|
|
|
113
|
-
/** Regex to extract entry script path from command path */
|
|
114
|
-
const COMMANDS_PATH_PATTERN = /\/commands\/.*$/;
|
|
115
|
-
|
|
116
162
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
163
|
// Config Path Resolution
|
|
118
164
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -156,9 +202,9 @@ function resolveClaudeCodePath(
|
|
|
156
202
|
*/
|
|
157
203
|
function resolveCodexPath(scope: McpScope, home: string, cwd: string): string {
|
|
158
204
|
if (scope === "user") {
|
|
159
|
-
return join(home, ".codex.
|
|
205
|
+
return join(home, ".codex/config.toml");
|
|
160
206
|
}
|
|
161
|
-
return join(cwd, ".codex
|
|
207
|
+
return join(cwd, ".codex/config.toml");
|
|
162
208
|
}
|
|
163
209
|
|
|
164
210
|
/**
|
|
@@ -175,17 +221,18 @@ function resolveCursorPath(scope: McpScope, home: string, cwd: string): string {
|
|
|
175
221
|
return join(cwd, ".cursor/mcp.json");
|
|
176
222
|
}
|
|
177
223
|
|
|
178
|
-
/**
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
224
|
+
/** Resolve the user-level Zed config path. */
|
|
225
|
+
function resolveZedPath(
|
|
226
|
+
home: string,
|
|
227
|
+
plat: NodeJS.Platform,
|
|
228
|
+
env: Readonly<Record<string, string | undefined>>
|
|
229
|
+
): string {
|
|
183
230
|
if (plat === "win32") {
|
|
184
|
-
|
|
185
|
-
return join(
|
|
231
|
+
const appData = env.APPDATA?.trim() || join(home, "AppData", "Roaming");
|
|
232
|
+
return join(appData, "Zed", "settings.json");
|
|
186
233
|
}
|
|
187
234
|
// macOS and Linux use XDG or fallback
|
|
188
|
-
const xdgConfig =
|
|
235
|
+
const xdgConfig = env.XDG_CONFIG_HOME;
|
|
189
236
|
if (xdgConfig) {
|
|
190
237
|
return join(xdgConfig, "zed/settings.json");
|
|
191
238
|
}
|
|
@@ -214,9 +261,9 @@ function resolveOpenCodePath(
|
|
|
214
261
|
if (scope === "user") {
|
|
215
262
|
const plat = platform();
|
|
216
263
|
if (plat === "win32") {
|
|
217
|
-
return join(home, ".config", "opencode", "
|
|
264
|
+
return join(home, ".config", "opencode", "opencode.json");
|
|
218
265
|
}
|
|
219
|
-
return join(home, ".config/opencode/
|
|
266
|
+
return join(home, ".config/opencode/opencode.json");
|
|
220
267
|
}
|
|
221
268
|
// Project scope: opencode.json in project root
|
|
222
269
|
return join(cwd, "opencode.json");
|
|
@@ -256,12 +303,15 @@ function resolveLibreChatPath(cwd: string): string {
|
|
|
256
303
|
* Resolve MCP config path for a given target and scope.
|
|
257
304
|
*/
|
|
258
305
|
export function resolveMcpConfigPath(opts: McpPathOptions): McpConfigPaths {
|
|
259
|
-
const {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
306
|
+
const { target, cwd = process.cwd(), homeDir = homedir() } = opts;
|
|
307
|
+
const runtimePlatform = opts.platform ?? platform();
|
|
308
|
+
const runtimeEnv = opts.env ?? process.env;
|
|
309
|
+
const scope = opts.scope ?? getDefaultTargetScope(target);
|
|
310
|
+
if (!getTargetScopes(target).includes(scope)) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`${getTargetDisplayName(target)} does not support ${scope} scope.`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
265
315
|
|
|
266
316
|
const configFormat = getTargetConfigFormat(target);
|
|
267
317
|
const supportsProjectScope = TARGETS_WITH_PROJECT_SCOPE.includes(target);
|
|
@@ -293,7 +343,7 @@ export function resolveMcpConfigPath(opts: McpPathOptions): McpConfigPaths {
|
|
|
293
343
|
};
|
|
294
344
|
case "zed":
|
|
295
345
|
return {
|
|
296
|
-
configPath: resolveZedPath(homeDir),
|
|
346
|
+
configPath: resolveZedPath(homeDir, runtimePlatform, runtimeEnv),
|
|
297
347
|
supportsProjectScope,
|
|
298
348
|
configFormat,
|
|
299
349
|
};
|
|
@@ -303,18 +353,24 @@ export function resolveMcpConfigPath(opts: McpPathOptions): McpConfigPaths {
|
|
|
303
353
|
supportsProjectScope,
|
|
304
354
|
configFormat,
|
|
305
355
|
};
|
|
306
|
-
case "opencode":
|
|
356
|
+
case "opencode": {
|
|
357
|
+
const configPath = resolveOpenCodePath(scope, homeDir, cwd);
|
|
307
358
|
return {
|
|
308
|
-
configPath
|
|
359
|
+
configPath,
|
|
360
|
+
alternativeConfigPaths: [configPath.replace(/\.json$/u, ".jsonc")],
|
|
309
361
|
supportsProjectScope,
|
|
310
362
|
configFormat,
|
|
311
363
|
};
|
|
312
|
-
|
|
364
|
+
}
|
|
365
|
+
case "amp": {
|
|
366
|
+
const configPath = resolveAmpPath(homeDir);
|
|
313
367
|
return {
|
|
314
|
-
configPath
|
|
368
|
+
configPath,
|
|
369
|
+
alternativeConfigPaths: [configPath.replace(/\.json$/u, ".jsonc")],
|
|
315
370
|
supportsProjectScope,
|
|
316
371
|
configFormat,
|
|
317
372
|
};
|
|
373
|
+
}
|
|
318
374
|
case "lmstudio":
|
|
319
375
|
return {
|
|
320
376
|
configPath: resolveLmStudioPath(homeDir),
|
|
@@ -340,7 +396,7 @@ export function resolveMcpConfigPath(opts: McpPathOptions): McpConfigPaths {
|
|
|
340
396
|
export function resolveAllMcpPaths(
|
|
341
397
|
scope: McpScope | "all" = "all",
|
|
342
398
|
target: McpTarget | "all" = "all",
|
|
343
|
-
overrides?:
|
|
399
|
+
overrides?: Pick<McpPathOptions, "cwd" | "env" | "homeDir" | "platform">
|
|
344
400
|
): Array<{ target: McpTarget; scope: McpScope; paths: McpConfigPaths }> {
|
|
345
401
|
const targets: McpTarget[] = target === "all" ? MCP_TARGETS : [target];
|
|
346
402
|
const results: Array<{
|
|
@@ -350,28 +406,29 @@ export function resolveAllMcpPaths(
|
|
|
350
406
|
}> = [];
|
|
351
407
|
|
|
352
408
|
for (const t of targets) {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (
|
|
409
|
+
const supportedScopes = getTargetScopes(t);
|
|
410
|
+
const scopes =
|
|
411
|
+
scope === "all"
|
|
412
|
+
? supportedScopes
|
|
413
|
+
: supportedScopes.includes(scope)
|
|
414
|
+
? [scope]
|
|
415
|
+
: [];
|
|
416
|
+
const seenConfigPaths = new Set<string>();
|
|
417
|
+
for (const targetScope of scopes) {
|
|
418
|
+
const paths = resolveMcpConfigPath({
|
|
419
|
+
target: t,
|
|
420
|
+
scope: targetScope,
|
|
421
|
+
...overrides,
|
|
422
|
+
});
|
|
423
|
+
const configIdentity = resolve(paths.configPath);
|
|
424
|
+
if (seenConfigPaths.has(configIdentity)) {
|
|
369
425
|
continue;
|
|
370
426
|
}
|
|
427
|
+
seenConfigPaths.add(configIdentity);
|
|
371
428
|
results.push({
|
|
372
429
|
target: t,
|
|
373
|
-
scope:
|
|
374
|
-
paths
|
|
430
|
+
scope: targetScope,
|
|
431
|
+
paths,
|
|
375
432
|
});
|
|
376
433
|
}
|
|
377
434
|
}
|
|
@@ -394,93 +451,41 @@ export function findBunPath(): string {
|
|
|
394
451
|
}
|
|
395
452
|
|
|
396
453
|
/**
|
|
397
|
-
*
|
|
398
|
-
* Uses absolute paths because
|
|
399
|
-
* Cross-platform: avoids shelling out to `which`.
|
|
454
|
+
* Return an MCP server entry bound to the GNO runtime installing it.
|
|
455
|
+
* Uses absolute paths because desktop clients have a limited PATH.
|
|
400
456
|
*/
|
|
401
457
|
export function buildMcpServerEntry(
|
|
402
|
-
options: {
|
|
403
|
-
enableWrite?: boolean;
|
|
404
|
-
} = {}
|
|
458
|
+
options: McpServerEntryOptions = {}
|
|
405
459
|
): McpServerEntry {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const isWindows = platform() === "win32";
|
|
409
|
-
|
|
410
|
-
// 1. Check if running from source (dev mode)
|
|
411
|
-
const scriptPath = process.argv[1];
|
|
412
|
-
if (
|
|
413
|
-
scriptPath?.includes("/gno/src/cli/") ||
|
|
414
|
-
scriptPath?.includes("\\gno\\src\\cli\\")
|
|
415
|
-
) {
|
|
416
|
-
// Dev mode: run the entry script directly with bun
|
|
417
|
-
const entryScript = scriptPath.replace(COMMANDS_PATH_PATTERN, "/index.ts");
|
|
418
|
-
const args = ["run", entryScript, "mcp"];
|
|
419
|
-
if (options.enableWrite) {
|
|
420
|
-
args.push("--enable-write");
|
|
421
|
-
}
|
|
422
|
-
return { command: bunPath, args };
|
|
460
|
+
if (options.indexName !== undefined) {
|
|
461
|
+
assertValidIndexName(options.indexName);
|
|
423
462
|
}
|
|
463
|
+
const bunPath = findBunPath();
|
|
464
|
+
const args = ["run", getCurrentGnoEntrypoint()];
|
|
465
|
+
appendMcpArguments(args, options);
|
|
466
|
+
const dirs = resolveDirs();
|
|
467
|
+
return {
|
|
468
|
+
command: bunPath,
|
|
469
|
+
args,
|
|
470
|
+
env: {
|
|
471
|
+
GNO_DATA_DIR: resolve(options.dataDir ?? dirs.data),
|
|
472
|
+
GNO_CACHE_DIR: resolve(options.cacheDir ?? dirs.cache),
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
424
476
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
: [
|
|
432
|
-
join(home, ".bun/bin/gno"),
|
|
433
|
-
"/usr/local/bin/gno",
|
|
434
|
-
"/opt/homebrew/bin/gno",
|
|
435
|
-
];
|
|
436
|
-
|
|
437
|
-
for (const gnoPath of gnoCandidates) {
|
|
438
|
-
if (existsSync(gnoPath)) {
|
|
439
|
-
const args = [gnoPath, "mcp"];
|
|
440
|
-
if (options.enableWrite) {
|
|
441
|
-
args.push("--enable-write");
|
|
442
|
-
}
|
|
443
|
-
return { command: bunPath, args };
|
|
444
|
-
}
|
|
477
|
+
function appendMcpArguments(
|
|
478
|
+
args: string[],
|
|
479
|
+
options: McpServerEntryOptions
|
|
480
|
+
): void {
|
|
481
|
+
if (options.indexName) {
|
|
482
|
+
args.push("--index", options.indexName);
|
|
445
483
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
484
|
+
if (options.configPath) {
|
|
485
|
+
args.push("--config", resolve(options.configPath));
|
|
486
|
+
}
|
|
487
|
+
args.push("mcp");
|
|
450
488
|
if (options.enableWrite) {
|
|
451
489
|
args.push("--enable-write");
|
|
452
490
|
}
|
|
453
|
-
return { command: bunPath, args };
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Get display name for a target.
|
|
458
|
-
*/
|
|
459
|
-
export function getTargetDisplayName(target: McpTarget): string {
|
|
460
|
-
switch (target) {
|
|
461
|
-
case "claude-desktop":
|
|
462
|
-
return "Claude Desktop";
|
|
463
|
-
case "claude-code":
|
|
464
|
-
return "Claude Code";
|
|
465
|
-
case "codex":
|
|
466
|
-
return "Codex";
|
|
467
|
-
case "cursor":
|
|
468
|
-
return "Cursor";
|
|
469
|
-
case "zed":
|
|
470
|
-
return "Zed";
|
|
471
|
-
case "windsurf":
|
|
472
|
-
return "Windsurf";
|
|
473
|
-
case "opencode":
|
|
474
|
-
return "OpenCode";
|
|
475
|
-
case "amp":
|
|
476
|
-
return "Amp";
|
|
477
|
-
case "lmstudio":
|
|
478
|
-
return "LM Studio";
|
|
479
|
-
case "librechat":
|
|
480
|
-
return "LibreChat";
|
|
481
|
-
default: {
|
|
482
|
-
const _exhaustive: never = target;
|
|
483
|
-
throw new Error(`Unknown target: ${String(_exhaustive)}`);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
491
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Structural validation at the MCP client-config write boundary. */
|
|
2
|
+
|
|
3
|
+
// node:path has no Bun equivalent for portable absolute-path validation.
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { McpServerEntry } from "./paths.js";
|
|
7
|
+
|
|
8
|
+
import { normalizeConnectorWorkspaceEnvironment } from "../../../core/connector-environment.js";
|
|
9
|
+
import { CliError } from "../../errors.js";
|
|
10
|
+
|
|
11
|
+
function hasControlCharacter(value: string): boolean {
|
|
12
|
+
for (const character of value) {
|
|
13
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
14
|
+
if (codePoint <= 31 || codePoint === 127) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeMcpServerEntryForInstall(
|
|
22
|
+
input: unknown
|
|
23
|
+
): McpServerEntry {
|
|
24
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
25
|
+
throw new CliError("VALIDATION", "MCP server entry must be an object.");
|
|
26
|
+
}
|
|
27
|
+
const prototype = Object.getPrototypeOf(input);
|
|
28
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
29
|
+
throw new CliError(
|
|
30
|
+
"VALIDATION",
|
|
31
|
+
"MCP server entry must be a plain object."
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const record = input as Record<string, unknown>;
|
|
35
|
+
if (
|
|
36
|
+
Object.keys(record).some(
|
|
37
|
+
(key) => key !== "command" && key !== "args" && key !== "env"
|
|
38
|
+
) ||
|
|
39
|
+
typeof record.command !== "string" ||
|
|
40
|
+
!record.command ||
|
|
41
|
+
hasControlCharacter(record.command) ||
|
|
42
|
+
!isAbsolute(record.command) ||
|
|
43
|
+
!Array.isArray(record.args) ||
|
|
44
|
+
record.args.length === 0 ||
|
|
45
|
+
!record.args.every(
|
|
46
|
+
(argument) =>
|
|
47
|
+
typeof argument === "string" &&
|
|
48
|
+
argument.length > 0 &&
|
|
49
|
+
!hasControlCharacter(argument)
|
|
50
|
+
)
|
|
51
|
+
) {
|
|
52
|
+
throw new CliError(
|
|
53
|
+
"VALIDATION",
|
|
54
|
+
"MCP server entry requires an absolute command and non-empty string arguments."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const env = normalizeConnectorWorkspaceEnvironment(record.env);
|
|
58
|
+
if (env === null) {
|
|
59
|
+
throw new CliError("VALIDATION", "Invalid MCP workspace environment.");
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
command: record.command,
|
|
63
|
+
args: record.args,
|
|
64
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|