@agentxm/extension-materialization 0.28.14-preview.1789139351.5cd4595aa

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 (89) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +16 -0
  3. package/dist/src/desired-state/errors.d.ts +22 -0
  4. package/dist/src/desired-state/errors.js +22 -0
  5. package/dist/src/desired-state/retained-materialization.d.ts +58 -0
  6. package/dist/src/desired-state/retained-materialization.js +119 -0
  7. package/dist/src/errors.d.ts +42 -0
  8. package/dist/src/errors.js +14 -0
  9. package/dist/src/extensions/canonical-directory.d.ts +106 -0
  10. package/dist/src/extensions/canonical-directory.js +210 -0
  11. package/dist/src/extensions/canonical-reuse.d.ts +43 -0
  12. package/dist/src/extensions/canonical-reuse.js +38 -0
  13. package/dist/src/extensions/copy-directory.d.ts +50 -0
  14. package/dist/src/extensions/copy-directory.js +78 -0
  15. package/dist/src/extensions/errors.d.ts +97 -0
  16. package/dist/src/extensions/errors.js +42 -0
  17. package/dist/src/extensions/materializable-from-disk.d.ts +37 -0
  18. package/dist/src/extensions/materializable-from-disk.js +89 -0
  19. package/dist/src/extensions/operations.d.ts +252 -0
  20. package/dist/src/extensions/operations.js +631 -0
  21. package/dist/src/hooks/errors.d.ts +33 -0
  22. package/dist/src/hooks/errors.js +18 -0
  23. package/dist/src/hooks/manager.d.ts +13 -0
  24. package/dist/src/hooks/manager.js +778 -0
  25. package/dist/src/index.d.ts +44 -0
  26. package/dist/src/index.js +56 -0
  27. package/dist/src/knowledge/errors.d.ts +75 -0
  28. package/dist/src/knowledge/errors.js +36 -0
  29. package/dist/src/knowledge/manager.d.ts +9 -0
  30. package/dist/src/knowledge/manager.js +709 -0
  31. package/dist/src/live.d.ts +20 -0
  32. package/dist/src/live.js +20 -0
  33. package/dist/src/manager-contract.d.ts +136 -0
  34. package/dist/src/manager-contract.js +19 -0
  35. package/dist/src/manager-registry-live.d.ts +11 -0
  36. package/dist/src/manager-registry-live.js +23 -0
  37. package/dist/src/manager-registry.d.ts +37 -0
  38. package/dist/src/manager-registry.js +22 -0
  39. package/dist/src/managers.d.ts +147 -0
  40. package/dist/src/managers.js +32 -0
  41. package/dist/src/mcps/artifact.d.ts +24 -0
  42. package/dist/src/mcps/artifact.js +74 -0
  43. package/dist/src/mcps/authored-materialization.d.ts +28 -0
  44. package/dist/src/mcps/authored-materialization.js +34 -0
  45. package/dist/src/mcps/errors.d.ts +105 -0
  46. package/dist/src/mcps/errors.js +40 -0
  47. package/dist/src/mcps/install-operation.d.ts +92 -0
  48. package/dist/src/mcps/install-operation.js +557 -0
  49. package/dist/src/mcps/manager.d.ts +15 -0
  50. package/dist/src/mcps/manager.js +253 -0
  51. package/dist/src/mcps/native-entry.d.ts +51 -0
  52. package/dist/src/mcps/native-entry.js +100 -0
  53. package/dist/src/mcps/secret-store-live.d.ts +14 -0
  54. package/dist/src/mcps/secret-store-live.js +58 -0
  55. package/dist/src/mcps/secret-store.d.ts +55 -0
  56. package/dist/src/mcps/secret-store.js +33 -0
  57. package/dist/src/packs/errors.d.ts +50 -0
  58. package/dist/src/packs/errors.js +24 -0
  59. package/dist/src/packs/manager.d.ts +16 -0
  60. package/dist/src/packs/manager.js +189 -0
  61. package/dist/src/projection-participants-live.d.ts +17 -0
  62. package/dist/src/projection-participants-live.js +66 -0
  63. package/dist/src/projection-step-failure.d.ts +19 -0
  64. package/dist/src/projection-step-failure.js +76 -0
  65. package/dist/src/registry-materialization.d.ts +52 -0
  66. package/dist/src/registry-materialization.js +67 -0
  67. package/dist/src/rules/errors.d.ts +31 -0
  68. package/dist/src/rules/errors.js +17 -0
  69. package/dist/src/rules/manager.d.ts +18 -0
  70. package/dist/src/rules/manager.js +492 -0
  71. package/dist/src/skills/errors.d.ts +42 -0
  72. package/dist/src/skills/errors.js +21 -0
  73. package/dist/src/skills/manager.d.ts +17 -0
  74. package/dist/src/skills/manager.js +339 -0
  75. package/dist/src/skills/materialization.d.ts +52 -0
  76. package/dist/src/skills/materialization.js +168 -0
  77. package/dist/src/skills/skill-artifact.d.ts +38 -0
  78. package/dist/src/skills/skill-artifact.js +70 -0
  79. package/dist/src/skills/source-hash.d.ts +2 -0
  80. package/dist/src/skills/source-hash.js +6 -0
  81. package/dist/src/subagents/errors.d.ts +43 -0
  82. package/dist/src/subagents/errors.js +20 -0
  83. package/dist/src/subagents/lock-entry-builder.d.ts +20 -0
  84. package/dist/src/subagents/lock-entry-builder.js +63 -0
  85. package/dist/src/subagents/manager.d.ts +17 -0
  86. package/dist/src/subagents/manager.js +736 -0
  87. package/dist/src/testing.d.ts +87 -0
  88. package/dist/src/testing.js +116 -0
  89. package/package.json +70 -0
@@ -0,0 +1,557 @@
1
+ /**
2
+ * The MCP server installation operation: acquire the package, reconcile the
3
+ * connection's credentials, record the accepted resolution and the settings
4
+ * entry, and project the connection into every configured agent.
5
+ *
6
+ * It lives in the materialization capability rather than in a feature because
7
+ * four surfaces need it — `axm mcps install`, pack member installation,
8
+ * reconciliation, and the authoring routes that install an MCP server they
9
+ * have just scaffolded, forked, adopted, or imported — and a feature may not
10
+ * import a peer feature. Requirements stay in `R` and failures stay typed in
11
+ * `E`; the caller composes the layer and renders the failure.
12
+ *
13
+ * @experimental This API is unstable and may change without notice.
14
+ */
15
+ import * as FileSystem from "effect/FileSystem";
16
+ import * as Path from "effect/Path";
17
+ import * as Array from "effect/Array";
18
+ import * as Effect from "effect/Effect";
19
+ import * as Option from "effect/Option";
20
+ import * as Schema from "effect/Schema";
21
+ import { NativeWriteAuthority } from "@agentxm/agent-integration";
22
+ import { CodingAgentRepository, applyProjectionPlansWithResults, planSingletonProjection, } from "@agentxm/workspace-projection";
23
+ import { mcpRegistryResolutionKey } from "@agentxm/workspace-state";
24
+ import { isPathSafe } from "@agentxm/extension-model/unstable/path-types";
25
+ import { acceptedRegistryVersionForRef, validateExactResolvedVersion, } from "@agentxm/workspace-state";
26
+ import { appendWarningsToMessage } from "@agentxm/workspace-operations";
27
+ import { WorkspaceMutations } from "@agentxm/workspace-state";
28
+ import { canReuseInstalledPackage } from "../extensions/canonical-directory.js";
29
+ import { materializeRegistryPackage } from "../registry-materialization.js";
30
+ import { computeExtensionPathsForLayout } from "@agentxm/workspace-state";
31
+ import { printSourceParams } from "@agentxm/extension-model/unstable/sources/printer";
32
+ import { computeMaterializedTreeIntegrity } from "@agentxm/workspace-state";
33
+ import { decodeVersionSync } from "@agentxm/extension-model/unstable/version-constraints";
34
+ import { MCP_SERVER_MANIFEST_FILENAME, McpServerManifestSchema, } from "@agentxm/extension-model/unstable/mcps/manifest-schema";
35
+ import { agentConfigTargets, mcpServerArtifact, mcpSettingsTarget, mcpSourceTarget, } from "./artifact.js";
36
+ import { McpAgentSyncRefused, McpCanonicalPathUnsafe, McpLocalNameConflict, McpRegistryOnlyInstall, McpRequiredInputsMissing, McpWorkspacePackageInvalid, } from "./errors.js";
37
+ import { McpSecretStore, mcpSecretAccount } from "./secret-store.js";
38
+ // -----------------------------------------------------------------------------
39
+ // Lock entry builder
40
+ // -----------------------------------------------------------------------------
41
+ const buildLockEntry = (ref, treeIntegrity) => ({
42
+ type: "registry",
43
+ sourceType: "registry",
44
+ packageFormat: "agentxm",
45
+ endpoint: ref.source.location,
46
+ extensionType: "mcp-server",
47
+ workspaceName: ref.server.name,
48
+ owner: ref.owner,
49
+ name: ref.name,
50
+ resolvedVersion: decodeVersionSync(ref.version),
51
+ integrity: Option.getOrElse(ref.integrity, () => ""),
52
+ sourceName: ref.source.name,
53
+ publisherBindingId: ref.publisherBindingId,
54
+ treeIntegrity,
55
+ });
56
+ const maybeSecretInputName = (input) => {
57
+ if (input.isSecret !== true)
58
+ return undefined;
59
+ if ("name" in input)
60
+ return input.name;
61
+ if ("valueHint" in input)
62
+ return input.valueHint;
63
+ return undefined;
64
+ };
65
+ /**
66
+ * Named environment inputs the manifest marks required. Only key/value inputs
67
+ * are collected: an unnamed input has nothing a caller could pass through
68
+ * `--env KEY=VALUE`, so it cannot be reported as a missing name.
69
+ */
70
+ const collectRequiredInputNames = (manifest) => {
71
+ const names = new Set();
72
+ const add = (input) => {
73
+ if (input.isRequired === true && input.value === undefined && input.default === undefined) {
74
+ names.add(input.name);
75
+ }
76
+ };
77
+ for (const pkg of manifest.server.packages ?? []) {
78
+ for (const input of pkg.environmentVariables ?? [])
79
+ add(input);
80
+ }
81
+ for (const remote of manifest.server.remotes ?? []) {
82
+ for (const input of remote.headers ?? [])
83
+ add(input);
84
+ }
85
+ return names;
86
+ };
87
+ export const collectSecretInputNames = (manifest) => {
88
+ const names = new Set();
89
+ const add = (input) => {
90
+ const name = maybeSecretInputName(input);
91
+ if (name !== undefined)
92
+ names.add(name);
93
+ };
94
+ for (const pkg of manifest.server.packages ?? []) {
95
+ for (const input of pkg.environmentVariables ?? [])
96
+ add(input);
97
+ for (const input of pkg.runtimeArguments ?? [])
98
+ add(input);
99
+ for (const input of pkg.packageArguments ?? [])
100
+ add(input);
101
+ }
102
+ for (const remote of manifest.server.remotes ?? []) {
103
+ for (const input of remote.headers ?? [])
104
+ add(input);
105
+ for (const [name, input] of Object.entries(remote.variables ?? {})) {
106
+ if (input.isSecret === true)
107
+ names.add(name);
108
+ }
109
+ }
110
+ return names;
111
+ };
112
+ // -----------------------------------------------------------------------------
113
+ // Registry install
114
+ // -----------------------------------------------------------------------------
115
+ const installFromRegistry = (ref, reuse) => Effect.gen(function* () {
116
+ const path = yield* Path.Path;
117
+ const ws = yield* WorkspaceMutations;
118
+ const canonicalPath = computeExtensionPathsForLayout(path.join, ws.layout, ref, "mcps", ref.name).canonicalPath;
119
+ if (!isPathSafe(path, ws.baseDir, canonicalPath)) {
120
+ return yield* new McpCanonicalPathUnsafe({ serverName: ref.name, canonicalPath });
121
+ }
122
+ const useExisting = yield* canReuseInstalledPackage({
123
+ installedPath: canonicalPath,
124
+ force: reuse.force,
125
+ refVersion: ref.version,
126
+ hasIntegrity: Option.isSome(ref.integrity),
127
+ ...(reuse.lockedVersion === undefined ? {} : { lockedVersion: reuse.lockedVersion }),
128
+ existsFailureDetail: (target) => `Failed to check if canonical path exists: ${target}`,
129
+ });
130
+ if (!useExisting) {
131
+ yield* materializeRegistryPackage({
132
+ baseDir: ws.baseDir,
133
+ destinationPath: canonicalPath,
134
+ sourceLocation: ref.source.location,
135
+ owner: ref.owner,
136
+ type: "mcp-server",
137
+ name: ref.name,
138
+ version: ref.version,
139
+ integrity: ref.integrity,
140
+ messages: {
141
+ integrityMismatchDetail: `Integrity mismatch for ${ref.name}@${ref.version}`,
142
+ },
143
+ });
144
+ }
145
+ return canonicalPath;
146
+ });
147
+ export const readMcpServerManifest = (canonicalPath) => Effect.gen(function* () {
148
+ const fs = yield* FileSystem.FileSystem;
149
+ const path = yield* Path.Path;
150
+ const manifestPath = path.join(canonicalPath, MCP_SERVER_MANIFEST_FILENAME);
151
+ const exists = yield* fs.exists(manifestPath).pipe(Effect.catch(() => Effect.succeed(false)));
152
+ if (!exists) {
153
+ return Option.none();
154
+ }
155
+ const manifest = yield* fs.readFileString(manifestPath).pipe(Effect.flatMap((raw) => Effect.try({
156
+ try: () => {
157
+ const parsed = JSON.parse(raw);
158
+ return Schema.decodeUnknownSync(McpServerManifestSchema)(parsed);
159
+ },
160
+ catch: () => undefined,
161
+ })), Effect.catch(() => Effect.void));
162
+ if (manifest === undefined) {
163
+ return Option.none();
164
+ }
165
+ return Option.some(manifest);
166
+ });
167
+ const isNothingRunnableManifest = (manifest) => Option.match(manifest, {
168
+ onNone: () => false,
169
+ onSome: (value) => (value.server.packages === undefined || value.server.packages.length === 0) &&
170
+ (value.server.remotes === undefined || value.server.remotes.length === 0),
171
+ });
172
+ const loadStoredMcpSecrets = (identity, secretNames) => Effect.gen(function* () {
173
+ const secrets = yield* McpSecretStore;
174
+ const entries = yield* Effect.forEach(secretNames, (name) => secrets
175
+ .read(mcpSecretAccount({ ...identity, inputName: name }))
176
+ .pipe(Effect.map((value) => ({ name, value }))), { concurrency: "unbounded" });
177
+ const loaded = {};
178
+ for (const { name, value } of entries) {
179
+ if (Option.isSome(value))
180
+ loaded[name] = value.value;
181
+ }
182
+ return loaded;
183
+ });
184
+ const persistMcpSecrets = (identity, secretNames, values) => Effect.gen(function* () {
185
+ const secrets = yield* McpSecretStore;
186
+ return yield* Effect.forEach(secretNames, (inputName) => {
187
+ const value = values[inputName];
188
+ return value === undefined
189
+ ? Effect.succeed({ _tag: "skipped", inputName })
190
+ : secrets
191
+ .write(mcpSecretAccount({ ...identity, inputName }), value)
192
+ .pipe(Effect.map((outcome) => ({ _tag: outcome, inputName })));
193
+ }, { concurrency: "unbounded" });
194
+ });
195
+ /**
196
+ * Erase every credential one connection holds. Erasure is reported per input:
197
+ * a credential store that is unavailable leaves the secret behind and says so,
198
+ * rather than failing the removal that has already settled.
199
+ */
200
+ export const deleteMcpSecrets = (identity, secretNames) => Effect.gen(function* () {
201
+ const secrets = yield* McpSecretStore;
202
+ return yield* Effect.forEach(secretNames, (inputName) => secrets
203
+ .erase(mcpSecretAccount({ ...identity, inputName }))
204
+ .pipe(Effect.map((outcome) => ({ _tag: outcome, inputName }))), { concurrency: "unbounded" });
205
+ });
206
+ const redactSettingsEnv = (values, secretNames) => {
207
+ const redacted = {};
208
+ for (const [name, value] of Object.entries(values)) {
209
+ if (!secretNames.has(name))
210
+ redacted[name] = value;
211
+ }
212
+ return redacted;
213
+ };
214
+ const preserveSecretReferences = (values, secretNames) => Object.fromEntries(Object.entries(values).map(([name, value]) => [
215
+ name,
216
+ secretNames.has(name) ? `\${${name}}` : value,
217
+ ]));
218
+ const REQUIRED_AGENT_IDS = new Set([
219
+ "claude-code",
220
+ "opencode",
221
+ "github-copilot-cli",
222
+ "cursor",
223
+ "gemini-cli",
224
+ "codex",
225
+ ]);
226
+ const formatAgentSyncWarning = (serverName, outcomes) => {
227
+ const warningMessage = outcomes
228
+ .map(({ agentId, outcome }) => outcome._tag === "success"
229
+ ? `${agentId}:success`
230
+ : outcome._tag === "fallback"
231
+ ? `${agentId}:fallback(${outcome.fallbackFrom}):${outcome.reason}`
232
+ : `${agentId}:${outcome.reason}`)
233
+ .join(", ");
234
+ return `MCP agent sync warnings for ${serverName}: ${warningMessage}`;
235
+ };
236
+ const summarizeAgentSync = (outcomes, warnings) => {
237
+ const degraded = outcomes.some(({ outcome }) => outcome._tag === "failed" || outcome._tag === "fallback");
238
+ const details = outcomes.map(({ agentId, outcome }) => outcome._tag === "success"
239
+ ? `${agentId}:success`
240
+ : outcome._tag === "fallback"
241
+ ? `${agentId}:fallback:${outcome.fallbackFrom}`
242
+ : `${agentId}:${outcome._tag}`);
243
+ return {
244
+ status: degraded ? "degraded" : "green",
245
+ details,
246
+ warnings,
247
+ outcomes,
248
+ };
249
+ };
250
+ const syncConfiguredAgentsOnInstall = (args) => Effect.gen(function* () {
251
+ const agentRepo = yield* CodingAgentRepository;
252
+ const warnings = [];
253
+ const unknownConfiguredAgentIds = yield* agentRepo.getUnknownConfiguredAgentIds();
254
+ if (args.strict && unknownConfiguredAgentIds.length > 0) {
255
+ return yield* new McpAgentSyncRefused({
256
+ serverName: args.serverName,
257
+ fault: "unknown-agents",
258
+ agentIds: unknownConfiguredAgentIds,
259
+ });
260
+ }
261
+ if (unknownConfiguredAgentIds.length > 0) {
262
+ warnings.push(`Skipping unknown configured agents: ${unknownConfiguredAgentIds.join(", ")}`);
263
+ }
264
+ const configuredAgents = yield* agentRepo.getConfiguredAgents();
265
+ let outcomes;
266
+ if (args.nothingRunnable) {
267
+ outcomes = configuredAgents.map((agent) => ({
268
+ agentId: agent.id,
269
+ outcome: {
270
+ _tag: "nothing-runnable",
271
+ reason: "manifest server has no packages or remotes",
272
+ },
273
+ }));
274
+ }
275
+ else {
276
+ outcomes = yield* applyProjectionPlansWithResults(configuredAgents.map((agent) => planSingletonProjection({
277
+ unitId: "mcp-server:native-config-entry",
278
+ targetFile: "mcp:configured-agents",
279
+ contributor: args,
280
+ adapter: {
281
+ observe: () => Effect.succeed({
282
+ unitId: "mcp-server:native-config-entry",
283
+ path: `${agent.id}:${args.serverName}`,
284
+ present: false,
285
+ current: false,
286
+ expectedContributors: [args.serverName],
287
+ observedContributors: [],
288
+ }),
289
+ apply: () => Effect.gen(function* () {
290
+ const outcome = yield* agent.addMcpServer({
291
+ workspaceRoot: args.wsBaseDir,
292
+ scope: args.scope,
293
+ serverName: args.serverName,
294
+ canonicalPath: args.canonicalPath,
295
+ owner: args.owner,
296
+ resolvedVersion: args.resolvedVersion,
297
+ enabled: args.enabled,
298
+ configValues: args.configValues,
299
+ });
300
+ return { agentId: agent.id, outcome };
301
+ }),
302
+ },
303
+ })));
304
+ }
305
+ const misconfigured = Array.filter(outcomes, ({ outcome }) => outcome._tag === "misconfigured");
306
+ if (misconfigured.length > 0) {
307
+ return yield* new McpAgentSyncRefused({
308
+ serverName: args.serverName,
309
+ fault: "misconfigured",
310
+ agentIds: misconfigured.map(({ agentId }) => agentId),
311
+ });
312
+ }
313
+ const failed = Array.filter(outcomes, ({ outcome }) => outcome._tag === "failed");
314
+ if (args.strict && failed.length > 0) {
315
+ return yield* new McpAgentSyncRefused({
316
+ serverName: args.serverName,
317
+ fault: "failed",
318
+ agentIds: failed.map(({ agentId }) => agentId),
319
+ });
320
+ }
321
+ const strictDisabledFailures = Array.filter(outcomes, ({ agentId, outcome }) => (outcome._tag === "disabled" ||
322
+ (outcome._tag === "fallback" && outcome.fallbackFrom === "disabled")) &&
323
+ args.strict &&
324
+ REQUIRED_AGENT_IDS.has(agentId));
325
+ if (strictDisabledFailures.length > 0) {
326
+ return yield* new McpAgentSyncRefused({
327
+ serverName: args.serverName,
328
+ fault: "disabled",
329
+ agentIds: strictDisabledFailures.map(({ agentId }) => agentId),
330
+ });
331
+ }
332
+ const warningOutcomes = Array.filter(outcomes, ({ outcome }) => outcome._tag === "unsupported" ||
333
+ outcome._tag === "disabled" ||
334
+ outcome._tag === "nothing-runnable" ||
335
+ outcome._tag === "needs-input" ||
336
+ outcome._tag === "failed" ||
337
+ outcome._tag === "fallback");
338
+ if (warningOutcomes.length > 0) {
339
+ warnings.push(formatAgentSyncWarning(args.serverName, warningOutcomes));
340
+ }
341
+ return summarizeAgentSync(outcomes, warnings);
342
+ });
343
+ /**
344
+ * Install one MCP server: acquire the package (registry archive or the
345
+ * workspace-authored directory), merge the connection's inputs with what the
346
+ * credential store already holds, record the settings entry and the accepted
347
+ * resolution, and project the connection into every configured agent.
348
+ *
349
+ * Failures stay typed: a refused request, an unrepresentable connection, and a
350
+ * credential store that will not persist a secret are three different facts,
351
+ * and the caller decides how each is reported.
352
+ */
353
+ export const installMcpServer = (op) => Effect.gen(function* () {
354
+ const ws = yield* WorkspaceMutations;
355
+ const path = yield* Path.Path;
356
+ const { ref } = op.args;
357
+ const localName = op.args.localName ?? ref.server.name;
358
+ if (ref.refType !== "registry" && ref.refType !== "workspace") {
359
+ return yield* new McpRegistryOnlyInstall({
360
+ serverName: ref.server.name,
361
+ refType: ref.refType,
362
+ });
363
+ }
364
+ const strictAgentSync = Option.getOrElse(op.args.strictAgentSync ?? Option.none(), () => false);
365
+ const env = Option.getOrElse(op.args.env ?? Option.none(), () => ({}));
366
+ const resolutionKey = ref.refType === "registry"
367
+ ? mcpRegistryResolutionKey({
368
+ authority: ref.source.location,
369
+ owner: ref.owner,
370
+ name: ref.server.name,
371
+ })
372
+ : undefined;
373
+ const sourceIdentity = resolutionKey ?? `workspace:${ref.owner}/mcps/${ref.server.name}`;
374
+ const desiredGraph = yield* ws.getDesiredStateGraph();
375
+ const existingLocalNode = desiredGraph.nodes.find((node) => node.type === "mcp-server" && node.name === localName);
376
+ if (existingLocalNode !== undefined &&
377
+ (existingLocalNode.authority === "inline" || existingLocalNode.identity !== sourceIdentity)) {
378
+ return yield* new McpLocalNameConflict({
379
+ localName,
380
+ requestedIdentity: sourceIdentity,
381
+ owningIdentity: existingLocalNode.authority === "inline" ? "inline" : existingLocalNode.identity,
382
+ });
383
+ }
384
+ const existingClosure = desiredGraph.mcpSourceClosures.find((closure) => closure.identity === sourceIdentity);
385
+ const lockedVersion = ref.refType === "registry"
386
+ ? acceptedRegistryVersionForRef(yield* ws.getLockedMcpServer(resolutionKey ?? ""), ref)
387
+ : undefined;
388
+ const canonicalPath = ref.refType === "registry"
389
+ ? yield* installFromRegistry(ref, { force: op.args.force, lockedVersion })
390
+ : yield* Effect.gen(function* () {
391
+ const fs = yield* FileSystem.FileSystem;
392
+ const path = yield* Path.Path;
393
+ const expectedPath = path.join(ws.layout.scope === "project"
394
+ ? ws.layout.authoredRoot("mcp-server")
395
+ : path.join(ws.layout.acquiredRoot, ref.owner, "mcps"), ref.name);
396
+ if (ref.scope !== ws.scope ||
397
+ path.resolve(ref.location) !== path.resolve(expectedPath)) {
398
+ return yield* new McpWorkspacePackageInvalid({
399
+ serverName: ref.server.name,
400
+ location: ref.location,
401
+ fault: "outside-workspace",
402
+ });
403
+ }
404
+ const exists = yield* fs.exists(ref.location).pipe(Effect.mapError((error) => new McpWorkspacePackageInvalid({
405
+ serverName: ref.server.name,
406
+ location: ref.location,
407
+ fault: "unreadable",
408
+ cause: error,
409
+ })));
410
+ if (!exists) {
411
+ return yield* new McpWorkspacePackageInvalid({
412
+ serverName: ref.server.name,
413
+ location: ref.location,
414
+ fault: "missing",
415
+ });
416
+ }
417
+ return ref.location;
418
+ });
419
+ const manifest = yield* readMcpServerManifest(canonicalPath);
420
+ const nothingRunnable = isNothingRunnableManifest(manifest);
421
+ const secretNames = Option.match(manifest, {
422
+ onNone: () => new Set(),
423
+ onSome: collectSecretInputNames,
424
+ });
425
+ if (ref.refType === "registry") {
426
+ yield* validateExactResolvedVersion(`mcpServers.${ref.server.name}.resolvedVersion`, ref.version);
427
+ }
428
+ const lockEntry = ref.refType === "registry"
429
+ ? buildLockEntry(ref, yield* computeMaterializedTreeIntegrity(canonicalPath))
430
+ : undefined;
431
+ const currentMcpServers = yield* ws.getConfiguredMcpServerEntries();
432
+ const currentEntry = currentMcpServers[localName];
433
+ const secretIdentity = {
434
+ scopeRoot: path.resolve(ws.baseDir),
435
+ localName,
436
+ sourceIdentity,
437
+ };
438
+ const storedSecrets = yield* loadStoredMcpSecrets(secretIdentity, secretNames);
439
+ const mergedEnv = { ...storedSecrets, ...(currentEntry?.env ?? {}), ...env };
440
+ // Under --non-interactive there is nobody to prompt, so a required input
441
+ // that nothing supplied would otherwise install a server that cannot start.
442
+ // Fail with the exact recipe instead.
443
+ const requiredInputNames = Option.match(manifest, {
444
+ onNone: () => new Set(),
445
+ onSome: collectRequiredInputNames,
446
+ });
447
+ const missingInputs = [...requiredInputNames]
448
+ .filter((name) => mergedEnv[name] === undefined || mergedEnv[name] === "")
449
+ .sort((left, right) => left.localeCompare(right));
450
+ if (missingInputs.length > 0 && op.args.nonInteractive) {
451
+ return yield* new McpRequiredInputsMissing({ localName, inputNames: missingInputs });
452
+ }
453
+ const persistedEnv = redactSettingsEnv(mergedEnv, secretNames);
454
+ const enabled = currentEntry?.enabled ?? true;
455
+ const settingsEntry = {
456
+ kind: "sourced",
457
+ source: ref.refType === "workspace" ? "workspace" : printSourceParams(ref.source),
458
+ env: persistedEnv,
459
+ enabled,
460
+ };
461
+ const writeEffect = op.args.skipStateWrites === true
462
+ ? Effect.void
463
+ : Option.getOrElse(op.args.skipSettings, () => false)
464
+ ? lockEntry === undefined
465
+ ? Effect.void
466
+ : ws.setMcpServerLock({
467
+ name: resolutionKey ?? ref.server.name,
468
+ resolutionKey: resolutionKey ?? ref.server.name,
469
+ lockEntry,
470
+ versionRange: Option.none(),
471
+ })
472
+ : lockEntry === undefined
473
+ ? ws.setMcpServerEntry(localName, {
474
+ ...settingsEntry,
475
+ })
476
+ : ws.setMcpServer({
477
+ name: localName,
478
+ resolutionKey: resolutionKey ?? localName,
479
+ lockEntry,
480
+ versionRange: op.args.versionRange,
481
+ env: persistedEnv,
482
+ enabled,
483
+ });
484
+ yield* writeEffect;
485
+ const projectionNames = ref.refType === "registry" && lockedVersion !== undefined && lockedVersion !== ref.version
486
+ ? [...new Set([...(existingClosure?.localNames ?? []), localName])].sort()
487
+ : [localName];
488
+ const agentSyncResults = yield* Effect.forEach(projectionNames, (projectionName) => Effect.gen(function* () {
489
+ const projectionEntry = projectionName === localName ? settingsEntry : currentMcpServers[projectionName];
490
+ if (projectionEntry === undefined || projectionEntry.kind === "inline") {
491
+ return undefined;
492
+ }
493
+ const projectionSecretIdentity = {
494
+ scopeRoot: path.resolve(ws.baseDir),
495
+ localName: projectionName,
496
+ sourceIdentity,
497
+ };
498
+ const projectionStoredSecrets = yield* loadStoredMcpSecrets(projectionSecretIdentity, secretNames);
499
+ const projectionEnv = projectionName === localName
500
+ ? mergedEnv
501
+ : { ...projectionStoredSecrets, ...projectionEntry.env };
502
+ return yield* syncConfiguredAgentsOnInstall({
503
+ wsBaseDir: ws.baseDir,
504
+ scope: ws.scope,
505
+ strict: strictAgentSync,
506
+ serverName: projectionName,
507
+ canonicalPath,
508
+ owner: ref.owner,
509
+ resolvedVersion: ref.version,
510
+ nothingRunnable,
511
+ enabled: projectionEntry.enabled,
512
+ configValues: preserveSecretReferences(projectionEnv, secretNames),
513
+ entry: projectionEntry,
514
+ });
515
+ }), { concurrency: 1 });
516
+ const agentSyncSummaries = agentSyncResults.filter((summary) => summary !== undefined);
517
+ const agentSync = {
518
+ status: agentSyncSummaries.some((summary) => summary.status === "degraded")
519
+ ? "degraded"
520
+ : "green",
521
+ details: agentSyncSummaries.flatMap((summary) => summary.details),
522
+ warnings: agentSyncSummaries.flatMap((summary) => summary.warnings),
523
+ outcomes: agentSyncSummaries.flatMap((summary) => summary.outcomes),
524
+ };
525
+ const secretPersistence = yield* persistMcpSecrets(secretIdentity, secretNames, mergedEnv);
526
+ const secretWarnings = secretPersistence.flatMap((outcome) => outcome._tag === "failed"
527
+ ? [
528
+ `${outcome.inputName} could not be saved to the system keychain; AXM state was applied and credential action is required`,
529
+ ]
530
+ : []);
531
+ const warnings = [...secretWarnings, ...agentSync.warnings];
532
+ const change = currentEntry === undefined ? "created" : "updated";
533
+ const agentOutcomes = agentSync.outcomes.flatMap(({ agentId, outcome }) => outcome._tag === "success" || outcome._tag === "fallback"
534
+ ? [
535
+ {
536
+ agentId,
537
+ ...(outcome.targets === undefined ? {} : { targets: outcome.targets }),
538
+ },
539
+ ]
540
+ : []);
541
+ return {
542
+ result: "success",
543
+ message: appendWarningsToMessage(`Installed ${localName} from ${ref.owner}/mcps/${ref.server.name} (canonical=success, agent-sync=${agentSync.status})`, warnings),
544
+ artifact: mcpServerArtifact({
545
+ lockEntry,
546
+ scope: ws.scope,
547
+ change,
548
+ agents: agentOutcomes.map(({ agentId }) => agentId),
549
+ targets: [
550
+ ...(lockEntry === undefined ? [] : [mcpSourceTarget(ws.scope, lockEntry, change)]),
551
+ mcpSettingsTarget(ws.scope, change),
552
+ ...agentConfigTargets(agentOutcomes),
553
+ ],
554
+ }),
555
+ };
556
+ });
557
+ //# sourceMappingURL=install-operation.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * MCP server extension manager service.
3
+ *
4
+ * Implements ExtensionManager<McpServerExtensionRef>. Delegates to existing
5
+ * MCP server materialization functions and workspace service methods.
6
+ *
7
+ * @experimental This API is unstable and may change without notice.
8
+ */
9
+ import * as FileSystem from "effect/FileSystem";
10
+ import * as Path from "effect/Path";
11
+ import * as Layer from "effect/Layer";
12
+ import { McpServerManager } from "../managers.js";
13
+ import { WorkspaceMutations } from "@agentxm/workspace-state";
14
+ export declare const McpServerManagerLive: Layer.Layer<McpServerManager, never, FileSystem.FileSystem | Path.Path | WorkspaceMutations>;
15
+ //# sourceMappingURL=manager.d.ts.map