@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.1.1] - 2026-09-10
8
+
9
+ ### Added
10
+
11
+ - Added standalone global and Project External MCP registries in `mcp.json` with hot reload, Project-over-global precedence, explicit `disabled` masking, whole-source last-known-good behavior, and permanent discovery of the existing `mcp.external` Capability even when the registry starts empty.
12
+ - Added human-operated External MCP OAuth through `forgerelay mcp auth/logout`, including machine-private Project-scoped credentials, desktop loopback and masked headless callback flows, silent refresh with cross-process rotation protection, scope step-up, DCR compatibility, and optional CIMD client metadata for authorization servers that require it.
13
+ - Added `forgerelay mcp list/test` and passive External MCP `doctor` diagnostics for resolved source, authentication state, configuration health, negotiated MCP protocol, tool discovery, and actionable failure reporting.
14
+
15
+ ### Changed
16
+
17
+ - Migrated ForgeRelay's MCP runtime to the split MCP SDK v2 and Apps SDK v2, supporting MCP 2026-07-28 modern stateless HTTP alongside the existing legacy sessionful Host path while preserving legacy External MCP and Workspace Relay interoperability.
18
+ - Modern stateless Host Turns now use stable Host conversation metadata when supplied and otherwise remain request-scoped instead of inferring conversation identity from transport or process lifetime. Legacy compatibility retains its bounded transport/session fallback behavior.
19
+ - `config.json.mcpServers` remains a deprecated compatibility source; new External MCP configuration uses `mcp.json`, and relayed External MCP configuration, credentials, refresh, process launch, and upstream calls remain owned by the Execution ForgeRelay.
20
+
21
+ ### Security
22
+
23
+ - External MCP OAuth credentials stay in private `mcp-auth.json` state rather than Project files, are bound to their Project/global identity plus resource/issuer state, and are never exposed through normal diagnostics. Interactive authorization remains human-only; runtime refresh cannot open a browser, and headless callback input is masked.
24
+ - Project-defined stdio MCP servers are explicitly documented as executable project configuration, while static header/env secrets remain user-managed and External MCP paths, URLs, and resource references continue to be pass-through unless the Agent or an explicit Transform Hook acts on them.
25
+
26
+ ## [1.1.0] - 2026-09-09
27
+
28
+ ### Added
29
+
30
+ - Added first-class image transport to the existing `read` tool for PNG, JPEG, WebP, and GIF, with magic-byte detection, unchanged image bytes, mixed bulk Read support, and explicit rejection of text ranges on image targets.
31
+ - Added registered external MCP servers through the existing workspace-scoped `capability` surface, including direct upstream `ImageContent` forwarding without expanding the always-visible core tool list.
32
+ - Added opt-in `ExternalMcpBeforeForward` and `ExternalMcpAfterForward` transform Hooks for server/tool-specific request or result adaptation while keeping path/URL/resource references pass-through by default.
33
+
34
+ ### Changed
35
+
36
+ - Workspace Relay now preserves MCP image content opaquely through Execution → Gateway routing and re-applies the Gateway media budget before Host delivery.
37
+ - Media payloads are transient MCP content rather than Artifacts: the default decoded-media budget is 20 MiB per tool result, aggregate media is bounded, and structured output, Activity/Audit, UI state, and logs retain metadata instead of image base64.
38
+
39
+ ### Security
40
+
41
+ - External MCP errors, Hook transforms, Relay ingress, and image Reads now enforce bounded media validation without persisting raw image payloads or credentials, and external path/URL/resource results are never implicitly dereferenced by ForgeRelay.
42
+
7
43
  ## [1.0.1] - 2026-09-09
8
44
 
9
45
  ### Fixed
package/README.md CHANGED
@@ -108,6 +108,7 @@ ForgeRelay 不会默认为每个任务创建 worktree。只有你明确要求隔
108
108
  - 项目里的 `AGENTS.md`、`CLAUDE.md` 和 Agent Skills 按需加载,不会每次都把整套说明重新塞进上下文。
109
109
  - 需要并行开发时可以创建真实 Git worktree;集成回主分支时只接受安全的 fast-forward,不自动制造 merge conflict。
110
110
  - Workspace Relay 可以把执行放到另一台 ForgeRelay;Composite Workspace 可以同时协调几个独立环境。
111
+ - `read` 可以直接把 PNG、JPEG、WebP 和 GIF 作为临时 MCP Media content 返回;外部 MCP 通过独立的 global/Project `mcp.json` 热加载到 `mcp.external` Capability,并支持人工 OAuth 认证,而不会自动打开它返回的路径或 URL。
111
112
 
112
113
  Lifecycle Hooks、Workspace Tasks、本地 Subagent、Activity/Audit、Checkpoint 和 Recovery 也已经包含在项目里,但第一次安装时不需要先学这些。需要哪个,再去 [Wiki](https://github.com/Akira-TL/forgerelay/wiki) 查哪个。
113
114
 
@@ -127,6 +128,7 @@ ForgeRelay 默认拒绝 elevated / administrator 启动。只有你显式选择
127
128
 
128
129
  - [快速开始](https://github.com/Akira-TL/forgerelay/wiki/Getting-Started)
129
130
  - [配置](https://github.com/Akira-TL/forgerelay/wiki/Configuration)
131
+ - [External MCP](https://github.com/Akira-TL/forgerelay/wiki/External-MCP)
130
132
  - [安全模型](https://github.com/Akira-TL/forgerelay/wiki/Security)
131
133
  - [故障排查](https://github.com/Akira-TL/forgerelay/wiki/Troubleshooting)
132
134
  - [完整 Wiki](https://github.com/Akira-TL/forgerelay/wiki)
@@ -220,6 +222,7 @@ Long commands do not require tight polling either. Once the current wait window
220
222
  - `AGENTS.md`, `CLAUDE.md`, and Agent Skills are loaded as needed instead of being resent in full on every open.
221
223
  - Managed worktrees provide real Git isolation when you ask for parallel work, with fast-forward-only finalization.
222
224
  - Workspace Relay runs work on another ForgeRelay instance; Composite Workspaces coordinate several independent environments from one Host.
225
+ - `read` can return PNG, JPEG, WebP, and GIF directly as transient MCP Media content; external MCP servers hot-reload from standalone global/Project `mcp.json` files into the `mcp.external` Capability, with explicit human OAuth when needed and no automatic dereferencing of returned paths or URLs.
223
226
 
224
227
  Lifecycle Hooks, Workspace Tasks, local Subagents, Activity/Audit, checkpoints, and recovery are included too. They are optional parts of the workflow; the [Wiki](https://github.com/Akira-TL/forgerelay/wiki) documents them when you need them.
225
228
 
@@ -239,6 +242,7 @@ See the [Security model](https://github.com/Akira-TL/forgerelay/wiki/Security) f
239
242
 
240
243
  - [Getting Started](https://github.com/Akira-TL/forgerelay/wiki/Getting-Started)
241
244
  - [Configuration](https://github.com/Akira-TL/forgerelay/wiki/Configuration)
245
+ - [External MCP](https://github.com/Akira-TL/forgerelay/wiki/External-MCP)
242
246
  - [Security model](https://github.com/Akira-TL/forgerelay/wiki/Security)
243
247
  - [Troubleshooting](https://github.com/Akira-TL/forgerelay/wiki/Troubleshooting)
244
248
  - [Full Wiki](https://github.com/Akira-TL/forgerelay/wiki)
@@ -0,0 +1,29 @@
1
+ # External MCP
2
+
3
+ Use `mcp.external` only for MCP servers the user has already configured in ForgeRelay. The Host remains the orchestrator: discover the configured server/tool surface, then explicitly choose the server, tool, and arguments for each call.
4
+
5
+ ## Operations
6
+
7
+ - `servers` — list configured server names and transport kinds. Connection details and credentials are not returned.
8
+ - `tools` — list the tools advertised by one configured server.
9
+ - `call` — invoke one tool that the selected configured server currently advertises.
10
+
11
+ ## Boundaries
12
+
13
+ - A capability call cannot supply a new MCP command, URL, credential, or connection target. Those belong to user configuration.
14
+ - ForgeRelay forwards the upstream MCP result. A path, URL, resource identifier, or textual file reference stays a reference; ForgeRelay does not automatically fetch it, call `read`, infer that it is an image, or create an Artifact.
15
+ - When an upstream result names an accessible Workspace file and you need its contents, make an explicit ForgeRelay `read` call.
16
+ - Direct upstream `ImageContent` for PNG, JPEG, WebP, and GIF is forwarded as transient Media content after MIME/base64 validation and the configured aggregate media budget. Image base64 exists only in the live MCP result; structured/Activity state keeps bounded MIME/byte metadata.
17
+ - Audio and arbitrary binary-resource forwarding are outside this Media contract. A path, URL, `resource_link`, or other non-media reference is not upgraded into an image automatically.
18
+ - ForgeRelay does not autonomously chain external MCP tools or retry through another configured server.
19
+ - External MCP processes/services keep the operating-system and network authority with which the user configured them. Routing through ForgeRelay does not make them a ForgeRelay filesystem sandbox.
20
+ - Hook policy may inspect or block Capability calls. Server/tool-specific transforms use the explicit `ExternalMcpBeforeForward` / `ExternalMcpAfterForward` contract; ordinary Hook stdout never rewrites MCP data.
21
+
22
+ ## Optional transform Hooks
23
+
24
+ Transform Hooks are opt-in user policy for one configured external MCP server/tool. Match them with `tool: "capability"`, `capability: "mcp.external"`, `externalServer`, and `externalTool`. ForgeRelay passes the current transform value over stdin as versioned JSON and accepts one structured stdout envelope only.
25
+
26
+ - Before forward: stdout must be `{"version":1,"request":{"arguments":{...}}}`. Only arguments can change; the configured server/tool target cannot.
27
+ - After forward: stdout must be `{"version":1,"result":{...}}`. The transformed result is revalidated as an MCP result and any ImageContent is rechecked against the normal MIME/base64/media-budget rules.
28
+ - A transform command may deliberately read a renderer-owned path or perform other work using the command's own OS authority. That is explicit Hook behavior, not an implicit ForgeRelay `read`, fetch, or Artifact operation.
29
+ - Activity/log state records only bounded transform identity/status metadata. Transform stdin/stdout, arbitrary upstream payloads, credentials, and image base64 are not persisted by ForgeRelay.
@@ -22,17 +22,27 @@
22
22
  - 可选 timeout;
23
23
  - 可选 `report`,默认 `true`。
24
24
 
25
- 当前事件包括:`WorkspaceOpen`、`BeforeTool`、`AfterTool`、`AfterToolFailure`、`AfterFileChange`、`BeforeWorktreeClose`、`AfterWorktreeClose`、`SubagentStart`、`SubagentStop`。
25
+ 当前事件包括:`WorkspaceOpen`、`BeforeTool`、`AfterTool`、`AfterToolFailure`、`ExternalMcpBeforeForward`、`ExternalMcpAfterForward`、`AfterFileChange`、`BeforeWorktreeClose`、`AfterWorktreeClose`、`SubagentStart`、`SubagentStop`。
26
+
27
+ External MCP transform 事件只用于 `mcp.external`。matcher 可额外使用 `capability`、`externalServer`、`externalTool`,从而绑定到已配置的 Capability/server/tool;这些字段只是现有目标的匹配条件,不能动态指定新的连接目标。
26
28
 
27
29
  ## 阻断与报告
28
30
 
29
- `BeforeTool` `BeforeWorktreeClose` 是阻断事件。命中的 Hook 失败或超时后,原操作不会继续。其他 after-event 只观察已经发生的结果,失败不会伪装成能够回滚先前副作用。
31
+ `BeforeTool`、`BeforeWorktreeClose` 以及两个 External MCP transform 事件都会在失败时令当前操作失败。Before-forward transform 失败时 upstream MCP call 不会发生;After-forward transform 失败时 upstream call 已经发生,只会阻止变换后结果继续交付并把 Capability 标成失败,不能声称回滚 upstream 已产生的副作用。其他普通 after-event 只观察已经发生的结果,失败也不会伪装成能够回滚先前副作用。
30
32
 
31
33
  Hook report 会随工具结果返回给 Host/Agent。`report: false` 只隐藏成功的高频报告;阻断失败始终可见。Agent 看到有意义的 Hook report 时必须告诉用户哪些 Hook 运行了、是否通过,以及操作是否被阻断;不能在 blocking Hook 阻止操作后声称原操作成功。
32
34
 
35
+ ## External MCP transform 协议
36
+
37
+ 普通 Hook 的 stdout 仍然只是命令输出,**不会**改写工具结果。只有 `ExternalMcpBeforeForward` / `ExternalMcpAfterForward` 使用显式 structured transform 协议:ForgeRelay 将 versioned JSON 写入 Hook stdin,并只接受 stdout 中一个合法 JSON envelope。
38
+
39
+ - request phase 输入包含当前 `server`、`tool` 与 `request.arguments`;输出必须是 `{"version":1,"request":{"arguments":{...}}}`。server/tool 不能被改写。
40
+ - result phase 输入包含当前 upstream MCP result;输出必须是 `{"version":1,"result":{...}}`。输出随后重新经过标准 MCP result、Media MIME/base64 与 `mediaMaxBytes` 校验。
41
+ - Hook 环境中的 `FORGERELAY_HOOK_PAYLOAD` 只包含 Capability/server/tool/phase 等有界匹配元数据;任意 arguments、upstream payload 和 image base64 只通过 transform stdin/stdout 瞬态传递。
42
+
33
43
  ## 安全边界
34
44
 
35
- 项目 Hook 属于项目执行约定,不需要额外审批,但不能扩大 allowed roots、覆盖认证边界或替换机器级全局规则。Hook command 与 shell 一样以运行 ForgeRelay 的本地用户权限执行;工作区文件边界不等于 OS sandbox。
45
+ 项目 Hook 属于项目执行约定,不需要额外审批,但不能扩大 allowed roots、覆盖认证边界或替换机器级全局规则。Hook command 与 shell 一样以运行 ForgeRelay 的本地用户权限执行;工作区文件边界不等于 OS sandbox。Transform Hook 如果主动读取文件或访问网络,那是该用户配置命令自身的 OS 权限,不代表 ForgeRelay 自动获得了新的 `read` 或网络权限。
36
46
 
37
47
  ## 检查入口
38
48
 
@@ -1,5 +1,6 @@
1
1
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
2
2
  import * as z from "zod/v4";
3
+ import { mcpHandlerRequestContext } from "../../mcp/request-context.js";
3
4
  import { logEvent, transportSessionIdPrefix } from "../../runtime/logging/logger.js";
4
5
  import { hostConversationScopeId } from "../../mcp/request-meta.js";
5
6
  import { ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY, ACTIVITY_PANEL_WORKSPACE_META_KEY, } from "../ui/contract.js";
@@ -68,11 +69,12 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
68
69
  idempotentHint: false,
69
70
  },
70
71
  }, async ({ workspaceId }, extra) => {
72
+ const requestContext = mcpHandlerRequestContext(extra);
71
73
  const workspace = workspacePanelState?.(workspaceId);
72
74
  if (!workspace) {
73
75
  throw new Error(`No Workspace presentation is available for ${workspaceId}. Call open_workspace for that workspace before activity_panel.`);
74
76
  }
75
- const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
77
+ const conversationScopeId = hostConversationScopeId(requestContext, connectionScopeId);
76
78
  const relayed = await relay?.panel(workspaceId, conversationScopeId);
77
79
  if (relayed) {
78
80
  return {
@@ -91,7 +93,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
91
93
  revision: snapshot.revision,
92
94
  state: snapshot.state,
93
95
  workspaceId,
94
- transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
96
+ transportSessionIdPrefix: transportSessionIdPrefix(requestContext.transportSessionId),
95
97
  });
96
98
  }
97
99
  return {
@@ -118,7 +120,8 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
118
120
  _meta: { ui: { visibility: ["app"] } },
119
121
  annotations: READ_ONLY_ANNOTATIONS,
120
122
  }, async ({ turnId, workspaceId, knownRevision }, extra) => {
121
- const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
123
+ const requestContext = mcpHandlerRequestContext(extra);
124
+ const conversationScopeId = hostConversationScopeId(requestContext, connectionScopeId);
122
125
  const relayed = await relay?.snapshot({ turnId, workspaceId, knownRevision }, conversationScopeId);
123
126
  if (relayed) {
124
127
  const workspace = workspaceId ? workspacePanelState?.(workspaceId) : undefined;
@@ -149,7 +152,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
149
152
  revision: snapshot.revision,
150
153
  changed: snapshot.changed,
151
154
  state: snapshot.state,
152
- transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
155
+ transportSessionIdPrefix: transportSessionIdPrefix(requestContext.transportSessionId),
153
156
  });
154
157
  }
155
158
  const workspace = workspaceId ? workspacePanelState?.(workspaceId) : undefined;
@@ -176,7 +179,8 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
176
179
  _meta: { ui: { visibility: ["app"] } },
177
180
  annotations: READ_ONLY_ANNOTATIONS,
178
181
  }, async ({ turnId, knownRevision }, extra) => {
179
- const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
182
+ const requestContext = mcpHandlerRequestContext(extra);
183
+ const conversationScopeId = hostConversationScopeId(requestContext, connectionScopeId);
180
184
  const relayed = await relay?.index(turnId, knownRevision, conversationScopeId);
181
185
  if (relayed)
182
186
  return relayed;
@@ -189,7 +193,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
189
193
  changed: index.changed,
190
194
  state: index.state,
191
195
  activities: index.activities.length,
192
- transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
196
+ transportSessionIdPrefix: transportSessionIdPrefix(requestContext.transportSessionId),
193
197
  });
194
198
  }
195
199
  return {
@@ -213,7 +217,8 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
213
217
  _meta: { ui: { visibility: ["app"] } },
214
218
  annotations: READ_ONLY_ANNOTATIONS,
215
219
  }, async ({ turnId, activityId }, extra) => {
216
- const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
220
+ const requestContext = mcpHandlerRequestContext(extra);
221
+ const conversationScopeId = hostConversationScopeId(requestContext, connectionScopeId);
217
222
  const relayed = await relay?.detail(turnId, activityId, conversationScopeId);
218
223
  if (relayed)
219
224
  return relayed;
@@ -225,7 +230,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
225
230
  tool: detail.activity.tool,
226
231
  kind: detail.activity.kind,
227
232
  state: detail.activity.state,
228
- transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
233
+ transportSessionIdPrefix: transportSessionIdPrefix(requestContext.transportSessionId),
229
234
  });
230
235
  }
231
236
  return {
@@ -258,7 +263,8 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
258
263
  _meta: { ui: { visibility: ["app"] } },
259
264
  annotations: READ_ONLY_ANNOTATIONS,
260
265
  }, async ({ turnId, outputId, cursor }, extra) => {
261
- const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
266
+ const requestContext = mcpHandlerRequestContext(extra);
267
+ const conversationScopeId = hostConversationScopeId(requestContext, connectionScopeId);
262
268
  const relayed = await relay?.output(turnId, outputId, conversationScopeId, cursor);
263
269
  if (relayed)
264
270
  return relayed;
@@ -273,7 +279,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
273
279
  processId: output.processId,
274
280
  status: output.status,
275
281
  outputBytes: Buffer.byteLength(output.output),
276
- transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
282
+ transportSessionIdPrefix: transportSessionIdPrefix(requestContext.transportSessionId),
277
283
  });
278
284
  }
279
285
  return {