@x-code-cli/core 0.2.10 → 0.3.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/dist/agent/compression.d.ts +12 -2
- package/dist/agent/compression.d.ts.map +1 -1
- package/dist/agent/compression.js +51 -2
- package/dist/agent/compression.js.map +1 -1
- package/dist/agent/file-ingest.js +2 -2
- package/dist/agent/file-ingest.js.map +1 -1
- package/dist/agent/loop-state.d.ts +3 -2
- package/dist/agent/loop-state.d.ts.map +1 -1
- package/dist/agent/loop-state.js.map +1 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +134 -5
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/memory-extractor.js +5 -5
- package/dist/agent/memory-extractor.js.map +1 -1
- package/dist/agent/plan-storage.js +1 -1
- package/dist/agent/plan-storage.js.map +1 -1
- package/dist/agent/sub-agents/index.d.ts +2 -1
- package/dist/agent/sub-agents/index.d.ts.map +1 -1
- package/dist/agent/sub-agents/index.js +1 -1
- package/dist/agent/sub-agents/index.js.map +1 -1
- package/dist/agent/sub-agents/loader.d.ts +13 -3
- package/dist/agent/sub-agents/loader.d.ts.map +1 -1
- package/dist/agent/sub-agents/loader.js +36 -9
- package/dist/agent/sub-agents/loader.js.map +1 -1
- package/dist/agent/sub-agents/registry.d.ts +18 -1
- package/dist/agent/sub-agents/registry.d.ts.map +1 -1
- package/dist/agent/sub-agents/registry.js +38 -5
- package/dist/agent/sub-agents/registry.js.map +1 -1
- package/dist/agent/sub-agents/runner.d.ts.map +1 -1
- package/dist/agent/sub-agents/runner.js +45 -1
- package/dist/agent/sub-agents/runner.js.map +1 -1
- package/dist/agent/sub-agents/types.d.ts +4 -1
- package/dist/agent/sub-agents/types.d.ts.map +1 -1
- package/dist/agent/system-prompt.d.ts +21 -0
- package/dist/agent/system-prompt.d.ts.map +1 -1
- package/dist/agent/system-prompt.js +68 -2
- package/dist/agent/system-prompt.js.map +1 -1
- package/dist/agent/tool-execution.d.ts.map +1 -1
- package/dist/agent/tool-execution.js +220 -1
- package/dist/agent/tool-execution.js.map +1 -1
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +3 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/loader.d.ts +13 -0
- package/dist/commands/loader.d.ts.map +1 -0
- package/dist/commands/loader.js +93 -0
- package/dist/commands/loader.js.map +1 -0
- package/dist/commands/registry.d.ts +44 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +102 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/types.d.ts +23 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +26 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +12 -10
- package/dist/config/index.js.map +1 -1
- package/dist/hooks/bus.d.ts +54 -0
- package/dist/hooks/bus.d.ts.map +1 -0
- package/dist/hooks/bus.js +165 -0
- package/dist/hooks/bus.js.map +1 -0
- package/dist/hooks/config-schema.d.ts +854 -0
- package/dist/hooks/config-schema.d.ts.map +1 -0
- package/dist/hooks/config-schema.js +79 -0
- package/dist/hooks/config-schema.js.map +1 -0
- package/dist/hooks/executor.d.ts +16 -0
- package/dist/hooks/executor.d.ts.map +1 -0
- package/dist/hooks/executor.js +183 -0
- package/dist/hooks/executor.js.map +1 -0
- package/dist/hooks/index.d.ts +10 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/registry.d.ts +23 -0
- package/dist/hooks/registry.d.ts.map +1 -0
- package/dist/hooks/registry.js +49 -0
- package/dist/hooks/registry.js.map +1 -0
- package/dist/hooks/types.d.ts +165 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +25 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/hooks/variables.d.ts +22 -0
- package/dist/hooks/variables.d.ts.map +1 -0
- package/dist/hooks/variables.js +80 -0
- package/dist/hooks/variables.js.map +1 -0
- package/dist/index.d.ts +56 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/dist/knowledge/auto-memory.d.ts +1 -1
- package/dist/knowledge/auto-memory.d.ts.map +1 -1
- package/dist/knowledge/auto-memory.js +10 -10
- package/dist/knowledge/auto-memory.js.map +1 -1
- package/dist/knowledge/loader.js +12 -12
- package/dist/knowledge/loader.js.map +1 -1
- package/dist/mcp/arg-parser.d.ts +49 -0
- package/dist/mcp/arg-parser.d.ts.map +1 -0
- package/dist/mcp/arg-parser.js +357 -0
- package/dist/mcp/arg-parser.js.map +1 -0
- package/dist/mcp/client.d.ts +73 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +376 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/config-schema.d.ts +64 -0
- package/dist/mcp/config-schema.d.ts.map +1 -0
- package/dist/mcp/config-schema.js +86 -0
- package/dist/mcp/config-schema.js.map +1 -0
- package/dist/mcp/config-writer.d.ts +41 -0
- package/dist/mcp/config-writer.d.ts.map +1 -0
- package/dist/mcp/config-writer.js +138 -0
- package/dist/mcp/config-writer.js.map +1 -0
- package/dist/mcp/env-safety.d.ts +12 -0
- package/dist/mcp/env-safety.d.ts.map +1 -0
- package/dist/mcp/env-safety.js +80 -0
- package/dist/mcp/env-safety.js.map +1 -0
- package/dist/mcp/expand-env.d.ts +14 -0
- package/dist/mcp/expand-env.d.ts.map +1 -0
- package/dist/mcp/expand-env.js +52 -0
- package/dist/mcp/expand-env.js.map +1 -0
- package/dist/mcp/loader.d.ts +81 -0
- package/dist/mcp/loader.d.ts.map +1 -0
- package/dist/mcp/loader.js +223 -0
- package/dist/mcp/loader.js.map +1 -0
- package/dist/mcp/name-mangling.d.ts +11 -0
- package/dist/mcp/name-mangling.d.ts.map +1 -0
- package/dist/mcp/name-mangling.js +82 -0
- package/dist/mcp/name-mangling.js.map +1 -0
- package/dist/mcp/oauth/callback-server.d.ts +25 -0
- package/dist/mcp/oauth/callback-server.d.ts.map +1 -0
- package/dist/mcp/oauth/callback-server.js +118 -0
- package/dist/mcp/oauth/callback-server.js.map +1 -0
- package/dist/mcp/oauth/provider.d.ts +80 -0
- package/dist/mcp/oauth/provider.d.ts.map +1 -0
- package/dist/mcp/oauth/provider.js +292 -0
- package/dist/mcp/oauth/provider.js.map +1 -0
- package/dist/mcp/oauth/token-storage.d.ts +42 -0
- package/dist/mcp/oauth/token-storage.d.ts.map +1 -0
- package/dist/mcp/oauth/token-storage.js +121 -0
- package/dist/mcp/oauth/token-storage.js.map +1 -0
- package/dist/mcp/permissions.d.ts +28 -0
- package/dist/mcp/permissions.d.ts.map +1 -0
- package/dist/mcp/permissions.js +105 -0
- package/dist/mcp/permissions.js.map +1 -0
- package/dist/mcp/registry.d.ts +150 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +334 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/resources.d.ts +7 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +40 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/tool-bridge.d.ts +16 -0
- package/dist/mcp/tool-bridge.d.ts.map +1 -0
- package/dist/mcp/tool-bridge.js +56 -0
- package/dist/mcp/tool-bridge.js.map +1 -0
- package/dist/mcp/trust.d.ts +31 -0
- package/dist/mcp/trust.d.ts.map +1 -0
- package/dist/mcp/trust.js +103 -0
- package/dist/mcp/trust.js.map +1 -0
- package/dist/mcp/types.d.ts +73 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +13 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/permissions/session-store.d.ts +4 -1
- package/dist/permissions/session-store.d.ts.map +1 -1
- package/dist/permissions/session-store.js +6 -1
- package/dist/permissions/session-store.js.map +1 -1
- package/dist/plugins/consent.d.ts +87 -0
- package/dist/plugins/consent.d.ts.map +1 -0
- package/dist/plugins/consent.js +181 -0
- package/dist/plugins/consent.js.map +1 -0
- package/dist/plugins/enable-state.d.ts +34 -0
- package/dist/plugins/enable-state.d.ts.map +1 -0
- package/dist/plugins/enable-state.js +159 -0
- package/dist/plugins/enable-state.js.map +1 -0
- package/dist/plugins/installer.d.ts +64 -0
- package/dist/plugins/installer.d.ts.map +1 -0
- package/dist/plugins/installer.js +416 -0
- package/dist/plugins/installer.js.map +1 -0
- package/dist/plugins/integration.d.ts +91 -0
- package/dist/plugins/integration.d.ts.map +1 -0
- package/dist/plugins/integration.js +233 -0
- package/dist/plugins/integration.js.map +1 -0
- package/dist/plugins/loader.d.ts +69 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +243 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/manifest.d.ts +23 -0
- package/dist/plugins/manifest.d.ts.map +1 -0
- package/dist/plugins/manifest.js +143 -0
- package/dist/plugins/manifest.js.map +1 -0
- package/dist/plugins/marketplace.d.ts +100 -0
- package/dist/plugins/marketplace.d.ts.map +1 -0
- package/dist/plugins/marketplace.js +529 -0
- package/dist/plugins/marketplace.js.map +1 -0
- package/dist/plugins/paths.d.ts +44 -0
- package/dist/plugins/paths.d.ts.map +1 -0
- package/dist/plugins/paths.js +89 -0
- package/dist/plugins/paths.js.map +1 -0
- package/dist/plugins/refresh.d.ts +61 -0
- package/dist/plugins/refresh.d.ts.map +1 -0
- package/dist/plugins/refresh.js +98 -0
- package/dist/plugins/refresh.js.map +1 -0
- package/dist/plugins/registry.d.ts +40 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/registry.js +80 -0
- package/dist/plugins/registry.js.map +1 -0
- package/dist/plugins/types.d.ts +225 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +16 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/plugins/user-config.d.ts +22 -0
- package/dist/plugins/user-config.d.ts.map +1 -0
- package/dist/plugins/user-config.js +96 -0
- package/dist/plugins/user-config.js.map +1 -0
- package/dist/skills/loader.d.ts +19 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +197 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/registry.d.ts +74 -0
- package/dist/skills/registry.d.ts.map +1 -0
- package/dist/skills/registry.js +136 -0
- package/dist/skills/registry.js.map +1 -0
- package/dist/skills/settings.d.ts +13 -0
- package/dist/skills/settings.d.ts.map +1 -0
- package/dist/skills/settings.js +100 -0
- package/dist/skills/settings.js.map +1 -0
- package/dist/tools/activate-skill.d.ts +5 -0
- package/dist/tools/activate-skill.d.ts.map +1 -0
- package/dist/tools/activate-skill.js +33 -0
- package/dist/tools/activate-skill.js.map +1 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/todo-write.d.ts +1 -1
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +2 -1
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/types/index.d.ts +46 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils.d.ts +23 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +76 -20
- package/dist/utils.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +47 -0
- package/dist/version.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// @x-code-cli/core — Read/write `mcpServers` in user / project config.json
|
|
2
|
+
//
|
|
3
|
+
// Drives `/mcp add` and `/mcp remove`. The job is small but error-prone:
|
|
4
|
+
// - preserve unrelated top-level fields (theme, model, thinking, etc.)
|
|
5
|
+
// - preserve other mcpServers entries when adding/removing one
|
|
6
|
+
// - write atomically so a Ctrl-C mid-write can't corrupt the file
|
|
7
|
+
// - never read once, write later — re-read at write time so we don't
|
|
8
|
+
// stomp on a concurrent edit (rare but cheap to guard against)
|
|
9
|
+
//
|
|
10
|
+
// The writer validates every config it persists against the same Zod
|
|
11
|
+
// schema the loader uses, so add-json input that would be rejected at
|
|
12
|
+
// load time is rejected here instead — fail-fast at the entry point.
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { getUserConfigPath } from '../config/index.js';
|
|
16
|
+
import { XCODE_DIR } from '../utils.js';
|
|
17
|
+
import { parseServerConfig } from './config-schema.js';
|
|
18
|
+
/** Where each scope's config.json lives. Mirrors the same paths the loader
|
|
19
|
+
* reads from, so a write here is guaranteed to be picked up on the next
|
|
20
|
+
* load (or `/mcp refresh`). */
|
|
21
|
+
export function getConfigPath(scope, cwd) {
|
|
22
|
+
if (scope === 'user')
|
|
23
|
+
return getUserConfigPath();
|
|
24
|
+
return path.join(cwd, XCODE_DIR, 'config.json');
|
|
25
|
+
}
|
|
26
|
+
/** Read the parsed JSON object at the given scope. Returns `{}` when the
|
|
27
|
+
* file doesn't exist, is empty, or is malformed — the caller treats
|
|
28
|
+
* those uniformly as "no MCP servers configured here yet". */
|
|
29
|
+
async function readConfigObject(scope, cwd) {
|
|
30
|
+
const file = getConfigPath(scope, cwd);
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = await fs.readFile(file, 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Malformed JSON. We deliberately don't overwrite without a parse —
|
|
46
|
+
// bail and let the caller surface an error. Returning {} here would
|
|
47
|
+
// mask a corrupt config and writing would clobber whatever was there.
|
|
48
|
+
throw new Error(`Config file at ${file} is not valid JSON. Fix it manually before running /mcp add or /mcp remove.`);
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
/** Atomic JSON write: write to tmp, then rename. Trailing newline + 2-space
|
|
53
|
+
* indent matches the convention used elsewhere (saveUserConfig). */
|
|
54
|
+
async function writeConfigObject(scope, cwd, obj) {
|
|
55
|
+
const file = getConfigPath(scope, cwd);
|
|
56
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
57
|
+
const tmp = file + '.tmp';
|
|
58
|
+
await fs.writeFile(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
|
|
59
|
+
await fs.rename(tmp, file);
|
|
60
|
+
}
|
|
61
|
+
export async function detectScope(name, cwd) {
|
|
62
|
+
const [user, project] = await Promise.all([serverExists(name, 'user', cwd), serverExists(name, 'project', cwd)]);
|
|
63
|
+
if (user && project)
|
|
64
|
+
return { kind: 'both' };
|
|
65
|
+
if (user)
|
|
66
|
+
return { kind: 'user' };
|
|
67
|
+
if (project)
|
|
68
|
+
return { kind: 'project' };
|
|
69
|
+
return { kind: 'not-found' };
|
|
70
|
+
}
|
|
71
|
+
export async function serverExists(name, scope, cwd) {
|
|
72
|
+
const obj = await readConfigObject(scope, cwd);
|
|
73
|
+
const servers = obj.mcpServers;
|
|
74
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers))
|
|
75
|
+
return false;
|
|
76
|
+
return Object.prototype.hasOwnProperty.call(servers, name);
|
|
77
|
+
}
|
|
78
|
+
/** Add a server to the given scope's config.json. Refuses to overwrite —
|
|
79
|
+
* caller must check duplicates first via `serverExists` and surface a
|
|
80
|
+
* helpful error including current vs. attempted config. */
|
|
81
|
+
export async function writeServerToConfig(name, config, scope, cwd) {
|
|
82
|
+
// Validate first. Bad JSON via /mcp add-json shouldn't get written and
|
|
83
|
+
// then explode at next launch — fail at the entry point with a clear
|
|
84
|
+
// schema error.
|
|
85
|
+
const validated = parseServerConfig(name, config);
|
|
86
|
+
const obj = await readConfigObject(scope, cwd);
|
|
87
|
+
const existing = obj.mcpServers;
|
|
88
|
+
const servers = existing && typeof existing === 'object' && !Array.isArray(existing)
|
|
89
|
+
? { ...existing }
|
|
90
|
+
: {};
|
|
91
|
+
servers[name] = validated;
|
|
92
|
+
obj.mcpServers = servers;
|
|
93
|
+
await writeConfigObject(scope, cwd, obj);
|
|
94
|
+
return { path: getConfigPath(scope, cwd) };
|
|
95
|
+
}
|
|
96
|
+
/** Remove a server from the given scope's config.json. Idempotent: returns
|
|
97
|
+
* `removed: false` when the name wasn't present (or the file didn't exist).
|
|
98
|
+
* Leaves the file with an empty `mcpServers: {}` rather than deleting the
|
|
99
|
+
* field — preserves the spot for future adds and avoids churn that would
|
|
100
|
+
* surprise users diffing the file in git. */
|
|
101
|
+
export async function removeServerFromConfig(name, scope, cwd) {
|
|
102
|
+
const file = getConfigPath(scope, cwd);
|
|
103
|
+
const obj = await readConfigObject(scope, cwd);
|
|
104
|
+
const existing = obj.mcpServers;
|
|
105
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
106
|
+
return { path: file, removed: false };
|
|
107
|
+
}
|
|
108
|
+
const servers = existing;
|
|
109
|
+
if (!Object.prototype.hasOwnProperty.call(servers, name)) {
|
|
110
|
+
return { path: file, removed: false };
|
|
111
|
+
}
|
|
112
|
+
const next = {};
|
|
113
|
+
for (const [k, v] of Object.entries(servers)) {
|
|
114
|
+
if (k !== name)
|
|
115
|
+
next[k] = v;
|
|
116
|
+
}
|
|
117
|
+
obj.mcpServers = next;
|
|
118
|
+
await writeConfigObject(scope, cwd, obj);
|
|
119
|
+
return { path: file, removed: true };
|
|
120
|
+
}
|
|
121
|
+
/** Read the current config for `name` from the given scope, for the
|
|
122
|
+
* "already exists, here's what's there" path of /mcp add. Returns null
|
|
123
|
+
* if not present. Best-effort: a malformed entry returns null rather
|
|
124
|
+
* than throwing — the duplicate-check use case shouldn't crash. */
|
|
125
|
+
export async function readServerConfig(name, scope, cwd) {
|
|
126
|
+
try {
|
|
127
|
+
const obj = await readConfigObject(scope, cwd);
|
|
128
|
+
const servers = obj.mcpServers;
|
|
129
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers))
|
|
130
|
+
return null;
|
|
131
|
+
const value = servers[name];
|
|
132
|
+
return value ?? null;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=config-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-writer.js","sourceRoot":"","sources":["../../src/mcp/config-writer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class UnsafeEnvError extends Error {
|
|
2
|
+
readonly key: string;
|
|
3
|
+
constructor(key: string);
|
|
4
|
+
}
|
|
5
|
+
/** Throw {@link UnsafeEnvError} if `env` contains a denylisted key.
|
|
6
|
+
*
|
|
7
|
+
* Comparison is case-insensitive: Windows env names are case-insensitive
|
|
8
|
+
* at the OS level, so rejecting `NODE_OPTIONS` while allowing
|
|
9
|
+
* `Node_Options` would be theatre. POSIX env names are case-sensitive but
|
|
10
|
+
* no legitimate config uses non-uppercase variants of these keys. */
|
|
11
|
+
export declare function assertSafeEnv(env: Record<string, string> | undefined): void;
|
|
12
|
+
//# sourceMappingURL=env-safety.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-safety.d.ts","sourceRoot":"","sources":["../../src/mcp/env-safety.ts"],"names":[],"mappings":"AAuDA,qBAAa,cAAe,SAAQ,KAAK;aACX,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM;CASxC;AAED;;;;;sEAKsE;AACtE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAAG,IAAI,CAO3E"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @x-code-cli/core — Reject env vars that are runtime code-injection vectors
|
|
2
|
+
//
|
|
3
|
+
// MCP stdio servers inherit an `env` map straight through to spawn(). That
|
|
4
|
+
// map can come from three sources:
|
|
5
|
+
// 1. the user typing `xc mcp add --env KEY=VAL`
|
|
6
|
+
// 2. the project / user mcp.json file
|
|
7
|
+
// 3. a plugin manifest declaring its own mcpServers
|
|
8
|
+
//
|
|
9
|
+
// (3) is the one this module exists to defend. Plugins run with the trust
|
|
10
|
+
// the user gave them at install time, but a key like `NODE_OPTIONS=--require
|
|
11
|
+
// ./evil.js` would let a plugin turn any node-based MCP server into
|
|
12
|
+
// arbitrary code execution the next time it starts — escalating from
|
|
13
|
+
// "manifest install" to "RCE under the user's account". The same trick
|
|
14
|
+
// works on Linux (LD_PRELOAD) and macOS (DYLD_INSERT_LIBRARIES), and on
|
|
15
|
+
// Python/Perl/Ruby runtimes via their respective *STARTUP / *OPT names.
|
|
16
|
+
//
|
|
17
|
+
// We sit at the spawn boundary (registry.connectOneServer) so every source
|
|
18
|
+
// is covered by one check, not just the CLI parser.
|
|
19
|
+
//
|
|
20
|
+
// This is a denylist, not an allowlist: legitimate MCP servers need to
|
|
21
|
+
// accept arbitrary env keys for API tokens / app config, so an allowlist
|
|
22
|
+
// would be unworkable. The denylist is short and targeted at names whose
|
|
23
|
+
// only legitimate purpose is "load this code on start".
|
|
24
|
+
/** Env names that runtimes interpret as "load this code on start".
|
|
25
|
+
* Compared case-insensitively (see {@link assertSafeEnv}). */
|
|
26
|
+
const DANGEROUS_ENV_KEYS = new Set([
|
|
27
|
+
// Node
|
|
28
|
+
'NODE_OPTIONS',
|
|
29
|
+
// Linux dynamic linker
|
|
30
|
+
'LD_PRELOAD',
|
|
31
|
+
'LD_LIBRARY_PATH',
|
|
32
|
+
'LD_AUDIT',
|
|
33
|
+
// macOS dynamic linker
|
|
34
|
+
'DYLD_INSERT_LIBRARIES',
|
|
35
|
+
'DYLD_LIBRARY_PATH',
|
|
36
|
+
'DYLD_FRAMEWORK_PATH',
|
|
37
|
+
'DYLD_FALLBACK_LIBRARY_PATH',
|
|
38
|
+
'DYLD_FALLBACK_FRAMEWORK_PATH',
|
|
39
|
+
// Shell init / per-command hooks. BASH_ENV runs on non-interactive bash;
|
|
40
|
+
// ENV runs on POSIX sh; PROMPT_COMMAND on every interactive prompt.
|
|
41
|
+
'BASH_ENV',
|
|
42
|
+
'ENV',
|
|
43
|
+
'PROMPT_COMMAND',
|
|
44
|
+
// Python
|
|
45
|
+
'PYTHONSTARTUP',
|
|
46
|
+
'PYTHONPATH',
|
|
47
|
+
// Perl
|
|
48
|
+
'PERL5OPT',
|
|
49
|
+
'PERL5LIB',
|
|
50
|
+
// Ruby
|
|
51
|
+
'RUBYOPT',
|
|
52
|
+
'RUBYLIB',
|
|
53
|
+
]);
|
|
54
|
+
export class UnsafeEnvError extends Error {
|
|
55
|
+
key;
|
|
56
|
+
constructor(key) {
|
|
57
|
+
super(`Env key "${key}" is blocked by the MCP env safety check: it is a runtime ` +
|
|
58
|
+
`code-loading hook (NODE_OPTIONS / LD_PRELOAD-class) and would let an MCP ` +
|
|
59
|
+
`config or plugin manifest run arbitrary code at server start. If you ` +
|
|
60
|
+
`really need this, export it in the shell that launches xc instead.`);
|
|
61
|
+
this.key = key;
|
|
62
|
+
this.name = 'UnsafeEnvError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Throw {@link UnsafeEnvError} if `env` contains a denylisted key.
|
|
66
|
+
*
|
|
67
|
+
* Comparison is case-insensitive: Windows env names are case-insensitive
|
|
68
|
+
* at the OS level, so rejecting `NODE_OPTIONS` while allowing
|
|
69
|
+
* `Node_Options` would be theatre. POSIX env names are case-sensitive but
|
|
70
|
+
* no legitimate config uses non-uppercase variants of these keys. */
|
|
71
|
+
export function assertSafeEnv(env) {
|
|
72
|
+
if (!env)
|
|
73
|
+
return;
|
|
74
|
+
for (const k of Object.keys(env)) {
|
|
75
|
+
if (DANGEROUS_ENV_KEYS.has(k.toUpperCase())) {
|
|
76
|
+
throw new UnsafeEnvError(k);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=env-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-safety.js","sourceRoot":"","sources":["../../src/mcp/env-safety.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,EAAE;AACF,2EAA2E;AAC3E,mCAAmC;AACnC,kDAAkD;AAClD,wCAAwC;AACxC,sDAAsD;AACtD,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,oEAAoE;AACpE,qEAAqE;AACrE,uEAAuE;AACvE,wEAAwE;AACxE,wEAAwE;AACxE,EAAE;AACF,2EAA2E;AAC3E,oDAAoD;AACpD,EAAE;AACF,uEAAuE;AACvE,yEAAyE;AACzE,yEAAyE;AACzE,wDAAwD;AAExD;+DAC+D;AAC/D,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAS;IACzC,OAAO;IACP,cAAc;IACd,uBAAuB;IACvB,YAAY;IACZ,iBAAiB;IACjB,UAAU;IACV,uBAAuB;IACvB,uBAAuB;IACvB,mBAAmB;IACnB,qBAAqB;IACrB,4BAA4B;IAC5B,8BAA8B;IAC9B,yEAAyE;IACzE,oEAAoE;IACpE,UAAU;IACV,KAAK;IACL,gBAAgB;IAChB,SAAS;IACT,eAAe;IACf,YAAY;IACZ,OAAO;IACP,UAAU;IACV,UAAU;IACV,OAAO;IACP,SAAS;IACT,SAAS;CACV,CAAC,CAAA;AAEF,MAAM,OAAO,cAAe,SAAQ,KAAK;IACX;IAA5B,YAA4B,GAAW;QACrC,KAAK,CACH,YAAY,GAAG,4DAA4D;YACzE,2EAA2E;YAC3E,uEAAuE;YACvE,oEAAoE,CACvE,CAAA;QANyB,QAAG,GAAH,GAAG,CAAQ;QAOrC,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAA;IAC9B,CAAC;CACF;AAED;;;;;sEAKsE;AACtE,MAAM,UAAU,aAAa,CAAC,GAAuC;IACnE,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,IAAI,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAC5C,MAAM,IAAI,cAAc,CAAC,CAAC,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Thrown when a ${VAR} reference can't be resolved. The loader catches
|
|
2
|
+
* this and marks the server `failed` so the rest of the CLI keeps going. */
|
|
3
|
+
export declare class EnvExpansionError extends Error {
|
|
4
|
+
varName: string;
|
|
5
|
+
constructor(varName: string);
|
|
6
|
+
}
|
|
7
|
+
/** Expand all ${VAR} references in a single string. */
|
|
8
|
+
export declare function expandEnvString(input: string, env?: NodeJS.ProcessEnv): string;
|
|
9
|
+
/** Recursively walk a config value and expand strings. Arrays / plain
|
|
10
|
+
* objects are traversed; numbers/booleans/null pass through unchanged.
|
|
11
|
+
* Returns a deep copy — never mutates the input (important: the input
|
|
12
|
+
* may come straight from a cached parsed config object). */
|
|
13
|
+
export declare function expandEnvDeep<T>(value: T, env?: NodeJS.ProcessEnv): T;
|
|
14
|
+
//# sourceMappingURL=expand-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand-env.d.ts","sourceRoot":"","sources":["../../src/mcp/expand-env.ts"],"names":[],"mappings":"AAUA;6EAC6E;AAC7E,qBAAa,iBAAkB,SAAQ,KAAK;IACvB,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM;CAInC;AAID,uDAAuD;AACvD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAO3F;AAED;;;6DAG6D;AAC7D,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,CAAC,CAelF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @x-code-cli/core — Environment variable expansion for MCP configs
|
|
2
|
+
//
|
|
3
|
+
// Supports two forms inside any string field of an MCP server config:
|
|
4
|
+
// ${VAR} — expand or throw if VAR is unset
|
|
5
|
+
// ${VAR:-fallback} — expand or use the literal fallback
|
|
6
|
+
//
|
|
7
|
+
// We intentionally do NOT support arbitrary shell expansion (no `$VAR`
|
|
8
|
+
// without braces, no command substitution, no nested `${${A}}`). Anything
|
|
9
|
+
// fancier should be done in user-land before X-Code launches.
|
|
10
|
+
/** Thrown when a ${VAR} reference can't be resolved. The loader catches
|
|
11
|
+
* this and marks the server `failed` so the rest of the CLI keeps going. */
|
|
12
|
+
export class EnvExpansionError extends Error {
|
|
13
|
+
varName;
|
|
14
|
+
constructor(varName) {
|
|
15
|
+
super(`Required environment variable not set: ${varName}`);
|
|
16
|
+
this.varName = varName;
|
|
17
|
+
this.name = 'EnvExpansionError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const REF_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
21
|
+
/** Expand all ${VAR} references in a single string. */
|
|
22
|
+
export function expandEnvString(input, env = process.env) {
|
|
23
|
+
return input.replace(REF_RE, (match, name, fallback) => {
|
|
24
|
+
const v = env[name];
|
|
25
|
+
if (v !== undefined && v !== '')
|
|
26
|
+
return v;
|
|
27
|
+
if (fallback !== undefined)
|
|
28
|
+
return fallback;
|
|
29
|
+
throw new EnvExpansionError(name);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Recursively walk a config value and expand strings. Arrays / plain
|
|
33
|
+
* objects are traversed; numbers/booleans/null pass through unchanged.
|
|
34
|
+
* Returns a deep copy — never mutates the input (important: the input
|
|
35
|
+
* may come straight from a cached parsed config object). */
|
|
36
|
+
export function expandEnvDeep(value, env = process.env) {
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
return expandEnvString(value, env);
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map((v) => expandEnvDeep(v, env));
|
|
42
|
+
}
|
|
43
|
+
if (value !== null && typeof value === 'object') {
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const [k, v] of Object.entries(value)) {
|
|
46
|
+
out[k] = expandEnvDeep(v, env);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=expand-env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand-env.js","sourceRoot":"","sources":["../../src/mcp/expand-env.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,EAAE;AACF,sEAAsE;AACtE,yDAAyD;AACzD,4DAA4D;AAC5D,EAAE;AACF,uEAAuE;AACvE,0EAA0E;AAC1E,8DAA8D;AAE9D;6EAC6E;AAC7E,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACvB;IAAnB,YAAmB,OAAe;QAChC,KAAK,CAAC,0CAA0C,OAAO,EAAE,CAAC,CAAA;QADzC,YAAO,GAAP,OAAO,CAAQ;QAEhC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAA;IACjC,CAAC;CACF;AAED,MAAM,MAAM,GAAG,+CAA+C,CAAA;AAE9D,uDAAuD;AACvD,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,MAAyB,OAAO,CAAC,GAAG;IACjF,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,QAAiB,EAAE,EAAE;QACtE,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;QACnB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE;YAAE,OAAO,CAAC,CAAA;QACzC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAA;QAC3C,MAAM,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAA;IACnC,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;6DAG6D;AAC7D,MAAM,UAAU,aAAa,CAAI,KAAQ,EAAE,MAAyB,OAAO,CAAC,GAAG;IAC7E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,eAAe,CAAC,KAAK,EAAE,GAAG,CAAiB,CAAA;IACpD,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,GAAG,CAAC,CAAiB,CAAA;IAChE,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,GAAG,GAA4B,EAAE,CAAA;QACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;YACtE,GAAG,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;QAChC,CAAC;QACD,OAAO,GAAmB,CAAA;IAC5B,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type ConnectResult, McpRegistry, type OAuthProviderFactory, type RegisteredServer } from './registry.js';
|
|
2
|
+
import { type McpResourceEntry, type McpServerConfig, type McpToolEntry } from './types.js';
|
|
3
|
+
export type { OAuthProviderFactory };
|
|
4
|
+
export type { RegisteredServer, ConnectResult };
|
|
5
|
+
export type { McpResourceEntry, McpToolEntry };
|
|
6
|
+
export interface LoadOptions {
|
|
7
|
+
/** mcpServers from ~/.x-code/config.json. Trusted implicitly. */
|
|
8
|
+
userServers: Record<string, McpServerConfig> | undefined;
|
|
9
|
+
/** mcpServers from <project>/.x-code/config.json. Requires consent. */
|
|
10
|
+
projectServers: Record<string, McpServerConfig> | undefined;
|
|
11
|
+
/** mcpServers contributed by enabled plugins. Trusted implicitly —
|
|
12
|
+
* the user already consented to the plugin at install time, so
|
|
13
|
+
* re-running the project-MCP trust dialog for plugin servers would
|
|
14
|
+
* be a duplicate prompt. Merged at the same precedence as
|
|
15
|
+
* `userServers` (project entries still override on name collision). */
|
|
16
|
+
extraServers?: Record<string, McpServerConfig>;
|
|
17
|
+
/** Absolute project path (cwd at CLI start). Used as the trust key. */
|
|
18
|
+
projectPath: string;
|
|
19
|
+
/** Renders the trust dialog. Same shape as `AgentCallbacks.onAskUser`. */
|
|
20
|
+
askUser: (question: string, options: Array<{
|
|
21
|
+
label: string;
|
|
22
|
+
description: string;
|
|
23
|
+
}>) => Promise<string>;
|
|
24
|
+
/** Factory for OAuth providers. Optional — pass undefined to disable
|
|
25
|
+
* OAuth (HTTP servers requiring auth will be marked `needs_auth`). */
|
|
26
|
+
oauthProviderFor?: OAuthProviderFactory;
|
|
27
|
+
/** Called after the loader decides to terminate the process — the CLI
|
|
28
|
+
* layer wires this to a clean shutdown path. Defaults to no-op
|
|
29
|
+
* (caller is responsible). */
|
|
30
|
+
onExitRequested?: () => void;
|
|
31
|
+
}
|
|
32
|
+
export interface LoadResult {
|
|
33
|
+
registry: McpRegistry;
|
|
34
|
+
/** Configuration / parse errors collected before any server was even
|
|
35
|
+
* contacted. Surfaced in `/mcp list` so users see typos in their
|
|
36
|
+
* config alongside actual connection failures. */
|
|
37
|
+
configErrors: Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
message: string;
|
|
40
|
+
}>;
|
|
41
|
+
/** True iff project-level mcpServers were skipped because the user
|
|
42
|
+
* declined trust. The CLI uses this to print a heads-up message. */
|
|
43
|
+
projectSkipped: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** Load the standard config files from disk + invoke the loader.
|
|
46
|
+
* Convenience wrapper used by the CLI entry point so it doesn't have
|
|
47
|
+
* to know about file paths. */
|
|
48
|
+
export declare function loadMcpFromDisk(opts: {
|
|
49
|
+
cwd: string;
|
|
50
|
+
askUser: LoadOptions['askUser'];
|
|
51
|
+
oauthProviderFor?: OAuthProviderFactory;
|
|
52
|
+
onExitRequested?: () => void;
|
|
53
|
+
/** Plugin-contributed mcpServers — already-trusted, merged into the
|
|
54
|
+
* effective config alongside user-level servers. Built by
|
|
55
|
+
* packages/core/src/plugins/integration.ts. */
|
|
56
|
+
extraServers?: Record<string, McpServerConfig>;
|
|
57
|
+
}): Promise<LoadResult>;
|
|
58
|
+
/** Re-read configs from disk + apply the trust gate, but DON'T spawn any
|
|
59
|
+
* servers. Used by `/mcp refresh` so the caller can hand the resulting
|
|
60
|
+
* merged map to `registry.restartAll(...)` — that mutates the existing
|
|
61
|
+
* registry in place rather than allocating a parallel one. */
|
|
62
|
+
export declare function loadMergedConfigsFromDisk(opts: {
|
|
63
|
+
cwd: string;
|
|
64
|
+
askUser: LoadOptions['askUser'];
|
|
65
|
+
/** Plugin-contributed mcpServers (from `buildPluginIntegration().mcpServers`).
|
|
66
|
+
* Merged between user and project, matching the precedence in
|
|
67
|
+
* [[loadMcpServers]]. Pass these on `/mcp refresh` and `/plugin refresh`
|
|
68
|
+
* so plugin-contributed servers aren't silently dropped during a reload. */
|
|
69
|
+
extraServers?: Record<string, McpServerConfig>;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
configs: Map<string, McpServerConfig>;
|
|
72
|
+
configErrors: Array<{
|
|
73
|
+
name: string;
|
|
74
|
+
message: string;
|
|
75
|
+
}>;
|
|
76
|
+
projectSkipped: boolean;
|
|
77
|
+
}>;
|
|
78
|
+
/** Pure loader (no disk I/O on configs — caller injects them).
|
|
79
|
+
* Easier to test and lets the CLI control config sourcing. */
|
|
80
|
+
export declare function loadMcpServers(options: LoadOptions): Promise<LoadResult>;
|
|
81
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/mcp/loader.ts"],"names":[],"mappings":"AAeA,OAAO,EACL,KAAK,aAAa,EAClB,WAAW,EACX,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EAGtB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAG3F,YAAY,EAAE,oBAAoB,EAAE,CAAA;AACpC,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,CAAA;AAC/C,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAA;AAE9C,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,SAAS,CAAA;IACxD,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,SAAS,CAAA;IAC3D;;;;4EAIwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC9C,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAA;IACnB,0EAA0E;IAC1E,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACtG;2EACuE;IACvE,gBAAgB,CAAC,EAAE,oBAAoB,CAAA;IACvC;;mCAE+B;IAC/B,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,WAAW,CAAA;IACrB;;uDAEmD;IACnD,YAAY,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtD;yEACqE;IACrE,cAAc,EAAE,OAAO,CAAA;CACxB;AAED;;gCAEgC;AAChC,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;IAC/B,gBAAgB,CAAC,EAAE,oBAAoB,CAAA;IACvC,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B;;oDAEgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CAC/C,GAAG,OAAO,CAAC,UAAU,CAAC,CAYtB;AAED;;;+DAG+D;AAC/D,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;IAC/B;;;iFAG6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CAC/C,GAAG,OAAO,CAAC;IACV,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IACrC,YAAY,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtD,cAAc,EAAE,OAAO,CAAA;CACxB,CAAC,CAmDD;AAED;+DAC+D;AAC/D,wBAAsB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CA8G9E"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// @x-code-cli/core — MCP startup loader
|
|
2
|
+
//
|
|
3
|
+
// One-shot orchestration called from the CLI entry: read user + project
|
|
4
|
+
// configs, apply the trust gate to anything project-level, expand env
|
|
5
|
+
// vars, spawn / dial every enabled server in parallel, build a registry
|
|
6
|
+
// that can later be mutated by `/mcp refresh` and `/mcp auth`. Failures
|
|
7
|
+
// on individual servers are recorded but never abort the boot —
|
|
8
|
+
// `/mcp list` is the user's window into what went wrong.
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { getUserConfigPath } from '../config/index.js';
|
|
12
|
+
import { XCODE_DIR, debugLog } from '../utils.js';
|
|
13
|
+
import { parseServersBlock } from './config-schema.js';
|
|
14
|
+
import { buildCallableName as buildCallable } from './name-mangling.js';
|
|
15
|
+
import { McpRegistry, connectOneServer, emptyRegistry, } from './registry.js';
|
|
16
|
+
import { buildServerPreview, isProjectTrusted, promptForTrust, trustProject } from './trust.js';
|
|
17
|
+
/** Load the standard config files from disk + invoke the loader.
|
|
18
|
+
* Convenience wrapper used by the CLI entry point so it doesn't have
|
|
19
|
+
* to know about file paths. */
|
|
20
|
+
export async function loadMcpFromDisk(opts) {
|
|
21
|
+
const userServers = await readMcpServersFromFile(getUserConfigPath());
|
|
22
|
+
const projectServers = await readMcpServersFromFile(path.join(opts.cwd, XCODE_DIR, 'config.json'));
|
|
23
|
+
return loadMcpServers({
|
|
24
|
+
userServers,
|
|
25
|
+
projectServers,
|
|
26
|
+
extraServers: opts.extraServers,
|
|
27
|
+
projectPath: opts.cwd,
|
|
28
|
+
askUser: opts.askUser,
|
|
29
|
+
oauthProviderFor: opts.oauthProviderFor,
|
|
30
|
+
onExitRequested: opts.onExitRequested,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Re-read configs from disk + apply the trust gate, but DON'T spawn any
|
|
34
|
+
* servers. Used by `/mcp refresh` so the caller can hand the resulting
|
|
35
|
+
* merged map to `registry.restartAll(...)` — that mutates the existing
|
|
36
|
+
* registry in place rather than allocating a parallel one. */
|
|
37
|
+
export async function loadMergedConfigsFromDisk(opts) {
|
|
38
|
+
const userServers = await readMcpServersFromFile(getUserConfigPath());
|
|
39
|
+
const projectServers = await readMcpServersFromFile(path.join(opts.cwd, XCODE_DIR, 'config.json'));
|
|
40
|
+
const configErrors = [];
|
|
41
|
+
let projectSkipped = false;
|
|
42
|
+
const userParsed = parseServersBlock(userServers);
|
|
43
|
+
configErrors.push(...userParsed.errors.map((e) => ({ name: `user:${e.name}`, message: e.message })));
|
|
44
|
+
const projectParsed = parseServersBlock(projectServers);
|
|
45
|
+
configErrors.push(...projectParsed.errors.map((e) => ({ name: `project:${e.name}`, message: e.message })));
|
|
46
|
+
let projectServersToUse = projectParsed.servers;
|
|
47
|
+
if (Object.keys(projectServersToUse).length > 0) {
|
|
48
|
+
const trusted = await isProjectTrusted(opts.cwd);
|
|
49
|
+
if (!trusted) {
|
|
50
|
+
const choice = await askForTrust({
|
|
51
|
+
// Synthesise just enough of a LoadOptions for askForTrust —
|
|
52
|
+
// only projectPath + askUser are read.
|
|
53
|
+
userServers,
|
|
54
|
+
projectServers,
|
|
55
|
+
projectPath: opts.cwd,
|
|
56
|
+
askUser: opts.askUser,
|
|
57
|
+
}, projectServersToUse);
|
|
58
|
+
if (choice === 'exit') {
|
|
59
|
+
// /mcp refresh deliberately ignores 'exit' — bailing the whole
|
|
60
|
+
// CLI from a slash command is too violent. We treat it as
|
|
61
|
+
// 'skip' so the user can pick again on a real restart.
|
|
62
|
+
projectServersToUse = {};
|
|
63
|
+
projectSkipped = true;
|
|
64
|
+
}
|
|
65
|
+
else if (choice === 'skip') {
|
|
66
|
+
projectServersToUse = {};
|
|
67
|
+
projectSkipped = true;
|
|
68
|
+
}
|
|
69
|
+
else if (choice === 'trust') {
|
|
70
|
+
await trustProject(opts.cwd).catch((err) => {
|
|
71
|
+
debugLog('mcp.trust-write-failed', String(err));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Merge order user → plugin → project, matching the precedence enforced by
|
|
77
|
+
// loadMcpServers (initial boot). Plugin-contributed entries sit between
|
|
78
|
+
// user and project so a project-level same-name entry still wins.
|
|
79
|
+
const merged = new Map(Object.entries({ ...userParsed.servers, ...(opts.extraServers ?? {}), ...projectServersToUse }));
|
|
80
|
+
return { configs: merged, configErrors, projectSkipped };
|
|
81
|
+
}
|
|
82
|
+
/** Pure loader (no disk I/O on configs — caller injects them).
|
|
83
|
+
* Easier to test and lets the CLI control config sourcing. */
|
|
84
|
+
export async function loadMcpServers(options) {
|
|
85
|
+
const configErrors = [];
|
|
86
|
+
let projectSkipped = false;
|
|
87
|
+
// Validate both blocks up front. parseServersBlock tolerates `undefined`
|
|
88
|
+
// and returns empty maps + zero errors in that case, so users with no
|
|
89
|
+
// mcpServers configured pay nothing.
|
|
90
|
+
const userParsed = parseServersBlock(options.userServers);
|
|
91
|
+
configErrors.push(...userParsed.errors.map((e) => ({ name: `user:${e.name}`, message: e.message })));
|
|
92
|
+
const projectParsed = parseServersBlock(options.projectServers);
|
|
93
|
+
configErrors.push(...projectParsed.errors.map((e) => ({ name: `project:${e.name}`, message: e.message })));
|
|
94
|
+
// Project-level trust gate. If the project has zero servers we skip the
|
|
95
|
+
// prompt entirely — there's nothing to consent to.
|
|
96
|
+
let projectServersToUse = projectParsed.servers;
|
|
97
|
+
const projectServerNames = Object.keys(projectServersToUse);
|
|
98
|
+
if (projectServerNames.length > 0) {
|
|
99
|
+
const trusted = await isProjectTrusted(options.projectPath);
|
|
100
|
+
if (!trusted) {
|
|
101
|
+
const choice = await askForTrust(options, projectServersToUse);
|
|
102
|
+
if (choice === 'exit') {
|
|
103
|
+
options.onExitRequested?.();
|
|
104
|
+
// Even if the CLI doesn't shut down, returning an empty registry
|
|
105
|
+
// keeps the rest of the loader well-defined.
|
|
106
|
+
return { registry: emptyRegistry(), configErrors, projectSkipped: true };
|
|
107
|
+
}
|
|
108
|
+
if (choice === 'skip') {
|
|
109
|
+
projectServersToUse = {};
|
|
110
|
+
projectSkipped = true;
|
|
111
|
+
}
|
|
112
|
+
if (choice === 'trust') {
|
|
113
|
+
await trustProject(options.projectPath).catch((err) => {
|
|
114
|
+
debugLog('mcp.trust-write-failed', String(err));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Merge order: user → plugin → project. Plugin-contributed servers
|
|
120
|
+
// (`extraServers`) sit between user and project on purpose:
|
|
121
|
+
// - They're already-trusted (consent happened at plugin install) so
|
|
122
|
+
// they don't need to pass the trust dialog above, but
|
|
123
|
+
// - A name collision with a project-level entry still gives the
|
|
124
|
+
// project entry the win (project config is authored by the same
|
|
125
|
+
// person whose CLI is running and they may want to override a
|
|
126
|
+
// plugin's server choice).
|
|
127
|
+
const merged = {
|
|
128
|
+
...userParsed.servers,
|
|
129
|
+
...(options.extraServers ?? {}),
|
|
130
|
+
...projectServersToUse,
|
|
131
|
+
};
|
|
132
|
+
// No servers configured anywhere → fast-path with an empty registry.
|
|
133
|
+
// We still pass the oauthFactory so a later /mcp refresh (after the
|
|
134
|
+
// user adds servers to config + restarts the CLI) would have it —
|
|
135
|
+
// although in practice the empty-registry path is only hit when both
|
|
136
|
+
// configs are empty at boot, and a later refresh rebuilds from disk
|
|
137
|
+
// via the CLI's own loadMcpFromDisk call.
|
|
138
|
+
if (Object.keys(merged).length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
registry: new McpRegistry({ servers: [], tools: [], resources: [], oauthFactory: options.oauthProviderFor }),
|
|
141
|
+
configErrors,
|
|
142
|
+
projectSkipped,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Spawn / dial in parallel. Each per-server promise is wrapped in
|
|
146
|
+
// .then/.catch so one timeout doesn't trip the whole boot.
|
|
147
|
+
const tasks = Object.entries(merged).map(async ([name, rawConfig]) => {
|
|
148
|
+
return connectOneServer(name, rawConfig, options.oauthProviderFor);
|
|
149
|
+
});
|
|
150
|
+
const results = await Promise.all(tasks);
|
|
151
|
+
// Assemble the registry. Tool name collisions are resolved in
|
|
152
|
+
// insertion order (first wins; subsequent get hash suffixes), so we
|
|
153
|
+
// sort by server name for stability — otherwise the order would
|
|
154
|
+
// depend on which connect() resolved first.
|
|
155
|
+
results.sort((a, b) => a.server.name.localeCompare(b.server.name));
|
|
156
|
+
const tools = [];
|
|
157
|
+
const resources = [];
|
|
158
|
+
const taken = new Set();
|
|
159
|
+
for (const r of results) {
|
|
160
|
+
for (const t of r.tools) {
|
|
161
|
+
const callable = buildCallable(r.server.name, t.name, taken);
|
|
162
|
+
taken.add(callable);
|
|
163
|
+
tools.push({
|
|
164
|
+
callableName: callable,
|
|
165
|
+
rawName: t.name,
|
|
166
|
+
serverName: r.server.name,
|
|
167
|
+
description: t.description ?? '',
|
|
168
|
+
inputSchema: t.inputSchema,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
for (const res of r.resources)
|
|
172
|
+
resources.push(res);
|
|
173
|
+
}
|
|
174
|
+
const configs = new Map(Object.entries(merged));
|
|
175
|
+
const registry = new McpRegistry({
|
|
176
|
+
servers: results.map((r) => r.server),
|
|
177
|
+
tools,
|
|
178
|
+
resources,
|
|
179
|
+
configs,
|
|
180
|
+
oauthFactory: options.oauthProviderFor,
|
|
181
|
+
});
|
|
182
|
+
return { registry, configErrors, projectSkipped };
|
|
183
|
+
}
|
|
184
|
+
async function askForTrust(options, projectServers) {
|
|
185
|
+
const summaries = Object.entries(projectServers).map(([name, cfg]) => ({
|
|
186
|
+
name,
|
|
187
|
+
preview: buildServerPreview(cfg),
|
|
188
|
+
}));
|
|
189
|
+
try {
|
|
190
|
+
return await promptForTrust(options.projectPath, summaries, options.askUser);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
// If the prompt machinery itself fails (no TTY etc.), err on the
|
|
194
|
+
// safe side: skip project config. Logged for debugging.
|
|
195
|
+
debugLog('mcp.trust-prompt-failed', String(err));
|
|
196
|
+
return 'skip';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Read just the `mcpServers` field out of a JSON config file. Returns
|
|
200
|
+
* undefined for missing file / parse error / missing field — all of
|
|
201
|
+
* which mean "no MCP servers configured here", never an error to
|
|
202
|
+
* surface upward. */
|
|
203
|
+
async function readMcpServersFromFile(filePath) {
|
|
204
|
+
let raw;
|
|
205
|
+
try {
|
|
206
|
+
raw = await fs.readFile(filePath, 'utf-8');
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(raw);
|
|
213
|
+
if (parsed && typeof parsed === 'object' && parsed.mcpServers) {
|
|
214
|
+
return parsed.mcpServers;
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
debugLog('mcp.config-parse-failed', `${filePath}: ${String(err)}`);
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=loader.js.map
|