@akira-tl/forgerelay 1.1.2 → 1.2.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/CHANGELOG.md +38 -0
- package/README.md +14 -0
- package/dist/activity/runtime/mcp-query-tools.js +15 -5
- package/dist/cli/config/inspect.js +331 -0
- package/dist/cli/config/migrate.js +351 -0
- package/dist/cli/init/setup-config.js +41 -0
- package/dist/cli/init.js +128 -130
- package/dist/cli/maintenance.js +15 -5
- package/dist/cli/mcp/external-mcp.js +14 -10
- package/dist/cli/mcp/status.js +11 -6
- package/dist/cli.js +40 -7
- package/dist/lsp/language-server-config.js +66 -144
- package/dist/lsp/runtime/manager.js +35 -4
- package/dist/mcp/hooks/config.js +141 -0
- package/dist/mcp/hooks/external-mcp-transform.js +22 -13
- package/dist/mcp/hooks/hook-cli.js +62 -52
- package/dist/mcp/hooks/hooks.js +111 -85
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +8 -5
- package/dist/mcp/operations/external-mcp/external-mcp.js +9 -2
- package/dist/mcp/server/core/capability-support.js +7 -0
- package/dist/runtime/config/config.js +142 -64
- package/dist/runtime/config/definition/catalog.js +12 -0
- package/dist/runtime/config/definition/definition.js +115 -0
- package/dist/runtime/config/definition/external-mcp.js +58 -0
- package/dist/runtime/config/definition/general-config.js +258 -0
- package/dist/runtime/config/definition/language-servers.js +116 -0
- package/dist/runtime/config/definition/schema.js +113 -0
- package/dist/runtime/config/definition/types.js +2 -0
- package/dist/runtime/config/external-mcp-auth-store.js +105 -29
- package/dist/runtime/config/external-mcp-config.js +39 -0
- package/dist/runtime/config/external-mcp-registry.js +284 -106
- package/dist/runtime/config/inspection/live.js +77 -0
- package/dist/runtime/config/resolution/general.js +93 -0
- package/dist/runtime/config/resolution/hooks.js +315 -0
- package/dist/runtime/config/resolution/language-servers.js +237 -0
- package/dist/runtime/config/resolution/project-sources.js +159 -0
- package/dist/runtime/config/resolution/resolver.js +485 -0
- package/dist/runtime/config/resolution/types.js +1 -0
- package/dist/runtime/config/runtime/config-runtime.js +74 -0
- package/dist/runtime/config/runtime/source-refresh.js +263 -0
- package/dist/runtime/config/user-config.js +38 -31
- package/dist/runtime/security/project-execution-trust.js +34 -0
- package/dist/runtime/testing/server-fixture.js +9 -8
- package/dist/server.js +10 -6
- package/dist/subagents/profiles.js +336 -53
- package/dist/subagents/sessions/execution.js +6 -35
- package/dist/subagents/sessions/manager.js +14 -2
- package/dist/ui/.vite/manifest.json +275 -275
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/{activity-panel-app-Bw7ZFX2g.js → activity-panel-app-BeDiwd7D.js} +3 -3
- package/dist/ui/assets/{angular-html-DeUNP12X.js → angular-html-DuoCE7nn.js} +1 -1
- package/dist/ui/assets/{angular-ts-B3v8FKi_.js → angular-ts-D5tGk0hL.js} +1 -1
- package/dist/ui/assets/{apl-Bs3iQm4P.js → apl-lciOTPbw.js} +1 -1
- package/dist/ui/assets/{astro-D6GHecBY.js → astro-vJbqfNnJ.js} +1 -1
- package/dist/ui/assets/{blade-BMNbVOHv.js → blade-C7655Q4K.js} +1 -1
- package/dist/ui/assets/{c-BquuA6bv.js → c-DdHp1Jrw.js} +1 -1
- package/dist/ui/assets/{cobol-BuZ6kfps.js → cobol-DgvzQxZP.js} +1 -1
- package/dist/ui/assets/{coffee-DgC5uBzL.js → coffee-BIjVUOUd.js} +1 -1
- package/dist/ui/assets/{cpp-BdvWqCaB.js → cpp-CU8tGzvI.js} +1 -1
- package/dist/ui/assets/{crystal-0Q5qBvNI.js → crystal-CUQAwsg9.js} +1 -1
- package/dist/ui/assets/{css-Be_T8idh.js → css-DtMTqD3F.js} +1 -1
- package/dist/ui/assets/{edge-DaGLdgh_.js → edge-DoCX9RnL.js} +1 -1
- package/dist/ui/assets/{elixir-CMgiFIzB.js → elixir-DJxV9_U6.js} +1 -1
- package/dist/ui/assets/{elm-B-ufMlMC.js → elm-DIgWqK4r.js} +1 -1
- package/dist/ui/assets/{erb-CaGiOZM-.js → erb-BHxP0c_e.js} +1 -1
- package/dist/ui/assets/{git-rebase-CpwUWCzK.js → git-rebase-HbI_Vo8d.js} +1 -1
- package/dist/ui/assets/{glimmer-js-BRxccvnE.js → glimmer-js-CthY4Ty_.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-CgQbPzlT.js → glimmer-ts-B2-_A1Xz.js} +1 -1
- package/dist/ui/assets/{glsl-D7JmHFr0.js → glsl-D9V9gq98.js} +1 -1
- package/dist/ui/assets/{graphql-C8n0eUBK.js → graphql-CddeAaS3.js} +1 -1
- package/dist/ui/assets/{hack-lVN7KVQv.js → hack-D_loGkOT.js} +1 -1
- package/dist/ui/assets/{haml-h18lG0pS.js → haml-BoWDczd0.js} +1 -1
- package/dist/ui/assets/{handlebars-lRFjRPfj.js → handlebars-ZoSOvw9y.js} +1 -1
- package/dist/ui/assets/{heavy-payload-Cqx9MOkH.js → heavy-payload-DO34LwoZ.js} +3 -3
- package/dist/ui/assets/{html-DtILqTE1.js → html-CE-obXVo.js} +1 -1
- package/dist/ui/assets/{html-derivative-CSTSmPup.js → html-derivative-Cd-smAbW.js} +1 -1
- package/dist/ui/assets/{http-DNI8fsbY.js → http-CZrkbrSl.js} +1 -1
- package/dist/ui/assets/{hurl-DgRi60JG.js → hurl-CxTuxBt1.js} +1 -1
- package/dist/ui/assets/{java-CW9pZ5bh.js → java-BshLjCat.js} +1 -1
- package/dist/ui/assets/{javascript-Cvg2UrWL.js → javascript-D2pxQTiX.js} +1 -1
- package/dist/ui/assets/{jinja-CZw_S1FB.js → jinja-kbfPP7-F.js} +1 -1
- package/dist/ui/assets/{jison-C56u_FZv.js → jison-C2mt9l1M.js} +1 -1
- package/dist/ui/assets/{json-DW2UTv1q.js → json-DU_rg_AU.js} +1 -1
- package/dist/ui/assets/{jsx-Zj5gobdD.js → jsx-D_8aQMlx.js} +1 -1
- package/dist/ui/assets/{julia-Cvcrre-L.js → julia-BmefTaVV.js} +1 -1
- package/dist/ui/assets/{just-8CZ2u6Tj.js → just-DXV8vk1S.js} +1 -1
- package/dist/ui/assets/{latex-D289AXWG.js → latex-Cjm2MvGW.js} +1 -1
- package/dist/ui/assets/{liquid-CFEX3SkZ.js → liquid-DyQDlo0u.js} +1 -1
- package/dist/ui/assets/{lua-BKpJ0ybO.js → lua-B2A1vzzy.js} +1 -1
- package/dist/ui/assets/{marko-D9maZ0a6.js → marko-B9vOkXOU.js} +1 -1
- package/dist/ui/assets/{mdc-rM9l0qWR.js → mdc-D42m6spU.js} +1 -1
- package/dist/ui/assets/{nginx-C9w6p2C9.js → nginx-TpfVxSUY.js} +1 -1
- package/dist/ui/assets/{nim-BRePHk0J.js → nim-CJr5T1FK.js} +1 -1
- package/dist/ui/assets/{perl-CskTmiPQ.js → perl-cJZcsdm9.js} +1 -1
- package/dist/ui/assets/{php-DCU_FvNI.js → php-CSZL5KRT.js} +1 -1
- package/dist/ui/assets/{pug-XLaVxTvv.js → pug-B1eMKzuA.js} +1 -1
- package/dist/ui/assets/{qml-CaONKSl-.js → qml-Dl2_7zc9.js} +1 -1
- package/dist/ui/assets/{r-BCVR0ZE3.js → r-F0aaO9gG.js} +1 -1
- package/dist/ui/assets/{razor-DDnGXxnU.js → razor-BPnkSMCw.js} +1 -1
- package/dist/ui/assets/{regexp-DWGTYYVx.js → regexp-BnGHMFbv.js} +1 -1
- package/dist/ui/assets/{rst-Dz3GWvQm.js → rst-B3CO7WYO.js} +1 -1
- package/dist/ui/assets/{ruby-DC3jGtMY.js → ruby-B6VMzT1T.js} +1 -1
- package/dist/ui/assets/{sas-7mNpvsjZ.js → sas-Df2lHVuU.js} +1 -1
- package/dist/ui/assets/{scss-uBsoCJVy.js → scss-4XxI6sgu.js} +1 -1
- package/dist/ui/assets/{shellscript-BCN9N_Jf.js → shellscript-DYASCN4r.js} +1 -1
- package/dist/ui/assets/{shellsession-Hsw46VXV.js → shellsession-Br6AGJO_.js} +1 -1
- package/dist/ui/assets/{soy-D6KV69HU.js → soy-p3ZNq1We.js} +1 -1
- package/dist/ui/assets/{sql-CVzVQkJh.js → sql-CAdv2OXL.js} +1 -1
- package/dist/ui/assets/{stata-BwUjXNhI.js → stata-Dx7yUbG_.js} +1 -1
- package/dist/ui/assets/{surrealql-CgloWqRa.js → surrealql-6YsE8HQa.js} +1 -1
- package/dist/ui/assets/{svelte-DcC3n6IC.js → svelte-YgzedwpD.js} +1 -1
- package/dist/ui/assets/{templ-DOFfnWTJ.js → templ-Bahqx3uD.js} +1 -1
- package/dist/ui/assets/{tex-ln89VPaX.js → tex-o6fTQjkH.js} +1 -1
- package/dist/ui/assets/{ts-tags-CWMzCstH.js → ts-tags-BO-Co0Ju.js} +1 -1
- package/dist/ui/assets/{tsx-DgzfiGeK.js → tsx-DtQe5AoH.js} +1 -1
- package/dist/ui/assets/{twig-Bi2itZL9.js → twig-BcUs6Kqc.js} +1 -1
- package/dist/ui/assets/{typescript-CQ24f4zW.js → typescript-BPtM8h4u.js} +1 -1
- package/dist/ui/assets/{vue-BS8jE5u6.js → vue-5KkGUzJJ.js} +1 -1
- package/dist/ui/assets/{vue-html-o38xqhyB.js → vue-html-Df_Xfsz9.js} +1 -1
- package/dist/ui/assets/{vue-vine-D2NvKv50.js → vue-vine-hUIe_u_Y.js} +1 -1
- package/dist/ui/assets/{xml-_44rx9_2.js → xml-C3Jy7as5.js} +1 -1
- package/dist/ui/assets/{xsl-B5QWQPoi.js → xsl-BnF9OuFo.js} +1 -1
- package/dist/ui/assets/{yaml-CKL2pEWe.js → yaml-DyrFgCou.js} +1 -1
- package/dist/workspaces/conversation-test-support.js +3 -11
- package/dist/workspaces/sessions.js +2 -0
- package/dist/workspaces/state/project-context.js +221 -0
- package/dist/workspaces.js +3 -1
- package/docs/chatgpt-coding-workflow.md +8 -3
- package/docs/configuration.md +99 -41
- package/docs/gotchas.md +24 -5
- package/docs/security.md +14 -3
- package/docs/setup.md +12 -7
- package/package.json +12 -3
- package/schemas/v1/config.project-local.schema.json +14 -0
- package/schemas/v1/config.project.schema.json +14 -0
- package/schemas/v1/config.user.schema.json +538 -0
- package/schemas/v1/hooks.project-local.schema.json +118 -0
- package/schemas/v1/hooks.project.schema.json +118 -0
- package/schemas/v1/hooks.user.schema.json +118 -0
- package/schemas/v1/language-servers.project-local.schema.json +230 -0
- package/schemas/v1/language-servers.project.schema.json +230 -0
- package/schemas/v1/language-servers.user.schema.json +230 -0
- package/schemas/v1/mcp.project-local.schema.json +143 -0
- package/schemas/v1/mcp.project.schema.json +143 -0
- package/schemas/v1/mcp.user.schema.json +143 -0
- package/scripts/ci/architecture.mjs +54 -4
- package/scripts/ci/cmd-acceptance.mjs +9 -9
- package/scripts/ci/config-v2-product-acceptance.mjs +639 -0
- package/scripts/ci/gates/acceptance-artifact.mjs +66 -0
- package/scripts/ci/gates/core-contract.mjs +27 -0
- package/scripts/ci/gates/platform-tests.mjs +38 -0
- package/scripts/ci/gates/prepare-windows-product.mjs +57 -0
- package/scripts/ci/gates/test-shard.mjs +71 -0
- package/scripts/ci/powershell51-acceptance.mjs +9 -9
- package/scripts/ci/pwsh-acceptance.mjs +9 -9
- package/scripts/ci/verify.mjs +2 -0
- package/scripts/ci/windows-product-acceptance.mjs +9 -10
- package/scripts/config/schema-text.mjs +4 -0
- package/scripts/config/schema-text.test.mjs +18 -0
- package/scripts/config/schema.mjs +74 -0
- package/scripts/lsp/interop.mjs +4 -1
- package/scripts/release/pack.mjs +64 -4
- package/scripts/release/release-gate.test.mjs +93 -27
- package/scripts/release-parity.mjs +19 -2
- package/templates/instructions/cmd.md +12 -0
- package/templates/instructions/fish.md +11 -0
- package/templates/instructions/powershell.md +14 -0
- package/templates/instructions/pwsh.md +14 -0
- package/templates/instructions/zsh.md +11 -0
- package/dist/runtime/instructions/powershell-skill.js +0 -65
|
@@ -13,10 +13,7 @@ export class ExternalMcpCredentialStore {
|
|
|
13
13
|
this.filePath = join(this.configDir, AUTH_FILE_NAME);
|
|
14
14
|
}
|
|
15
15
|
read(identity) {
|
|
16
|
-
|
|
17
|
-
if (!record || !sameIdentity(record.identity, identity))
|
|
18
|
-
return undefined;
|
|
19
|
-
return cloneRecord(record);
|
|
16
|
+
return findCredential(this.readFile(), identity);
|
|
20
17
|
}
|
|
21
18
|
inspect() {
|
|
22
19
|
const exists = existsSync(this.filePath);
|
|
@@ -33,7 +30,7 @@ export class ExternalMcpCredentialStore {
|
|
|
33
30
|
revision: randomUUID(),
|
|
34
31
|
updatedAt: new Date().toISOString(),
|
|
35
32
|
};
|
|
36
|
-
file.credentials[credentialKey(identity)] = written;
|
|
33
|
+
file.credentials[credentialKey(identity)] = persistedRecord(written);
|
|
37
34
|
});
|
|
38
35
|
return cloneRecord(written);
|
|
39
36
|
}
|
|
@@ -41,11 +38,10 @@ export class ExternalMcpCredentialStore {
|
|
|
41
38
|
let written;
|
|
42
39
|
await this.updateFile((file) => {
|
|
43
40
|
const key = credentialKey(identity);
|
|
44
|
-
const
|
|
45
|
-
const current = existing && sameIdentity(existing.identity, identity) ? cloneRecord(existing) : undefined;
|
|
41
|
+
const current = findCredential(file, identity);
|
|
46
42
|
const next = update(current);
|
|
47
43
|
if (!next) {
|
|
48
|
-
|
|
44
|
+
deleteMatchingCredentials(file, identity);
|
|
49
45
|
written = undefined;
|
|
50
46
|
return;
|
|
51
47
|
}
|
|
@@ -56,19 +52,14 @@ export class ExternalMcpCredentialStore {
|
|
|
56
52
|
revision: randomUUID(),
|
|
57
53
|
updatedAt: new Date().toISOString(),
|
|
58
54
|
};
|
|
59
|
-
file.credentials[key] = written;
|
|
55
|
+
file.credentials[key] = persistedRecord(written);
|
|
60
56
|
});
|
|
61
57
|
return written ? cloneRecord(written) : undefined;
|
|
62
58
|
}
|
|
63
59
|
async remove(identity) {
|
|
64
60
|
let removed = false;
|
|
65
61
|
await this.updateFile((file) => {
|
|
66
|
-
|
|
67
|
-
const existing = file.credentials[key];
|
|
68
|
-
if (!existing || !sameIdentity(existing.identity, identity))
|
|
69
|
-
return;
|
|
70
|
-
delete file.credentials[key];
|
|
71
|
-
removed = true;
|
|
62
|
+
removed = deleteMatchingCredentials(file, identity);
|
|
72
63
|
});
|
|
73
64
|
return removed;
|
|
74
65
|
}
|
|
@@ -101,10 +92,18 @@ export class ExternalMcpCredentialStore {
|
|
|
101
92
|
}, { mode: 0o600 });
|
|
102
93
|
}
|
|
103
94
|
}
|
|
104
|
-
export function externalMcpCredentialIdentity(source, server,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
export function externalMcpCredentialIdentity(source, server, project) {
|
|
96
|
+
if (source === "legacy" || source === "global")
|
|
97
|
+
return { kind: "global", server };
|
|
98
|
+
if (!project || typeof project === "string" || !project.id) {
|
|
99
|
+
throw new Error(`External MCP ${source} credential identity requires a Project ID.`);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
kind: "project",
|
|
103
|
+
server,
|
|
104
|
+
projectId: project.id,
|
|
105
|
+
...(project.projectRoot ? { legacyProjectRoot: resolve(project.projectRoot) } : {}),
|
|
106
|
+
};
|
|
108
107
|
}
|
|
109
108
|
export function credentialKey(identity) {
|
|
110
109
|
const normalized = normalizeIdentity(identity);
|
|
@@ -116,20 +115,85 @@ function credentialKeyHash(identity) {
|
|
|
116
115
|
const normalized = normalizeIdentity(identity);
|
|
117
116
|
const material = normalized.kind === "global"
|
|
118
117
|
? `global\0${normalized.server}`
|
|
119
|
-
: `project\0${normalized.
|
|
118
|
+
: `project-id\0${normalized.projectId}\0${normalized.server}`;
|
|
120
119
|
return createHash("sha256").update(material).digest("hex");
|
|
121
120
|
}
|
|
121
|
+
function legacyCredentialKey(server, projectRoot) {
|
|
122
|
+
const normalizedRoot = resolve(projectRoot);
|
|
123
|
+
const hash = createHash("sha256").update(`project\0${normalizedRoot}\0${server}`).digest("hex");
|
|
124
|
+
return `project:${hash}:${server}`;
|
|
125
|
+
}
|
|
122
126
|
function normalizeIdentity(identity) {
|
|
123
127
|
return identity.kind === "project"
|
|
124
|
-
? {
|
|
128
|
+
? {
|
|
129
|
+
kind: "project",
|
|
130
|
+
server: identity.server,
|
|
131
|
+
projectId: identity.projectId,
|
|
132
|
+
...(identity.legacyProjectRoot ? { legacyProjectRoot: resolve(identity.legacyProjectRoot) } : {}),
|
|
133
|
+
}
|
|
125
134
|
: { kind: "global", server: identity.server };
|
|
126
135
|
}
|
|
127
|
-
function
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
function storedIdentity(identity) {
|
|
137
|
+
const normalized = normalizeIdentity(identity);
|
|
138
|
+
return normalized.kind === "project"
|
|
139
|
+
? { kind: "project", server: normalized.server, projectId: normalized.projectId }
|
|
140
|
+
: normalized;
|
|
141
|
+
}
|
|
142
|
+
function persistedIdentityMatches(persisted, requested) {
|
|
143
|
+
const normalized = normalizeIdentity(requested);
|
|
144
|
+
if (normalized.kind === "global") {
|
|
145
|
+
return persisted.kind === "global" && persisted.server === normalized.server;
|
|
146
|
+
}
|
|
147
|
+
return persisted.kind === "project"
|
|
148
|
+
&& "projectId" in persisted
|
|
149
|
+
&& persisted.server === normalized.server
|
|
150
|
+
&& persisted.projectId === normalized.projectId;
|
|
151
|
+
}
|
|
152
|
+
function legacyIdentityMatches(persisted, requested) {
|
|
153
|
+
return persisted.kind === "project"
|
|
154
|
+
&& "projectRoot" in persisted
|
|
155
|
+
&& persisted.server === requested.server
|
|
156
|
+
&& requested.legacyProjectRoot !== undefined
|
|
157
|
+
&& resolve(persisted.projectRoot) === resolve(requested.legacyProjectRoot);
|
|
158
|
+
}
|
|
159
|
+
function findCredential(file, identity) {
|
|
160
|
+
const normalized = normalizeIdentity(identity);
|
|
161
|
+
const primary = file.credentials[credentialKey(normalized)];
|
|
162
|
+
if (primary && persistedIdentityMatches(primary.identity, normalized)) {
|
|
163
|
+
return publicRecord(primary, normalized);
|
|
164
|
+
}
|
|
165
|
+
if (normalized.kind !== "project" || !normalized.legacyProjectRoot)
|
|
166
|
+
return undefined;
|
|
167
|
+
const legacy = file.credentials[legacyCredentialKey(normalized.server, normalized.legacyProjectRoot)];
|
|
168
|
+
if (!legacy || !legacyIdentityMatches(legacy.identity, normalized))
|
|
169
|
+
return undefined;
|
|
170
|
+
return publicRecord(legacy, normalized);
|
|
171
|
+
}
|
|
172
|
+
function deleteMatchingCredentials(file, identity) {
|
|
173
|
+
const normalized = normalizeIdentity(identity);
|
|
174
|
+
let removed = false;
|
|
175
|
+
const key = credentialKey(normalized);
|
|
176
|
+
const primary = file.credentials[key];
|
|
177
|
+
if (primary && persistedIdentityMatches(primary.identity, normalized)) {
|
|
178
|
+
delete file.credentials[key];
|
|
179
|
+
removed = true;
|
|
180
|
+
}
|
|
181
|
+
if (normalized.kind === "project" && normalized.legacyProjectRoot) {
|
|
182
|
+
const legacyKey = legacyCredentialKey(normalized.server, normalized.legacyProjectRoot);
|
|
183
|
+
const legacy = file.credentials[legacyKey];
|
|
184
|
+
if (legacy && legacyIdentityMatches(legacy.identity, normalized)) {
|
|
185
|
+
delete file.credentials[legacyKey];
|
|
186
|
+
removed = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return removed;
|
|
190
|
+
}
|
|
191
|
+
function persistedRecord(record) {
|
|
192
|
+
return { ...cloneRecord(record), identity: storedIdentity(record.identity) };
|
|
193
|
+
}
|
|
194
|
+
function publicRecord(record, identity) {
|
|
195
|
+
const { identity: _persistedIdentity, ...rest } = cloneRecord(record);
|
|
196
|
+
return { ...rest, identity: normalizeIdentity(identity) };
|
|
133
197
|
}
|
|
134
198
|
function emptyAuthFile() {
|
|
135
199
|
return { version: AUTH_FILE_VERSION, credentials: {} };
|
|
@@ -167,7 +231,7 @@ function parseCredentialRecord(value, filePath) {
|
|
|
167
231
|
}
|
|
168
232
|
const reauthorization = parseReauthorization(value.reauthorization, filePath);
|
|
169
233
|
return {
|
|
170
|
-
identity:
|
|
234
|
+
identity: normalizePersistedIdentity(value.identity),
|
|
171
235
|
serverUrl: value.serverUrl,
|
|
172
236
|
revision: value.revision,
|
|
173
237
|
updatedAt: value.updatedAt,
|
|
@@ -205,7 +269,19 @@ function isCredentialIdentity(value) {
|
|
|
205
269
|
return false;
|
|
206
270
|
if (value.kind === "global")
|
|
207
271
|
return true;
|
|
208
|
-
|
|
272
|
+
if (value.kind !== "project")
|
|
273
|
+
return false;
|
|
274
|
+
const hasProjectId = typeof value.projectId === "string" && Boolean(value.projectId);
|
|
275
|
+
const hasProjectRoot = typeof value.projectRoot === "string" && Boolean(value.projectRoot);
|
|
276
|
+
return hasProjectId !== hasProjectRoot;
|
|
277
|
+
}
|
|
278
|
+
function normalizePersistedIdentity(identity) {
|
|
279
|
+
if (identity.kind === "global")
|
|
280
|
+
return { kind: "global", server: identity.server };
|
|
281
|
+
if ("projectId" in identity) {
|
|
282
|
+
return { kind: "project", server: identity.server, projectId: identity.projectId };
|
|
283
|
+
}
|
|
284
|
+
return { kind: "project", server: identity.server, projectRoot: resolve(identity.projectRoot) };
|
|
209
285
|
}
|
|
210
286
|
function cloneRecord(value) {
|
|
211
287
|
return structuredClone(value);
|
|
@@ -1,5 +1,44 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
1
2
|
const MCP_SERVER_NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
2
3
|
const MAX_MCP_SERVERS = 32;
|
|
4
|
+
const nonEmptyStringSchema = z.string().trim().min(1);
|
|
5
|
+
const stringRecordSchema = z.record(z.string(), z.string());
|
|
6
|
+
const httpUrlSchema = z.string().url().refine((value) => {
|
|
7
|
+
const protocol = new URL(value).protocol;
|
|
8
|
+
return protocol === "http:" || protocol === "https:";
|
|
9
|
+
}, "URL must use http or https.");
|
|
10
|
+
const clientMetadataUrlSchema = z.string().url().refine((value) => {
|
|
11
|
+
const parsed = new URL(value);
|
|
12
|
+
return parsed.protocol === "https:" && parsed.pathname !== "/";
|
|
13
|
+
}, "clientMetadataUrl must use https and contain a non-root path.");
|
|
14
|
+
const oauthSourceSchema = z.object({
|
|
15
|
+
clientMetadataUrl: clientMetadataUrlSchema,
|
|
16
|
+
callbackPort: z.number().int().min(1024).max(65535),
|
|
17
|
+
}).strict();
|
|
18
|
+
const stdioSourceSchema = z.object({
|
|
19
|
+
transport: z.literal("stdio"),
|
|
20
|
+
command: nonEmptyStringSchema,
|
|
21
|
+
args: z.array(z.string()).optional(),
|
|
22
|
+
env: stringRecordSchema.optional(),
|
|
23
|
+
cwd: nonEmptyStringSchema.optional(),
|
|
24
|
+
disabled: z.literal(false).optional(),
|
|
25
|
+
}).strict();
|
|
26
|
+
const httpSourceSchema = z.object({
|
|
27
|
+
transport: z.literal("streamable-http"),
|
|
28
|
+
url: httpUrlSchema,
|
|
29
|
+
headers: stringRecordSchema.optional(),
|
|
30
|
+
oauth: oauthSourceSchema.optional(),
|
|
31
|
+
disabled: z.literal(false).optional(),
|
|
32
|
+
}).strict();
|
|
33
|
+
const disabledSourceSchema = z.object({ disabled: z.literal(true) }).strict();
|
|
34
|
+
export const externalMcpStandaloneServerSchema = z.union([
|
|
35
|
+
disabledSourceSchema,
|
|
36
|
+
stdioSourceSchema,
|
|
37
|
+
httpSourceSchema,
|
|
38
|
+
]);
|
|
39
|
+
export const externalMcpStandaloneServersSchema = z.record(z.string().regex(MCP_SERVER_NAME_PATTERN, "Use a lowercase stable server name."), externalMcpStandaloneServerSchema).refine((value) => Object.keys(value).length <= MAX_MCP_SERVERS, {
|
|
40
|
+
message: `servers may contain at most ${MAX_MCP_SERVERS} configured servers.`,
|
|
41
|
+
});
|
|
3
42
|
export function parseExternalMcpServers(value) {
|
|
4
43
|
return parseServerRegistry(value, "mcpServers", false);
|
|
5
44
|
}
|
|
@@ -1,48 +1,122 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
1
|
import { join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
2
|
+
import { externalMcpConfigDefinition } from "./definition/external-mcp.js";
|
|
3
|
+
import { parseExternalMcpServers, parseExternalMcpStandaloneConfig, } from "./external-mcp-config.js";
|
|
4
|
+
import { refreshLegacyUserConfigField } from "./resolution/project-sources.js";
|
|
5
|
+
import { resolveConfigDomain } from "./resolution/resolver.js";
|
|
6
|
+
import { ConfigSourceRuntime } from "./runtime/source-refresh.js";
|
|
7
|
+
import { projectExecutionRequirement } from "../security/project-execution-trust.js";
|
|
8
|
+
const CANONICAL_PRIORITY = 100;
|
|
9
|
+
const LEGACY_PRIORITY = 0;
|
|
6
10
|
const MAX_DIAGNOSTIC_LENGTH = 320;
|
|
11
|
+
const LEGACY_DEPRECATION = {
|
|
12
|
+
since: "1.2.0",
|
|
13
|
+
removeIn: "1.4.0",
|
|
14
|
+
replacement: "mcp.json",
|
|
15
|
+
};
|
|
7
16
|
export class ExternalMcpConfigRegistry {
|
|
8
17
|
configDir;
|
|
9
|
-
|
|
18
|
+
environment;
|
|
19
|
+
sourceRuntime;
|
|
10
20
|
onDiagnostic;
|
|
11
|
-
global;
|
|
12
|
-
projects = new Map();
|
|
13
21
|
constructor(options) {
|
|
14
22
|
this.configDir = resolve(options.configDir);
|
|
15
|
-
this.
|
|
23
|
+
this.environment = options.environment ?? process.env;
|
|
24
|
+
this.sourceRuntime = options.sourceRuntime ?? new ConfigSourceRuntime();
|
|
16
25
|
this.onDiagnostic = options.onDiagnostic;
|
|
17
26
|
}
|
|
18
|
-
resolve(
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const projectPath = join(projectRoot, ".forgerelay", "mcp.json");
|
|
22
|
-
const project = this.loadDynamicSource("project", projectPath, this.projects.get(projectRoot));
|
|
23
|
-
this.projects.set(projectRoot, project);
|
|
24
|
-
return this.composeSnapshot(global, project);
|
|
27
|
+
resolve(project) {
|
|
28
|
+
const dynamicSources = this.projectSources(project.sharedConfigDir, project.localConfigDir);
|
|
29
|
+
return this.composeSnapshot(dynamicSources, this.composeResolution(dynamicSources), project.id);
|
|
25
30
|
}
|
|
26
31
|
resolveGlobal() {
|
|
27
|
-
|
|
32
|
+
const dynamicSources = this.projectSources();
|
|
33
|
+
return this.composeSnapshot(dynamicSources, this.composeResolution(dynamicSources));
|
|
28
34
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return this.
|
|
35
|
+
/** Internal Config v2 inspection seam. Resolved values may contain secrets; callers must use provenance for display. */
|
|
36
|
+
resolveConfiguration(input = {}) {
|
|
37
|
+
const dynamicSources = this.projectSources(input.projectSharedConfigDir, input.projectLocalConfigDir);
|
|
38
|
+
return this.composeResolution(dynamicSources);
|
|
33
39
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
projectSources(projectSharedConfigDir, projectLocalConfigDir) {
|
|
41
|
+
return [
|
|
42
|
+
this.loadCanonicalSource("global", "user", join(this.configDir, "mcp.json")),
|
|
43
|
+
...(projectSharedConfigDir
|
|
44
|
+
? [this.loadCanonicalSource("project", "project", join(projectSharedConfigDir, "mcp.json"))]
|
|
45
|
+
: []),
|
|
46
|
+
...(projectLocalConfigDir
|
|
47
|
+
? [this.loadCanonicalSource("project-local", "project-local", join(projectLocalConfigDir, "mcp.json"))]
|
|
48
|
+
: []),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
composeResolution(dynamicSources) {
|
|
52
|
+
const sourceInputs = [];
|
|
53
|
+
const legacy = this.loadLegacyInlineSource();
|
|
54
|
+
if (legacy.source)
|
|
55
|
+
sourceInputs.push(legacy.source);
|
|
56
|
+
for (const dynamic of dynamicSources) {
|
|
57
|
+
const input = dynamicSourceInput(dynamic);
|
|
58
|
+
if (input)
|
|
59
|
+
sourceInputs.push(input);
|
|
60
|
+
}
|
|
61
|
+
const resolution = resolveConfigDomain({
|
|
62
|
+
definition: externalMcpConfigDefinition,
|
|
63
|
+
sources: sourceInputs,
|
|
64
|
+
environment: this.environment,
|
|
65
|
+
});
|
|
66
|
+
if (legacy.diagnostic)
|
|
67
|
+
resolution.diagnostics.push(legacy.diagnostic);
|
|
68
|
+
for (const dynamic of dynamicSources) {
|
|
69
|
+
if (!dynamic.diagnostic || !dynamic.status.usingLastKnownGood)
|
|
70
|
+
continue;
|
|
71
|
+
const source = dynamic.status.source;
|
|
72
|
+
if (source === "legacy")
|
|
73
|
+
continue;
|
|
74
|
+
resolution.diagnostics.push({
|
|
75
|
+
severity: "error",
|
|
76
|
+
code: dynamic.diagnostic.code,
|
|
77
|
+
source: {
|
|
78
|
+
id: sourceId(source),
|
|
79
|
+
scope: scopeForSource(source),
|
|
80
|
+
kind: "file",
|
|
81
|
+
location: dynamic.status.path,
|
|
82
|
+
priority: CANONICAL_PRIORITY,
|
|
83
|
+
},
|
|
84
|
+
message: dynamic.diagnostic.message,
|
|
85
|
+
usingLastKnownGood: true,
|
|
86
|
+
diagnosticChanged: dynamic.diagnosticChanged,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return resolution;
|
|
90
|
+
}
|
|
91
|
+
composeSnapshot(dynamicSources, resolution, projectId) {
|
|
92
|
+
const normalized = normalizeResolvedServers(resolution);
|
|
93
|
+
const origins = {};
|
|
94
|
+
const executionRequirements = {};
|
|
37
95
|
const masked = {};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
96
|
+
for (const [entryName, entry] of Object.entries(resolution.entries)) {
|
|
97
|
+
if (!entryName.startsWith("servers."))
|
|
98
|
+
continue;
|
|
99
|
+
const name = entryName.slice("servers.".length);
|
|
100
|
+
const source = sourceFromId(entry.effective.source.id);
|
|
101
|
+
if (entry.tombstone) {
|
|
102
|
+
if (source !== "legacy")
|
|
103
|
+
masked[name] = source;
|
|
104
|
+
}
|
|
105
|
+
else if (name in normalized) {
|
|
106
|
+
origins[name] = source;
|
|
107
|
+
const requirement = projectId ? projectExecutionRequirement({
|
|
108
|
+
projectId, resolution, entryKey: entryName,
|
|
109
|
+
display: { kind: "external-mcp", name },
|
|
110
|
+
}) : undefined;
|
|
111
|
+
if (requirement)
|
|
112
|
+
executionRequirements[name] = requirement;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const diagnostics = mergeDiagnostics(resolution.diagnostics.map(configDiagnostic), []);
|
|
43
116
|
return {
|
|
44
|
-
servers,
|
|
117
|
+
servers: normalized,
|
|
45
118
|
origins,
|
|
119
|
+
executionRequirements,
|
|
46
120
|
masked,
|
|
47
121
|
sources: [
|
|
48
122
|
{
|
|
@@ -51,99 +125,203 @@ export class ExternalMcpConfigRegistry {
|
|
|
51
125
|
state: "valid",
|
|
52
126
|
usingLastKnownGood: false,
|
|
53
127
|
},
|
|
54
|
-
|
|
55
|
-
...(project ? [project.status] : []),
|
|
128
|
+
...dynamicSources.map((source) => source.status),
|
|
56
129
|
],
|
|
57
130
|
diagnostics,
|
|
58
131
|
};
|
|
59
132
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
133
|
+
loadLegacyInlineSource() {
|
|
134
|
+
const path = join(this.configDir, "config.json");
|
|
135
|
+
const refreshed = refreshLegacyUserConfigField({
|
|
136
|
+
sourceRuntime: this.sourceRuntime,
|
|
137
|
+
configDir: this.configDir,
|
|
138
|
+
field: "mcpServers",
|
|
139
|
+
parse: (value) => value === undefined ? undefined : parseExternalMcpServers(value),
|
|
140
|
+
invalidMessage: "Legacy inline External MCP configuration is invalid.",
|
|
141
|
+
});
|
|
142
|
+
if (refreshed.status.state === "missing")
|
|
143
|
+
return {};
|
|
144
|
+
const source = refreshed.value === undefined ? undefined : {
|
|
145
|
+
id: "legacy:user:mcpServers",
|
|
146
|
+
scope: "user",
|
|
147
|
+
kind: "file",
|
|
148
|
+
location: path,
|
|
149
|
+
priority: LEGACY_PRIORITY,
|
|
150
|
+
value: { servers: refreshed.value },
|
|
151
|
+
deprecation: LEGACY_DEPRECATION,
|
|
152
|
+
};
|
|
153
|
+
if (refreshed.status.state !== "invalid" || !refreshed.issue)
|
|
154
|
+
return source ? { source } : {};
|
|
155
|
+
if (refreshed.issueScope === "container")
|
|
156
|
+
return source ? { source } : {};
|
|
157
|
+
const diagnostic = {
|
|
158
|
+
severity: "error",
|
|
159
|
+
code: refreshed.issue.code,
|
|
160
|
+
source: source ? {
|
|
161
|
+
id: source.id,
|
|
162
|
+
scope: source.scope,
|
|
163
|
+
kind: source.kind,
|
|
164
|
+
...(source.location ? { location: source.location } : {}),
|
|
165
|
+
priority: source.priority,
|
|
166
|
+
} : {
|
|
167
|
+
id: "legacy:user:mcpServers",
|
|
168
|
+
scope: "user",
|
|
169
|
+
kind: "file",
|
|
170
|
+
location: path,
|
|
171
|
+
priority: LEGACY_PRIORITY,
|
|
172
|
+
},
|
|
173
|
+
message: refreshed.issue.message,
|
|
174
|
+
usingLastKnownGood: refreshed.status.usingLastKnownGood,
|
|
175
|
+
diagnosticChanged: refreshed.status.diagnosticChanged,
|
|
176
|
+
};
|
|
177
|
+
if (refreshed.status.diagnosticChanged)
|
|
178
|
+
this.onDiagnostic?.(configDiagnostic(diagnostic));
|
|
179
|
+
if (source)
|
|
180
|
+
return { source, diagnostic };
|
|
181
|
+
return {
|
|
182
|
+
source: {
|
|
183
|
+
id: "legacy:user:mcpServers",
|
|
184
|
+
scope: "user",
|
|
185
|
+
kind: "file",
|
|
186
|
+
location: path,
|
|
187
|
+
priority: LEGACY_PRIORITY,
|
|
188
|
+
deprecation: LEGACY_DEPRECATION,
|
|
189
|
+
error: { code: refreshed.issue.code, message: refreshed.issue.message },
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
loadCanonicalSource(source, scope, path) {
|
|
194
|
+
const refreshed = this.sourceRuntime.refreshFile({
|
|
195
|
+
key: `external-mcp:${source}:${path}`,
|
|
196
|
+
path,
|
|
197
|
+
parse: (content) => JSON.parse(content),
|
|
198
|
+
parseIssue: {
|
|
199
|
+
code: "invalid_source",
|
|
200
|
+
message: "Configuration source is not valid JSON.",
|
|
201
|
+
},
|
|
202
|
+
readIssue: {
|
|
203
|
+
code: "invalid_source",
|
|
204
|
+
message: "Configuration source could not be read.",
|
|
205
|
+
},
|
|
206
|
+
validate: (value) => {
|
|
207
|
+
const validation = resolveConfigDomain({
|
|
208
|
+
definition: externalMcpConfigDefinition,
|
|
209
|
+
sources: [canonicalSourceInput(source, scope, path, value)],
|
|
210
|
+
environment: this.environment,
|
|
211
|
+
});
|
|
212
|
+
const error = validation.diagnostics.find((diagnostic) => diagnostic.severity === "error");
|
|
213
|
+
if (!error)
|
|
214
|
+
return undefined;
|
|
69
215
|
return {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
hasLastKnownGood: true,
|
|
73
|
-
status: {
|
|
74
|
-
source,
|
|
75
|
-
path,
|
|
76
|
-
state: "missing",
|
|
77
|
-
usingLastKnownGood: false,
|
|
78
|
-
},
|
|
216
|
+
code: error.code === "missing_environment" ? "missing_environment" : "invalid_source",
|
|
217
|
+
message: error.message,
|
|
79
218
|
};
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
const diagnostic = refreshed.status.state === "invalid" && refreshed.issue
|
|
222
|
+
? {
|
|
223
|
+
source,
|
|
224
|
+
path,
|
|
225
|
+
severity: "error",
|
|
226
|
+
code: refreshed.issue.code,
|
|
227
|
+
message: boundedDiagnosticMessage(refreshed.issue.message),
|
|
80
228
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return this.invalidSource(source, path, fingerprint, previous, error);
|
|
85
|
-
}
|
|
86
|
-
const fingerprint = createHash("sha256").update(content).digest("base64url");
|
|
87
|
-
if (previous?.observedFingerprint === fingerprint)
|
|
88
|
-
return previous;
|
|
89
|
-
let parsed;
|
|
90
|
-
try {
|
|
91
|
-
parsed = JSON.parse(content);
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return this.invalidSource(source, path, fingerprint, previous, new Error("Invalid JSON syntax."));
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
const entries = parseExternalMcpStandaloneConfig(parsed);
|
|
98
|
-
return {
|
|
99
|
-
observedFingerprint: fingerprint,
|
|
100
|
-
entries,
|
|
101
|
-
hasLastKnownGood: true,
|
|
102
|
-
status: {
|
|
103
|
-
source,
|
|
104
|
-
path,
|
|
105
|
-
state: "valid",
|
|
106
|
-
usingLastKnownGood: false,
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
return this.invalidSource(source, path, fingerprint, previous, error);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
invalidSource(source, path, observedFingerprint, previous, error) {
|
|
115
|
-
const diagnostic = {
|
|
116
|
-
source,
|
|
117
|
-
path,
|
|
118
|
-
message: boundedDiagnosticMessage(error),
|
|
119
|
-
};
|
|
120
|
-
this.onDiagnostic?.(diagnostic);
|
|
229
|
+
: undefined;
|
|
230
|
+
if (diagnostic && refreshed.status.diagnosticChanged)
|
|
231
|
+
this.onDiagnostic?.(diagnostic);
|
|
121
232
|
return {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
hasLastKnownGood: previous?.hasLastKnownGood ?? false,
|
|
233
|
+
...(refreshed.value !== undefined ? { value: refreshed.value } : {}),
|
|
234
|
+
hasLastKnownGood: refreshed.status.usingLastKnownGood || refreshed.status.state === "valid",
|
|
125
235
|
status: {
|
|
126
236
|
source,
|
|
127
237
|
path,
|
|
128
|
-
state:
|
|
129
|
-
usingLastKnownGood:
|
|
238
|
+
state: refreshed.status.state,
|
|
239
|
+
usingLastKnownGood: refreshed.status.usingLastKnownGood,
|
|
130
240
|
},
|
|
131
|
-
|
|
241
|
+
diagnosticChanged: refreshed.status.diagnosticChanged,
|
|
242
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
132
243
|
};
|
|
133
244
|
}
|
|
134
245
|
}
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
origins[name] = source;
|
|
145
|
-
delete masked[name];
|
|
246
|
+
function dynamicSourceInput(source) {
|
|
247
|
+
if (source.status.state === "missing")
|
|
248
|
+
return undefined;
|
|
249
|
+
const dynamicSource = source.status.source;
|
|
250
|
+
if (dynamicSource === "legacy")
|
|
251
|
+
throw new Error("Legacy External MCP configuration cannot be a dynamic source.");
|
|
252
|
+
const scope = scopeForSource(dynamicSource);
|
|
253
|
+
if (source.value) {
|
|
254
|
+
return canonicalSourceInput(dynamicSource, scope, source.status.path, source.value);
|
|
146
255
|
}
|
|
256
|
+
return {
|
|
257
|
+
id: sourceId(dynamicSource),
|
|
258
|
+
scope,
|
|
259
|
+
kind: "file",
|
|
260
|
+
location: source.status.path,
|
|
261
|
+
priority: CANONICAL_PRIORITY,
|
|
262
|
+
shadowsLowerPriority: true,
|
|
263
|
+
error: {
|
|
264
|
+
code: source.diagnostic?.code === "missing_environment" ? "missing_environment" : "invalid_source",
|
|
265
|
+
message: source.diagnostic?.message ?? "External MCP configuration is invalid.",
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function canonicalSourceInput(source, scope, path, value) {
|
|
270
|
+
return {
|
|
271
|
+
id: sourceId(source),
|
|
272
|
+
scope,
|
|
273
|
+
kind: "file",
|
|
274
|
+
location: path,
|
|
275
|
+
priority: CANONICAL_PRIORITY,
|
|
276
|
+
shadowsLowerPriority: true,
|
|
277
|
+
value,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function sourceId(source) {
|
|
281
|
+
return source === "global" ? "canonical:user:mcp" : `canonical:${source}:mcp`;
|
|
282
|
+
}
|
|
283
|
+
function sourceFromId(id) {
|
|
284
|
+
if (id === "legacy:user:mcpServers")
|
|
285
|
+
return "legacy";
|
|
286
|
+
if (id === "canonical:user:mcp")
|
|
287
|
+
return "global";
|
|
288
|
+
if (id === "canonical:project:mcp")
|
|
289
|
+
return "project";
|
|
290
|
+
if (id === "canonical:project-local:mcp")
|
|
291
|
+
return "project-local";
|
|
292
|
+
throw new Error(`Unknown External MCP resolver source: ${id}`);
|
|
293
|
+
}
|
|
294
|
+
function scopeForSource(source) {
|
|
295
|
+
if (source === "global")
|
|
296
|
+
return "user";
|
|
297
|
+
return source;
|
|
298
|
+
}
|
|
299
|
+
function normalizeResolvedServers(resolution) {
|
|
300
|
+
const raw = resolution.values.servers;
|
|
301
|
+
if (raw === undefined)
|
|
302
|
+
return {};
|
|
303
|
+
const parsed = parseExternalMcpStandaloneConfig({ servers: raw });
|
|
304
|
+
return Object.fromEntries(Object.entries(parsed)
|
|
305
|
+
.filter((entry) => !("disabled" in entry[1] && entry[1].disabled === true)));
|
|
306
|
+
}
|
|
307
|
+
function configDiagnostic(diagnostic) {
|
|
308
|
+
return {
|
|
309
|
+
source: sourceFromId(diagnostic.source.id),
|
|
310
|
+
path: diagnostic.source.location ?? diagnostic.source.id,
|
|
311
|
+
severity: diagnostic.severity === "error" ? "error" : "warning",
|
|
312
|
+
code: diagnostic.code,
|
|
313
|
+
message: boundedDiagnosticMessage(diagnostic.message),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function mergeDiagnostics(left, right) {
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
return [...left, ...right].filter((diagnostic) => {
|
|
319
|
+
const key = `${diagnostic.source}\0${diagnostic.path}\0${diagnostic.code}\0${diagnostic.message}`;
|
|
320
|
+
if (seen.has(key))
|
|
321
|
+
return false;
|
|
322
|
+
seen.add(key);
|
|
323
|
+
return true;
|
|
324
|
+
});
|
|
147
325
|
}
|
|
148
326
|
function boundedDiagnosticMessage(error) {
|
|
149
327
|
const raw = error instanceof Error ? error.message : String(error);
|