@akira-tl/forgerelay 0.5.6 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +7 -3
  3. package/capabilities/host-integration/GUIDE.md +15 -9
  4. package/dist/activity/host-turn-store.js +9 -7
  5. package/dist/activity/lifecycle.js +1 -1
  6. package/dist/activity/mcp-query-tools.js +110 -18
  7. package/dist/activity/query-service.js +4 -4
  8. package/dist/activity/ui/contract.js +2 -0
  9. package/dist/cli.js +47 -32
  10. package/dist/config.js +31 -3
  11. package/dist/db/migrations.js +30 -0
  12. package/dist/db/schema.js +2 -0
  13. package/dist/logger.js +6 -5
  14. package/dist/lsp/test-support/server-fixture.js +1 -1
  15. package/dist/mcp/server-instructions.js +7 -6
  16. package/dist/mcp-app-template.js +108 -12
  17. package/dist/oauth/public-url.js +14 -0
  18. package/dist/oauth/router.js +49 -0
  19. package/dist/process-sessions.js +2 -1
  20. package/dist/request-meta.js +4 -0
  21. package/dist/server.js +250 -84
  22. package/dist/skills.js +58 -1
  23. package/dist/ui/.vite/manifest.json +360 -312
  24. package/dist/ui/activity-panel-app.html +17 -0
  25. package/dist/ui/assets/activity-panel-app-CjZVvVNc.js +2 -0
  26. package/dist/ui/assets/angular-html-BkgDQl-G.js +1 -0
  27. package/dist/ui/assets/{angular-ts-BwPE8YDt.js → angular-ts-Bxh4-O1-.js} +1 -1
  28. package/dist/ui/assets/{apl-CGbONVvU.js → apl-BmYHKQ6m.js} +1 -1
  29. package/dist/ui/assets/{astro-DIAPy7cj.js → astro-C51vqDKq.js} +1 -1
  30. package/dist/ui/assets/{blade-Cgtd19Ky.js → blade-CdhVsmwq.js} +1 -1
  31. package/dist/ui/assets/c-IBzBpxAO.js +1 -0
  32. package/dist/ui/assets/chunk-EyZ2wyi3.js +1 -0
  33. package/dist/ui/assets/{cobol-G-7HLVKW.js → cobol-Cf6azoN_.js} +1 -1
  34. package/dist/ui/assets/{coffee-DQFyKxEB.js → coffee-vVdGzlmt.js} +1 -1
  35. package/dist/ui/assets/cpp-CVHeO-Jk.js +1 -0
  36. package/dist/ui/assets/{crystal-DuiWhEO3.js → crystal-5UZy6KIX.js} +1 -1
  37. package/dist/ui/assets/css-DEfus9J5.js +1 -0
  38. package/dist/ui/assets/{edge-COkalhFi.js → edge-CESgo4ia.js} +1 -1
  39. package/dist/ui/assets/{elixir-cdlzbYR1.js → elixir-9FKfw4cJ.js} +1 -1
  40. package/dist/ui/assets/{elm-ZwDYuYpb.js → elm-Dv7A0aLa.js} +1 -1
  41. package/dist/ui/assets/{erb-C8WG9TtC.js → erb-DHFmDvM0.js} +1 -1
  42. package/dist/ui/assets/{git-rebase-CMlDVJBT.js → git-rebase-utbU6CzV.js} +1 -1
  43. package/dist/ui/assets/{glimmer-js-DeLOQlol.js → glimmer-js-t2kiCkyB.js} +1 -1
  44. package/dist/ui/assets/{glimmer-ts-DTEM04TK.js → glimmer-ts-F0Jv5okV.js} +1 -1
  45. package/dist/ui/assets/glsl-_ooCjaeB.js +1 -0
  46. package/dist/ui/assets/graphql-B9LqP-cY.js +1 -0
  47. package/dist/ui/assets/{hack-C14sh6Lh.js → hack-8yjdbaSA.js} +1 -1
  48. package/dist/ui/assets/haml-BU5EoZBn.js +1 -0
  49. package/dist/ui/assets/{handlebars-eS-J0O-M.js → handlebars-BBFEg4Lo.js} +1 -1
  50. package/dist/ui/assets/{heavy-payload-BKB0QDOI.js → heavy-payload-vGgBRvNX.js} +1 -1
  51. package/dist/ui/assets/html-D-C6YZk4.js +1 -0
  52. package/dist/ui/assets/{html-derivative-BlVZU2id.js → html-derivative-DJAQcZyc.js} +1 -1
  53. package/dist/ui/assets/{http-BmbZzA5x.js → http-Dg5LSYRK.js} +1 -1
  54. package/dist/ui/assets/{hurl-Dnd_YpOD.js → hurl-Y5uYzMZR.js} +1 -1
  55. package/dist/ui/assets/java-ppHvycj5.js +1 -0
  56. package/dist/ui/assets/javascript-DDTjKLlQ.js +1 -0
  57. package/dist/ui/assets/{jinja-DTvkZdHA.js → jinja-Bt30h8Ik.js} +1 -1
  58. package/dist/ui/assets/{jison-DUJG-tmh.js → jison-D5uZ1Nyo.js} +1 -1
  59. package/dist/ui/assets/json-Cw91r8h1.js +1 -0
  60. package/dist/ui/assets/jsx-04X2FT_1.js +1 -0
  61. package/dist/ui/assets/{julia-P5B4pHFa.js → julia-76hRaaGz.js} +1 -1
  62. package/dist/ui/assets/{just-DYaoh7C6.js → just-CDE7W0vW.js} +1 -1
  63. package/dist/ui/assets/{latex-CI5lnkWV.js → latex-CVO2Byab.js} +1 -1
  64. package/dist/ui/assets/{liquid-D3LnAs_z.js → liquid-CA-AS3EX.js} +1 -1
  65. package/dist/ui/assets/lua-CiyBp52Y.js +1 -0
  66. package/dist/ui/assets/{marko-CRDrjqfS.js → marko-_BWrPfny.js} +1 -1
  67. package/dist/ui/assets/{mdc-ZzwgmJVn.js → mdc-PyJv85eS.js} +1 -1
  68. package/dist/ui/assets/{nginx-BAvFXGSV.js → nginx-DAWVw8yu.js} +1 -1
  69. package/dist/ui/assets/{nim-BHIFawe9.js → nim-CKE1dzpU.js} +1 -1
  70. package/dist/ui/assets/{perl-C_f5AnLa.js → perl-BBhWva19.js} +1 -1
  71. package/dist/ui/assets/{php-DDqpegL5.js → php-ChIeaAoF.js} +1 -1
  72. package/dist/ui/assets/{pug-CSaAZXyd.js → pug-CMgI5afi.js} +1 -1
  73. package/dist/ui/assets/{qml-CNHvRs_z.js → qml-CjxTkcFH.js} +1 -1
  74. package/dist/ui/assets/r-DfFga4si.js +1 -0
  75. package/dist/ui/assets/{razor-BDfAPy7I.js → razor-i4DbHr8p.js} +1 -1
  76. package/dist/ui/assets/regexp-iC9_nbSI.js +1 -0
  77. package/dist/ui/assets/{review-payload-D7GIdYDj.js → review-payload-4erWKckt.js} +1 -1
  78. package/dist/ui/assets/{rst-BzPP2gle.js → rst-oy_97Kl4.js} +1 -1
  79. package/dist/ui/assets/{ruby-CQjzokzX.js → ruby-C5zYZIyl.js} +1 -1
  80. package/dist/ui/assets/{sas-Bp37KGr7.js → sas-BnMEh2S3.js} +1 -1
  81. package/dist/ui/assets/{scrollbar-HUMBdLLK.js → scrollbar-CaOPzUJd.js} +6 -6
  82. package/dist/ui/assets/scss-BfZXsAWQ.js +1 -0
  83. package/dist/ui/assets/shellscript-BC2srNaB.js +1 -0
  84. package/dist/ui/assets/{shellsession-CNc62UhY.js → shellsession-BpE5o7_3.js} +1 -1
  85. package/dist/ui/assets/{soy-CF44SpBo.js → soy-COesmhTY.js} +1 -1
  86. package/dist/ui/assets/sql-Ccv_yeUm.js +1 -0
  87. package/dist/ui/assets/{stata-DVeMA68W.js → stata-CGQEuSZq.js} +1 -1
  88. package/dist/ui/assets/{surrealql-52sF96Yv.js → surrealql-Bz4JbU-9.js} +1 -1
  89. package/dist/ui/assets/{svelte-ydwT7UbF.js → svelte-1jM2ejXj.js} +1 -1
  90. package/dist/ui/assets/{templ-BXcoVoGh.js → templ-C-VwELcp.js} +1 -1
  91. package/dist/ui/assets/{tex-COsYFp7O.js → tex-t1rUiQCm.js} +1 -1
  92. package/dist/ui/assets/{ts-tags-CF--LE1s.js → ts-tags-Bewoa6pS.js} +1 -1
  93. package/dist/ui/assets/tsx-CTXzrBkN.js +1 -0
  94. package/dist/ui/assets/{twig-YoM-DiI5.js → twig-DVg-vELl.js} +1 -1
  95. package/dist/ui/assets/typescript--ElHoFkw.js +1 -0
  96. package/dist/ui/assets/{vue-html-CPdGGk4T.js → vue-html-BHtJ8geX.js} +1 -1
  97. package/dist/ui/assets/{vue-vine-B5bjgk0a.js → vue-vine-CNSGAWwc.js} +1 -1
  98. package/dist/ui/assets/{vue-B5LDEy4l.js → vue-wPQ7UlMy.js} +1 -1
  99. package/dist/ui/assets/workspace-app-CcrHAUIn.css +1 -0
  100. package/dist/ui/assets/workspace-app-DJmkPYJC.js +1 -0
  101. package/dist/ui/assets/workspace-app-DkAiSl_0.js +111 -0
  102. package/dist/ui/assets/workspace-app-QyauBrJX.js +5 -0
  103. package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +1 -0
  104. package/dist/ui/assets/xml-JnmX6vyS.js +1 -0
  105. package/dist/ui/assets/{xsl-wls_CEK_.js → xsl-3_vJ27aE.js} +1 -1
  106. package/dist/ui/assets/yaml-OAcZkP-w.js +1 -0
  107. package/dist/ui/workspace-app.html +5 -2
  108. package/dist/ui/workspace-lifecycle-app.html +18 -0
  109. package/dist/workspaces.js +11 -0
  110. package/docs/configuration.md +36 -4
  111. package/docs/debugging.md +75 -17
  112. package/docs/gotchas.md +15 -9
  113. package/docs/setup.md +24 -10
  114. package/package.json +4 -3
  115. package/scripts/debug/accept.mjs +79 -27
  116. package/scripts/debug/host/run.sh +180 -0
  117. package/scripts/debug/host/serve.mjs +64 -0
  118. package/scripts/debug/runtime.mjs +57 -2
  119. package/scripts/debug/runtime.test.mjs +74 -0
  120. package/scripts/debug/serve.mjs +4 -5
  121. package/scripts/dev-server.mjs +32 -3
  122. package/scripts/release/release-gate.test.mjs +43 -0
  123. package/scripts/release/release-version.test.mjs +107 -0
  124. package/scripts/release-parity.mjs +70 -46
  125. package/scripts/release-proof.mjs +2 -2
  126. package/scripts/release-proof.test.mjs +18 -0
  127. package/scripts/release-version.mjs +17 -8
  128. package/dist/ui/assets/angular-html-BT2Z6jee.js +0 -1
  129. package/dist/ui/assets/c-acnvZ8tm.js +0 -1
  130. package/dist/ui/assets/cpp-UyrmHXan.js +0 -1
  131. package/dist/ui/assets/css-D_WCbY6m.js +0 -1
  132. package/dist/ui/assets/glsl-IJV4SB-M.js +0 -1
  133. package/dist/ui/assets/graphql-PM-WDnoB.js +0 -1
  134. package/dist/ui/assets/haml-B_5nS6Kx.js +0 -1
  135. package/dist/ui/assets/html-C3TPjrC5.js +0 -1
  136. package/dist/ui/assets/java-ne9HdIVn.js +0 -1
  137. package/dist/ui/assets/javascript-DpHT58rr.js +0 -1
  138. package/dist/ui/assets/json-DJmnBdxW.js +0 -1
  139. package/dist/ui/assets/jsx-BclJnZnN.js +0 -1
  140. package/dist/ui/assets/lua-dZC7ZQIY.js +0 -1
  141. package/dist/ui/assets/r-CAfoQND2.js +0 -1
  142. package/dist/ui/assets/regexp-Mug5jltQ.js +0 -1
  143. package/dist/ui/assets/scss-C_Z1DYnw.js +0 -1
  144. package/dist/ui/assets/shellscript-CLfJpn3I.js +0 -1
  145. package/dist/ui/assets/sql-CYnzfiQ1.js +0 -1
  146. package/dist/ui/assets/tsx-DLBt2-2f.js +0 -1
  147. package/dist/ui/assets/typescript-Ce9JGkNI.js +0 -1
  148. package/dist/ui/assets/workspace-app-Cm9EX5x4.js +0 -117
  149. package/dist/ui/assets/workspace-app-DHvE2ojW.css +0 -1
  150. package/dist/ui/assets/xml-Cun8xb0T.js +0 -1
  151. package/dist/ui/assets/yaml-BxuXn9OL.js +0 -1
  152. package/scripts/release/tag-local-ci.mjs +0 -144
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import { resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
9
- import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
9
+ import { getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
10
10
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
11
11
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
12
12
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
@@ -36,6 +36,8 @@ import { createOpenAIIncomingArtifactAdapter, IncomingArtifactAdapterRegistry, }
36
36
  import { logEvent, requestIp, requestPath, commandPreview, transportSessionIdPrefix, workspaceLogLabel, } from "./logger.js";
37
37
  import { editFileTool, readFileTool, writeFileTool, } from "./pi-tools.js";
38
38
  import { SingleUserOAuthProvider } from "./oauth-provider.js";
39
+ import { createForgeRelayAuthRouter } from "./oauth/router.js";
40
+ import { publicEndpointUrl } from "./oauth/public-url.js";
39
41
  import { BatchExecutor } from "./operations/batch/executor.js";
40
42
  import { executeBulkRead } from "./operations/bulk-read.js";
41
43
  import { NativeBulkMutationExecutor } from "./operations/native-bulk-mutations.js";
@@ -43,8 +45,8 @@ import { createCoreOperationExecutor, } from "./operations/core-operation-execut
43
45
  import { McpTransportRegistry, } from "./mcp-sessions.js";
44
46
  import { ProcessManager, resolveProcessId, } from "./process-sessions.js";
45
47
  import { createReviewCheckpointManager } from "./review-checkpoints.js";
46
- import { openAiConversationScopeId } from "./request-meta.js";
47
- import { readWorkspaceAppManifestEntry, resolveWorkspaceAppIdentity, WORKSPACE_APP_LEGACY_URI, WORKSPACE_APP_URI_TEMPLATE, } from "./mcp-app-template.js";
48
+ import { hostConversationScopeId, openAiConversationScopeId } from "./request-meta.js";
49
+ import { ACTIVITY_PANEL_APP_LEGACY_URI, ACTIVITY_PANEL_APP_URI_TEMPLATE, MCP_APP_RESOURCE_TEMPLATE_REVISION, readActivityPanelAppManifestEntry, readWorkspaceAppManifestEntry, readWorkspaceLifecycleAppManifestEntry, resolveActivityPanelAppIdentity, resolveWorkspaceAppIdentity, resolveWorkspaceLifecycleAppIdentity, WORKSPACE_APP_LEGACY_URI, WORKSPACE_APP_URI_TEMPLATE, WORKSPACE_LIFECYCLE_APP_LEGACY_URI, WORKSPACE_LIFECYCLE_APP_URI_TEMPLATE, } from "./mcp-app-template.js";
48
50
  import { shutdownHttpServer } from "./server-shutdown.js";
49
51
  import { formatPathForPrompt } from "./skills.js";
50
52
  import { createWorkspaceStore } from "./workspace-store.js";
@@ -77,26 +79,22 @@ const SHELL_TOOL_ANNOTATIONS = {
77
79
  openWorldHint: true,
78
80
  };
79
81
  function shouldAttachWidget(mode, kind) {
80
- switch (mode) {
81
- case "off":
82
- return false;
83
- case "changes":
84
- return kind === "workspace" || kind === "capability";
85
- case "full":
86
- return true;
87
- }
82
+ if (mode === "off")
83
+ return false;
84
+ return kind === "activity";
88
85
  }
89
86
  function toolWidgetDescriptorMeta(config, kind) {
90
87
  if (!shouldAttachWidget(config.widgets, kind))
91
88
  return { _meta: {} };
92
- const resourceUri = currentWorkspaceAppIdentity().uri;
89
+ const resourceUri = kind === "activity"
90
+ ? currentActivityPanelAppIdentity(config).uri
91
+ : currentWorkspaceLifecycleAppIdentity(config).uri;
93
92
  return {
94
93
  _meta: {
95
94
  ui: {
96
95
  resourceUri,
97
96
  visibility: ["model", "app"],
98
97
  },
99
- "openai/outputTemplate": resourceUri,
100
98
  },
101
99
  };
102
100
  }
@@ -152,8 +150,40 @@ function resultOutputSchema(extra = {}) {
152
150
  const workspaceSkillOutputSchema = z.object({
153
151
  name: z.string(),
154
152
  description: z.string(),
155
- path: z.string(),
156
153
  });
154
+ const workspaceSkillDiagnosticOutputSchema = z.object({
155
+ type: z.enum(["warning", "error", "collision"]),
156
+ message: z.string(),
157
+ collision: z.object({
158
+ resourceType: z.enum(["extension", "skill", "prompt", "theme"]),
159
+ name: z.string(),
160
+ }).optional(),
161
+ });
162
+ function redactSkillDiagnosticPaths(diagnostics) {
163
+ return diagnostics.map((diagnostic) => {
164
+ let message = diagnostic.message;
165
+ const hiddenPaths = [
166
+ diagnostic.path,
167
+ diagnostic.collision?.winnerPath,
168
+ diagnostic.collision?.loserPath,
169
+ ].filter((path) => Boolean(path));
170
+ for (const path of hiddenPaths) {
171
+ message = message.split(path).join("<skill-path>");
172
+ }
173
+ return {
174
+ type: diagnostic.type,
175
+ message,
176
+ ...(diagnostic.collision
177
+ ? {
178
+ collision: {
179
+ resourceType: diagnostic.collision.resourceType,
180
+ name: diagnostic.collision.name,
181
+ },
182
+ }
183
+ : {}),
184
+ };
185
+ });
186
+ }
157
187
  const capabilityFingerprintOutputSchema = z.object({
158
188
  version: z.string(),
159
189
  toolMode: z.enum(["minimal", "full", "codex"]),
@@ -270,6 +300,10 @@ function mcpRequestDebugFields(body) {
270
300
  const params = request.params && typeof request.params === "object" && !Array.isArray(request.params)
271
301
  ? request.params
272
302
  : undefined;
303
+ const rpcMeta = params?._meta && typeof params._meta === "object" && !Array.isArray(params._meta)
304
+ ? params._meta
305
+ : undefined;
306
+ const rpcMetaKeys = rpcMeta ? Object.keys(rpcMeta).sort() : [];
273
307
  let rpcTarget;
274
308
  if (rpcMethod === "resources/read" && typeof params?.uri === "string") {
275
309
  rpcTarget = params.uri;
@@ -277,7 +311,11 @@ function mcpRequestDebugFields(body) {
277
311
  else if (rpcMethod === "tools/call" && typeof params?.name === "string") {
278
312
  rpcTarget = params.name;
279
313
  }
280
- return { rpcMethod, rpcTarget };
314
+ return {
315
+ rpcMethod,
316
+ rpcTarget,
317
+ ...(rpcMetaKeys.length > 0 ? { rpcMetaKeys } : {}),
318
+ };
281
319
  }
282
320
  function logToolCall(config, fields) {
283
321
  if (!config.logging.toolCalls)
@@ -368,24 +406,64 @@ function uiManifestUrl() {
368
406
  function uiBuildDirectoryUrl() {
369
407
  return new URL("../dist/ui/", import.meta.url);
370
408
  }
371
- let cachedWorkspaceAppIdentity;
372
- function currentWorkspaceAppIdentity() {
373
- cachedWorkspaceAppIdentity ??= resolveWorkspaceAppIdentity({
409
+ const cachedWorkspaceAppIdentities = new Map();
410
+ const cachedWorkspaceLifecycleAppIdentities = new Map();
411
+ const cachedActivityPanelAppIdentities = new Map();
412
+ function appResourceContractRevision(config) {
413
+ return [
414
+ MCP_APP_RESOURCE_TEMPLATE_REVISION,
415
+ `publicBaseUrls=${JSON.stringify(config.publicBaseUrls)}`,
416
+ ].join("\0");
417
+ }
418
+ function appIdentityOptions(config) {
419
+ return {
374
420
  manifestUrl: uiManifestUrl(),
375
421
  buildDirectoryUrl: uiBuildDirectoryUrl(),
376
422
  fallbackRevision: FORGERELAY_VERSION,
377
- });
378
- return cachedWorkspaceAppIdentity;
423
+ resourceTemplateRevision: appResourceContractRevision(config),
424
+ };
425
+ }
426
+ function currentWorkspaceAppIdentity(config) {
427
+ const key = appResourceContractRevision(config);
428
+ let identity = cachedWorkspaceAppIdentities.get(key);
429
+ if (!identity) {
430
+ identity = resolveWorkspaceAppIdentity(appIdentityOptions(config));
431
+ cachedWorkspaceAppIdentities.set(key, identity);
432
+ }
433
+ return identity;
434
+ }
435
+ function currentWorkspaceLifecycleAppIdentity(config) {
436
+ const key = appResourceContractRevision(config);
437
+ let identity = cachedWorkspaceLifecycleAppIdentities.get(key);
438
+ if (!identity) {
439
+ identity = resolveWorkspaceLifecycleAppIdentity(appIdentityOptions(config));
440
+ cachedWorkspaceLifecycleAppIdentities.set(key, identity);
441
+ }
442
+ return identity;
443
+ }
444
+ function currentActivityPanelAppIdentity(config) {
445
+ const key = appResourceContractRevision(config);
446
+ let identity = cachedActivityPanelAppIdentities.get(key);
447
+ if (!identity) {
448
+ identity = resolveActivityPanelAppIdentity(appIdentityOptions(config));
449
+ cachedActivityPanelAppIdentities.set(key, identity);
450
+ }
451
+ return identity;
379
452
  }
380
453
  function getWorkspaceAppManifestEntry() {
381
454
  return readWorkspaceAppManifestEntry(uiManifestUrl());
382
455
  }
456
+ function getWorkspaceLifecycleAppManifestEntry() {
457
+ return readWorkspaceLifecycleAppManifestEntry(uiManifestUrl());
458
+ }
459
+ function getActivityPanelAppManifestEntry() {
460
+ return readActivityPanelAppManifestEntry(uiManifestUrl());
461
+ }
383
462
  function assetUrl(baseUrl, assetPath) {
384
463
  return `${baseUrl}/${assetPath.replace(/^\/+/, "")}`;
385
464
  }
386
- function workspaceAppHtml(config) {
465
+ function mcpAppHtml(config, entry, title, waitingMessage) {
387
466
  const baseUrl = assetBaseUrl(config);
388
- const entry = getWorkspaceAppManifestEntry();
389
467
  const stylesheets = (entry.css ?? [])
390
468
  .map((stylesheet) => ` <link rel="stylesheet" crossorigin href="${assetUrl(baseUrl, stylesheet)}" />`)
391
469
  .join("\n");
@@ -394,25 +472,33 @@ function workspaceAppHtml(config) {
394
472
  <head>
395
473
  <meta charset="UTF-8" />
396
474
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
397
- <title>ForgeRelay Workspace</title>
475
+ <title>${title}</title>
398
476
  <script type="module" crossorigin src="${assetUrl(baseUrl, entry.file)}"></script>
399
477
  ${stylesheets}
400
478
  </head>
401
479
  <body>
402
480
  <main id="app" class="shell">
403
- <section class="empty">Waiting for a tool result.</section>
481
+ <section class="empty">${waitingMessage}</section>
404
482
  </main>
405
483
  </body>
406
484
  </html>`;
407
485
  }
486
+ function workspaceAppHtml(config) {
487
+ return mcpAppHtml(config, getWorkspaceAppManifestEntry(), "ForgeRelay Workspace", "Waiting for a workspace result.");
488
+ }
489
+ function workspaceLifecycleAppHtml(config) {
490
+ return mcpAppHtml(config, getWorkspaceLifecycleAppManifestEntry(), "ForgeRelay Workspace Lifecycle", "Waiting for a workspace result.");
491
+ }
492
+ function activityPanelAppHtml(config) {
493
+ return mcpAppHtml(config, getActivityPanelAppManifestEntry(), "ForgeRelay Activity Panel", "Waiting for Activity Panel state.");
494
+ }
408
495
  function appDomain(config) {
409
496
  return new URL(config.publicBaseUrl).origin;
410
497
  }
411
498
  function appCsp(config) {
412
- const publicBaseUrl = config.publicBaseUrl.replace(/\/+$/, "");
413
499
  return {
414
- resourceDomains: [publicBaseUrl],
415
- connectDomains: [publicBaseUrl],
500
+ resourceDomains: [...config.publicBaseUrls],
501
+ connectDomains: [...config.publicBaseUrls],
416
502
  };
417
503
  }
418
504
  function uiBuildDirectory() {
@@ -424,59 +510,89 @@ function setAssetHeaders(res) {
424
510
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Range");
425
511
  res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
426
512
  }
427
- async function assertWorkspaceAppAssets() {
428
- const entry = getWorkspaceAppManifestEntry();
513
+ async function assertMcpAppAssets(entry) {
429
514
  const candidates = [entry.file, ...(entry.css ?? [])].map((assetPath) => new URL(`../dist/ui/${assetPath}`, import.meta.url));
430
- for (const candidate of candidates) {
515
+ for (const candidate of candidates)
431
516
  await access(candidate);
432
- }
433
517
  }
434
- function workspaceAppCompatibilityKind(requestedUri, currentUri) {
518
+ function appResourceVariant(requestedUri, currentUri, legacyUri) {
435
519
  if (requestedUri === currentUri)
436
520
  return "current";
437
- if (requestedUri === WORKSPACE_APP_LEGACY_URI)
521
+ if (requestedUri === legacyUri)
438
522
  return "legacy";
439
523
  return "historical";
440
524
  }
441
- async function readWorkspaceAppResource(config, requestedUri, transportSessionId) {
442
- const currentUri = currentWorkspaceAppIdentity().uri;
443
- const compatibility = workspaceAppCompatibilityKind(requestedUri, currentUri);
525
+ async function readMcpAppResource(config, options) {
526
+ const compatibility = appResourceVariant(options.requestedUri, options.currentUri, options.legacyUri);
444
527
  try {
445
- await assertWorkspaceAppAssets();
528
+ await assertMcpAppAssets(options.entry);
446
529
  const result = {
447
- contents: [
448
- {
449
- uri: requestedUri,
530
+ contents: [{
531
+ uri: options.requestedUri,
450
532
  mimeType: RESOURCE_MIME_TYPE,
451
- text: workspaceAppHtml(config),
533
+ text: options.html,
452
534
  _meta: {
453
535
  ui: {
454
536
  domain: appDomain(config),
455
537
  csp: appCsp(config),
456
538
  },
539
+ // MCP Apps defines resource metadata under `_meta.ui`. Inspector 2.3.0
540
+ // reads the content-item CSP/domain from `_meta` directly, so mirror
541
+ // these values until that installed-host compatibility gap is gone.
542
+ domain: appDomain(config),
543
+ csp: appCsp(config),
457
544
  },
458
- },
459
- ],
545
+ }],
460
546
  };
461
547
  logEvent(config.logging, "debug", "mcp_app_template_read", {
462
- requestedUri,
463
- currentUri,
548
+ requestedUri: options.requestedUri,
549
+ currentUri: options.currentUri,
464
550
  compatibility,
465
- transportSessionIdPrefix: transportSessionIdPrefix(transportSessionId),
551
+ transportSessionIdPrefix: transportSessionIdPrefix(options.transportSessionId),
466
552
  });
467
553
  return result;
468
554
  }
469
555
  catch (error) {
470
556
  logEvent(config.logging, "warn", "mcp_app_template_read_failed", {
471
- requestedUri,
472
- currentUri,
557
+ requestedUri: options.requestedUri,
558
+ currentUri: options.currentUri,
473
559
  compatibility,
474
560
  error: error instanceof Error ? error.message : String(error),
475
- transportSessionIdPrefix: transportSessionIdPrefix(transportSessionId),
561
+ transportSessionIdPrefix: transportSessionIdPrefix(options.transportSessionId),
476
562
  });
477
563
  throw error;
478
564
  }
479
565
  }
566
+ function readWorkspaceAppResource(config, requestedUri, transportSessionId, currentUri = currentWorkspaceAppIdentity(config).uri, legacyUri = WORKSPACE_APP_LEGACY_URI) {
567
+ return readMcpAppResource(config, {
568
+ requestedUri,
569
+ currentUri,
570
+ legacyUri,
571
+ entry: getWorkspaceAppManifestEntry(),
572
+ html: workspaceAppHtml(config),
573
+ transportSessionId,
574
+ });
575
+ }
576
+ function readWorkspaceLifecycleAppResource(config, requestedUri, transportSessionId) {
577
+ return readMcpAppResource(config, {
578
+ requestedUri,
579
+ currentUri: currentWorkspaceLifecycleAppIdentity(config).uri,
580
+ legacyUri: WORKSPACE_LIFECYCLE_APP_LEGACY_URI,
581
+ entry: getWorkspaceLifecycleAppManifestEntry(),
582
+ html: workspaceLifecycleAppHtml(config),
583
+ transportSessionId,
584
+ });
585
+ }
586
+ function readActivityPanelAppResource(config, requestedUri, transportSessionId) {
587
+ return readMcpAppResource(config, {
588
+ requestedUri,
589
+ currentUri: currentActivityPanelAppIdentity(config).uri,
590
+ legacyUri: ACTIVITY_PANEL_APP_LEGACY_URI,
591
+ entry: getActivityPanelAppManifestEntry(),
592
+ html: activityPanelAppHtml(config),
593
+ transportSessionId,
594
+ });
595
+ }
480
596
  const PROCESS_RESPONSE_OUTPUT_LINES = 10;
481
597
  function compactProcessOutput(output) {
482
598
  if (!output)
@@ -839,19 +955,19 @@ function activityRelationFor(context) {
839
955
  ...(context.turnId ? { turnId: context.turnId } : {}),
840
956
  };
841
957
  }
842
- function runActivityTool(lifecycle, workspace, requestMeta, tool, request, operation, outcome = standardActivityOutcome, relation = {}) {
958
+ function runActivityTool(lifecycle, workspace, conversationScopeId, tool, request, operation, outcome = standardActivityOutcome, relation = {}) {
843
959
  return lifecycle.run({
844
960
  tool,
845
961
  workspace: workspaceActivitySnapshot(workspace),
846
- conversationScopeId: openAiConversationScopeId(requestMeta),
962
+ conversationScopeId,
847
963
  request,
848
964
  operation,
849
965
  outcome,
850
966
  ...relation,
851
967
  });
852
968
  }
853
- function runActivityToolWithHooks(lifecycle, hooks, workspace, requestMeta, request, hookOptions, relation = {}) {
854
- return runActivityTool(lifecycle, workspace, requestMeta, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation);
969
+ function runActivityToolWithHooks(lifecycle, hooks, workspace, conversationScopeId, request, hookOptions, relation = {}) {
970
+ return runActivityTool(lifecycle, workspace, conversationScopeId, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation);
855
971
  }
856
972
  function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun) {
857
973
  if (config.toolMode === "codex") {
@@ -1007,6 +1123,8 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1007
1123
  });
1008
1124
  }
1009
1125
  export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries) {
1126
+ const connectionScopeId = `mcp-connection:${randomUUID()}`;
1127
+ const hostScopeIdFor = (requestMeta, transportSessionId) => hostConversationScopeId(requestMeta, transportSessionId, connectionScopeId);
1010
1128
  const toolDescriptions = buildToolDescriptions(config);
1011
1129
  const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)));
1012
1130
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
@@ -1108,7 +1226,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1108
1226
  read: async (input, context) => {
1109
1227
  const { workspaceId, ...readInput } = input;
1110
1228
  const workspace = workspaces.getWorkspace(workspaceId);
1111
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1229
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1112
1230
  signal: context.signal,
1113
1231
  tool: toolNames.read,
1114
1232
  invocation: workspaceHookInvocation(workspace),
@@ -1180,7 +1298,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1180
1298
  write: async (input, context) => {
1181
1299
  const { workspaceId, ...writeInput } = input;
1182
1300
  const workspace = workspaces.getWorkspace(workspaceId);
1183
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1301
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1184
1302
  signal: context.signal,
1185
1303
  tool: toolNames.write,
1186
1304
  invocation: workspaceHookInvocation(workspace),
@@ -1241,7 +1359,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1241
1359
  edit: async (input, context) => {
1242
1360
  const { workspaceId, ...editInput } = input;
1243
1361
  const workspace = workspaces.getWorkspace(workspaceId);
1244
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1362
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1245
1363
  signal: context.signal,
1246
1364
  tool: toolNames.edit,
1247
1365
  invocation: workspaceHookInvocation(workspace),
@@ -1303,7 +1421,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1303
1421
  rename: async (input, context) => {
1304
1422
  const { workspaceId, path, newPath } = input;
1305
1423
  const workspace = workspaces.getWorkspace(workspaceId);
1306
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1424
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1307
1425
  signal: context.signal,
1308
1426
  tool: toolNames.rename,
1309
1427
  invocation: workspaceHookInvocation(workspace),
@@ -1362,7 +1480,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1362
1480
  delete: async (input, context) => {
1363
1481
  const { workspaceId, path, recursive } = input;
1364
1482
  const workspace = workspaces.getWorkspace(workspaceId);
1365
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1483
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1366
1484
  signal: context.signal,
1367
1485
  tool: toolNames.delete,
1368
1486
  invocation: workspaceHookInvocation(workspace),
@@ -1446,7 +1564,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1446
1564
  maxOutputTokens,
1447
1565
  };
1448
1566
  let undeliveredProcessId;
1449
- const activityResult = await runActivityTool(activityLifecycle, workspace, context.requestMeta, surface, activityRequest, async (activityContext) => {
1567
+ const activityResult = await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), surface, activityRequest, async (activityContext) => {
1450
1568
  try {
1451
1569
  const result = await runToolWithHooks(hooks, {
1452
1570
  signal: context.signal,
@@ -1520,7 +1638,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1520
1638
  const { workspaceId, name, arguments: capabilityArguments, file } = input;
1521
1639
  const workspace = workspaces.getWorkspace(workspaceId);
1522
1640
  let changedPaths = [];
1523
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, { workspaceId, name, action: "run", arguments: capabilityArguments, file }, {
1641
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), { workspaceId, name, action: "run", arguments: capabilityArguments, file }, {
1524
1642
  signal: context.signal,
1525
1643
  tool: toolNames.capability,
1526
1644
  invocation: workspaceHookInvocation(workspace),
@@ -1616,9 +1734,18 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1616
1734
  }, {
1617
1735
  instructions: buildServerInstructions(config),
1618
1736
  });
1619
- const currentWorkspaceAppUri = currentWorkspaceAppIdentity().uri;
1737
+ const workspacePanelStates = new Map();
1738
+ const rememberWorkspacePanelState = (workspaceId, response) => {
1739
+ if (typeof response._meta !== "object" || response._meta === null)
1740
+ return;
1741
+ const meta = response._meta;
1742
+ const card = meta.card;
1743
+ if (meta.tool !== toolNames.openWorkspace || typeof card !== "object" || card === null)
1744
+ return;
1745
+ workspacePanelStates.set(workspaceId, card);
1746
+ };
1620
1747
  const workspaceAppResourceMetadata = {
1621
- description: "Interactive card for viewing ForgeRelay file diffs.",
1748
+ description: "Historical ForgeRelay tool card UI.",
1622
1749
  _meta: {
1623
1750
  ui: {
1624
1751
  domain: appDomain(config),
@@ -1626,12 +1753,23 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1626
1753
  },
1627
1754
  },
1628
1755
  };
1629
- registerAppResource(server, "ForgeRelay Diff Card", currentWorkspaceAppUri, workspaceAppResourceMetadata, async (uri, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
1630
- registerAppResource(server, "ForgeRelay Diff Card legacy", WORKSPACE_APP_LEGACY_URI, workspaceAppResourceMetadata, async (uri, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
1631
- server.registerResource("ForgeRelay Diff Card compatibility", new ResourceTemplate(WORKSPACE_APP_URI_TEMPLATE, { list: undefined }), {
1632
- ...workspaceAppResourceMetadata,
1633
- mimeType: RESOURCE_MIME_TYPE,
1634
- }, async (uri, _variables, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
1756
+ const workspaceLifecycleResourceMetadata = {
1757
+ description: "Historical ForgeRelay Workspace lifecycle UI compatibility resource.",
1758
+ _meta: workspaceAppResourceMetadata._meta,
1759
+ };
1760
+ const activityPanelResourceMetadata = {
1761
+ description: "ForgeRelay unified Workspace and Activity UI for one Host Turn.",
1762
+ _meta: workspaceAppResourceMetadata._meta,
1763
+ };
1764
+ const currentWorkspaceAppUri = currentWorkspaceAppIdentity(config).uri;
1765
+ registerAppResource(server, "ForgeRelay historical tool card", currentWorkspaceAppUri, workspaceAppResourceMetadata, async (uri, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
1766
+ registerAppResource(server, "ForgeRelay historical tool card legacy", WORKSPACE_APP_LEGACY_URI, workspaceAppResourceMetadata, async (uri, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
1767
+ server.registerResource("ForgeRelay historical tool card compatibility", new ResourceTemplate(WORKSPACE_APP_URI_TEMPLATE, { list: undefined }), { ...workspaceAppResourceMetadata, mimeType: RESOURCE_MIME_TYPE }, async (uri, _variables, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
1768
+ server.registerResource("ForgeRelay historical Workspace Lifecycle compatibility", new ResourceTemplate(WORKSPACE_LIFECYCLE_APP_URI_TEMPLATE, { list: undefined }), { ...workspaceLifecycleResourceMetadata, mimeType: RESOURCE_MIME_TYPE }, async (uri, _variables, extra) => readWorkspaceLifecycleAppResource(config, uri.toString(), extra.sessionId));
1769
+ const currentActivityPanelAppUri = currentActivityPanelAppIdentity(config).uri;
1770
+ registerAppResource(server, "ForgeRelay Activity Panel", currentActivityPanelAppUri, activityPanelResourceMetadata, async (uri, extra) => readActivityPanelAppResource(config, uri.toString(), extra.sessionId));
1771
+ registerAppResource(server, "ForgeRelay Activity Panel legacy", ACTIVITY_PANEL_APP_LEGACY_URI, activityPanelResourceMetadata, async (uri, extra) => readActivityPanelAppResource(config, uri.toString(), extra.sessionId));
1772
+ server.registerResource("ForgeRelay Activity Panel compatibility", new ResourceTemplate(ACTIVITY_PANEL_APP_URI_TEMPLATE, { list: undefined }), { ...activityPanelResourceMetadata, mimeType: RESOURCE_MIME_TYPE }, async (uri, _variables, extra) => readActivityPanelAppResource(config, uri.toString(), extra.sessionId));
1635
1773
  registerAppTool(server, "open_workspace", {
1636
1774
  title: "Open workspace",
1637
1775
  description: "Open or resume a local coding workspace. Reuse the returned workspaceId for later calls. Default to checkout; use mode=\"worktree\" only when the user explicitly requests isolated or parallel Git work. Every call returns lightweight workspace metadata; bootstrap context is delivered automatically only when needed and can be explicitly suppressed or refreshed.",
@@ -1745,13 +1883,13 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1745
1883
  skills: z.array(workspaceSkillOutputSchema).optional(),
1746
1884
  agentProviders: z.array(workspaceLocalAgentProviderOutputSchema).optional(),
1747
1885
  agents: z.array(workspaceLocalAgentOutputSchema).optional(),
1748
- skillDiagnostics: z.array(z.unknown()).optional(),
1886
+ skillDiagnostics: z.array(workspaceSkillDiagnosticOutputSchema).optional(),
1749
1887
  workspaces: z.array(workspaceInventoryEntryOutputSchema).optional(),
1750
1888
  summary: workspaceInventorySummaryOutputSchema.optional(),
1751
1889
  page: workspaceInventoryPageOutputSchema.optional(),
1752
1890
  instruction: z.string(),
1753
1891
  },
1754
- ...toolWidgetDescriptorMeta(config, "workspace"),
1892
+ _meta: {},
1755
1893
  annotations: {
1756
1894
  readOnlyHint: false,
1757
1895
  destructiveHint: false,
@@ -1831,7 +1969,6 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1831
1969
  .map((skill) => ({
1832
1970
  name: skill.name,
1833
1971
  description: skill.description,
1834
- path: formatPathForPrompt(skill.filePath),
1835
1972
  }));
1836
1973
  const capabilityGuides = workspace.capabilityGuides.map((guide) => ({
1837
1974
  name: guide.name,
@@ -1858,6 +1995,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1858
1995
  path: formatAgentsPath(file.path, workspace.root),
1859
1996
  }));
1860
1997
  const visibleSkills = includeBootstrapContext ? cardSkills : [];
1998
+ const visibleSkillDiagnostics = includeBootstrapContext
1999
+ ? redactSkillDiagnosticPaths(workspace.skillDiagnostics)
2000
+ : [];
1861
2001
  const visibleCapabilityGuides = includeBootstrapContext ? capabilityGuides : [];
1862
2002
  const visibleAgentProviders = includeBootstrapContext ? cardAgentProviders : [];
1863
2003
  const visibleAgents = includeBootstrapContext ? cardAgents : [];
@@ -1866,7 +2006,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1866
2006
  const workspaceContextInstruction = "For later open_workspace calls, context=\"auto\" avoids repeating unchanged bootstrap context; use context=\"none\" when only the workspace handle/metadata is needed, or context=\"full\" to force a refresh.";
1867
2007
  const workspaceManagementInstruction = "When you need to continue an earlier logical workspace or organize workspace state, use open_workspace(action=\"list\") to inspect candidates, then resume a selected workspaceId or ask the user before close_workspace cleanup.";
1868
2008
  const cardInstruction = config.skillsEnabled
1869
- ? `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches an available skill or capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`
2009
+ ? `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches an available skill, load it with read(path=\"skills://<name>\") before proceeding. When a task matches a capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`
1870
2010
  : `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches a capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`;
1871
2011
  const instruction = workspaceReused
1872
2012
  ? includeBootstrapContext
@@ -1940,7 +2080,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1940
2080
  success: true,
1941
2081
  durationMs: Math.round(performance.now() - startedAt),
1942
2082
  });
1943
- return hooks.decorateResult(workspace.id, attachHookReports({
2083
+ const response = hooks.decorateResult(workspace.id, attachHookReports({
1944
2084
  content: resultContent,
1945
2085
  _meta: {
1946
2086
  tool: "open_workspace",
@@ -1995,14 +2135,16 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1995
2135
  skills: visibleSkills,
1996
2136
  agentProviders: visibleAgentProviders,
1997
2137
  agents: visibleAgents,
1998
- skillDiagnostics: workspace.skillDiagnostics,
2138
+ skillDiagnostics: visibleSkillDiagnostics,
1999
2139
  }
2000
2140
  : {}),
2001
2141
  instruction,
2002
2142
  },
2003
2143
  }, hookReports));
2144
+ rememberWorkspacePanelState(workspace.id, response);
2145
+ return response;
2004
2146
  });
2005
- registerActivityQueryTools(server, activityQueries);
2147
+ registerActivityQueryTools(server, activityQueries, connectionScopeId, toolWidgetDescriptorMeta(config, "activity")._meta, config.activityPanelExpanded, config.logging, (workspaceId) => workspacePanelStates.get(workspaceId));
2006
2148
  registerAppTool(server, toolNames.capability, {
2007
2149
  title: "Use optional capability",
2008
2150
  description: "Describe or run one optional ForgeRelay capability advertised by open_workspace. Use describe when the capability contract is unfamiliar, then read its advertised guide if needed. Run dispatches only explicitly registered capabilities; it cannot invoke arbitrary shell commands, URLs, or methods.",
@@ -2096,7 +2238,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2096
2238
  });
2097
2239
  }
2098
2240
  const workspace = workspaces.getWorkspace(workspaceId);
2099
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, name, action, arguments: capabilityArguments, file }, {
2241
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), { workspaceId, name, action, arguments: capabilityArguments, file }, {
2100
2242
  signal: extra.signal,
2101
2243
  tool: toolNames.capability,
2102
2244
  invocation: workspaceHookInvocation(workspace),
@@ -2177,7 +2319,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2177
2319
  annotations: WRITE_TOOL_ANNOTATIONS,
2178
2320
  }, async ({ workspaceId, commitMessage }, extra) => {
2179
2321
  const workspace = workspaces.getWorkspace(workspaceId);
2180
- return runToolWithHooks(hooks, {
2322
+ const response = await runToolWithHooks(hooks, {
2181
2323
  signal: extra.signal,
2182
2324
  tool: toolNames.closeWorkspace,
2183
2325
  invocation: workspaceHookInvocation(workspace),
@@ -2225,6 +2367,21 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2225
2367
  });
2226
2368
  return attachHookReports({
2227
2369
  content: [textBlock(result)],
2370
+ _meta: {
2371
+ tool: toolNames.closeWorkspace,
2372
+ card: {
2373
+ workspaceId,
2374
+ mode: "worktree",
2375
+ sourceRoot: closed.sourceRoot,
2376
+ branch: closed.branch,
2377
+ targetBranch: closed.targetBranch,
2378
+ commitSha: closed.commitSha,
2379
+ mergedSha: closed.mergedSha,
2380
+ committed: closed.committed,
2381
+ cleanupWarning: closed.cleanupWarning,
2382
+ payload: { content: [textBlock(result)] },
2383
+ },
2384
+ },
2228
2385
  structuredContent: {
2229
2386
  result,
2230
2387
  workspaceId,
@@ -2250,10 +2407,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2250
2407
  const result = `Closed checkout-backed workspace ${workspaceId}. Physical project files were not removed.`;
2251
2408
  return {
2252
2409
  content: [textBlock(result)],
2410
+ _meta: {
2411
+ tool: toolNames.closeWorkspace,
2412
+ card: {
2413
+ workspaceId,
2414
+ mode: "checkout",
2415
+ payload: { content: [textBlock(result)] },
2416
+ },
2417
+ },
2253
2418
  structuredContent: { result, workspaceId, mode: "checkout" },
2254
2419
  };
2255
2420
  },
2256
2421
  });
2422
+ workspacePanelStates.delete(workspaceId);
2423
+ return response;
2257
2424
  });
2258
2425
  registerAppTool(server, toolNames.read, {
2259
2426
  title: "Read file",
@@ -2266,7 +2433,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2266
2433
  .string()
2267
2434
  .optional()
2268
2435
  .describe(config.skillsEnabled
2269
- ? "One file path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill or capability-guide path from open_workspace, including a ~/... home-relative path. Use exactly one of path or paths."
2436
+ ? "One file path to read, relative to the workspace root or absolute inside the OS temp directory. Load an available skill with skills://<name>; after loading it, read files in that skill with skills://<name>/<relative-path>. Advertised capability-guide paths from open_workspace are also readable. Use exactly one of path or paths."
2270
2437
  : "One file path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised capability-guide path from open_workspace. Use exactly one of path or paths."),
2271
2438
  paths: z
2272
2439
  .array(z.string())
@@ -2312,7 +2479,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2312
2479
  }
2313
2480
  const workspace = workspaces.getWorkspace(workspaceId);
2314
2481
  let response;
2315
- await runActivityTool(activityLifecycle, workspace, extra._meta, toolNames.read, { workspaceId, paths, offset, limit }, async (parentContext) => {
2482
+ await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(extra._meta, extra.sessionId), toolNames.read, { workspaceId, paths, offset, limit }, async (parentContext) => {
2316
2483
  const execution = await executeBulkRead({
2317
2484
  paths: paths,
2318
2485
  signal: extra.signal,
@@ -2536,7 +2703,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2536
2703
  annotations: EDIT_TOOL_ANNOTATIONS,
2537
2704
  }, async ({ workspaceId, patch }, extra) => {
2538
2705
  const workspace = workspaces.getWorkspace(workspaceId);
2539
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, patch }, {
2706
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), { workspaceId, patch }, {
2540
2707
  signal: extra.signal,
2541
2708
  tool: "apply_patch",
2542
2709
  invocation: workspaceHookInvocation(workspace),
@@ -2785,7 +2952,7 @@ export function createServer(config = loadConfig(), options = {}) {
2785
2952
  const transports = new McpTransportRegistry({
2786
2953
  maxTransports: MAX_MCP_TRANSPORT_SESSIONS,
2787
2954
  });
2788
- const mcpUrl = new URL("/mcp", config.publicBaseUrl);
2955
+ const mcpUrl = publicEndpointUrl(config.publicBaseUrl, "mcp");
2789
2956
  const resourceServerUrl = resourceUrlFromServerUrl(mcpUrl);
2790
2957
  const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl, config.stateDir);
2791
2958
  const bearerAuth = requireBearerAuth({
@@ -2800,7 +2967,7 @@ export function createServer(config = loadConfig(), options = {}) {
2800
2967
  const hostTurnStore = new HostTurnStore(config.stateDir);
2801
2968
  const activityQueries = new ActivityQueryService(hostTurnStore, activityAuditStore, bashOutputStore);
2802
2969
  const activityLifecycle = new ActivityLifecycle(activityAuditStore, {
2803
- turnIdForConversation: (conversationScopeId) => activityQueries.currentTurnId(conversationScopeId),
2970
+ turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
2804
2971
  });
2805
2972
  const reviewCheckpoints = createReviewCheckpointManager();
2806
2973
  const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
@@ -2900,10 +3067,9 @@ export function createServer(config = loadConfig(), options = {}) {
2900
3067
  });
2901
3068
  next();
2902
3069
  });
2903
- app.use(mcpAuthRouter({
3070
+ app.use(createForgeRelayAuthRouter({
2904
3071
  provider: oauthProvider,
2905
3072
  issuerUrl: new URL(config.publicBaseUrl),
2906
- baseUrl: new URL(config.publicBaseUrl),
2907
3073
  resourceServerUrl,
2908
3074
  scopesSupported: config.oauth.scopes,
2909
3075
  resourceName: "ForgeRelay",