@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.
Files changed (251) hide show
  1. package/dist/agent/compression.d.ts +12 -2
  2. package/dist/agent/compression.d.ts.map +1 -1
  3. package/dist/agent/compression.js +51 -2
  4. package/dist/agent/compression.js.map +1 -1
  5. package/dist/agent/file-ingest.js +2 -2
  6. package/dist/agent/file-ingest.js.map +1 -1
  7. package/dist/agent/loop-state.d.ts +3 -2
  8. package/dist/agent/loop-state.d.ts.map +1 -1
  9. package/dist/agent/loop-state.js.map +1 -1
  10. package/dist/agent/loop.d.ts.map +1 -1
  11. package/dist/agent/loop.js +134 -5
  12. package/dist/agent/loop.js.map +1 -1
  13. package/dist/agent/memory-extractor.js +5 -5
  14. package/dist/agent/memory-extractor.js.map +1 -1
  15. package/dist/agent/plan-storage.js +1 -1
  16. package/dist/agent/plan-storage.js.map +1 -1
  17. package/dist/agent/sub-agents/index.d.ts +2 -1
  18. package/dist/agent/sub-agents/index.d.ts.map +1 -1
  19. package/dist/agent/sub-agents/index.js +1 -1
  20. package/dist/agent/sub-agents/index.js.map +1 -1
  21. package/dist/agent/sub-agents/loader.d.ts +13 -3
  22. package/dist/agent/sub-agents/loader.d.ts.map +1 -1
  23. package/dist/agent/sub-agents/loader.js +36 -9
  24. package/dist/agent/sub-agents/loader.js.map +1 -1
  25. package/dist/agent/sub-agents/registry.d.ts +18 -1
  26. package/dist/agent/sub-agents/registry.d.ts.map +1 -1
  27. package/dist/agent/sub-agents/registry.js +38 -5
  28. package/dist/agent/sub-agents/registry.js.map +1 -1
  29. package/dist/agent/sub-agents/runner.d.ts.map +1 -1
  30. package/dist/agent/sub-agents/runner.js +45 -1
  31. package/dist/agent/sub-agents/runner.js.map +1 -1
  32. package/dist/agent/sub-agents/types.d.ts +4 -1
  33. package/dist/agent/sub-agents/types.d.ts.map +1 -1
  34. package/dist/agent/system-prompt.d.ts +21 -0
  35. package/dist/agent/system-prompt.d.ts.map +1 -1
  36. package/dist/agent/system-prompt.js +68 -2
  37. package/dist/agent/system-prompt.js.map +1 -1
  38. package/dist/agent/tool-execution.d.ts.map +1 -1
  39. package/dist/agent/tool-execution.js +220 -1
  40. package/dist/agent/tool-execution.js.map +1 -1
  41. package/dist/commands/index.d.ts +6 -0
  42. package/dist/commands/index.d.ts.map +1 -0
  43. package/dist/commands/index.js +3 -0
  44. package/dist/commands/index.js.map +1 -0
  45. package/dist/commands/loader.d.ts +13 -0
  46. package/dist/commands/loader.d.ts.map +1 -0
  47. package/dist/commands/loader.js +93 -0
  48. package/dist/commands/loader.js.map +1 -0
  49. package/dist/commands/registry.d.ts +44 -0
  50. package/dist/commands/registry.d.ts.map +1 -0
  51. package/dist/commands/registry.js +102 -0
  52. package/dist/commands/registry.js.map +1 -0
  53. package/dist/commands/types.d.ts +23 -0
  54. package/dist/commands/types.d.ts.map +1 -0
  55. package/dist/commands/types.js +26 -0
  56. package/dist/commands/types.js.map +1 -0
  57. package/dist/config/index.d.ts +9 -0
  58. package/dist/config/index.d.ts.map +1 -1
  59. package/dist/config/index.js +12 -10
  60. package/dist/config/index.js.map +1 -1
  61. package/dist/hooks/bus.d.ts +54 -0
  62. package/dist/hooks/bus.d.ts.map +1 -0
  63. package/dist/hooks/bus.js +165 -0
  64. package/dist/hooks/bus.js.map +1 -0
  65. package/dist/hooks/config-schema.d.ts +854 -0
  66. package/dist/hooks/config-schema.d.ts.map +1 -0
  67. package/dist/hooks/config-schema.js +79 -0
  68. package/dist/hooks/config-schema.js.map +1 -0
  69. package/dist/hooks/executor.d.ts +16 -0
  70. package/dist/hooks/executor.d.ts.map +1 -0
  71. package/dist/hooks/executor.js +183 -0
  72. package/dist/hooks/executor.js.map +1 -0
  73. package/dist/hooks/index.d.ts +10 -0
  74. package/dist/hooks/index.d.ts.map +1 -0
  75. package/dist/hooks/index.js +6 -0
  76. package/dist/hooks/index.js.map +1 -0
  77. package/dist/hooks/registry.d.ts +23 -0
  78. package/dist/hooks/registry.d.ts.map +1 -0
  79. package/dist/hooks/registry.js +49 -0
  80. package/dist/hooks/registry.js.map +1 -0
  81. package/dist/hooks/types.d.ts +165 -0
  82. package/dist/hooks/types.d.ts.map +1 -0
  83. package/dist/hooks/types.js +25 -0
  84. package/dist/hooks/types.js.map +1 -0
  85. package/dist/hooks/variables.d.ts +22 -0
  86. package/dist/hooks/variables.d.ts.map +1 -0
  87. package/dist/hooks/variables.js +80 -0
  88. package/dist/hooks/variables.js.map +1 -0
  89. package/dist/index.d.ts +56 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +37 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/knowledge/auto-memory.d.ts +1 -1
  94. package/dist/knowledge/auto-memory.d.ts.map +1 -1
  95. package/dist/knowledge/auto-memory.js +10 -10
  96. package/dist/knowledge/auto-memory.js.map +1 -1
  97. package/dist/knowledge/loader.js +12 -12
  98. package/dist/knowledge/loader.js.map +1 -1
  99. package/dist/mcp/arg-parser.d.ts +49 -0
  100. package/dist/mcp/arg-parser.d.ts.map +1 -0
  101. package/dist/mcp/arg-parser.js +357 -0
  102. package/dist/mcp/arg-parser.js.map +1 -0
  103. package/dist/mcp/client.d.ts +73 -0
  104. package/dist/mcp/client.d.ts.map +1 -0
  105. package/dist/mcp/client.js +376 -0
  106. package/dist/mcp/client.js.map +1 -0
  107. package/dist/mcp/config-schema.d.ts +64 -0
  108. package/dist/mcp/config-schema.d.ts.map +1 -0
  109. package/dist/mcp/config-schema.js +86 -0
  110. package/dist/mcp/config-schema.js.map +1 -0
  111. package/dist/mcp/config-writer.d.ts +41 -0
  112. package/dist/mcp/config-writer.d.ts.map +1 -0
  113. package/dist/mcp/config-writer.js +138 -0
  114. package/dist/mcp/config-writer.js.map +1 -0
  115. package/dist/mcp/env-safety.d.ts +12 -0
  116. package/dist/mcp/env-safety.d.ts.map +1 -0
  117. package/dist/mcp/env-safety.js +80 -0
  118. package/dist/mcp/env-safety.js.map +1 -0
  119. package/dist/mcp/expand-env.d.ts +14 -0
  120. package/dist/mcp/expand-env.d.ts.map +1 -0
  121. package/dist/mcp/expand-env.js +52 -0
  122. package/dist/mcp/expand-env.js.map +1 -0
  123. package/dist/mcp/loader.d.ts +81 -0
  124. package/dist/mcp/loader.d.ts.map +1 -0
  125. package/dist/mcp/loader.js +223 -0
  126. package/dist/mcp/loader.js.map +1 -0
  127. package/dist/mcp/name-mangling.d.ts +11 -0
  128. package/dist/mcp/name-mangling.d.ts.map +1 -0
  129. package/dist/mcp/name-mangling.js +82 -0
  130. package/dist/mcp/name-mangling.js.map +1 -0
  131. package/dist/mcp/oauth/callback-server.d.ts +25 -0
  132. package/dist/mcp/oauth/callback-server.d.ts.map +1 -0
  133. package/dist/mcp/oauth/callback-server.js +118 -0
  134. package/dist/mcp/oauth/callback-server.js.map +1 -0
  135. package/dist/mcp/oauth/provider.d.ts +80 -0
  136. package/dist/mcp/oauth/provider.d.ts.map +1 -0
  137. package/dist/mcp/oauth/provider.js +292 -0
  138. package/dist/mcp/oauth/provider.js.map +1 -0
  139. package/dist/mcp/oauth/token-storage.d.ts +42 -0
  140. package/dist/mcp/oauth/token-storage.d.ts.map +1 -0
  141. package/dist/mcp/oauth/token-storage.js +121 -0
  142. package/dist/mcp/oauth/token-storage.js.map +1 -0
  143. package/dist/mcp/permissions.d.ts +28 -0
  144. package/dist/mcp/permissions.d.ts.map +1 -0
  145. package/dist/mcp/permissions.js +105 -0
  146. package/dist/mcp/permissions.js.map +1 -0
  147. package/dist/mcp/registry.d.ts +150 -0
  148. package/dist/mcp/registry.d.ts.map +1 -0
  149. package/dist/mcp/registry.js +334 -0
  150. package/dist/mcp/registry.js.map +1 -0
  151. package/dist/mcp/resources.d.ts +7 -0
  152. package/dist/mcp/resources.d.ts.map +1 -0
  153. package/dist/mcp/resources.js +40 -0
  154. package/dist/mcp/resources.js.map +1 -0
  155. package/dist/mcp/tool-bridge.d.ts +16 -0
  156. package/dist/mcp/tool-bridge.d.ts.map +1 -0
  157. package/dist/mcp/tool-bridge.js +56 -0
  158. package/dist/mcp/tool-bridge.js.map +1 -0
  159. package/dist/mcp/trust.d.ts +31 -0
  160. package/dist/mcp/trust.d.ts.map +1 -0
  161. package/dist/mcp/trust.js +103 -0
  162. package/dist/mcp/trust.js.map +1 -0
  163. package/dist/mcp/types.d.ts +73 -0
  164. package/dist/mcp/types.d.ts.map +1 -0
  165. package/dist/mcp/types.js +13 -0
  166. package/dist/mcp/types.js.map +1 -0
  167. package/dist/permissions/session-store.d.ts +4 -1
  168. package/dist/permissions/session-store.d.ts.map +1 -1
  169. package/dist/permissions/session-store.js +6 -1
  170. package/dist/permissions/session-store.js.map +1 -1
  171. package/dist/plugins/consent.d.ts +87 -0
  172. package/dist/plugins/consent.d.ts.map +1 -0
  173. package/dist/plugins/consent.js +181 -0
  174. package/dist/plugins/consent.js.map +1 -0
  175. package/dist/plugins/enable-state.d.ts +34 -0
  176. package/dist/plugins/enable-state.d.ts.map +1 -0
  177. package/dist/plugins/enable-state.js +159 -0
  178. package/dist/plugins/enable-state.js.map +1 -0
  179. package/dist/plugins/installer.d.ts +64 -0
  180. package/dist/plugins/installer.d.ts.map +1 -0
  181. package/dist/plugins/installer.js +416 -0
  182. package/dist/plugins/installer.js.map +1 -0
  183. package/dist/plugins/integration.d.ts +91 -0
  184. package/dist/plugins/integration.d.ts.map +1 -0
  185. package/dist/plugins/integration.js +233 -0
  186. package/dist/plugins/integration.js.map +1 -0
  187. package/dist/plugins/loader.d.ts +69 -0
  188. package/dist/plugins/loader.d.ts.map +1 -0
  189. package/dist/plugins/loader.js +243 -0
  190. package/dist/plugins/loader.js.map +1 -0
  191. package/dist/plugins/manifest.d.ts +23 -0
  192. package/dist/plugins/manifest.d.ts.map +1 -0
  193. package/dist/plugins/manifest.js +143 -0
  194. package/dist/plugins/manifest.js.map +1 -0
  195. package/dist/plugins/marketplace.d.ts +100 -0
  196. package/dist/plugins/marketplace.d.ts.map +1 -0
  197. package/dist/plugins/marketplace.js +529 -0
  198. package/dist/plugins/marketplace.js.map +1 -0
  199. package/dist/plugins/paths.d.ts +44 -0
  200. package/dist/plugins/paths.d.ts.map +1 -0
  201. package/dist/plugins/paths.js +89 -0
  202. package/dist/plugins/paths.js.map +1 -0
  203. package/dist/plugins/refresh.d.ts +61 -0
  204. package/dist/plugins/refresh.d.ts.map +1 -0
  205. package/dist/plugins/refresh.js +98 -0
  206. package/dist/plugins/refresh.js.map +1 -0
  207. package/dist/plugins/registry.d.ts +40 -0
  208. package/dist/plugins/registry.d.ts.map +1 -0
  209. package/dist/plugins/registry.js +80 -0
  210. package/dist/plugins/registry.js.map +1 -0
  211. package/dist/plugins/types.d.ts +225 -0
  212. package/dist/plugins/types.d.ts.map +1 -0
  213. package/dist/plugins/types.js +16 -0
  214. package/dist/plugins/types.js.map +1 -0
  215. package/dist/plugins/user-config.d.ts +22 -0
  216. package/dist/plugins/user-config.d.ts.map +1 -0
  217. package/dist/plugins/user-config.js +96 -0
  218. package/dist/plugins/user-config.js.map +1 -0
  219. package/dist/skills/loader.d.ts +19 -0
  220. package/dist/skills/loader.d.ts.map +1 -0
  221. package/dist/skills/loader.js +197 -0
  222. package/dist/skills/loader.js.map +1 -0
  223. package/dist/skills/registry.d.ts +74 -0
  224. package/dist/skills/registry.d.ts.map +1 -0
  225. package/dist/skills/registry.js +136 -0
  226. package/dist/skills/registry.js.map +1 -0
  227. package/dist/skills/settings.d.ts +13 -0
  228. package/dist/skills/settings.d.ts.map +1 -0
  229. package/dist/skills/settings.js +100 -0
  230. package/dist/skills/settings.js.map +1 -0
  231. package/dist/tools/activate-skill.d.ts +5 -0
  232. package/dist/tools/activate-skill.d.ts.map +1 -0
  233. package/dist/tools/activate-skill.js +33 -0
  234. package/dist/tools/activate-skill.js.map +1 -0
  235. package/dist/tools/index.d.ts +1 -1
  236. package/dist/tools/todo-write.d.ts +1 -1
  237. package/dist/tools/web-fetch.d.ts.map +1 -1
  238. package/dist/tools/web-fetch.js +2 -1
  239. package/dist/tools/web-fetch.js.map +1 -1
  240. package/dist/types/index.d.ts +46 -1
  241. package/dist/types/index.d.ts.map +1 -1
  242. package/dist/types/index.js.map +1 -1
  243. package/dist/utils.d.ts +23 -2
  244. package/dist/utils.d.ts.map +1 -1
  245. package/dist/utils.js +76 -20
  246. package/dist/utils.js.map +1 -1
  247. package/dist/version.d.ts +2 -0
  248. package/dist/version.d.ts.map +1 -0
  249. package/dist/version.js +47 -0
  250. package/dist/version.js.map +1 -0
  251. 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":"AAAA,2EAA2E;AAC3E,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,iEAAiE;AACjE,oEAAoE;AACpE,uEAAuE;AACvE,mEAAmE;AACnE,EAAE;AACF,qEAAqE;AACrE,sEAAsE;AACtE,qEAAqE;AACrE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AACjC,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAKtD;;gCAEgC;AAChC,MAAM,UAAU,aAAa,CAAC,KAAkB,EAAE,GAAW;IAC3D,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,iBAAiB,EAAE,CAAA;IAChD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;AACjD,CAAC;AAED;;+DAE+D;AAC/D,KAAK,UAAU,gBAAgB,CAAC,KAAkB,EAAE,GAAW;IAC7D,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IACtC,IAAI,GAAW,CAAA;IACf,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAA;QACzC,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACnE,OAAO,MAAiC,CAAA;QAC1C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,oEAAoE;QACpE,sEAAsE;QACtE,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,6EAA6E,CAAC,CAAA;IACtH,CAAC;IACD,OAAO,EAAE,CAAA;AACX,CAAC;AAED;qEACqE;AACrE,KAAK,UAAU,iBAAiB,CAAC,KAAkB,EAAE,GAAW,EAAE,GAA4B;IAC5F,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IACtC,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACvD,MAAM,GAAG,GAAG,IAAI,GAAG,MAAM,CAAA;IACzB,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAA;IACrE,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;AAC5B,CAAC;AAOD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,GAAW;IACzD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IAChH,IAAI,IAAI,IAAI,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IAC5C,IAAI,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IACjC,IAAI,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IACvC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;AAC9B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,KAAkB,EAAE,GAAW;IAC9E,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC9C,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAA;IAC9B,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAA;IACnF,OAAO,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;AAC5D,CAAC;AAED;;4DAE4D;AAC5D,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAY,EACZ,MAAuB,EACvB,KAAkB,EAClB,GAAW;IAEX,uEAAuE;IACvE,qEAAqE;IACrE,gBAAgB;IAChB,MAAM,SAAS,GAAG,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAEjD,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC9C,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAA;IAC/B,MAAM,OAAO,GACX,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAClE,CAAC,CAAC,EAAE,GAAI,QAAoC,EAAE;QAC9C,CAAC,CAAC,EAAE,CAAA;IACR,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAAA;IACzB,GAAG,CAAC,UAAU,GAAG,OAAO,CAAA;IACxB,MAAM,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;IACxC,OAAO,EAAE,IAAI,EAAE,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAA;AAC5C,CAAC;AAED;;;;8CAI8C;AAC9C,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,IAAY,EACZ,KAAkB,EAClB,GAAW;IAEX,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IACtC,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC9C,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAA;IAC/B,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAA;IACvC,CAAC;IACD,MAAM,OAAO,GAAG,QAAmC,CAAA;IACnD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;QACzD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAA;IACvC,CAAC;IACD,MAAM,IAAI,GAA4B,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7C,IAAI,CAAC,KAAK,IAAI;YAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAC7B,CAAC;IACD,GAAG,CAAC,UAAU,GAAG,IAAI,CAAA;IACrB,MAAM,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;IACxC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AACtC,CAAC;AAED;;;oEAGoE;AACpE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAkB,EAAE,GAAW;IAClF,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;QAC9C,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAA;QAC9B,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAA;QAClF,MAAM,KAAK,GAAI,OAAmC,CAAC,IAAI,CAAC,CAAA;QACxD,OAAO,KAAK,IAAI,IAAI,CAAA;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC"}
@@ -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