@akira-tl/forgerelay 1.1.0 → 1.1.2
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 +26 -0
- package/README.md +4 -2
- package/dist/activity/runtime/mcp-query-tools.js +16 -10
- package/dist/cli/mcp/external-mcp.js +481 -0
- package/dist/cli/mcp/status.js +295 -0
- package/dist/cli.js +14 -1
- package/dist/lsp/test-support/server-fixture.js +1 -2
- package/dist/mcp/oauth/auth-protocol.js +375 -0
- package/dist/mcp/oauth/oauth-provider.js +11 -12
- package/dist/mcp/oauth/oauth-store.js +2 -2
- package/dist/mcp/oauth/router.js +2 -6
- package/dist/mcp/operations/batch/executor.js +1 -1
- package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +387 -0
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +23 -6
- package/dist/mcp/operations/external-mcp/external-mcp.js +178 -23
- package/dist/mcp/operations/native-bulk-mutations.js +1 -1
- package/dist/mcp/process/tools.js +19 -15
- package/dist/mcp/request-context.js +18 -0
- package/dist/mcp/request-meta.js +28 -3
- package/dist/mcp/server/core/capabilities.js +1 -4
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +35 -28
- package/dist/mcp/server/operations/runtime/operation-runtime.js +7 -7
- package/dist/mcp/server/transport/http-server.js +41 -20
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +2 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +13 -9
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +31 -27
- package/dist/runtime/config/external-mcp-auth-store.js +225 -0
- package/dist/runtime/config/external-mcp-config.js +76 -22
- package/dist/runtime/config/external-mcp-registry.js +165 -0
- package/dist/runtime/testing/server-fixture.js +4 -4
- package/dist/server.js +15 -9
- package/dist/ui/.vite/manifest.json +297 -298
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/activity-panel-app-Bw7ZFX2g.js +114 -0
- package/dist/ui/assets/angular-html-DeUNP12X.js +1 -0
- package/dist/ui/assets/{angular-ts-BearSZDb.js → angular-ts-B3v8FKi_.js} +1 -1
- package/dist/ui/assets/{apl-CYQtr-y6.js → apl-Bs3iQm4P.js} +1 -1
- package/dist/ui/assets/{astro-DDzqKIjK.js → astro-D6GHecBY.js} +1 -1
- package/dist/ui/assets/{blade-DkwEyHvQ.js → blade-BMNbVOHv.js} +1 -1
- package/dist/ui/assets/c-BquuA6bv.js +1 -0
- package/dist/ui/assets/{cobol-xBn5JxHE.js → cobol-BuZ6kfps.js} +1 -1
- package/dist/ui/assets/{coffee-BaXXvoJw.js → coffee-DgC5uBzL.js} +1 -1
- package/dist/ui/assets/cpp-BdvWqCaB.js +1 -0
- package/dist/ui/assets/{crystal-DjOpXVU1.js → crystal-0Q5qBvNI.js} +1 -1
- package/dist/ui/assets/css-Be_T8idh.js +1 -0
- package/dist/ui/assets/{edge-DNaRizoR.js → edge-DaGLdgh_.js} +1 -1
- package/dist/ui/assets/{elixir-CwBMwZmy.js → elixir-CMgiFIzB.js} +1 -1
- package/dist/ui/assets/{elm-gOUD2CW3.js → elm-B-ufMlMC.js} +1 -1
- package/dist/ui/assets/{erb-BcR-fjUp.js → erb-CaGiOZM-.js} +1 -1
- package/dist/ui/assets/{git-rebase-BAQPKHF6.js → git-rebase-CpwUWCzK.js} +1 -1
- package/dist/ui/assets/{glimmer-js-P_wOfSz_.js → glimmer-js-BRxccvnE.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-B44aY1fZ.js → glimmer-ts-CgQbPzlT.js} +1 -1
- package/dist/ui/assets/glsl-D7JmHFr0.js +1 -0
- package/dist/ui/assets/graphql-C8n0eUBK.js +1 -0
- package/dist/ui/assets/{hack-ddU2k0nt.js → hack-lVN7KVQv.js} +1 -1
- package/dist/ui/assets/haml-h18lG0pS.js +1 -0
- package/dist/ui/assets/{handlebars-BpzaUpmz.js → handlebars-lRFjRPfj.js} +1 -1
- package/dist/ui/assets/heavy-payload-Cqx9MOkH.js +290 -0
- package/dist/ui/assets/html-DtILqTE1.js +1 -0
- package/dist/ui/assets/{html-derivative-B2beVdmA.js → html-derivative-CSTSmPup.js} +1 -1
- package/dist/ui/assets/{http-iEskZy8U.js → http-DNI8fsbY.js} +1 -1
- package/dist/ui/assets/{hurl-BuUsLSKv.js → hurl-DgRi60JG.js} +1 -1
- package/dist/ui/assets/java-CW9pZ5bh.js +1 -0
- package/dist/ui/assets/javascript-Cvg2UrWL.js +1 -0
- package/dist/ui/assets/{jinja-CRpkWwrH.js → jinja-CZw_S1FB.js} +1 -1
- package/dist/ui/assets/{jison-BYfSnuHj.js → jison-C56u_FZv.js} +1 -1
- package/dist/ui/assets/json-DW2UTv1q.js +1 -0
- package/dist/ui/assets/jsx-Zj5gobdD.js +1 -0
- package/dist/ui/assets/{julia-B8Rxy7vD.js → julia-Cvcrre-L.js} +1 -1
- package/dist/ui/assets/{just-DzhIWpFP.js → just-8CZ2u6Tj.js} +1 -1
- package/dist/ui/assets/{latex-CUNjUUL-.js → latex-D289AXWG.js} +1 -1
- package/dist/ui/assets/{liquid-hUg7nC61.js → liquid-CFEX3SkZ.js} +1 -1
- package/dist/ui/assets/lua-BKpJ0ybO.js +1 -0
- package/dist/ui/assets/{marko-CQmIkwMF.js → marko-D9maZ0a6.js} +1 -1
- package/dist/ui/assets/{mdc-CB-6dHYa.js → mdc-rM9l0qWR.js} +1 -1
- package/dist/ui/assets/{nginx-CQORo_Bt.js → nginx-C9w6p2C9.js} +1 -1
- package/dist/ui/assets/{nim-BNwBNzdn.js → nim-BRePHk0J.js} +1 -1
- package/dist/ui/assets/{perl-Cnz14fAJ.js → perl-CskTmiPQ.js} +1 -1
- package/dist/ui/assets/{php-YywDo63b.js → php-DCU_FvNI.js} +1 -1
- package/dist/ui/assets/{pug-BwbJp4cM.js → pug-XLaVxTvv.js} +1 -1
- package/dist/ui/assets/{qml-D2DY6dMH.js → qml-CaONKSl-.js} +1 -1
- package/dist/ui/assets/r-BCVR0ZE3.js +1 -0
- package/dist/ui/assets/{razor-pMTG7k41.js → razor-DDnGXxnU.js} +1 -1
- package/dist/ui/assets/regexp-DWGTYYVx.js +1 -0
- package/dist/ui/assets/{rst-CO_ndDm4.js → rst-Dz3GWvQm.js} +1 -1
- package/dist/ui/assets/{ruby-xzRn9gTh.js → ruby-DC3jGtMY.js} +1 -1
- package/dist/ui/assets/{sas-CBdtFKLA.js → sas-7mNpvsjZ.js} +1 -1
- package/dist/ui/assets/scss-uBsoCJVy.js +1 -0
- package/dist/ui/assets/shellscript-BCN9N_Jf.js +1 -0
- package/dist/ui/assets/{shellsession-BaZnm5rZ.js → shellsession-Hsw46VXV.js} +1 -1
- package/dist/ui/assets/{soy-IouG3xdB.js → soy-D6KV69HU.js} +1 -1
- package/dist/ui/assets/sql-CVzVQkJh.js +1 -0
- package/dist/ui/assets/{stata-dgbkIZHP.js → stata-BwUjXNhI.js} +1 -1
- package/dist/ui/assets/{surrealql-CBROnw_p.js → surrealql-CgloWqRa.js} +1 -1
- package/dist/ui/assets/{svelte-6r6BpX4-.js → svelte-DcC3n6IC.js} +1 -1
- package/dist/ui/assets/{templ-BKD_PsbL.js → templ-DOFfnWTJ.js} +1 -1
- package/dist/ui/assets/{tex-CKY1bwHb.js → tex-ln89VPaX.js} +1 -1
- package/dist/ui/assets/{ts-tags-BiYP5N_C.js → ts-tags-CWMzCstH.js} +1 -1
- package/dist/ui/assets/tsx-DgzfiGeK.js +1 -0
- package/dist/ui/assets/{twig-BTe1oPFW.js → twig-Bi2itZL9.js} +1 -1
- package/dist/ui/assets/typescript-CQ24f4zW.js +1 -0
- package/dist/ui/assets/{vue-CT0IiySh.js → vue-BS8jE5u6.js} +1 -1
- package/dist/ui/assets/{vue-html-B-tXcbat.js → vue-html-o38xqhyB.js} +1 -1
- package/dist/ui/assets/{vue-vine-C5ZyDCrs.js → vue-vine-D2NvKv50.js} +1 -1
- package/dist/ui/assets/xml-_44rx9_2.js +1 -0
- package/dist/ui/assets/{xsl-DvDSndNI.js → xsl-B5QWQPoi.js} +1 -1
- package/dist/ui/assets/yaml-CKL2pEWe.js +1 -0
- package/dist/workspaces/relay/auth/remote-auth.js +3 -5
- package/dist/workspaces/relay/tests/test-support.js +7 -4
- package/dist/workspaces/relay/workspace-relay.js +14 -8
- package/dist/workspaces/resources/skills.js +1 -1
- package/docs/chatgpt-coding-workflow.md +12 -6
- package/docs/configuration.md +158 -6
- package/docs/debugging.md +8 -5
- package/docs/roadmap.md +64 -3
- package/docs/security.md +18 -0
- package/package.json +11 -4
- package/scripts/debug/accept/bootstrap.mjs +2 -0
- package/scripts/debug/accept/harness.mjs +9 -2
- package/scripts/debug/accept/modern-http.mjs +169 -0
- package/scripts/debug/accept/support.mjs +10 -6
- package/scripts/debug/accept.mjs +1 -0
- package/scripts/debug/runtime.mjs +4 -1
- package/scripts/debug/runtime.test.mjs +9 -0
- package/scripts/release/parity-sandbox.mjs +35 -0
- package/scripts/release/release-gate.test.mjs +42 -1
- package/scripts/release-parity.mjs +3 -22
- package/scripts/release-proof.mjs +24 -4
- package/scripts/release-proof.test.mjs +12 -4
- package/dist/ui/assets/activity-panel-app-XlqSanXT.js +0 -115
- package/dist/ui/assets/angular-html-tB9EchbM.js +0 -1
- package/dist/ui/assets/c-Dv-N5hjn.js +0 -1
- package/dist/ui/assets/cpp-C3unXX-3.js +0 -1
- package/dist/ui/assets/css-8Xr4kNNH.js +0 -1
- package/dist/ui/assets/glsl-BvCQg9MI.js +0 -1
- package/dist/ui/assets/graphql-C9S90gn_.js +0 -1
- package/dist/ui/assets/haml-Bb1w9rMG.js +0 -1
- package/dist/ui/assets/heavy-payload-CJKqi8Ac.js +0 -290
- package/dist/ui/assets/html-dUOs5pA9.js +0 -1
- package/dist/ui/assets/java-BFZCrORY.js +0 -1
- package/dist/ui/assets/javascript-DaIQ0Zqj.js +0 -1
- package/dist/ui/assets/json-BQC_Yp48.js +0 -1
- package/dist/ui/assets/jsx-Dd-CJRf9.js +0 -1
- package/dist/ui/assets/lua-BoZTiL2I.js +0 -1
- package/dist/ui/assets/r-B5nYsy-M.js +0 -1
- package/dist/ui/assets/regexp-oP0lbks_.js +0 -1
- package/dist/ui/assets/scss-CEqgKQNf.js +0 -1
- package/dist/ui/assets/shellscript-DzJJiWQC.js +0 -1
- package/dist/ui/assets/sql-DC2rdTzV.js +0 -1
- package/dist/ui/assets/tsx-D2OMiPNF.js +0 -1
- package/dist/ui/assets/typescript-BGIABksB.js +0 -1
- package/dist/ui/assets/xml-CfeBNlMP.js +0 -1
- package/dist/ui/assets/yaml-umbj2r1g.js +0 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { withFileLock } from "../state/lock/file-lock.js";
|
|
5
|
+
import { forgerelayConfigDir } from "./user-config.js";
|
|
6
|
+
const AUTH_FILE_VERSION = 1;
|
|
7
|
+
const AUTH_FILE_NAME = "mcp-auth.json";
|
|
8
|
+
export class ExternalMcpCredentialStore {
|
|
9
|
+
configDir;
|
|
10
|
+
filePath;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.configDir = resolve(options.configDir ?? forgerelayConfigDir(options.env));
|
|
13
|
+
this.filePath = join(this.configDir, AUTH_FILE_NAME);
|
|
14
|
+
}
|
|
15
|
+
read(identity) {
|
|
16
|
+
const record = this.readFile().credentials[credentialKey(identity)];
|
|
17
|
+
if (!record || !sameIdentity(record.identity, identity))
|
|
18
|
+
return undefined;
|
|
19
|
+
return cloneRecord(record);
|
|
20
|
+
}
|
|
21
|
+
inspect() {
|
|
22
|
+
const exists = existsSync(this.filePath);
|
|
23
|
+
const file = this.readFile();
|
|
24
|
+
return { exists, credentialCount: Object.keys(file.credentials).length };
|
|
25
|
+
}
|
|
26
|
+
async replace(identity, serverUrl, record) {
|
|
27
|
+
let written;
|
|
28
|
+
await this.updateFile((file) => {
|
|
29
|
+
written = {
|
|
30
|
+
...cloneRecord(record),
|
|
31
|
+
identity: normalizeIdentity(identity),
|
|
32
|
+
serverUrl,
|
|
33
|
+
revision: randomUUID(),
|
|
34
|
+
updatedAt: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
file.credentials[credentialKey(identity)] = written;
|
|
37
|
+
});
|
|
38
|
+
return cloneRecord(written);
|
|
39
|
+
}
|
|
40
|
+
async update(identity, serverUrl, update) {
|
|
41
|
+
let written;
|
|
42
|
+
await this.updateFile((file) => {
|
|
43
|
+
const key = credentialKey(identity);
|
|
44
|
+
const existing = file.credentials[key];
|
|
45
|
+
const current = existing && sameIdentity(existing.identity, identity) ? cloneRecord(existing) : undefined;
|
|
46
|
+
const next = update(current);
|
|
47
|
+
if (!next) {
|
|
48
|
+
delete file.credentials[key];
|
|
49
|
+
written = undefined;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
written = {
|
|
53
|
+
...cloneRecord(next),
|
|
54
|
+
identity: normalizeIdentity(identity),
|
|
55
|
+
serverUrl,
|
|
56
|
+
revision: randomUUID(),
|
|
57
|
+
updatedAt: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
file.credentials[key] = written;
|
|
60
|
+
});
|
|
61
|
+
return written ? cloneRecord(written) : undefined;
|
|
62
|
+
}
|
|
63
|
+
async remove(identity) {
|
|
64
|
+
let removed = false;
|
|
65
|
+
await this.updateFile((file) => {
|
|
66
|
+
const key = credentialKey(identity);
|
|
67
|
+
const existing = file.credentials[key];
|
|
68
|
+
if (!existing || !sameIdentity(existing.identity, identity))
|
|
69
|
+
return;
|
|
70
|
+
delete file.credentials[key];
|
|
71
|
+
removed = true;
|
|
72
|
+
});
|
|
73
|
+
return removed;
|
|
74
|
+
}
|
|
75
|
+
withIdentityLock(identity, operation) {
|
|
76
|
+
mkdirSync(this.configDir, { recursive: true });
|
|
77
|
+
return withFileLock(join(this.configDir, `.mcp-auth-${credentialKeyHash(identity)}.lock`), operation, {
|
|
78
|
+
timeoutMs: 15_000,
|
|
79
|
+
staleMs: 60_000,
|
|
80
|
+
mode: 0o600,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
readFile() {
|
|
84
|
+
if (!existsSync(this.filePath))
|
|
85
|
+
return emptyAuthFile();
|
|
86
|
+
let raw;
|
|
87
|
+
try {
|
|
88
|
+
raw = JSON.parse(readFileSync(this.filePath, "utf8"));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error(`Unable to read External MCP credential store ${this.filePath}: invalid JSON.`);
|
|
92
|
+
}
|
|
93
|
+
return parseAuthFile(raw, this.filePath);
|
|
94
|
+
}
|
|
95
|
+
async updateFile(update) {
|
|
96
|
+
mkdirSync(this.configDir, { recursive: true });
|
|
97
|
+
await withFileLock(`${this.filePath}.lock`, () => {
|
|
98
|
+
const file = this.readFile();
|
|
99
|
+
update(file);
|
|
100
|
+
writePrivateJsonFile(this.filePath, file);
|
|
101
|
+
}, { mode: 0o600 });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function externalMcpCredentialIdentity(source, server, projectRoot) {
|
|
105
|
+
return source === "project"
|
|
106
|
+
? { kind: "project", server, projectRoot: resolve(projectRoot) }
|
|
107
|
+
: { kind: "global", server };
|
|
108
|
+
}
|
|
109
|
+
export function credentialKey(identity) {
|
|
110
|
+
const normalized = normalizeIdentity(identity);
|
|
111
|
+
return normalized.kind === "global"
|
|
112
|
+
? `global:${normalized.server}`
|
|
113
|
+
: `project:${credentialKeyHash(normalized)}:${normalized.server}`;
|
|
114
|
+
}
|
|
115
|
+
function credentialKeyHash(identity) {
|
|
116
|
+
const normalized = normalizeIdentity(identity);
|
|
117
|
+
const material = normalized.kind === "global"
|
|
118
|
+
? `global\0${normalized.server}`
|
|
119
|
+
: `project\0${normalized.projectRoot}\0${normalized.server}`;
|
|
120
|
+
return createHash("sha256").update(material).digest("hex");
|
|
121
|
+
}
|
|
122
|
+
function normalizeIdentity(identity) {
|
|
123
|
+
return identity.kind === "project"
|
|
124
|
+
? { kind: "project", server: identity.server, projectRoot: resolve(identity.projectRoot) }
|
|
125
|
+
: { kind: "global", server: identity.server };
|
|
126
|
+
}
|
|
127
|
+
function sameIdentity(left, right) {
|
|
128
|
+
const a = normalizeIdentity(left);
|
|
129
|
+
const b = normalizeIdentity(right);
|
|
130
|
+
return a.kind === b.kind
|
|
131
|
+
&& a.server === b.server
|
|
132
|
+
&& (a.kind === "global" || (b.kind === "project" && a.projectRoot === b.projectRoot));
|
|
133
|
+
}
|
|
134
|
+
function emptyAuthFile() {
|
|
135
|
+
return { version: AUTH_FILE_VERSION, credentials: {} };
|
|
136
|
+
}
|
|
137
|
+
function parseAuthFile(value, filePath) {
|
|
138
|
+
if (!isRecord(value) || value.version !== AUTH_FILE_VERSION || !isRecord(value.credentials)) {
|
|
139
|
+
throw new Error(`Unable to read External MCP credential store ${filePath}: unsupported format.`);
|
|
140
|
+
}
|
|
141
|
+
const credentials = {};
|
|
142
|
+
for (const [key, raw] of Object.entries(value.credentials)) {
|
|
143
|
+
credentials[key] = parseCredentialRecord(raw, filePath);
|
|
144
|
+
}
|
|
145
|
+
return { version: AUTH_FILE_VERSION, credentials };
|
|
146
|
+
}
|
|
147
|
+
function parseCredentialRecord(value, filePath) {
|
|
148
|
+
if (!isRecord(value) || !isCredentialIdentity(value.identity)) {
|
|
149
|
+
throw new Error(`Unable to read External MCP credential store ${filePath}: invalid credential identity.`);
|
|
150
|
+
}
|
|
151
|
+
if (typeof value.serverUrl !== "string"
|
|
152
|
+
|| typeof value.revision !== "string"
|
|
153
|
+
|| typeof value.updatedAt !== "string") {
|
|
154
|
+
throw new Error(`Unable to read External MCP credential store ${filePath}: invalid credential metadata.`);
|
|
155
|
+
}
|
|
156
|
+
if (value.tokens !== undefined
|
|
157
|
+
&& (!isRecord(value.tokens) || typeof value.tokens.access_token !== "string")) {
|
|
158
|
+
throw new Error(`Unable to read External MCP credential store ${filePath}: invalid token record.`);
|
|
159
|
+
}
|
|
160
|
+
if (value.clientInformation !== undefined
|
|
161
|
+
&& (!isRecord(value.clientInformation) || typeof value.clientInformation.client_id !== "string")) {
|
|
162
|
+
throw new Error(`Unable to read External MCP credential store ${filePath}: invalid client record.`);
|
|
163
|
+
}
|
|
164
|
+
if (value.discoveryState !== undefined
|
|
165
|
+
&& (!isRecord(value.discoveryState) || typeof value.discoveryState.authorizationServerUrl !== "string")) {
|
|
166
|
+
throw new Error(`Unable to read External MCP credential store ${filePath}: invalid discovery record.`);
|
|
167
|
+
}
|
|
168
|
+
const reauthorization = parseReauthorization(value.reauthorization, filePath);
|
|
169
|
+
return {
|
|
170
|
+
identity: normalizeIdentity(value.identity),
|
|
171
|
+
serverUrl: value.serverUrl,
|
|
172
|
+
revision: value.revision,
|
|
173
|
+
updatedAt: value.updatedAt,
|
|
174
|
+
...(value.tokens ? { tokens: structuredClone(value.tokens) } : {}),
|
|
175
|
+
...(value.clientInformation
|
|
176
|
+
? { clientInformation: structuredClone(value.clientInformation) }
|
|
177
|
+
: {}),
|
|
178
|
+
...(value.discoveryState
|
|
179
|
+
? { discoveryState: structuredClone(value.discoveryState) }
|
|
180
|
+
: {}),
|
|
181
|
+
...(typeof value.authorizationServerUrl === "string"
|
|
182
|
+
? { authorizationServerUrl: value.authorizationServerUrl }
|
|
183
|
+
: {}),
|
|
184
|
+
...(typeof value.resourceUrl === "string" ? { resourceUrl: value.resourceUrl } : {}),
|
|
185
|
+
...(reauthorization ? { reauthorization } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function parseReauthorization(value, filePath) {
|
|
189
|
+
if (value === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
if (!isRecord(value)
|
|
192
|
+
|| !["authorization_required", "invalid_grant", "insufficient_scope", "binding_changed"].includes(String(value.reason))
|
|
193
|
+
|| typeof value.observedAt !== "string"
|
|
194
|
+
|| (value.scope !== undefined && typeof value.scope !== "string")) {
|
|
195
|
+
throw new Error(`Unable to read External MCP credential store ${filePath}: invalid reauthorization state.`);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
reason: value.reason,
|
|
199
|
+
observedAt: value.observedAt,
|
|
200
|
+
...(typeof value.scope === "string" && value.scope.trim() ? { scope: value.scope.trim() } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function isCredentialIdentity(value) {
|
|
204
|
+
if (!isRecord(value) || typeof value.server !== "string" || !value.server)
|
|
205
|
+
return false;
|
|
206
|
+
if (value.kind === "global")
|
|
207
|
+
return true;
|
|
208
|
+
return value.kind === "project" && typeof value.projectRoot === "string" && Boolean(value.projectRoot);
|
|
209
|
+
}
|
|
210
|
+
function cloneRecord(value) {
|
|
211
|
+
return structuredClone(value);
|
|
212
|
+
}
|
|
213
|
+
function writePrivateJsonFile(filePath, value) {
|
|
214
|
+
const temporary = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
215
|
+
try {
|
|
216
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
217
|
+
renameSync(temporary, filePath);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
rmSync(temporary, { force: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function isRecord(value) {
|
|
224
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
225
|
+
}
|
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
const MCP_SERVER_NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
2
2
|
const MAX_MCP_SERVERS = 32;
|
|
3
3
|
export function parseExternalMcpServers(value) {
|
|
4
|
-
|
|
4
|
+
return parseServerRegistry(value, "mcpServers", false);
|
|
5
|
+
}
|
|
6
|
+
export function parseExternalMcpStandaloneConfig(value) {
|
|
7
|
+
if (!isRecord(value))
|
|
8
|
+
throw new Error("External MCP configuration must be a JSON object.");
|
|
9
|
+
rejectUnknownKeys(value, new Set(["servers"]), "External MCP configuration");
|
|
10
|
+
if (!("servers" in value))
|
|
11
|
+
throw new Error("External MCP configuration must contain a servers object.");
|
|
12
|
+
return parseServerRegistry(value.servers, "servers", true);
|
|
13
|
+
}
|
|
14
|
+
function parseServerRegistry(value, label, allowDisabled) {
|
|
15
|
+
if (value === undefined && !allowDisabled)
|
|
5
16
|
return {};
|
|
6
17
|
if (!isRecord(value))
|
|
7
|
-
throw new Error(
|
|
18
|
+
throw new Error(`${label} must be an object keyed by server name.`);
|
|
8
19
|
const entries = Object.entries(value);
|
|
9
20
|
if (entries.length > MAX_MCP_SERVERS) {
|
|
10
|
-
throw new Error(
|
|
21
|
+
throw new Error(`${label} may contain at most ${MAX_MCP_SERVERS} configured servers.`);
|
|
11
22
|
}
|
|
12
23
|
return Object.fromEntries(entries.map(([name, raw]) => {
|
|
13
24
|
if (!MCP_SERVER_NAME_PATTERN.test(name)) {
|
|
14
|
-
throw new Error(`Invalid
|
|
25
|
+
throw new Error(`Invalid ${label} name '${name}'. Use a lowercase stable name.`);
|
|
15
26
|
}
|
|
16
27
|
if (!isRecord(raw))
|
|
17
|
-
throw new Error(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
throw new Error(`${label}.${name} must be an object.`);
|
|
29
|
+
const disabled = allowDisabled && "disabled" in raw ? parseDisabled(raw.disabled, `${label}.${name}.disabled`) : false;
|
|
30
|
+
const serverValue = allowDisabled && "disabled" in raw
|
|
31
|
+
? Object.fromEntries(Object.entries(raw).filter(([key]) => key !== "disabled"))
|
|
32
|
+
: raw;
|
|
33
|
+
if (disabled && Object.keys(serverValue).length === 0) {
|
|
34
|
+
return [name, { disabled: true }];
|
|
35
|
+
}
|
|
36
|
+
const server = serverValue.transport === "stdio"
|
|
37
|
+
? parseStdio(name, serverValue, label)
|
|
38
|
+
: serverValue.transport === "streamable-http"
|
|
39
|
+
? parseHttp(name, serverValue, label)
|
|
40
|
+
: undefined;
|
|
41
|
+
if (!server)
|
|
42
|
+
throw new Error(`${label}.${name}.transport must be 'stdio' or 'streamable-http'.`);
|
|
43
|
+
return [name, disabled ? { disabled: true } : server];
|
|
23
44
|
}));
|
|
24
45
|
}
|
|
25
|
-
function parseStdio(name, value) {
|
|
26
|
-
const command = requiredString(value.command,
|
|
27
|
-
const args = optionalStringArray(value.args,
|
|
28
|
-
const env = optionalStringRecord(value.env,
|
|
29
|
-
const cwd = optionalString(value.cwd,
|
|
30
|
-
rejectUnknownKeys(value, new Set(["transport", "command", "args", "env", "cwd"]),
|
|
46
|
+
function parseStdio(name, value, label) {
|
|
47
|
+
const command = requiredString(value.command, `${label}.${name}.command`);
|
|
48
|
+
const args = optionalStringArray(value.args, `${label}.${name}.args`);
|
|
49
|
+
const env = optionalStringRecord(value.env, `${label}.${name}.env`);
|
|
50
|
+
const cwd = optionalString(value.cwd, `${label}.${name}.cwd`);
|
|
51
|
+
rejectUnknownKeys(value, new Set(["transport", "command", "args", "env", "cwd"]), `${label}.${name}`);
|
|
31
52
|
return {
|
|
32
53
|
transport: "stdio",
|
|
33
54
|
command,
|
|
@@ -36,26 +57,59 @@ function parseStdio(name, value) {
|
|
|
36
57
|
...(cwd ? { cwd } : {}),
|
|
37
58
|
};
|
|
38
59
|
}
|
|
39
|
-
function parseHttp(name, value) {
|
|
40
|
-
const url = requiredString(value.url,
|
|
60
|
+
function parseHttp(name, value, label) {
|
|
61
|
+
const url = requiredString(value.url, `${label}.${name}.url`);
|
|
41
62
|
let parsed;
|
|
42
63
|
try {
|
|
43
64
|
parsed = new URL(url);
|
|
44
65
|
}
|
|
45
66
|
catch {
|
|
46
|
-
throw new Error(
|
|
67
|
+
throw new Error(`${label}.${name}.url must be a valid HTTP(S) URL.`);
|
|
47
68
|
}
|
|
48
69
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
49
|
-
throw new Error(
|
|
70
|
+
throw new Error(`${label}.${name}.url must use http or https.`);
|
|
50
71
|
}
|
|
51
|
-
const headers = optionalStringRecord(value.headers,
|
|
52
|
-
|
|
72
|
+
const headers = optionalStringRecord(value.headers, `${label}.${name}.headers`);
|
|
73
|
+
const oauth = parseOAuthClientConfig(value.oauth, `${label}.${name}.oauth`);
|
|
74
|
+
rejectUnknownKeys(value, new Set(["transport", "url", "headers", "oauth"]), `${label}.${name}`);
|
|
53
75
|
return {
|
|
54
76
|
transport: "streamable-http",
|
|
55
77
|
url: parsed.toString(),
|
|
56
78
|
...(headers ? { headers } : {}),
|
|
79
|
+
...(oauth ? { oauth } : {}),
|
|
57
80
|
};
|
|
58
81
|
}
|
|
82
|
+
function parseOAuthClientConfig(value, label) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (!isRecord(value))
|
|
86
|
+
throw new Error(`${label} must be an object.`);
|
|
87
|
+
rejectUnknownKeys(value, new Set(["clientMetadataUrl", "callbackPort"]), label);
|
|
88
|
+
const clientMetadataUrl = requiredString(value.clientMetadataUrl, `${label}.clientMetadataUrl`);
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = new URL(clientMetadataUrl);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new Error(`${label}.clientMetadataUrl must be a valid HTTPS URL.`);
|
|
95
|
+
}
|
|
96
|
+
if (parsed.protocol !== "https:" || parsed.pathname === "/") {
|
|
97
|
+
throw new Error(`${label}.clientMetadataUrl must use https and contain a non-root path.`);
|
|
98
|
+
}
|
|
99
|
+
const callbackPort = value.callbackPort;
|
|
100
|
+
if (!Number.isInteger(callbackPort) || Number(callbackPort) < 1024 || Number(callbackPort) > 65535) {
|
|
101
|
+
throw new Error(`${label}.callbackPort must be an integer from 1024 to 65535.`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
clientMetadataUrl: parsed.toString(),
|
|
105
|
+
callbackPort: Number(callbackPort),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function parseDisabled(value, label) {
|
|
109
|
+
if (typeof value !== "boolean")
|
|
110
|
+
throw new Error(`${label} must be a boolean.`);
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
59
113
|
function requiredString(value, label) {
|
|
60
114
|
if (typeof value !== "string" || !value.trim())
|
|
61
115
|
throw new Error(`${label} must be a non-empty string.`);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { parseExternalMcpStandaloneConfig, } from "./external-mcp-config.js";
|
|
5
|
+
const MISSING_FINGERPRINT = "missing";
|
|
6
|
+
const MAX_DIAGNOSTIC_LENGTH = 320;
|
|
7
|
+
export class ExternalMcpConfigRegistry {
|
|
8
|
+
configDir;
|
|
9
|
+
legacyServers;
|
|
10
|
+
onDiagnostic;
|
|
11
|
+
global;
|
|
12
|
+
projects = new Map();
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.configDir = resolve(options.configDir);
|
|
15
|
+
this.legacyServers = { ...(options.legacyServers ?? {}) };
|
|
16
|
+
this.onDiagnostic = options.onDiagnostic;
|
|
17
|
+
}
|
|
18
|
+
resolve(workspaceRoot) {
|
|
19
|
+
const projectRoot = resolve(workspaceRoot);
|
|
20
|
+
const global = this.loadGlobalSource();
|
|
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);
|
|
25
|
+
}
|
|
26
|
+
resolveGlobal() {
|
|
27
|
+
return this.composeSnapshot(this.loadGlobalSource());
|
|
28
|
+
}
|
|
29
|
+
loadGlobalSource() {
|
|
30
|
+
const globalPath = join(this.configDir, "mcp.json");
|
|
31
|
+
this.global = this.loadDynamicSource("global", globalPath, this.global);
|
|
32
|
+
return this.global;
|
|
33
|
+
}
|
|
34
|
+
composeSnapshot(global, project) {
|
|
35
|
+
const servers = { ...this.legacyServers };
|
|
36
|
+
const origins = Object.fromEntries(Object.keys(this.legacyServers).map((name) => [name, "legacy"]));
|
|
37
|
+
const masked = {};
|
|
38
|
+
applySource(servers, origins, masked, global.entries, "global");
|
|
39
|
+
if (project)
|
|
40
|
+
applySource(servers, origins, masked, project.entries, "project");
|
|
41
|
+
const diagnostics = [global.diagnostic, project?.diagnostic]
|
|
42
|
+
.filter((entry) => entry !== undefined);
|
|
43
|
+
return {
|
|
44
|
+
servers,
|
|
45
|
+
origins,
|
|
46
|
+
masked,
|
|
47
|
+
sources: [
|
|
48
|
+
{
|
|
49
|
+
source: "legacy",
|
|
50
|
+
path: join(this.configDir, "config.json"),
|
|
51
|
+
state: "valid",
|
|
52
|
+
usingLastKnownGood: false,
|
|
53
|
+
},
|
|
54
|
+
global.status,
|
|
55
|
+
...(project ? [project.status] : []),
|
|
56
|
+
],
|
|
57
|
+
diagnostics,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
loadDynamicSource(source, path, previous) {
|
|
61
|
+
let content;
|
|
62
|
+
try {
|
|
63
|
+
content = readFileSync(path, "utf8");
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (isMissingFileError(error)) {
|
|
67
|
+
if (previous?.observedFingerprint === MISSING_FINGERPRINT)
|
|
68
|
+
return previous;
|
|
69
|
+
return {
|
|
70
|
+
observedFingerprint: MISSING_FINGERPRINT,
|
|
71
|
+
entries: {},
|
|
72
|
+
hasLastKnownGood: true,
|
|
73
|
+
status: {
|
|
74
|
+
source,
|
|
75
|
+
path,
|
|
76
|
+
state: "missing",
|
|
77
|
+
usingLastKnownGood: false,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const fingerprint = `read-error:${errorCode(error)}`;
|
|
82
|
+
if (previous?.observedFingerprint === fingerprint)
|
|
83
|
+
return previous;
|
|
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);
|
|
121
|
+
return {
|
|
122
|
+
observedFingerprint,
|
|
123
|
+
entries: previous?.entries ?? {},
|
|
124
|
+
hasLastKnownGood: previous?.hasLastKnownGood ?? false,
|
|
125
|
+
status: {
|
|
126
|
+
source,
|
|
127
|
+
path,
|
|
128
|
+
state: "invalid",
|
|
129
|
+
usingLastKnownGood: previous?.hasLastKnownGood === true,
|
|
130
|
+
},
|
|
131
|
+
diagnostic,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function applySource(servers, origins, masked, entries, source) {
|
|
136
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
137
|
+
if ("disabled" in entry && entry.disabled === true) {
|
|
138
|
+
delete servers[name];
|
|
139
|
+
delete origins[name];
|
|
140
|
+
masked[name] = source;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
servers[name] = entry;
|
|
144
|
+
origins[name] = source;
|
|
145
|
+
delete masked[name];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function boundedDiagnosticMessage(error) {
|
|
149
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
150
|
+
const normalized = raw.replace(/\s+/g, " ").trim() || "Invalid External MCP configuration.";
|
|
151
|
+
return normalized.length <= MAX_DIAGNOSTIC_LENGTH
|
|
152
|
+
? normalized
|
|
153
|
+
: `${normalized.slice(0, MAX_DIAGNOSTIC_LENGTH - 3)}...`;
|
|
154
|
+
}
|
|
155
|
+
function isMissingFileError(error) {
|
|
156
|
+
return errorCode(error) === "ENOENT";
|
|
157
|
+
}
|
|
158
|
+
function errorCode(error) {
|
|
159
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
160
|
+
const code = error.code;
|
|
161
|
+
if (typeof code === "string" && code)
|
|
162
|
+
return code;
|
|
163
|
+
}
|
|
164
|
+
return "UNKNOWN";
|
|
165
|
+
}
|
|
@@ -4,8 +4,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import { Client } from "@modelcontextprotocol/
|
|
8
|
-
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
7
|
+
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
9
8
|
import { ActivityAuditStore } from "../../activity/history/audit-store.js";
|
|
10
9
|
import { BashOutputStore } from "../../activity/history/bash-output-store.js";
|
|
11
10
|
import { HostTurnStore } from "../../activity/history/host-turn-store.js";
|
|
@@ -148,8 +147,9 @@ export async function callOpen(client, path, conversationScopeId, mode, newWorkt
|
|
|
148
147
|
return client.callTool(params);
|
|
149
148
|
}
|
|
150
149
|
export function structuredContent(result) {
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
const structured = result.structuredContent;
|
|
151
|
+
assert.ok(structured);
|
|
152
|
+
return structured;
|
|
153
153
|
}
|
|
154
154
|
export function responseText(result) {
|
|
155
155
|
const content = result.content;
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
5
5
|
import { registerAppResource } from "@modelcontextprotocol/ext-apps/server";
|
|
6
6
|
import { registerActivityQueryTools } from "./activity/runtime/mcp-query-tools.js";
|
|
7
7
|
import { buildCapabilityFingerprint, loadCapabilityGuides } from "./mcp/server/core/capabilities.js";
|
|
@@ -21,7 +21,8 @@ import { attachCompletedProcessNotices, recordBashCompletion } from "./mcp/proce
|
|
|
21
21
|
import { CompositeActivityCoordinator } from "./workspaces/composite/composite-activity.js";
|
|
22
22
|
import { CompositeWorkspaceRegistry } from "./workspaces/composite/composite-workspaces.js";
|
|
23
23
|
import { RemoteWorkspaceRelay } from "./workspaces/relay/workspace-relay.js";
|
|
24
|
-
import { hostConversationScopeId } from "./mcp/request-meta.js";
|
|
24
|
+
import { hostContextDeliveryScopeId, hostConversationScopeId } from "./mcp/request-meta.js";
|
|
25
|
+
import { mcpHandlerRequestContext } from "./mcp/request-context.js";
|
|
25
26
|
import { createActivityPanelApp } from "./mcp/panel/app.js";
|
|
26
27
|
import { shutdownHttpServer } from "./mcp/server/transport/server-shutdown.js";
|
|
27
28
|
import { formatPathForPrompt } from "./workspaces/resources/skills.js";
|
|
@@ -110,16 +111,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
110
111
|
const reminderWorkspaceId = taskReminderWorkspaceIdFor(target);
|
|
111
112
|
return attachWorkspaceTaskReminder(presented, reminderWorkspaceId ? taskReminders.recordWork(reminderWorkspaceId) : undefined);
|
|
112
113
|
};
|
|
113
|
-
const hostScopeIdFor = (
|
|
114
|
-
const
|
|
115
|
-
|
|
114
|
+
const hostScopeIdFor = (requestContext) => hostConversationScopeId(requestContext, connectionScopeId);
|
|
115
|
+
const contextDeliveryScopeIdFor = (requestContext) => hostContextDeliveryScopeId(requestContext, connectionScopeId);
|
|
116
|
+
const prepareExecutionContext = async (target, requestContext) => {
|
|
117
|
+
const conversationScopeId = hostScopeIdFor(requestContext);
|
|
116
118
|
const turnId = target.compositeWorkspaceId && target.memberName
|
|
117
119
|
? await compositeActivity.prepareMember(target.compositeWorkspaceId, target.memberName, target.executionWorkspaceId, conversationScopeId)
|
|
118
120
|
: undefined;
|
|
119
121
|
return {
|
|
120
|
-
requestMeta,
|
|
121
|
-
signal,
|
|
122
|
-
sessionId,
|
|
122
|
+
requestMeta: requestContext.requestMeta,
|
|
123
|
+
signal: requestContext.signal,
|
|
124
|
+
sessionId: requestContext.transportSessionId,
|
|
125
|
+
requestId: requestContext.requestId,
|
|
126
|
+
protocolEra: requestContext.protocolEra,
|
|
127
|
+
conversationScopeId,
|
|
123
128
|
...(turnId ? { turnId } : {}),
|
|
124
129
|
...(target.memberName ? { activityMember: target.memberName } : {}),
|
|
125
130
|
};
|
|
@@ -460,10 +465,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
460
465
|
...compact,
|
|
461
466
|
});
|
|
462
467
|
};
|
|
463
|
-
registerAppResource(server, "ForgeRelay Activity Panel", activityPanelApp.uri, activityPanelApp.resourceMetadata, async (uri, extra) => activityPanelApp.readResource(uri.toString(), extra.
|
|
468
|
+
registerAppResource(server, "ForgeRelay Activity Panel", activityPanelApp.uri, activityPanelApp.resourceMetadata, async (uri, extra) => activityPanelApp.readResource(uri.toString(), mcpHandlerRequestContext(extra).transportSessionId));
|
|
464
469
|
registerOpenWorkspaceTool({
|
|
465
470
|
server, config, workspaces, remoteWorkspaces, compositeWorkspaces, workspaceTasks, processSessions,
|
|
466
471
|
capabilityRegistry, compositeTaskGuides, loadCompositeMemberContext, rememberWorkspacePanelState, hostScopeIdFor,
|
|
472
|
+
contextDeliveryScopeIdFor,
|
|
467
473
|
presentation: {
|
|
468
474
|
config, forgerelayVersion: FORGERELAY_VERSION, workspaces, workspaceTasks, reviewCheckpoints,
|
|
469
475
|
capabilityRegistry, subagentProviders, hooks, rememberWorkspacePanelState,
|