@agentxm/extension-materialization 0.29.2 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/hooks/manager.d.ts +0 -5
- package/dist/src/hooks/manager.js +73 -36
- package/dist/src/index.d.ts +1 -6
- package/dist/src/index.js +0 -9
- package/dist/src/knowledge/manager.d.ts +0 -1
- package/dist/src/knowledge/manager.js +36 -29
- package/dist/src/manager-contract.d.ts +16 -18
- package/dist/src/manager-registry.d.ts +2 -3
- package/dist/src/managers.d.ts +22 -3
- package/dist/src/managers.js +0 -13
- package/dist/src/mcps/manager.js +84 -44
- package/dist/src/packs/manager.js +21 -18
- package/dist/src/rules/manager.d.ts +0 -5
- package/dist/src/rules/manager.js +22 -32
- package/dist/src/skills/errors.d.ts +2 -1
- package/dist/src/skills/manager.js +45 -53
- package/dist/src/skills/materialization.d.ts +2 -2
- package/dist/src/skills/skill-artifact.d.ts +5 -1
- package/dist/src/subagents/manager.js +31 -52
- package/dist/src/testing.d.ts +0 -66
- package/dist/src/testing.js +1 -89
- package/package.json +19 -22
- package/dist/src/desired-state/errors.d.ts +0 -22
- package/dist/src/desired-state/errors.js +0 -22
- package/dist/src/desired-state/retained-materialization.d.ts +0 -58
- package/dist/src/desired-state/retained-materialization.js +0 -119
- package/dist/src/extensions/operations.d.ts +0 -252
- package/dist/src/extensions/operations.js +0 -631
- package/dist/src/mcps/authored-materialization.d.ts +0 -28
- package/dist/src/mcps/authored-materialization.js +0 -34
- package/dist/src/mcps/install-operation.d.ts +0 -92
- package/dist/src/mcps/install-operation.js +0 -557
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Materializing an authored MCP server package during create, fork, adopt, and
|
|
3
|
-
* native import.
|
|
4
|
-
*
|
|
5
|
-
* The authored closure recipe accepts a type-specific materialization because
|
|
6
|
-
* an MCP server's canonical content is realized by the install operation
|
|
7
|
-
* rather than by its manager. Both live in this capability, so every authoring
|
|
8
|
-
* route reaches the same realization without importing a peer feature.
|
|
9
|
-
*
|
|
10
|
-
* @experimental This API is unstable and may change without notice.
|
|
11
|
-
*/
|
|
12
|
-
import * as Effect from "effect/Effect";
|
|
13
|
-
import * as Option from "effect/Option";
|
|
14
|
-
import { installMcpServer } from "./install-operation.js";
|
|
15
|
-
/**
|
|
16
|
-
* Realize the authored package's canonical content and native projections
|
|
17
|
-
* without touching workspace state: the authored closure commits desired state
|
|
18
|
-
* itself. Reports no acquisition facts — an authored package has no accepted
|
|
19
|
-
* registry resolution for the entry writers to record.
|
|
20
|
-
*/
|
|
21
|
-
export const materializeAuthoredMcpServer = (args) => installMcpServer({
|
|
22
|
-
name: "install-mcp-server",
|
|
23
|
-
args: {
|
|
24
|
-
ref: args.ref,
|
|
25
|
-
nonInteractive: args.nonInteractive,
|
|
26
|
-
force: false,
|
|
27
|
-
allowWorkspaceSourceTransition: true,
|
|
28
|
-
versionRange: Option.none(),
|
|
29
|
-
skipSettings: Option.none(),
|
|
30
|
-
skipStateWrites: true,
|
|
31
|
-
env: Option.none(),
|
|
32
|
-
},
|
|
33
|
-
}).pipe(Effect.as(Option.none()));
|
|
34
|
-
//# sourceMappingURL=authored-materialization.js.map
|
|
@@ -1,92 +0,0 @@
|
|
|
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 type * as HttpClient from "effect/unstable/http/HttpClient";
|
|
17
|
-
import * as Path from "effect/Path";
|
|
18
|
-
import * as Effect from "effect/Effect";
|
|
19
|
-
import * as Option from "effect/Option";
|
|
20
|
-
import { NativeWriteAuthority } from "@agentxm/agent-integration";
|
|
21
|
-
import { CodingAgentRepository } from "@agentxm/workspace-projection";
|
|
22
|
-
import type { JobStepResult, Operation } from "@agentxm/workspace-operations";
|
|
23
|
-
import { WorkspaceMutations } from "@agentxm/workspace-state";
|
|
24
|
-
import type { McpServerExtensionRef } from "@agentxm/extension-model/unstable/extensions/refs/mcp-server";
|
|
25
|
-
import { type McpServerManifest } from "@agentxm/extension-model/unstable/mcps/manifest-schema";
|
|
26
|
-
import type { ExtensionManagerFailure } from "../errors.js";
|
|
27
|
-
import { McpSecretStore, type McpSecretIdentity } from "./secret-store.js";
|
|
28
|
-
/**
|
|
29
|
-
* Args for the install-mcp-server operation.
|
|
30
|
-
*/
|
|
31
|
-
export type InstallMcpServerOperationArgs = {
|
|
32
|
-
readonly ref: McpServerExtensionRef;
|
|
33
|
-
/** Local connection identity and exact agent-native MCP key. */
|
|
34
|
-
readonly localName?: string;
|
|
35
|
-
readonly force: boolean;
|
|
36
|
-
/** Explicitly permit a workspace-authored relocation during reconciliation. */
|
|
37
|
-
readonly allowWorkspaceSourceTransition?: boolean;
|
|
38
|
-
readonly versionRange: Option.Option<string>;
|
|
39
|
-
/** When true, write to lockfile only (skip settings). Used for pack dependencies. */
|
|
40
|
-
readonly skipSettings: Option.Option<boolean>;
|
|
41
|
-
/** Materialize only; the enclosing authored-package transaction owns state writes. */
|
|
42
|
-
readonly skipStateWrites?: boolean;
|
|
43
|
-
/** When true, enforce strict policy for MCP sync outcomes. */
|
|
44
|
-
readonly strictAgentSync?: Option.Option<boolean>;
|
|
45
|
-
/** Resolved MCP input values from `--env KEY=VALUE` flags. */
|
|
46
|
-
readonly env?: Option.Option<Readonly<Record<string, string>>>;
|
|
47
|
-
/**
|
|
48
|
-
* Whether the invoking surface can prompt for missing required inputs.
|
|
49
|
-
* The transport boundary resolves flag, CI, and TTY state.
|
|
50
|
-
*/
|
|
51
|
-
readonly nonInteractive: boolean;
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* Add an MCP server to the workspace.
|
|
55
|
-
*
|
|
56
|
-
* @experimental This API is unstable and may change without notice.
|
|
57
|
-
*/
|
|
58
|
-
export type InstallMcpServerOperation = Operation<"install-mcp-server", InstallMcpServerOperationArgs>;
|
|
59
|
-
export type McpSecretDeletionOutcome = {
|
|
60
|
-
readonly _tag: "deleted";
|
|
61
|
-
readonly inputName: string;
|
|
62
|
-
} | {
|
|
63
|
-
readonly _tag: "absent";
|
|
64
|
-
readonly inputName: string;
|
|
65
|
-
} | {
|
|
66
|
-
readonly _tag: "failed";
|
|
67
|
-
readonly inputName: string;
|
|
68
|
-
};
|
|
69
|
-
export declare const collectSecretInputNames: (manifest: McpServerManifest) => ReadonlySet<string>;
|
|
70
|
-
export declare const readMcpServerManifest: (canonicalPath: string) => Effect.Effect<Option.Option<McpServerManifest>, never, FileSystem.FileSystem | Path.Path>;
|
|
71
|
-
/**
|
|
72
|
-
* Erase every credential one connection holds. Erasure is reported per input:
|
|
73
|
-
* a credential store that is unavailable leaves the secret behind and says so,
|
|
74
|
-
* rather than failing the removal that has already settled.
|
|
75
|
-
*/
|
|
76
|
-
export declare const deleteMcpSecrets: (identity: McpSecretIdentity, secretNames: ReadonlySet<string>) => Effect.Effect<ReadonlyArray<McpSecretDeletionOutcome>, never, McpSecretStore>;
|
|
77
|
-
/**
|
|
78
|
-
* Everything the MCP install operation needs from its composition root.
|
|
79
|
-
*/
|
|
80
|
-
export type McpServerInstallRequirements = FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspaceMutations | CodingAgentRepository | NativeWriteAuthority | McpSecretStore;
|
|
81
|
-
/**
|
|
82
|
-
* Install one MCP server: acquire the package (registry archive or the
|
|
83
|
-
* workspace-authored directory), merge the connection's inputs with what the
|
|
84
|
-
* credential store already holds, record the settings entry and the accepted
|
|
85
|
-
* resolution, and project the connection into every configured agent.
|
|
86
|
-
*
|
|
87
|
-
* Failures stay typed: a refused request, an unrepresentable connection, and a
|
|
88
|
-
* credential store that will not persist a secret are three different facts,
|
|
89
|
-
* and the caller decides how each is reported.
|
|
90
|
-
*/
|
|
91
|
-
export declare const installMcpServer: (op: InstallMcpServerOperation) => Effect.Effect<JobStepResult, ExtensionManagerFailure, McpServerInstallRequirements>;
|
|
92
|
-
//# sourceMappingURL=install-operation.d.ts.map
|
|
@@ -1,557 +0,0 @@
|
|
|
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
|