@akira-tl/forgerelay 1.0.1 → 1.1.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.
Files changed (169) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -0
  3. package/capabilities/host-integration/external-mcp/GUIDE.md +29 -0
  4. package/capabilities/lifecycle-hooks/GUIDE.md +13 -3
  5. package/dist/activity/runtime/mcp-query-tools.js +16 -10
  6. package/dist/cli/mcp/external-mcp.js +481 -0
  7. package/dist/cli/mcp/status.js +295 -0
  8. package/dist/cli.js +14 -1
  9. package/dist/lsp/test-support/server-fixture.js +1 -2
  10. package/dist/mcp/filesystem/filesystem-tools.js +7 -3
  11. package/dist/mcp/hooks/command-runner.js +12 -7
  12. package/dist/mcp/hooks/external-mcp-transform.js +224 -0
  13. package/dist/mcp/hooks/hook-cli.js +3 -0
  14. package/dist/mcp/hooks/hooks.js +49 -3
  15. package/dist/mcp/oauth/auth-protocol.js +375 -0
  16. package/dist/mcp/oauth/oauth-provider.js +11 -12
  17. package/dist/mcp/oauth/oauth-store.js +2 -2
  18. package/dist/mcp/oauth/router.js +2 -6
  19. package/dist/mcp/operations/batch/executor.js +1 -1
  20. package/dist/mcp/operations/bulk-read.js +7 -6
  21. package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +387 -0
  22. package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +59 -0
  23. package/dist/mcp/operations/external-mcp/external-mcp.js +382 -0
  24. package/dist/mcp/operations/media-content.js +28 -0
  25. package/dist/mcp/operations/native-bulk-mutations.js +1 -1
  26. package/dist/mcp/process/tools.js +19 -15
  27. package/dist/mcp/request-context.js +18 -0
  28. package/dist/mcp/request-meta.js +28 -3
  29. package/dist/mcp/server/core/activity-support.js +2 -2
  30. package/dist/mcp/server/core/capabilities/external-mcp.js +31 -0
  31. package/dist/mcp/server/core/capabilities.js +7 -0
  32. package/dist/mcp/server/core/capability-registry.js +2 -0
  33. package/dist/mcp/server/core/tool-support.js +29 -0
  34. package/dist/mcp/server/operations/runtime/filesystem-tools.js +91 -48
  35. package/dist/mcp/server/operations/runtime/operation-runtime.js +18 -11
  36. package/dist/mcp/server/transport/http-server.js +42 -21
  37. package/dist/mcp/server/workspace/runtime/workspace-open.js +13 -9
  38. package/dist/mcp/server/workspace/runtime/workspace-tools.js +29 -25
  39. package/dist/runtime/config/config.js +4 -0
  40. package/dist/runtime/config/external-mcp-auth-store.js +225 -0
  41. package/dist/runtime/config/external-mcp-config.js +146 -0
  42. package/dist/runtime/config/external-mcp-registry.js +165 -0
  43. package/dist/runtime/testing/server-fixture.js +10 -5
  44. package/dist/server.js +19 -10
  45. package/dist/subagents/sessions/mcp/audit.js +81 -0
  46. package/dist/ui/.vite/manifest.json +297 -298
  47. package/dist/ui/activity-panel-app.html +1 -1
  48. package/dist/ui/assets/activity-panel-app-Bw7ZFX2g.js +114 -0
  49. package/dist/ui/assets/angular-html-DeUNP12X.js +1 -0
  50. package/dist/ui/assets/{angular-ts-BearSZDb.js → angular-ts-B3v8FKi_.js} +1 -1
  51. package/dist/ui/assets/{apl-CYQtr-y6.js → apl-Bs3iQm4P.js} +1 -1
  52. package/dist/ui/assets/{astro-DDzqKIjK.js → astro-D6GHecBY.js} +1 -1
  53. package/dist/ui/assets/{blade-DkwEyHvQ.js → blade-BMNbVOHv.js} +1 -1
  54. package/dist/ui/assets/c-BquuA6bv.js +1 -0
  55. package/dist/ui/assets/{cobol-xBn5JxHE.js → cobol-BuZ6kfps.js} +1 -1
  56. package/dist/ui/assets/{coffee-BaXXvoJw.js → coffee-DgC5uBzL.js} +1 -1
  57. package/dist/ui/assets/cpp-BdvWqCaB.js +1 -0
  58. package/dist/ui/assets/{crystal-DjOpXVU1.js → crystal-0Q5qBvNI.js} +1 -1
  59. package/dist/ui/assets/css-Be_T8idh.js +1 -0
  60. package/dist/ui/assets/{edge-DNaRizoR.js → edge-DaGLdgh_.js} +1 -1
  61. package/dist/ui/assets/{elixir-CwBMwZmy.js → elixir-CMgiFIzB.js} +1 -1
  62. package/dist/ui/assets/{elm-gOUD2CW3.js → elm-B-ufMlMC.js} +1 -1
  63. package/dist/ui/assets/{erb-BcR-fjUp.js → erb-CaGiOZM-.js} +1 -1
  64. package/dist/ui/assets/{git-rebase-BAQPKHF6.js → git-rebase-CpwUWCzK.js} +1 -1
  65. package/dist/ui/assets/{glimmer-js-P_wOfSz_.js → glimmer-js-BRxccvnE.js} +1 -1
  66. package/dist/ui/assets/{glimmer-ts-B44aY1fZ.js → glimmer-ts-CgQbPzlT.js} +1 -1
  67. package/dist/ui/assets/glsl-D7JmHFr0.js +1 -0
  68. package/dist/ui/assets/graphql-C8n0eUBK.js +1 -0
  69. package/dist/ui/assets/{hack-ddU2k0nt.js → hack-lVN7KVQv.js} +1 -1
  70. package/dist/ui/assets/haml-h18lG0pS.js +1 -0
  71. package/dist/ui/assets/{handlebars-BpzaUpmz.js → handlebars-lRFjRPfj.js} +1 -1
  72. package/dist/ui/assets/heavy-payload-Cqx9MOkH.js +290 -0
  73. package/dist/ui/assets/html-DtILqTE1.js +1 -0
  74. package/dist/ui/assets/{html-derivative-B2beVdmA.js → html-derivative-CSTSmPup.js} +1 -1
  75. package/dist/ui/assets/{http-iEskZy8U.js → http-DNI8fsbY.js} +1 -1
  76. package/dist/ui/assets/{hurl-BuUsLSKv.js → hurl-DgRi60JG.js} +1 -1
  77. package/dist/ui/assets/java-CW9pZ5bh.js +1 -0
  78. package/dist/ui/assets/javascript-Cvg2UrWL.js +1 -0
  79. package/dist/ui/assets/{jinja-CRpkWwrH.js → jinja-CZw_S1FB.js} +1 -1
  80. package/dist/ui/assets/{jison-BYfSnuHj.js → jison-C56u_FZv.js} +1 -1
  81. package/dist/ui/assets/json-DW2UTv1q.js +1 -0
  82. package/dist/ui/assets/jsx-Zj5gobdD.js +1 -0
  83. package/dist/ui/assets/{julia-B8Rxy7vD.js → julia-Cvcrre-L.js} +1 -1
  84. package/dist/ui/assets/{just-DzhIWpFP.js → just-8CZ2u6Tj.js} +1 -1
  85. package/dist/ui/assets/{latex-CUNjUUL-.js → latex-D289AXWG.js} +1 -1
  86. package/dist/ui/assets/{liquid-hUg7nC61.js → liquid-CFEX3SkZ.js} +1 -1
  87. package/dist/ui/assets/lua-BKpJ0ybO.js +1 -0
  88. package/dist/ui/assets/{marko-CQmIkwMF.js → marko-D9maZ0a6.js} +1 -1
  89. package/dist/ui/assets/{mdc-CB-6dHYa.js → mdc-rM9l0qWR.js} +1 -1
  90. package/dist/ui/assets/{nginx-CQORo_Bt.js → nginx-C9w6p2C9.js} +1 -1
  91. package/dist/ui/assets/{nim-BNwBNzdn.js → nim-BRePHk0J.js} +1 -1
  92. package/dist/ui/assets/{perl-Cnz14fAJ.js → perl-CskTmiPQ.js} +1 -1
  93. package/dist/ui/assets/{php-YywDo63b.js → php-DCU_FvNI.js} +1 -1
  94. package/dist/ui/assets/{pug-BwbJp4cM.js → pug-XLaVxTvv.js} +1 -1
  95. package/dist/ui/assets/{qml-D2DY6dMH.js → qml-CaONKSl-.js} +1 -1
  96. package/dist/ui/assets/r-BCVR0ZE3.js +1 -0
  97. package/dist/ui/assets/{razor-pMTG7k41.js → razor-DDnGXxnU.js} +1 -1
  98. package/dist/ui/assets/regexp-DWGTYYVx.js +1 -0
  99. package/dist/ui/assets/{rst-CO_ndDm4.js → rst-Dz3GWvQm.js} +1 -1
  100. package/dist/ui/assets/{ruby-xzRn9gTh.js → ruby-DC3jGtMY.js} +1 -1
  101. package/dist/ui/assets/{sas-CBdtFKLA.js → sas-7mNpvsjZ.js} +1 -1
  102. package/dist/ui/assets/scss-uBsoCJVy.js +1 -0
  103. package/dist/ui/assets/shellscript-BCN9N_Jf.js +1 -0
  104. package/dist/ui/assets/{shellsession-BaZnm5rZ.js → shellsession-Hsw46VXV.js} +1 -1
  105. package/dist/ui/assets/{soy-IouG3xdB.js → soy-D6KV69HU.js} +1 -1
  106. package/dist/ui/assets/sql-CVzVQkJh.js +1 -0
  107. package/dist/ui/assets/{stata-dgbkIZHP.js → stata-BwUjXNhI.js} +1 -1
  108. package/dist/ui/assets/{surrealql-CBROnw_p.js → surrealql-CgloWqRa.js} +1 -1
  109. package/dist/ui/assets/{svelte-6r6BpX4-.js → svelte-DcC3n6IC.js} +1 -1
  110. package/dist/ui/assets/{templ-BKD_PsbL.js → templ-DOFfnWTJ.js} +1 -1
  111. package/dist/ui/assets/{tex-CKY1bwHb.js → tex-ln89VPaX.js} +1 -1
  112. package/dist/ui/assets/{ts-tags-BiYP5N_C.js → ts-tags-CWMzCstH.js} +1 -1
  113. package/dist/ui/assets/tsx-DgzfiGeK.js +1 -0
  114. package/dist/ui/assets/{twig-BTe1oPFW.js → twig-Bi2itZL9.js} +1 -1
  115. package/dist/ui/assets/typescript-CQ24f4zW.js +1 -0
  116. package/dist/ui/assets/{vue-CT0IiySh.js → vue-BS8jE5u6.js} +1 -1
  117. package/dist/ui/assets/{vue-html-B-tXcbat.js → vue-html-o38xqhyB.js} +1 -1
  118. package/dist/ui/assets/{vue-vine-C5ZyDCrs.js → vue-vine-D2NvKv50.js} +1 -1
  119. package/dist/ui/assets/xml-_44rx9_2.js +1 -0
  120. package/dist/ui/assets/{xsl-DvDSndNI.js → xsl-B5QWQPoi.js} +1 -1
  121. package/dist/ui/assets/yaml-CKL2pEWe.js +1 -0
  122. package/dist/workspaces/relay/auth/remote-auth.js +3 -5
  123. package/dist/workspaces/relay/result-support.js +19 -0
  124. package/dist/workspaces/relay/tests/test-support.js +10 -4
  125. package/dist/workspaces/relay/workspace-relay.js +20 -12
  126. package/docs/configuration.md +200 -2
  127. package/docs/debugging.md +8 -5
  128. package/docs/roadmap.md +64 -3
  129. package/docs/security.md +18 -0
  130. package/package.json +12 -4
  131. package/scripts/debug/accept/bootstrap.mjs +5 -0
  132. package/scripts/debug/accept/harness.mjs +10 -3
  133. package/scripts/debug/accept/media.mjs +335 -0
  134. package/scripts/debug/accept/modern-http.mjs +169 -0
  135. package/scripts/debug/accept/support.mjs +10 -6
  136. package/scripts/debug/accept.mjs +12 -1
  137. package/scripts/debug/relay-accept/support.mjs +40 -0
  138. package/scripts/debug/relay-accept.mjs +8 -9
  139. package/scripts/debug/runtime.mjs +4 -1
  140. package/scripts/debug/runtime.test.mjs +9 -0
  141. package/scripts/release/parity-sandbox.mjs +35 -0
  142. package/scripts/release/release-gate.test.mjs +42 -1
  143. package/scripts/release-parity.mjs +3 -22
  144. package/scripts/release-proof.mjs +24 -4
  145. package/scripts/release-proof.test.mjs +12 -4
  146. package/dist/ui/assets/activity-panel-app-XlqSanXT.js +0 -115
  147. package/dist/ui/assets/angular-html-tB9EchbM.js +0 -1
  148. package/dist/ui/assets/c-Dv-N5hjn.js +0 -1
  149. package/dist/ui/assets/cpp-C3unXX-3.js +0 -1
  150. package/dist/ui/assets/css-8Xr4kNNH.js +0 -1
  151. package/dist/ui/assets/glsl-BvCQg9MI.js +0 -1
  152. package/dist/ui/assets/graphql-C9S90gn_.js +0 -1
  153. package/dist/ui/assets/haml-Bb1w9rMG.js +0 -1
  154. package/dist/ui/assets/heavy-payload-CJKqi8Ac.js +0 -290
  155. package/dist/ui/assets/html-dUOs5pA9.js +0 -1
  156. package/dist/ui/assets/java-BFZCrORY.js +0 -1
  157. package/dist/ui/assets/javascript-DaIQ0Zqj.js +0 -1
  158. package/dist/ui/assets/json-BQC_Yp48.js +0 -1
  159. package/dist/ui/assets/jsx-Dd-CJRf9.js +0 -1
  160. package/dist/ui/assets/lua-BoZTiL2I.js +0 -1
  161. package/dist/ui/assets/r-B5nYsy-M.js +0 -1
  162. package/dist/ui/assets/regexp-oP0lbks_.js +0 -1
  163. package/dist/ui/assets/scss-CEqgKQNf.js +0 -1
  164. package/dist/ui/assets/shellscript-DzJJiWQC.js +0 -1
  165. package/dist/ui/assets/sql-DC2rdTzV.js +0 -1
  166. package/dist/ui/assets/tsx-D2OMiPNF.js +0 -1
  167. package/dist/ui/assets/typescript-BGIABksB.js +0 -1
  168. package/dist/ui/assets/xml-CfeBNlMP.js +0 -1
  169. package/dist/ui/assets/yaml-umbj2r1g.js +0 -1
@@ -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/sdk/client/index.js";
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";
@@ -30,7 +29,12 @@ export async function fixture(t, options = {}) {
30
29
  const project = join(root, "project");
31
30
  const agentDir = join(root, "agent");
32
31
  const stateDir = join(root, ".state");
32
+ const configDir = join(root, ".config");
33
33
  await mkdir(join(project, ".forgerelay", "agents"), { recursive: true });
34
+ await mkdir(configDir, { recursive: true });
35
+ if (options.userConfig) {
36
+ await writeFile(join(configDir, "config.json"), JSON.stringify(options.userConfig, null, 2) + "\n");
37
+ }
34
38
  await mkdir(agentDir, { recursive: true });
35
39
  await writeFile(join(agentDir, "AGENTS.md"), "global instructions\n");
36
40
  await writeFile(join(project, "AGENTS.md"), "project instructions\n");
@@ -51,7 +55,7 @@ export async function fixture(t, options = {}) {
51
55
  await git(project, ["commit", "-m", "Initial commit"]);
52
56
  }
53
57
  const loadedConfig = loadConfig({
54
- FORGERELAY_CONFIG_DIR: join(root, ".config"),
58
+ FORGERELAY_CONFIG_DIR: configDir,
55
59
  FORGERELAY_STATE_DIR: stateDir,
56
60
  FORGERELAY_ALLOWED_ROOTS: root,
57
61
  FORGERELAY_WORKTREE_ROOT: join(root, ".worktrees"),
@@ -143,8 +147,9 @@ export async function callOpen(client, path, conversationScopeId, mode, newWorkt
143
147
  return client.callTool(params);
144
148
  }
145
149
  export function structuredContent(result) {
146
- assert.ok(result.structuredContent);
147
- return result.structuredContent;
150
+ const structured = result.structuredContent;
151
+ assert.ok(structured);
152
+ return structured;
148
153
  }
149
154
  export function responseText(result) {
150
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/sdk/server/mcp.js";
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";
@@ -15,12 +15,14 @@ import { HookRunner } from "./mcp/hooks/hooks.js";
15
15
  import { checkHookConfiguration } from "./mcp/hooks/hook-cli.js";
16
16
  import { buildExecutionShellContext, buildServerInstructions, buildToolDescriptions, toolNames } from "./mcp/server-instructions.js";
17
17
  import { IncomingArtifactAdapterRegistry } from "./mcp/artifacts/incoming-artifacts.js";
18
+ import { createExternalMcpCapabilityRuntime } from "./mcp/operations/external-mcp/external-mcp-runtime.js";
18
19
  import { registerProcessTools } from "./mcp/process/tools.js";
19
20
  import { attachCompletedProcessNotices, recordBashCompletion } from "./mcp/process/runtime.js";
20
21
  import { CompositeActivityCoordinator } from "./workspaces/composite/composite-activity.js";
21
22
  import { CompositeWorkspaceRegistry } from "./workspaces/composite/composite-workspaces.js";
22
23
  import { RemoteWorkspaceRelay } from "./workspaces/relay/workspace-relay.js";
23
- import { hostConversationScopeId } from "./mcp/request-meta.js";
24
+ import { hostContextDeliveryScopeId, hostConversationScopeId } from "./mcp/request-meta.js";
25
+ import { mcpHandlerRequestContext } from "./mcp/request-context.js";
24
26
  import { createActivityPanelApp } from "./mcp/panel/app.js";
25
27
  import { shutdownHttpServer } from "./mcp/server/transport/server-shutdown.js";
26
28
  import { formatPathForPrompt } from "./workspaces/resources/skills.js";
@@ -46,7 +48,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
46
48
  const activityPanelApp = createActivityPanelApp(config, FORGERELAY_VERSION);
47
49
  const ownsRemoteWorkspaces = options.remoteWorkspaces === undefined;
48
50
  const remoteWorkspaces = options.remoteWorkspaces
49
- ?? new RemoteWorkspaceRelay(config.configDir, config.stateDir);
51
+ ?? new RemoteWorkspaceRelay(config.configDir, config.stateDir, config.mediaMaxBytes);
50
52
  const compositeWorkspaces = options.compositeWorkspaces
51
53
  ?? new CompositeWorkspaceRegistry(config.stateDir);
52
54
  const workspaceTasks = new WorkspaceTaskStore(config.stateDir);
@@ -109,16 +111,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
109
111
  const reminderWorkspaceId = taskReminderWorkspaceIdFor(target);
110
112
  return attachWorkspaceTaskReminder(presented, reminderWorkspaceId ? taskReminders.recordWork(reminderWorkspaceId) : undefined);
111
113
  };
112
- const hostScopeIdFor = (requestMeta, transportSessionId) => hostConversationScopeId(requestMeta, transportSessionId, connectionScopeId);
113
- const prepareExecutionContext = async (target, requestMeta, signal, sessionId) => {
114
- const conversationScopeId = hostScopeIdFor(requestMeta, sessionId);
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);
115
118
  const turnId = target.compositeWorkspaceId && target.memberName
116
119
  ? await compositeActivity.prepareMember(target.compositeWorkspaceId, target.memberName, target.executionWorkspaceId, conversationScopeId)
117
120
  : undefined;
118
121
  return {
119
- requestMeta,
120
- signal,
121
- sessionId,
122
+ requestMeta: requestContext.requestMeta,
123
+ signal: requestContext.signal,
124
+ sessionId: requestContext.transportSessionId,
125
+ requestId: requestContext.requestId,
126
+ protocolEra: requestContext.protocolEra,
127
+ conversationScopeId,
122
128
  ...(turnId ? { turnId } : {}),
123
129
  ...(target.memberName ? { activityMember: target.memberName } : {}),
124
130
  };
@@ -128,11 +134,13 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
128
134
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
129
135
  const artifactDownloadAvailable = config.artifactsEnabled && isArtifactDownloadSupportedPlatform();
130
136
  const reviewChangesAvailable = config.widgets === "changes";
137
+ const externalMcpCapability = createExternalMcpCapabilityRuntime(config);
131
138
  let batchExecutor;
132
139
  const batchExecuteAvailable = config.toolMode !== "codex";
133
140
  const capabilityRegistry = createCapabilityRegistry({
134
141
  inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
135
142
  ...subagentMcp.registryDependencies,
143
+ externalMcp: externalMcpCapability,
136
144
  workspaceRecovery: {
137
145
  available: true,
138
146
  run: async (input, context) => ({
@@ -457,10 +465,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
457
465
  ...compact,
458
466
  });
459
467
  };
460
- registerAppResource(server, "ForgeRelay Activity Panel", activityPanelApp.uri, activityPanelApp.resourceMetadata, async (uri, extra) => activityPanelApp.readResource(uri.toString(), extra.sessionId));
468
+ registerAppResource(server, "ForgeRelay Activity Panel", activityPanelApp.uri, activityPanelApp.resourceMetadata, async (uri, extra) => activityPanelApp.readResource(uri.toString(), mcpHandlerRequestContext(extra).transportSessionId));
461
469
  registerOpenWorkspaceTool({
462
470
  server, config, workspaces, remoteWorkspaces, compositeWorkspaces, workspaceTasks, processSessions,
463
471
  capabilityRegistry, compositeTaskGuides, loadCompositeMemberContext, rememberWorkspacePanelState, hostScopeIdFor,
472
+ contextDeliveryScopeIdFor,
464
473
  presentation: {
465
474
  config, forgerelayVersion: FORGERELAY_VERSION, workspaces, workspaceTasks, reviewCheckpoints,
466
475
  capabilityRegistry, subagentProviders, hooks, rememberWorkspacePanelState,
@@ -1,4 +1,19 @@
1
1
  export function capabilityActivityAuditRequest(input) {
2
+ if (input.name === "mcp.external") {
3
+ const argumentsValue = isAuditRecord(input.arguments) ? input.arguments : {};
4
+ const callArguments = isAuditRecord(argumentsValue.arguments) ? argumentsValue.arguments : undefined;
5
+ return {
6
+ workspaceId: input.workspaceId,
7
+ name: input.name,
8
+ action: "run",
9
+ arguments: {
10
+ operation: argumentsValue.operation,
11
+ ...(typeof argumentsValue.server === "string" ? { server: argumentsValue.server } : {}),
12
+ ...(typeof argumentsValue.tool === "string" ? { tool: argumentsValue.tool } : {}),
13
+ ...(callArguments ? { argumentKeys: Object.keys(callArguments).sort() } : {}),
14
+ },
15
+ };
16
+ }
2
17
  if (input.name !== "subagent.session") {
3
18
  return {
4
19
  workspaceId: input.workspaceId,
@@ -24,6 +39,23 @@ export function capabilityActivityAuditRequest(input) {
24
39
  };
25
40
  }
26
41
  export function capabilityActivityAuditResult(name, result) {
42
+ if (name === "mcp.external" && isAuditRecord(result)) {
43
+ const structuredContent = isAuditRecord(result.structuredContent) ? result.structuredContent : undefined;
44
+ const capabilityResult = structuredContent && isAuditRecord(structuredContent.result)
45
+ ? structuredContent.result
46
+ : undefined;
47
+ const error = structuredContent && isAuditRecord(structuredContent.error)
48
+ ? structuredContent.error
49
+ : undefined;
50
+ return {
51
+ name,
52
+ action: "run",
53
+ ...(capabilityResult ? { result: summarizeExternalMcpCapabilityResult(capabilityResult) } : {}),
54
+ ...(error
55
+ ? { error: { code: error.code } }
56
+ : {}),
57
+ };
58
+ }
27
59
  if (name !== "subagent.session" || !isAuditRecord(result))
28
60
  return result;
29
61
  const structuredContent = isAuditRecord(result.structuredContent) ? result.structuredContent : undefined;
@@ -50,6 +82,55 @@ export function capabilityActivityAuditResult(name, result) {
50
82
  function isAuditRecord(value) {
51
83
  return typeof value === "object" && value !== null && !Array.isArray(value);
52
84
  }
85
+ function summarizeExternalMcpCapabilityResult(result) {
86
+ const summary = {
87
+ operation: result.operation,
88
+ ...(typeof result.server === "string" ? { server: result.server } : {}),
89
+ ...(typeof result.tool === "string" ? { tool: result.tool } : {}),
90
+ };
91
+ if (Array.isArray(result.servers)) {
92
+ summary.servers = result.servers.flatMap((entry) => {
93
+ if (!isAuditRecord(entry) || typeof entry.name !== "string")
94
+ return [];
95
+ return [{ name: entry.name, transport: entry.transport }];
96
+ });
97
+ }
98
+ if (Array.isArray(result.tools)) {
99
+ summary.tools = result.tools.flatMap((entry) => isAuditRecord(entry) && typeof entry.name === "string" ? [entry.name] : []);
100
+ }
101
+ if (Array.isArray(result.content)) {
102
+ summary.contentTypes = result.content.flatMap((entry) => isAuditRecord(entry) && typeof entry.type === "string" ? [entry.type] : []);
103
+ const media = result.content.flatMap((entry, index) => {
104
+ if (!isAuditRecord(entry)
105
+ || entry.type !== "image"
106
+ || typeof entry.mimeType !== "string"
107
+ || typeof entry.bytes !== "number")
108
+ return [];
109
+ return [{ index, mimeType: entry.mimeType, bytes: entry.bytes }];
110
+ });
111
+ if (media.length > 0)
112
+ summary.media = media;
113
+ }
114
+ if (Array.isArray(result.transforms)) {
115
+ const transforms = result.transforms.flatMap((entry) => {
116
+ if (!isAuditRecord(entry)
117
+ || (entry.phase !== "request" && entry.phase !== "result")
118
+ || typeof entry.name !== "string"
119
+ || (entry.scope !== "global" && entry.scope !== "project")
120
+ || entry.status !== "passed")
121
+ return [];
122
+ return [{
123
+ phase: entry.phase,
124
+ name: entry.name.slice(0, 200),
125
+ scope: entry.scope,
126
+ status: "passed",
127
+ }];
128
+ });
129
+ if (transforms.length > 0)
130
+ summary.transforms = transforms;
131
+ }
132
+ return summary;
133
+ }
53
134
  function summarizeSubagentCapabilityResult(result) {
54
135
  const summary = { operation: result.operation };
55
136
  const session = isAuditRecord(result.session) ? result.session : undefined;