@agentxm/extension-lifecycle 0.28.4-bootstrap.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/LICENSE +110 -0
- package/dist/src/configured-entry-resolution.d.ts +110 -0
- package/dist/src/configured-entry-resolution.js +723 -0
- package/dist/src/errors.d.ts +39 -0
- package/dist/src/errors.js +45 -0
- package/dist/src/failure-adapter.d.ts +35 -0
- package/dist/src/failure-adapter.js +23 -0
- package/dist/src/hooks/configured-agent-outcomes-provider.d.ts +18 -0
- package/dist/src/hooks/configured-agent-outcomes-provider.js +39 -0
- package/dist/src/hooks/manager.d.ts +15 -0
- package/dist/src/hooks/manager.js +751 -0
- package/dist/src/index.d.ts +38 -0
- package/dist/src/index.js +41 -0
- package/dist/src/internal/fs-helpers.d.ts +12 -0
- package/dist/src/internal/fs-helpers.js +13 -0
- package/dist/src/internal/integrity.d.ts +13 -0
- package/dist/src/internal/integrity.js +17 -0
- package/dist/src/knowledge/manager.d.ts +11 -0
- package/dist/src/knowledge/manager.js +655 -0
- package/dist/src/live.d.ts +18 -0
- package/dist/src/live.js +18 -0
- package/dist/src/mcps/manager.d.ts +16 -0
- package/dist/src/mcps/manager.js +213 -0
- package/dist/src/mcps/operations/artifact.d.ts +24 -0
- package/dist/src/mcps/operations/artifact.js +74 -0
- package/dist/src/mcps/operations/disable.d.ts +18 -0
- package/dist/src/mcps/operations/disable.js +113 -0
- package/dist/src/mcps/operations/enable.d.ts +18 -0
- package/dist/src/mcps/operations/enable.js +218 -0
- package/dist/src/mcps/operations/install.d.ts +59 -0
- package/dist/src/mcps/operations/install.js +566 -0
- package/dist/src/mcps/operations/sync-outcome.d.ts +10 -0
- package/dist/src/mcps/operations/sync-outcome.js +27 -0
- package/dist/src/mcps/operations/uninstall.d.ts +41 -0
- package/dist/src/mcps/operations/uninstall.js +189 -0
- package/dist/src/packs/dependency-resolution.d.ts +65 -0
- package/dist/src/packs/dependency-resolution.js +414 -0
- package/dist/src/packs/expansion.d.ts +59 -0
- package/dist/src/packs/expansion.js +44 -0
- package/dist/src/packs/manager.d.ts +16 -0
- package/dist/src/packs/manager.js +181 -0
- package/dist/src/packs/operations/install.d.ts +67 -0
- package/dist/src/packs/operations/install.js +156 -0
- package/dist/src/packs/resolved-dependency.d.ts +29 -0
- package/dist/src/packs/resolved-dependency.js +28 -0
- package/dist/src/registry-materialization.d.ts +52 -0
- package/dist/src/registry-materialization.js +67 -0
- package/dist/src/resolution-progress.d.ts +18 -0
- package/dist/src/resolution-progress.js +11 -0
- package/dist/src/resolution-timeout.d.ts +5 -0
- package/dist/src/resolution-timeout.js +11 -0
- package/dist/src/rules/manager.d.ts +20 -0
- package/dist/src/rules/manager.js +475 -0
- package/dist/src/skills/manager.d.ts +18 -0
- package/dist/src/skills/manager.js +346 -0
- package/dist/src/skills/materialization.d.ts +54 -0
- package/dist/src/skills/materialization.js +158 -0
- package/dist/src/skills/operations/disable.d.ts +31 -0
- package/dist/src/skills/operations/disable.js +110 -0
- package/dist/src/skills/operations/enable.d.ts +31 -0
- package/dist/src/skills/operations/enable.js +113 -0
- package/dist/src/skills/operations/install-result.d.ts +33 -0
- package/dist/src/skills/operations/install-result.js +10 -0
- package/dist/src/skills/operations/install.d.ts +67 -0
- package/dist/src/skills/operations/install.js +590 -0
- package/dist/src/skills/operations/source-hash.d.ts +2 -0
- package/dist/src/skills/operations/source-hash.js +6 -0
- package/dist/src/skills/operations/uninstall.d.ts +40 -0
- package/dist/src/skills/operations/uninstall.js +157 -0
- package/dist/src/skills/utils.d.ts +11 -0
- package/dist/src/skills/utils.js +29 -0
- package/dist/src/subagents/manager.d.ts +18 -0
- package/dist/src/subagents/manager.js +677 -0
- package/dist/src/subagents/operations/artifact.d.ts +18 -0
- package/dist/src/subagents/operations/artifact.js +43 -0
- package/dist/src/subagents/operations/disable.d.ts +32 -0
- package/dist/src/subagents/operations/disable.js +116 -0
- package/dist/src/subagents/operations/enable.d.ts +30 -0
- package/dist/src/subagents/operations/enable.js +160 -0
- package/dist/src/workflows/install-command/workflow.d.ts +57 -0
- package/dist/src/workflows/install-command/workflow.js +41 -0
- package/dist/src/workflows/uninstall-command/workflow.d.ts +42 -0
- package/dist/src/workflows/uninstall-command/workflow.js +29 -0
- package/package.json +62 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install MCP server executor — orchestrates the per-server installation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Registry-only install: fetch archive, extract to canonical path, update lockfile/settings.
|
|
5
|
+
* Simpler than skills — no agent symlinks.
|
|
6
|
+
*
|
|
7
|
+
* @experimental This API is unstable and may change without notice.
|
|
8
|
+
*/
|
|
9
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
10
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
11
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
12
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return path;
|
|
16
|
+
};
|
|
17
|
+
import * as FileSystem from "effect/FileSystem";
|
|
18
|
+
import * as Path from "effect/Path";
|
|
19
|
+
import * as Array from "effect/Array";
|
|
20
|
+
import * as Effect from "effect/Effect";
|
|
21
|
+
import * as Option from "effect/Option";
|
|
22
|
+
import * as Schema from "effect/Schema";
|
|
23
|
+
import { CodingAgentRepository, McpSharedTargetConflict, applyProjectionPlansWithResults, planSingletonProjection, inspectAgentMcpServer, sharedMcpTargetPolicyConflict, } from "@agentxm/extension-workspace";
|
|
24
|
+
import { isPathSafe } from "@agentxm/workspace-state";
|
|
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 "@agentxm/extension-workspace";
|
|
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 { isMcpServerApplicableToAgent } from "@agentxm/workspace-state";
|
|
36
|
+
import { agentConfigTargets, mcpServerArtifact, mcpSettingsTarget, mcpSourceTarget, } from "./artifact.js";
|
|
37
|
+
import { LifecycleFailureAdapter, withAdaptedStepFailures } from "../../failure-adapter.js";
|
|
38
|
+
import { ExtensionLifecycleFailed } from "../../errors.js";
|
|
39
|
+
// -----------------------------------------------------------------------------
|
|
40
|
+
// Lock entry builder
|
|
41
|
+
// -----------------------------------------------------------------------------
|
|
42
|
+
const buildLockEntry = (ref, treeIntegrity) => ({
|
|
43
|
+
type: "registry",
|
|
44
|
+
sourceType: "registry",
|
|
45
|
+
packageFormat: "agentxm",
|
|
46
|
+
endpoint: ref.source.location,
|
|
47
|
+
extensionType: "mcp-server",
|
|
48
|
+
workspaceName: ref.server.name,
|
|
49
|
+
owner: ref.owner,
|
|
50
|
+
name: ref.name,
|
|
51
|
+
resolvedVersion: decodeVersionSync(ref.version),
|
|
52
|
+
integrity: Option.getOrElse(ref.integrity, () => ""),
|
|
53
|
+
sourceName: ref.source.name,
|
|
54
|
+
publisherBindingId: ref.publisherBindingId,
|
|
55
|
+
treeIntegrity,
|
|
56
|
+
});
|
|
57
|
+
const MCP_SECRET_SERVICE = "axm-mcp";
|
|
58
|
+
const mcpSecretAccount = (serverName, inputName) => `${serverName}:${inputName}`;
|
|
59
|
+
const keyringModuleSpecifier = ["@napi-rs", "keyring"].join("/");
|
|
60
|
+
const loadKeyringEntry = Effect.tryPromise({
|
|
61
|
+
try: async () => {
|
|
62
|
+
const keyring = await import(__rewriteRelativeImportExtension(keyringModuleSpecifier));
|
|
63
|
+
return keyring.Entry;
|
|
64
|
+
},
|
|
65
|
+
catch: () => undefined,
|
|
66
|
+
});
|
|
67
|
+
const saveMcpSecret = (serverName, inputName, value) => Effect.gen(function* () {
|
|
68
|
+
const Entry = yield* loadKeyringEntry;
|
|
69
|
+
return yield* Effect.try({
|
|
70
|
+
try: () => {
|
|
71
|
+
const entry = new Entry(MCP_SECRET_SERVICE, mcpSecretAccount(serverName, inputName));
|
|
72
|
+
entry.setPassword(value);
|
|
73
|
+
return { _tag: "saved", inputName };
|
|
74
|
+
},
|
|
75
|
+
catch: () => undefined,
|
|
76
|
+
});
|
|
77
|
+
}).pipe(Effect.catch(() => Effect.succeed({ _tag: "failed", inputName })));
|
|
78
|
+
const loadMcpSecret = (serverName, inputName) => Effect.gen(function* () {
|
|
79
|
+
const Entry = yield* loadKeyringEntry;
|
|
80
|
+
return yield* Effect.try({
|
|
81
|
+
try: () => {
|
|
82
|
+
const entry = new Entry(MCP_SECRET_SERVICE, mcpSecretAccount(serverName, inputName));
|
|
83
|
+
return Option.fromNullOr(entry.getPassword());
|
|
84
|
+
},
|
|
85
|
+
catch: () => undefined,
|
|
86
|
+
}).pipe(Effect.catch(() => Effect.succeed(Option.none())));
|
|
87
|
+
}).pipe(Effect.catch(() => Effect.succeed(Option.none())));
|
|
88
|
+
const maybeSecretInputName = (input) => {
|
|
89
|
+
if (input.isSecret !== true)
|
|
90
|
+
return undefined;
|
|
91
|
+
if ("name" in input)
|
|
92
|
+
return input.name;
|
|
93
|
+
if ("valueHint" in input)
|
|
94
|
+
return input.valueHint;
|
|
95
|
+
return undefined;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Named environment inputs the manifest marks required. Only key/value inputs
|
|
99
|
+
* are collected: an unnamed input has nothing a caller could pass through
|
|
100
|
+
* `--env KEY=VALUE`, so it cannot be reported as a missing name.
|
|
101
|
+
*/
|
|
102
|
+
const collectRequiredInputNames = (manifest) => {
|
|
103
|
+
const names = new Set();
|
|
104
|
+
const add = (input) => {
|
|
105
|
+
if (input.isRequired === true && input.value === undefined && input.default === undefined) {
|
|
106
|
+
names.add(input.name);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
for (const pkg of manifest.server.packages ?? []) {
|
|
110
|
+
for (const input of pkg.environmentVariables ?? [])
|
|
111
|
+
add(input);
|
|
112
|
+
}
|
|
113
|
+
for (const remote of manifest.server.remotes ?? []) {
|
|
114
|
+
for (const input of remote.headers ?? [])
|
|
115
|
+
add(input);
|
|
116
|
+
}
|
|
117
|
+
return names;
|
|
118
|
+
};
|
|
119
|
+
const collectSecretInputNames = (manifest) => {
|
|
120
|
+
const names = new Set();
|
|
121
|
+
const add = (input) => {
|
|
122
|
+
const name = maybeSecretInputName(input);
|
|
123
|
+
if (name !== undefined)
|
|
124
|
+
names.add(name);
|
|
125
|
+
};
|
|
126
|
+
for (const pkg of manifest.server.packages ?? []) {
|
|
127
|
+
for (const input of pkg.environmentVariables ?? [])
|
|
128
|
+
add(input);
|
|
129
|
+
for (const input of pkg.runtimeArguments ?? [])
|
|
130
|
+
add(input);
|
|
131
|
+
for (const input of pkg.packageArguments ?? [])
|
|
132
|
+
add(input);
|
|
133
|
+
}
|
|
134
|
+
for (const remote of manifest.server.remotes ?? []) {
|
|
135
|
+
for (const input of remote.headers ?? [])
|
|
136
|
+
add(input);
|
|
137
|
+
for (const [name, input] of Object.entries(remote.variables ?? {})) {
|
|
138
|
+
if (input.isSecret === true)
|
|
139
|
+
names.add(name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return names;
|
|
143
|
+
};
|
|
144
|
+
// -----------------------------------------------------------------------------
|
|
145
|
+
// Registry install
|
|
146
|
+
// -----------------------------------------------------------------------------
|
|
147
|
+
const installFromRegistry = (ref, reuse) => Effect.gen(function* () {
|
|
148
|
+
const path = yield* Path.Path;
|
|
149
|
+
const ws = yield* WorkspaceMutations;
|
|
150
|
+
const canonicalPath = computeExtensionPathsForLayout(path.join, ws.layout, ref, "mcps", ref.name).canonicalPath;
|
|
151
|
+
if (!isPathSafe(path, ws.baseDir, canonicalPath)) {
|
|
152
|
+
return yield* new ExtensionLifecycleFailed({
|
|
153
|
+
category: "internal",
|
|
154
|
+
detail: `Path traversal detected: ${canonicalPath}`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const useExisting = yield* canReuseInstalledPackage({
|
|
158
|
+
installedPath: canonicalPath,
|
|
159
|
+
force: reuse.force,
|
|
160
|
+
refVersion: ref.version,
|
|
161
|
+
hasIntegrity: Option.isSome(ref.integrity),
|
|
162
|
+
...(reuse.lockedVersion === undefined ? {} : { lockedVersion: reuse.lockedVersion }),
|
|
163
|
+
existsFailureDetail: (target) => `Failed to check if canonical path exists: ${target}`,
|
|
164
|
+
});
|
|
165
|
+
if (!useExisting) {
|
|
166
|
+
yield* materializeRegistryPackage({
|
|
167
|
+
baseDir: ws.baseDir,
|
|
168
|
+
destinationPath: canonicalPath,
|
|
169
|
+
sourceLocation: ref.source.location,
|
|
170
|
+
owner: ref.owner,
|
|
171
|
+
type: "mcp-server",
|
|
172
|
+
name: ref.name,
|
|
173
|
+
version: ref.version,
|
|
174
|
+
integrity: ref.integrity,
|
|
175
|
+
messages: {
|
|
176
|
+
integrityMismatchDetail: `Integrity mismatch for ${ref.name}@${ref.version}`,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return canonicalPath;
|
|
181
|
+
});
|
|
182
|
+
const readManifest = (canonicalPath) => Effect.gen(function* () {
|
|
183
|
+
const fs = yield* FileSystem.FileSystem;
|
|
184
|
+
const path = yield* Path.Path;
|
|
185
|
+
const manifestPath = path.join(canonicalPath, MCP_SERVER_MANIFEST_FILENAME);
|
|
186
|
+
const exists = yield* fs.exists(manifestPath).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
187
|
+
if (!exists) {
|
|
188
|
+
return Option.none();
|
|
189
|
+
}
|
|
190
|
+
const manifest = yield* fs.readFileString(manifestPath).pipe(Effect.flatMap((raw) => Effect.try({
|
|
191
|
+
try: () => {
|
|
192
|
+
const parsed = JSON.parse(raw);
|
|
193
|
+
return Schema.decodeUnknownSync(McpServerManifestSchema)(parsed);
|
|
194
|
+
},
|
|
195
|
+
catch: () => undefined,
|
|
196
|
+
})), Effect.catch(() => Effect.void));
|
|
197
|
+
if (manifest === undefined) {
|
|
198
|
+
return Option.none();
|
|
199
|
+
}
|
|
200
|
+
return Option.some(manifest);
|
|
201
|
+
});
|
|
202
|
+
const isNothingRunnableManifest = (manifest) => Option.match(manifest, {
|
|
203
|
+
onNone: () => false,
|
|
204
|
+
onSome: (value) => (value.server.packages === undefined || value.server.packages.length === 0) &&
|
|
205
|
+
(value.server.remotes === undefined || value.server.remotes.length === 0),
|
|
206
|
+
});
|
|
207
|
+
const loadStoredMcpSecrets = (serverName, secretNames) => Effect.gen(function* () {
|
|
208
|
+
const entries = yield* Effect.forEach(secretNames, (name) => loadMcpSecret(serverName, name).pipe(Effect.map((value) => ({ name, value }))), { concurrency: "unbounded" });
|
|
209
|
+
const loaded = {};
|
|
210
|
+
for (const { name, value } of entries) {
|
|
211
|
+
if (Option.isSome(value))
|
|
212
|
+
loaded[name] = value.value;
|
|
213
|
+
}
|
|
214
|
+
return loaded;
|
|
215
|
+
});
|
|
216
|
+
const persistMcpSecrets = (serverName, secretNames, values) => Effect.forEach(secretNames, (name) => {
|
|
217
|
+
const value = values[name];
|
|
218
|
+
return value === undefined
|
|
219
|
+
? Effect.succeed({ _tag: "skipped", inputName: name })
|
|
220
|
+
: saveMcpSecret(serverName, name, value);
|
|
221
|
+
}, { concurrency: "unbounded" });
|
|
222
|
+
const redactSettingsEnv = (values, secretNames) => {
|
|
223
|
+
const redacted = {};
|
|
224
|
+
for (const [name, value] of Object.entries(values)) {
|
|
225
|
+
if (!secretNames.has(name))
|
|
226
|
+
redacted[name] = value;
|
|
227
|
+
}
|
|
228
|
+
return redacted;
|
|
229
|
+
};
|
|
230
|
+
const preserveSecretReferences = (values, secretNames) => Object.fromEntries(Object.entries(values).map(([name, value]) => [
|
|
231
|
+
name,
|
|
232
|
+
secretNames.has(name) ? `\${${name}}` : value,
|
|
233
|
+
]));
|
|
234
|
+
const REQUIRED_AGENT_IDS = new Set([
|
|
235
|
+
"claude-code",
|
|
236
|
+
"opencode",
|
|
237
|
+
"github-copilot-cli",
|
|
238
|
+
"cursor",
|
|
239
|
+
"gemini-cli",
|
|
240
|
+
"codex",
|
|
241
|
+
]);
|
|
242
|
+
const formatAgentSyncWarning = (serverName, outcomes) => {
|
|
243
|
+
const warningMessage = outcomes
|
|
244
|
+
.map(({ agentId, outcome }) => outcome._tag === "success"
|
|
245
|
+
? `${agentId}:success`
|
|
246
|
+
: outcome._tag === "fallback"
|
|
247
|
+
? `${agentId}:fallback(${outcome.fallbackFrom}):${outcome.reason}`
|
|
248
|
+
: `${agentId}:${outcome.reason}`)
|
|
249
|
+
.join(", ");
|
|
250
|
+
return `MCP agent sync warnings for ${serverName}: ${warningMessage}`;
|
|
251
|
+
};
|
|
252
|
+
const summarizeAgentSync = (outcomes, warnings) => {
|
|
253
|
+
const degraded = outcomes.some(({ outcome }) => outcome._tag === "failed" || outcome._tag === "fallback");
|
|
254
|
+
const details = outcomes.map(({ agentId, outcome }) => outcome._tag === "success"
|
|
255
|
+
? `${agentId}:success`
|
|
256
|
+
: outcome._tag === "fallback"
|
|
257
|
+
? `${agentId}:fallback:${outcome.fallbackFrom}`
|
|
258
|
+
: `${agentId}:${outcome._tag}`);
|
|
259
|
+
return {
|
|
260
|
+
status: degraded ? "degraded" : "green",
|
|
261
|
+
details,
|
|
262
|
+
warnings,
|
|
263
|
+
outcomes,
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
const syncConfiguredAgentsOnInstall = (args) => Effect.gen(function* () {
|
|
267
|
+
const agentRepo = yield* CodingAgentRepository;
|
|
268
|
+
const fs = yield* FileSystem.FileSystem;
|
|
269
|
+
const path = yield* Path.Path;
|
|
270
|
+
const warnings = [];
|
|
271
|
+
const unknownConfiguredAgentIds = yield* agentRepo.getUnknownConfiguredAgentIds();
|
|
272
|
+
if (args.strict && unknownConfiguredAgentIds.length > 0) {
|
|
273
|
+
const message = `Unknown configured agents in strict mode: ${unknownConfiguredAgentIds.join(", ")}`;
|
|
274
|
+
return yield* new ExtensionLifecycleFailed({
|
|
275
|
+
category: "not_found",
|
|
276
|
+
detail: message,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (unknownConfiguredAgentIds.length > 0) {
|
|
280
|
+
warnings.push(`Skipping unknown configured agents: ${unknownConfiguredAgentIds.join(", ")}`);
|
|
281
|
+
}
|
|
282
|
+
const configuredAgents = yield* agentRepo.getConfiguredAgents();
|
|
283
|
+
const sharedTargetConflict = sharedMcpTargetPolicyConflict({
|
|
284
|
+
entry: args.entry,
|
|
285
|
+
agentIds: configuredAgents.map((agent) => agent.id),
|
|
286
|
+
scope: args.scope,
|
|
287
|
+
});
|
|
288
|
+
if (sharedTargetConflict !== undefined) {
|
|
289
|
+
return yield* new ExtensionLifecycleFailed({
|
|
290
|
+
category: "conflict",
|
|
291
|
+
detail: sharedTargetConflict,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
let outcomes;
|
|
295
|
+
if (args.nothingRunnable) {
|
|
296
|
+
outcomes = configuredAgents.map((agent) => ({
|
|
297
|
+
agentId: agent.id,
|
|
298
|
+
outcome: {
|
|
299
|
+
_tag: "nothing-runnable",
|
|
300
|
+
reason: "manifest server has no packages or remotes",
|
|
301
|
+
},
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
outcomes = yield* applyProjectionPlansWithResults(configuredAgents.map((agent) => planSingletonProjection({
|
|
306
|
+
unitId: "mcp-server:native-config-entry",
|
|
307
|
+
targetFile: "mcp:configured-agents",
|
|
308
|
+
contributor: args,
|
|
309
|
+
adapter: {
|
|
310
|
+
observe: () => Effect.succeed({
|
|
311
|
+
unitId: "mcp-server:native-config-entry",
|
|
312
|
+
path: `${agent.id}:${args.serverName}`,
|
|
313
|
+
present: false,
|
|
314
|
+
current: false,
|
|
315
|
+
expectedContributors: [args.serverName],
|
|
316
|
+
observedContributors: [],
|
|
317
|
+
}),
|
|
318
|
+
apply: () => Effect.gen(function* () {
|
|
319
|
+
if (!isMcpServerApplicableToAgent(args.entry, agent.id)) {
|
|
320
|
+
const inspection = yield* inspectAgentMcpServer({
|
|
321
|
+
workspaceRoot: args.wsBaseDir,
|
|
322
|
+
scope: args.scope,
|
|
323
|
+
agentId: agent.id,
|
|
324
|
+
serverName: args.serverName,
|
|
325
|
+
entry: args.entry,
|
|
326
|
+
});
|
|
327
|
+
if (inspection.status === "unmanaged") {
|
|
328
|
+
return yield* new McpSharedTargetConflict({
|
|
329
|
+
reason: `${agent.id} has an unmanaged MCP server named ${args.serverName}; AXM will not remove it while applying the target policy`,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const outcome = inspection.status === "drift"
|
|
333
|
+
? yield* agent.removeMcpServer({
|
|
334
|
+
workspaceRoot: args.wsBaseDir,
|
|
335
|
+
scope: args.scope,
|
|
336
|
+
serverName: args.serverName,
|
|
337
|
+
})
|
|
338
|
+
: { _tag: "success", targets: [] };
|
|
339
|
+
return { agentId: agent.id, outcome };
|
|
340
|
+
}
|
|
341
|
+
const outcome = yield* agent.addMcpServer({
|
|
342
|
+
workspaceRoot: args.wsBaseDir,
|
|
343
|
+
scope: args.scope,
|
|
344
|
+
serverName: args.serverName,
|
|
345
|
+
canonicalPath: args.canonicalPath,
|
|
346
|
+
owner: args.owner,
|
|
347
|
+
resolvedVersion: args.resolvedVersion,
|
|
348
|
+
enabled: args.enabled,
|
|
349
|
+
configValues: args.configValues,
|
|
350
|
+
});
|
|
351
|
+
return { agentId: agent.id, outcome };
|
|
352
|
+
}).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path)),
|
|
353
|
+
},
|
|
354
|
+
})));
|
|
355
|
+
}
|
|
356
|
+
const misconfigured = Array.filter(outcomes, ({ outcome }) => outcome._tag === "misconfigured");
|
|
357
|
+
if (misconfigured.length > 0) {
|
|
358
|
+
return yield* new ExtensionLifecycleFailed({
|
|
359
|
+
category: "internal",
|
|
360
|
+
detail: `MCP server ${args.serverName} could not be synced to configured agents`,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
const failed = Array.filter(outcomes, ({ outcome }) => outcome._tag === "failed");
|
|
364
|
+
if (args.strict && failed.length > 0) {
|
|
365
|
+
return yield* new ExtensionLifecycleFailed({
|
|
366
|
+
category: "internal",
|
|
367
|
+
detail: `MCP server ${args.serverName} sync failed in strict mode`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const strictDisabledFailures = Array.filter(outcomes, ({ agentId, outcome }) => (outcome._tag === "disabled" ||
|
|
371
|
+
(outcome._tag === "fallback" && outcome.fallbackFrom === "disabled")) &&
|
|
372
|
+
args.strict &&
|
|
373
|
+
REQUIRED_AGENT_IDS.has(agentId));
|
|
374
|
+
if (strictDisabledFailures.length > 0) {
|
|
375
|
+
return yield* new ExtensionLifecycleFailed({
|
|
376
|
+
category: "internal",
|
|
377
|
+
detail: `MCP server ${args.serverName} sync disabled for required configured agents`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const warningOutcomes = Array.filter(outcomes, ({ outcome }) => outcome._tag === "unsupported" ||
|
|
381
|
+
outcome._tag === "disabled" ||
|
|
382
|
+
outcome._tag === "nothing-runnable" ||
|
|
383
|
+
outcome._tag === "needs-input" ||
|
|
384
|
+
outcome._tag === "failed" ||
|
|
385
|
+
outcome._tag === "fallback");
|
|
386
|
+
if (warningOutcomes.length > 0) {
|
|
387
|
+
warnings.push(formatAgentSyncWarning(args.serverName, warningOutcomes));
|
|
388
|
+
}
|
|
389
|
+
return summarizeAgentSync(outcomes, warnings);
|
|
390
|
+
});
|
|
391
|
+
// -----------------------------------------------------------------------------
|
|
392
|
+
// Public API
|
|
393
|
+
// -----------------------------------------------------------------------------
|
|
394
|
+
/**
|
|
395
|
+
* Install-mcp-server operation handler.
|
|
396
|
+
*
|
|
397
|
+
* Registry-only: fetch archive, validate integrity, extract to canonical path,
|
|
398
|
+
* then update lockfile/settings.
|
|
399
|
+
*/
|
|
400
|
+
export const installMcpServer = (op) => Effect.gen(function* () {
|
|
401
|
+
const adapter = yield* LifecycleFailureAdapter;
|
|
402
|
+
const ws = yield* WorkspaceMutations;
|
|
403
|
+
const { ref } = op.args;
|
|
404
|
+
if (ref.refType !== "registry" && ref.refType !== "workspace") {
|
|
405
|
+
return yield* new ExtensionLifecycleFailed({
|
|
406
|
+
category: "usage",
|
|
407
|
+
detail: `MCP servers install from a registry or a workspace package, not from a ${ref.refType} source`,
|
|
408
|
+
suggestions: [
|
|
409
|
+
{
|
|
410
|
+
description: "Install from the registry",
|
|
411
|
+
cmd: `axm mcps install @owner/mcps/${ref.server.name}`,
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const strictAgentSync = Option.getOrElse(op.args.strictAgentSync ?? Option.none(), () => false);
|
|
417
|
+
const env = Option.getOrElse(op.args.env ?? Option.none(), () => ({}));
|
|
418
|
+
const lockedVersion = ref.refType === "registry"
|
|
419
|
+
? acceptedRegistryVersionForRef(yield* ws.getLockedMcpServer(ref.server.name), ref)
|
|
420
|
+
: undefined;
|
|
421
|
+
const canonicalPath = ref.refType === "registry"
|
|
422
|
+
? yield* installFromRegistry(ref, { force: op.args.force, lockedVersion })
|
|
423
|
+
: yield* Effect.gen(function* () {
|
|
424
|
+
const fs = yield* FileSystem.FileSystem;
|
|
425
|
+
const path = yield* Path.Path;
|
|
426
|
+
const expectedPath = path.join(ws.layout.scope === "project"
|
|
427
|
+
? ws.layout.authoredRoot("mcp-server")
|
|
428
|
+
: path.join(ws.layout.acquiredRoot, ref.owner, "mcps"), ref.name);
|
|
429
|
+
if (ref.scope !== ws.scope ||
|
|
430
|
+
path.resolve(ref.location) !== path.resolve(expectedPath)) {
|
|
431
|
+
return yield* new ExtensionLifecycleFailed({
|
|
432
|
+
category: "validation",
|
|
433
|
+
detail: `Invalid workspace MCP server source location: ${ref.location}`,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const exists = yield* fs.exists(ref.location).pipe(Effect.mapError((error) => new ExtensionLifecycleFailed({
|
|
437
|
+
category: "internal",
|
|
438
|
+
detail: `Failed to inspect workspace MCP server package: ${ref.location}`,
|
|
439
|
+
cause: error,
|
|
440
|
+
})));
|
|
441
|
+
if (!exists) {
|
|
442
|
+
return yield* new ExtensionLifecycleFailed({
|
|
443
|
+
category: "validation",
|
|
444
|
+
detail: `Workspace MCP server package is missing: ${ref.location}`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return ref.location;
|
|
448
|
+
});
|
|
449
|
+
const manifest = yield* readManifest(canonicalPath);
|
|
450
|
+
const nothingRunnable = isNothingRunnableManifest(manifest);
|
|
451
|
+
const secretNames = Option.match(manifest, {
|
|
452
|
+
onNone: () => new Set(),
|
|
453
|
+
onSome: collectSecretInputNames,
|
|
454
|
+
});
|
|
455
|
+
if (ref.refType === "registry") {
|
|
456
|
+
yield* validateExactResolvedVersion(`mcpServers.${ref.server.name}.resolvedVersion`, ref.version);
|
|
457
|
+
}
|
|
458
|
+
const lockEntry = ref.refType === "registry"
|
|
459
|
+
? buildLockEntry(ref, yield* computeMaterializedTreeIntegrity(canonicalPath))
|
|
460
|
+
: undefined;
|
|
461
|
+
const currentMcpServers = yield* ws.getConfiguredMcpServerEntries();
|
|
462
|
+
const currentEntry = currentMcpServers[ref.server.name];
|
|
463
|
+
const storedSecrets = yield* loadStoredMcpSecrets(ref.server.name, secretNames);
|
|
464
|
+
const mergedEnv = { ...storedSecrets, ...(currentEntry?.env ?? {}), ...env };
|
|
465
|
+
// Under --non-interactive there is nobody to prompt, so a required input
|
|
466
|
+
// that nothing supplied would otherwise install a server that cannot start.
|
|
467
|
+
// Fail with the exact recipe instead.
|
|
468
|
+
const requiredInputNames = Option.match(manifest, {
|
|
469
|
+
onNone: () => new Set(),
|
|
470
|
+
onSome: collectRequiredInputNames,
|
|
471
|
+
});
|
|
472
|
+
const missingInputs = [...requiredInputNames]
|
|
473
|
+
.filter((name) => mergedEnv[name] === undefined || mergedEnv[name] === "")
|
|
474
|
+
.sort((left, right) => left.localeCompare(right));
|
|
475
|
+
if (missingInputs.length > 0 && op.args.nonInteractive) {
|
|
476
|
+
return yield* new ExtensionLifecycleFailed({
|
|
477
|
+
category: "usage",
|
|
478
|
+
detail: `${ref.server.name} needs ${missingInputs.join(", ")}, and --non-interactive cannot prompt for them`,
|
|
479
|
+
suggestions: [
|
|
480
|
+
{
|
|
481
|
+
description: "Supply each required input on the command line",
|
|
482
|
+
cmd: missingInputs.map((name) => `--env ${name}=<value>`).join(" "),
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const persistedEnv = redactSettingsEnv(mergedEnv, secretNames);
|
|
488
|
+
const enabled = currentEntry?.enabled ?? true;
|
|
489
|
+
const agents = op.args.agents ?? currentEntry?.agents;
|
|
490
|
+
const settingsEntry = {
|
|
491
|
+
kind: "sourced",
|
|
492
|
+
source: ref.refType === "workspace" ? "workspace" : printSourceParams(ref.source),
|
|
493
|
+
env: persistedEnv,
|
|
494
|
+
enabled,
|
|
495
|
+
...(agents === undefined ? {} : { agents }),
|
|
496
|
+
};
|
|
497
|
+
const agentSync = yield* syncConfiguredAgentsOnInstall({
|
|
498
|
+
wsBaseDir: ws.baseDir,
|
|
499
|
+
scope: ws.scope,
|
|
500
|
+
strict: strictAgentSync,
|
|
501
|
+
serverName: ref.server.name,
|
|
502
|
+
canonicalPath,
|
|
503
|
+
owner: ref.owner,
|
|
504
|
+
resolvedVersion: ref.version,
|
|
505
|
+
nothingRunnable,
|
|
506
|
+
enabled,
|
|
507
|
+
configValues: preserveSecretReferences(mergedEnv, secretNames),
|
|
508
|
+
entry: settingsEntry,
|
|
509
|
+
});
|
|
510
|
+
const secretPersistence = yield* persistMcpSecrets(ref.server.name, secretNames, mergedEnv);
|
|
511
|
+
const secretWarnings = secretPersistence.flatMap((outcome) => outcome._tag === "failed"
|
|
512
|
+
? [`${outcome.inputName} could not be saved to the system keychain`]
|
|
513
|
+
: []);
|
|
514
|
+
const writeEffect = op.args.skipStateWrites === true
|
|
515
|
+
? Effect.void
|
|
516
|
+
: Option.getOrElse(op.args.skipSettings, () => false)
|
|
517
|
+
? lockEntry === undefined
|
|
518
|
+
? Effect.void
|
|
519
|
+
: ws.setMcpServerLock({
|
|
520
|
+
name: ref.server.name,
|
|
521
|
+
lockEntry,
|
|
522
|
+
versionRange: Option.none(),
|
|
523
|
+
})
|
|
524
|
+
: lockEntry === undefined
|
|
525
|
+
? ws.setMcpServerEntry(ref.server.name, {
|
|
526
|
+
...settingsEntry,
|
|
527
|
+
})
|
|
528
|
+
: ws.setMcpServer({
|
|
529
|
+
name: ref.server.name,
|
|
530
|
+
lockEntry,
|
|
531
|
+
versionRange: op.args.versionRange,
|
|
532
|
+
env: persistedEnv,
|
|
533
|
+
enabled,
|
|
534
|
+
...(agents === undefined ? {} : { agents }),
|
|
535
|
+
});
|
|
536
|
+
const writeWarning = yield* writeEffect.pipe(Effect.as(Option.none()), Effect.catch((e) => Effect.succeed(Option.some(`MCP server update failed: ${adapter.describeFailure(e)}`))));
|
|
537
|
+
const warnings = Option.match(writeWarning, {
|
|
538
|
+
onNone: () => [...secretWarnings, ...agentSync.warnings],
|
|
539
|
+
onSome: (warning) => [warning, ...secretWarnings, ...agentSync.warnings],
|
|
540
|
+
});
|
|
541
|
+
const change = currentEntry === undefined ? "created" : "updated";
|
|
542
|
+
const agentOutcomes = agentSync.outcomes.flatMap(({ agentId, outcome }) => outcome._tag === "success" || outcome._tag === "fallback"
|
|
543
|
+
? [
|
|
544
|
+
{
|
|
545
|
+
agentId,
|
|
546
|
+
...(outcome.targets === undefined ? {} : { targets: outcome.targets }),
|
|
547
|
+
},
|
|
548
|
+
]
|
|
549
|
+
: []);
|
|
550
|
+
return {
|
|
551
|
+
result: "success",
|
|
552
|
+
message: appendWarningsToMessage(`Installed ${ref.server.name} (canonical=success, agent-sync=${agentSync.status})`, warnings),
|
|
553
|
+
artifact: mcpServerArtifact({
|
|
554
|
+
lockEntry,
|
|
555
|
+
scope: ws.scope,
|
|
556
|
+
change,
|
|
557
|
+
agents: agentOutcomes.map(({ agentId }) => agentId),
|
|
558
|
+
targets: [
|
|
559
|
+
...(lockEntry === undefined ? [] : [mcpSourceTarget(ws.scope, lockEntry, change)]),
|
|
560
|
+
mcpSettingsTarget(ws.scope, change),
|
|
561
|
+
...agentConfigTargets(agentOutcomes),
|
|
562
|
+
],
|
|
563
|
+
}),
|
|
564
|
+
};
|
|
565
|
+
}).pipe(withAdaptedStepFailures);
|
|
566
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type { McpServerSyncOutcome } from "@agentxm/extension-workspace";
|
|
3
|
+
import { ExtensionLifecycleFailed } from "../../errors.js";
|
|
4
|
+
export interface AgentMcpSyncOutcome {
|
|
5
|
+
readonly agentId: string;
|
|
6
|
+
readonly outcome: McpServerSyncOutcome;
|
|
7
|
+
}
|
|
8
|
+
export declare const requireSuccessfulMcpSync: (serverName: string, outcomes: ReadonlyArray<AgentMcpSyncOutcome>) => Effect.Effect<void, ExtensionLifecycleFailed>;
|
|
9
|
+
export declare const mcpSyncWarnings: (serverName: string, outcomes: ReadonlyArray<AgentMcpSyncOutcome>) => ReadonlyArray<string>;
|
|
10
|
+
//# sourceMappingURL=sync-outcome.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { ExtensionLifecycleFailed } from "../../errors.js";
|
|
3
|
+
const outcomeSummary = ({ agentId, outcome }) => outcome._tag === "fallback"
|
|
4
|
+
? `${agentId}: fallback(${outcome.fallbackFrom}):${outcome.reason}`
|
|
5
|
+
: outcome._tag === "success"
|
|
6
|
+
? `${agentId}: success`
|
|
7
|
+
: `${agentId}: ${outcome.reason}`;
|
|
8
|
+
export const requireSuccessfulMcpSync = (serverName, outcomes) => {
|
|
9
|
+
const failures = outcomes.filter(({ outcome }) => outcome._tag === "disabled" ||
|
|
10
|
+
outcome._tag === "nothing-runnable" ||
|
|
11
|
+
outcome._tag === "needs-input" ||
|
|
12
|
+
outcome._tag === "misconfigured" ||
|
|
13
|
+
outcome._tag === "failed");
|
|
14
|
+
if (failures.length === 0)
|
|
15
|
+
return Effect.void;
|
|
16
|
+
return new ExtensionLifecycleFailed({
|
|
17
|
+
category: "conflict",
|
|
18
|
+
detail: `MCP agent sync failed for ${serverName}: ${failures.map(outcomeSummary).join(", ")}`,
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
export const mcpSyncWarnings = (serverName, outcomes) => {
|
|
22
|
+
const warnings = outcomes.filter(({ outcome }) => outcome._tag === "fallback" || outcome._tag === "unsupported");
|
|
23
|
+
return warnings.length === 0
|
|
24
|
+
? []
|
|
25
|
+
: [`MCP agent sync warnings for ${serverName}: ${warnings.map(outcomeSummary).join(", ")}`];
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=sync-outcome.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uninstall MCP server executor — orchestrates per-server removal pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Pipeline: resolve desired/observed state -> remove canonical source -> clear
|
|
5
|
+
* settings and accepted resolution.
|
|
6
|
+
* Simpler than skills — no agent symlinks.
|
|
7
|
+
*
|
|
8
|
+
* @experimental This API is unstable and may change without notice.
|
|
9
|
+
*/
|
|
10
|
+
import * as FileSystem from "effect/FileSystem";
|
|
11
|
+
import * as Path from "effect/Path";
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Option from "effect/Option";
|
|
14
|
+
import { CodingAgentRepository } from "@agentxm/extension-workspace";
|
|
15
|
+
import type { StepFailure } from "@agentxm/workspace-operations";
|
|
16
|
+
import type { JobStepResult, Operation } from "@agentxm/workspace-operations";
|
|
17
|
+
import { WorkspaceMutations } from "@agentxm/workspace-state";
|
|
18
|
+
import { LifecycleFailureAdapter } from "../../failure-adapter.js";
|
|
19
|
+
/**
|
|
20
|
+
* Args for the uninstall-mcp-server operation.
|
|
21
|
+
*/
|
|
22
|
+
export interface UninstallMcpServerOperationArgs {
|
|
23
|
+
readonly serverName: string;
|
|
24
|
+
/** When true, enforce strict policy for MCP sync outcomes. */
|
|
25
|
+
readonly strictAgentSync?: Option.Option<boolean>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Remove an MCP server from the workspace.
|
|
29
|
+
*
|
|
30
|
+
* @experimental This API is unstable and may change without notice.
|
|
31
|
+
*/
|
|
32
|
+
export type UninstallMcpServerOperation = Operation<"uninstall-mcp-server", UninstallMcpServerOperationArgs>;
|
|
33
|
+
/**
|
|
34
|
+
* Uninstall-mcp-server operation handler.
|
|
35
|
+
*
|
|
36
|
+
* 1. Resolve configured and observed state
|
|
37
|
+
* 2. Remove canonical directory from disk (if exists)
|
|
38
|
+
* 3. Remove settings and accepted resolution
|
|
39
|
+
*/
|
|
40
|
+
export declare const uninstallMcpServer: (op: UninstallMcpServerOperation) => Effect.Effect<JobStepResult, StepFailure, FileSystem.FileSystem | Path.Path | WorkspaceMutations | CodingAgentRepository | LifecycleFailureAdapter>;
|
|
41
|
+
//# sourceMappingURL=uninstall.d.ts.map
|