@akira-tl/forgerelay 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/logger.js +56 -2
- package/dist/mcp-app-template.js +45 -0
- package/dist/roots.js +1 -1
- package/dist/server.js +107 -39
- package/docs/configuration.md +5 -2
- package/docs/debugging.md +35 -7
- package/docs/gotchas.md +22 -1
- package/docs/roadmap.md +26 -0
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +70 -2
- package/scripts/debug/runtime.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.5] - 2026-08-10
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Human-facing `pretty` logs are workspace-first: normal tool and hook lines no longer show transient MCP transport session IDs, MCP session create/close lifecycle events are debug-only, and project names receive stable per-project terminal colors while the logical `ws_...` identifier remains visible.
|
|
12
|
+
- MCP App tool metadata now uses a content-hashed UI resource URI derived from the built JavaScript/CSS, advertises both `model` and `app` visibility, and includes the ChatGPT `openai/outputTemplate` compatibility alias. Legacy `ui://forgerelay/workspace-app.html` and historical `workspace-app-*.html` pointers continue to resolve to the current template so stale ChatGPT metadata snapshots do not fail with a missing resource. Debug logs now distinguish current, legacy, and historical template reads, and real acceptance exercises current/compatibility resources plus the referenced JavaScript asset.
|
|
13
|
+
- `~/...` paths are expanded before allowed-root resolution, so advertised skills rendered with home-relative paths can be read directly instead of being interpreted as a literal `~` directory under the workspace.
|
|
14
|
+
- Successful MCP session shutdown and idle-cleanup lifecycle logs moved to `debug`; individual session close failures remain visible as warnings.
|
|
15
|
+
|
|
7
16
|
## [0.2.4] - 2026-08-09
|
|
8
17
|
|
|
9
18
|
### Added
|
package/dist/logger.js
CHANGED
|
@@ -13,6 +13,14 @@ const LEVEL_STYLE = {
|
|
|
13
13
|
info: "green",
|
|
14
14
|
debug: "gray",
|
|
15
15
|
};
|
|
16
|
+
const WORKSPACE_PROJECT_COLORS = [
|
|
17
|
+
"cyanBright",
|
|
18
|
+
"greenBright",
|
|
19
|
+
"yellowBright",
|
|
20
|
+
"magentaBright",
|
|
21
|
+
"blueBright",
|
|
22
|
+
"whiteBright",
|
|
23
|
+
];
|
|
16
24
|
export function shouldLog(config, level) {
|
|
17
25
|
return LEVEL_WEIGHT[config.level] >= LEVEL_WEIGHT[level];
|
|
18
26
|
}
|
|
@@ -70,11 +78,13 @@ export function formatPrettyLogEntry(entry, options = {}) {
|
|
|
70
78
|
const level = logLevel(entry.level);
|
|
71
79
|
const time = formatTimestamp(entry.ts);
|
|
72
80
|
const source = stringField(entry.workspace) ?? stringField(entry.workspaceId) ?? "forgerelay";
|
|
73
|
-
const session =
|
|
81
|
+
const session = level === "debug"
|
|
82
|
+
? stringField(entry.session) ?? stringField(entry.sessionIdPrefix)
|
|
83
|
+
: undefined;
|
|
74
84
|
const prefix = [
|
|
75
85
|
style("gray", time, options),
|
|
76
86
|
`[${style(LEVEL_STYLE[level], level.toUpperCase(), options)}]`,
|
|
77
|
-
|
|
87
|
+
formatPrettySource(source, options),
|
|
78
88
|
session ? style("gray", `session:${session}`, options) : undefined,
|
|
79
89
|
style("gray", "|", options),
|
|
80
90
|
].filter((value) => Boolean(value)).join(" ");
|
|
@@ -92,10 +102,18 @@ function formatPrettyMessage(entry, options) {
|
|
|
92
102
|
return formatHookMessage(entry, options);
|
|
93
103
|
case "http_request":
|
|
94
104
|
return formatHttpMessage(entry, options);
|
|
105
|
+
case "mcp_request":
|
|
106
|
+
return formatMcpRequestMessage(entry);
|
|
107
|
+
case "mcp_app_template_read":
|
|
108
|
+
return formatAppTemplateMessage(entry, options, false);
|
|
109
|
+
case "mcp_app_template_read_failed":
|
|
110
|
+
return formatAppTemplateMessage(entry, options, true);
|
|
95
111
|
case "mcp_session_created":
|
|
96
112
|
return `session ${stringField(entry.sessionIdPrefix) ?? "unknown"} created`;
|
|
97
113
|
case "mcp_session_closed":
|
|
98
114
|
return `session ${stringField(entry.sessionIdPrefix) ?? "unknown"} closed`;
|
|
115
|
+
case "mcp_sessions_closed":
|
|
116
|
+
return `${numberField(entry.count) ?? 0} sessions closed`;
|
|
99
117
|
case "mcp_session_close_failed":
|
|
100
118
|
return `session ${stringField(entry.sessionIdPrefix) ?? "unknown"} close -> ${style("red", "error", options)}`;
|
|
101
119
|
case "auth_denied":
|
|
@@ -161,6 +179,42 @@ function formatHttpMessage(entry, options) {
|
|
|
161
179
|
const statusStyle = status !== undefined && status >= 400 ? "red" : "green";
|
|
162
180
|
return `http ${method} ${path} -> ${style(statusStyle, statusText, options)}`;
|
|
163
181
|
}
|
|
182
|
+
function formatMcpRequestMessage(entry) {
|
|
183
|
+
const method = stringField(entry.rpcMethod) ?? stringField(entry.httpMethod) ?? "request";
|
|
184
|
+
const target = stringField(entry.rpcTarget);
|
|
185
|
+
return target ? `mcp ${method} ${target}` : `mcp ${method}`;
|
|
186
|
+
}
|
|
187
|
+
function formatAppTemplateMessage(entry, options, failed) {
|
|
188
|
+
const requestedUri = stringField(entry.requestedUri) ?? "unknown";
|
|
189
|
+
const compatibility = stringField(entry.compatibility) ?? "unknown";
|
|
190
|
+
if (failed) {
|
|
191
|
+
const error = stringField(entry.error);
|
|
192
|
+
return `app template ${compatibility} ${requestedUri} -> ${style("red", error ? `error: ${error}` : "error", options)}`;
|
|
193
|
+
}
|
|
194
|
+
const currentUri = stringField(entry.currentUri);
|
|
195
|
+
const target = compatibility === "current" || !currentUri
|
|
196
|
+
? requestedUri
|
|
197
|
+
: `${requestedUri} => ${currentUri}`;
|
|
198
|
+
return `app template ${compatibility} ${target} -> ${style("green", "ok", options)}`;
|
|
199
|
+
}
|
|
200
|
+
function formatPrettySource(source, options) {
|
|
201
|
+
const separator = source.lastIndexOf("/");
|
|
202
|
+
if (separator <= 0 || separator === source.length - 1) {
|
|
203
|
+
return style(["cyan", "underline"], source, options);
|
|
204
|
+
}
|
|
205
|
+
const project = source.slice(0, separator);
|
|
206
|
+
const workspace = source.slice(separator + 1);
|
|
207
|
+
const projectColor = WORKSPACE_PROJECT_COLORS[stableColorIndex(project)];
|
|
208
|
+
return `${style([projectColor, "bold"], project, options)}/${style(["cyan", "underline"], workspace, options)}`;
|
|
209
|
+
}
|
|
210
|
+
function stableColorIndex(value) {
|
|
211
|
+
let hash = 2166136261;
|
|
212
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
213
|
+
hash ^= value.charCodeAt(index);
|
|
214
|
+
hash = Math.imul(hash, 16777619);
|
|
215
|
+
}
|
|
216
|
+
return (hash >>> 0) % WORKSPACE_PROJECT_COLORS.length;
|
|
217
|
+
}
|
|
164
218
|
function formatGenericMessage(entry) {
|
|
165
219
|
const event = String(entry.event ?? "log");
|
|
166
220
|
const detail = [entry.reason, entry.error]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
export const WORKSPACE_APP_MANIFEST_ENTRY = "workspace-app.html";
|
|
4
|
+
export const WORKSPACE_APP_LEGACY_URI = "ui://forgerelay/workspace-app.html";
|
|
5
|
+
export const WORKSPACE_APP_URI_TEMPLATE = "ui://forgerelay/workspace-app-{revision}.html";
|
|
6
|
+
export function workspaceAppUriForRevision(revision) {
|
|
7
|
+
return `ui://forgerelay/workspace-app-${encodeURIComponent(revision)}.html`;
|
|
8
|
+
}
|
|
9
|
+
export function readWorkspaceAppManifestEntry(manifestUrl) {
|
|
10
|
+
const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
|
|
11
|
+
const entry = manifest[WORKSPACE_APP_MANIFEST_ENTRY];
|
|
12
|
+
if (!entry?.file) {
|
|
13
|
+
throw new Error(`Missing ${WORKSPACE_APP_MANIFEST_ENTRY} in UI manifest.`);
|
|
14
|
+
}
|
|
15
|
+
return entry;
|
|
16
|
+
}
|
|
17
|
+
export function workspaceAppBundleRevision(entry, buildDirectoryUrl) {
|
|
18
|
+
const hash = createHash("sha256");
|
|
19
|
+
const assetPaths = [entry.file, ...(entry.css ?? [])];
|
|
20
|
+
for (const assetPath of assetPaths) {
|
|
21
|
+
hash.update(assetPath);
|
|
22
|
+
hash.update("\0");
|
|
23
|
+
hash.update(readFileSync(new URL(assetPath, buildDirectoryUrl)));
|
|
24
|
+
hash.update("\0");
|
|
25
|
+
}
|
|
26
|
+
return hash.digest("hex").slice(0, 12);
|
|
27
|
+
}
|
|
28
|
+
export function resolveWorkspaceAppIdentity(options) {
|
|
29
|
+
try {
|
|
30
|
+
const entry = readWorkspaceAppManifestEntry(options.manifestUrl);
|
|
31
|
+
const revision = workspaceAppBundleRevision(entry, options.buildDirectoryUrl);
|
|
32
|
+
return {
|
|
33
|
+
revision,
|
|
34
|
+
uri: workspaceAppUriForRevision(revision),
|
|
35
|
+
source: "bundle",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return {
|
|
40
|
+
revision: options.fallbackRevision,
|
|
41
|
+
uri: workspaceAppUriForRevision(options.fallbackRevision),
|
|
42
|
+
source: "fallback",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/roots.js
CHANGED
|
@@ -33,7 +33,7 @@ export function assertAllowedPath(path, allowedRoots) {
|
|
|
33
33
|
throw new AccessDeniedError(`Path is outside allowed roots: ${path}`);
|
|
34
34
|
}
|
|
35
35
|
export function resolveAllowedPath(inputPath, cwd, allowedRoots) {
|
|
36
|
-
const absolutePath = resolve(cwd, inputPath);
|
|
36
|
+
const absolutePath = resolve(cwd, expandHomePath(inputPath));
|
|
37
37
|
return assertAllowedPath(absolutePath, allowedRoots);
|
|
38
38
|
}
|
|
39
39
|
export async function resolveCanonicalAllowedPath(inputPath, cwd, allowedRoots) {
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { access, realpath } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
8
8
|
import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
9
9
|
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
@@ -27,6 +27,7 @@ import { McpSessionRegistry, } from "./mcp-sessions.js";
|
|
|
27
27
|
import { ProcessSessionManager, } from "./process-sessions.js";
|
|
28
28
|
import { createReviewCheckpointManager } from "./review-checkpoints.js";
|
|
29
29
|
import { openAiConversationScopeId } from "./request-meta.js";
|
|
30
|
+
import { readWorkspaceAppManifestEntry, resolveWorkspaceAppIdentity, WORKSPACE_APP_LEGACY_URI, WORKSPACE_APP_URI_TEMPLATE, } from "./mcp-app-template.js";
|
|
30
31
|
import { shutdownHttpServer } from "./server-shutdown.js";
|
|
31
32
|
import { formatPathForPrompt } from "./skills.js";
|
|
32
33
|
import { createWorkspaceStore } from "./workspace-store.js";
|
|
@@ -38,8 +39,6 @@ import { formatLocalAgentProviderAvailabilitySummary, getLocalAgentProviderAvail
|
|
|
38
39
|
const MCP_SESSION_IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1_000;
|
|
39
40
|
const FORGERELAY_VERSION = readForgeRelayVersion();
|
|
40
41
|
const MCP_SESSION_CLEANUP_INTERVAL_MS = 5 * 60 * 1_000;
|
|
41
|
-
const WORKSPACE_APP_URI = "ui://forgerelay/workspace-app.html";
|
|
42
|
-
const WORKSPACE_APP_MANIFEST_ENTRY = "workspace-app.html";
|
|
43
42
|
const WRITE_TOOL_ANNOTATIONS = {
|
|
44
43
|
readOnlyHint: false,
|
|
45
44
|
destructiveHint: true,
|
|
@@ -71,20 +70,21 @@ function shouldAttachWidget(mode, kind) {
|
|
|
71
70
|
function toolWidgetDescriptorMeta(config, kind) {
|
|
72
71
|
if (!shouldAttachWidget(config.widgets, kind))
|
|
73
72
|
return { _meta: {} };
|
|
73
|
+
const resourceUri = currentWorkspaceAppIdentity().uri;
|
|
74
74
|
return {
|
|
75
75
|
_meta: {
|
|
76
76
|
ui: {
|
|
77
|
-
resourceUri
|
|
78
|
-
visibility: ["model"],
|
|
77
|
+
resourceUri,
|
|
78
|
+
visibility: ["model", "app"],
|
|
79
79
|
},
|
|
80
|
+
"openai/outputTemplate": resourceUri,
|
|
80
81
|
},
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
|
-
function workspaceLogContext(workspace,
|
|
84
|
+
function workspaceLogContext(workspace, _sessionId) {
|
|
84
85
|
return {
|
|
85
86
|
workspaceId: workspace.id,
|
|
86
87
|
workspace: workspaceLogLabel(workspace.root, workspace.id),
|
|
87
|
-
session: sessionIdPrefix(sessionId),
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
function formatVisibleAgent(agent) {
|
|
@@ -161,6 +161,23 @@ function requestLogFields(req, config) {
|
|
|
161
161
|
contentLength: req.header("content-length"),
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
|
+
function mcpRequestDebugFields(body) {
|
|
165
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
166
|
+
return {};
|
|
167
|
+
const request = body;
|
|
168
|
+
const rpcMethod = typeof request.method === "string" ? request.method : undefined;
|
|
169
|
+
const params = request.params && typeof request.params === "object" && !Array.isArray(request.params)
|
|
170
|
+
? request.params
|
|
171
|
+
: undefined;
|
|
172
|
+
let rpcTarget;
|
|
173
|
+
if (rpcMethod === "resources/read" && typeof params?.uri === "string") {
|
|
174
|
+
rpcTarget = params.uri;
|
|
175
|
+
}
|
|
176
|
+
else if (rpcMethod === "tools/call" && typeof params?.name === "string") {
|
|
177
|
+
rpcTarget = params.name;
|
|
178
|
+
}
|
|
179
|
+
return { rpcMethod, rpcTarget };
|
|
180
|
+
}
|
|
164
181
|
function logToolCall(config, fields) {
|
|
165
182
|
if (!config.logging.toolCalls)
|
|
166
183
|
return;
|
|
@@ -247,16 +264,20 @@ function assetBaseUrl(config) {
|
|
|
247
264
|
function uiManifestUrl() {
|
|
248
265
|
return new URL("../dist/ui/.vite/manifest.json", import.meta.url);
|
|
249
266
|
}
|
|
250
|
-
function
|
|
251
|
-
return
|
|
267
|
+
function uiBuildDirectoryUrl() {
|
|
268
|
+
return new URL("../dist/ui/", import.meta.url);
|
|
269
|
+
}
|
|
270
|
+
let cachedWorkspaceAppIdentity;
|
|
271
|
+
function currentWorkspaceAppIdentity() {
|
|
272
|
+
cachedWorkspaceAppIdentity ??= resolveWorkspaceAppIdentity({
|
|
273
|
+
manifestUrl: uiManifestUrl(),
|
|
274
|
+
buildDirectoryUrl: uiBuildDirectoryUrl(),
|
|
275
|
+
fallbackRevision: FORGERELAY_VERSION,
|
|
276
|
+
});
|
|
277
|
+
return cachedWorkspaceAppIdentity;
|
|
252
278
|
}
|
|
253
279
|
function getWorkspaceAppManifestEntry() {
|
|
254
|
-
|
|
255
|
-
const entry = manifest[WORKSPACE_APP_MANIFEST_ENTRY];
|
|
256
|
-
if (!entry?.file) {
|
|
257
|
-
throw new Error(`Missing ${WORKSPACE_APP_MANIFEST_ENTRY} in UI manifest.`);
|
|
258
|
-
}
|
|
259
|
-
return entry;
|
|
280
|
+
return readWorkspaceAppManifestEntry(uiManifestUrl());
|
|
260
281
|
}
|
|
261
282
|
function assetUrl(baseUrl, assetPath) {
|
|
262
283
|
return `${baseUrl}/${assetPath.replace(/^\/+/, "")}`;
|
|
@@ -306,6 +327,51 @@ async function assertWorkspaceAppAssets() {
|
|
|
306
327
|
await access(candidate);
|
|
307
328
|
}
|
|
308
329
|
}
|
|
330
|
+
function workspaceAppCompatibilityKind(requestedUri, currentUri) {
|
|
331
|
+
if (requestedUri === currentUri)
|
|
332
|
+
return "current";
|
|
333
|
+
if (requestedUri === WORKSPACE_APP_LEGACY_URI)
|
|
334
|
+
return "legacy";
|
|
335
|
+
return "historical";
|
|
336
|
+
}
|
|
337
|
+
async function readWorkspaceAppResource(config, requestedUri, transportSessionId) {
|
|
338
|
+
const currentUri = currentWorkspaceAppIdentity().uri;
|
|
339
|
+
const compatibility = workspaceAppCompatibilityKind(requestedUri, currentUri);
|
|
340
|
+
try {
|
|
341
|
+
await assertWorkspaceAppAssets();
|
|
342
|
+
const result = {
|
|
343
|
+
contents: [
|
|
344
|
+
{
|
|
345
|
+
uri: requestedUri,
|
|
346
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
347
|
+
text: workspaceAppHtml(config),
|
|
348
|
+
_meta: {
|
|
349
|
+
ui: {
|
|
350
|
+
csp: appCsp(config),
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
logEvent(config.logging, "debug", "mcp_app_template_read", {
|
|
357
|
+
requestedUri,
|
|
358
|
+
currentUri,
|
|
359
|
+
compatibility,
|
|
360
|
+
sessionIdPrefix: sessionIdPrefix(transportSessionId),
|
|
361
|
+
});
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
logEvent(config.logging, "warn", "mcp_app_template_read_failed", {
|
|
366
|
+
requestedUri,
|
|
367
|
+
currentUri,
|
|
368
|
+
compatibility,
|
|
369
|
+
error: error instanceof Error ? error.message : String(error),
|
|
370
|
+
sessionIdPrefix: sessionIdPrefix(transportSessionId),
|
|
371
|
+
});
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
309
375
|
function processResult(snapshot) {
|
|
310
376
|
const status = snapshot.running
|
|
311
377
|
? `Process running with session ID ${snapshot.sessionId}.`
|
|
@@ -568,30 +634,21 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
568
634
|
artifactDownloadSupported: isArtifactDownloadSupportedPlatform(),
|
|
569
635
|
}),
|
|
570
636
|
});
|
|
571
|
-
|
|
637
|
+
const currentWorkspaceAppUri = currentWorkspaceAppIdentity().uri;
|
|
638
|
+
const workspaceAppResourceMetadata = {
|
|
572
639
|
description: "Interactive card for viewing ForgeRelay file diffs.",
|
|
573
640
|
_meta: {
|
|
574
641
|
ui: {
|
|
575
642
|
csp: appCsp(config),
|
|
576
643
|
},
|
|
577
644
|
},
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
text: workspaceAppHtml(config),
|
|
586
|
-
_meta: {
|
|
587
|
-
ui: {
|
|
588
|
-
csp: appCsp(config),
|
|
589
|
-
},
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
],
|
|
593
|
-
};
|
|
594
|
-
});
|
|
645
|
+
};
|
|
646
|
+
registerAppResource(server, "ForgeRelay Diff Card", currentWorkspaceAppUri, workspaceAppResourceMetadata, async (uri, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
|
|
647
|
+
registerAppResource(server, "ForgeRelay Diff Card legacy", WORKSPACE_APP_LEGACY_URI, workspaceAppResourceMetadata, async (uri, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
|
|
648
|
+
server.registerResource("ForgeRelay Diff Card compatibility", new ResourceTemplate(WORKSPACE_APP_URI_TEMPLATE, { list: undefined }), {
|
|
649
|
+
...workspaceAppResourceMetadata,
|
|
650
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
651
|
+
}, async (uri, _variables, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
|
|
595
652
|
registerAppTool(server, "open_workspace", {
|
|
596
653
|
title: "Open workspace",
|
|
597
654
|
description: "Open or resume a local coding workspace. A conversation keeps a stable workspaceId for a project, while different conversations normally receive different logical workspaceIds that may point at the same physical checkout or worktree. Pass workspaceId to explicitly resume an existing logical workspace in this conversation. Default to checkout mode and only use mode=\"worktree\" when the user explicitly asks for isolated or parallel Git work. Workspaces idle for more than two days are reported for user-directed cleanup or resumption.",
|
|
@@ -944,7 +1001,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
944
1001
|
path: z
|
|
945
1002
|
.string()
|
|
946
1003
|
.describe(config.skillsEnabled
|
|
947
|
-
? "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill path from open_workspace skills."
|
|
1004
|
+
? "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill path from open_workspace skills, including a ~/... home-relative path."
|
|
948
1005
|
: "File path to read, relative to the workspace root or absolute inside the OS temp directory."),
|
|
949
1006
|
offset: z
|
|
950
1007
|
.number()
|
|
@@ -1746,6 +1803,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
1746
1803
|
? getLocalAgentProviderAvailabilitySnapshot()
|
|
1747
1804
|
: [];
|
|
1748
1805
|
const logSessionCloseResults = (reason, results) => {
|
|
1806
|
+
let closedCount = 0;
|
|
1749
1807
|
for (const result of results) {
|
|
1750
1808
|
if (result.error) {
|
|
1751
1809
|
logEvent(config.logging, "warn", "mcp_session_close_failed", {
|
|
@@ -1757,9 +1815,18 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
1757
1815
|
});
|
|
1758
1816
|
continue;
|
|
1759
1817
|
}
|
|
1760
|
-
|
|
1818
|
+
closedCount += 1;
|
|
1819
|
+
if (reason === "idle_timeout") {
|
|
1820
|
+
logEvent(config.logging, "debug", "mcp_session_closed", {
|
|
1821
|
+
reason,
|
|
1822
|
+
sessionIdPrefix: sessionIdPrefix(result.sessionId),
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (reason === "server_shutdown" && closedCount > 0) {
|
|
1827
|
+
logEvent(config.logging, "debug", "mcp_sessions_closed", {
|
|
1761
1828
|
reason,
|
|
1762
|
-
|
|
1829
|
+
count: closedCount,
|
|
1763
1830
|
});
|
|
1764
1831
|
}
|
|
1765
1832
|
};
|
|
@@ -1841,10 +1908,11 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
1841
1908
|
}
|
|
1842
1909
|
logEvent(config.logging, "debug", "mcp_request", {
|
|
1843
1910
|
requestId,
|
|
1844
|
-
|
|
1911
|
+
httpMethod: req.method,
|
|
1845
1912
|
sessionIdPresent: Boolean(sessionId),
|
|
1846
1913
|
sessionIdPrefix: sessionIdPrefix(sessionId),
|
|
1847
1914
|
isInitialize: initializeRequest,
|
|
1915
|
+
...mcpRequestDebugFields(req.body),
|
|
1848
1916
|
});
|
|
1849
1917
|
try {
|
|
1850
1918
|
let transport;
|
|
@@ -1861,7 +1929,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
1861
1929
|
onsessioninitialized: (newSessionId) => {
|
|
1862
1930
|
if (transport)
|
|
1863
1931
|
transports.register(newSessionId, transport);
|
|
1864
|
-
logEvent(config.logging, "
|
|
1932
|
+
logEvent(config.logging, "debug", "mcp_session_created", {
|
|
1865
1933
|
requestId,
|
|
1866
1934
|
sessionIdPrefix: sessionIdPrefix(newSessionId),
|
|
1867
1935
|
...requestLogFields(req, config),
|
|
@@ -1871,7 +1939,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
1871
1939
|
transport.onclose = () => {
|
|
1872
1940
|
const closedSessionId = transport?.sessionId;
|
|
1873
1941
|
if (closedSessionId && transports.remove(closedSessionId)) {
|
|
1874
|
-
logEvent(config.logging, "
|
|
1942
|
+
logEvent(config.logging, "debug", "mcp_session_closed", {
|
|
1875
1943
|
reason: "transport_close",
|
|
1876
1944
|
sessionIdPrefix: sessionIdPrefix(closedSessionId),
|
|
1877
1945
|
});
|
package/docs/configuration.md
CHANGED
|
@@ -336,8 +336,11 @@ forgerelay agents show <id>
|
|
|
336
336
|
| `FORGERELAY_TRUST_PROXY` | `0` |
|
|
337
337
|
|
|
338
338
|
`pretty` is the human-facing local console format. It uses terminal-aware color,
|
|
339
|
-
short timestamps, workspace
|
|
340
|
-
keeping HTTP request records off by default.
|
|
339
|
+
short timestamps, workspace-first context, and compact operation results while
|
|
340
|
+
keeping HTTP request records off by default. Project names receive stable
|
|
341
|
+
per-project colors and logical `ws_...` identifiers remain visible; transient MCP
|
|
342
|
+
transport session IDs and normal session lifecycle events are shown only at
|
|
343
|
+
`debug` level. Shell command previews are enabled
|
|
341
344
|
in this mode and truncated to 120 characters; set
|
|
342
345
|
`FORGERELAY_LOG_SHELL_COMMANDS=0` when command arguments may contain secrets.
|
|
343
346
|
|
package/docs/debugging.md
CHANGED
|
@@ -59,17 +59,45 @@ The acceptance checks:
|
|
|
59
59
|
3. unauthenticated `/mcp` rejection;
|
|
60
60
|
4. dynamic OAuth client registration, PKCE Owner-password approval, and access-token exchange;
|
|
61
61
|
5. MCP `initialize`, including package/server version consistency and the shell mutation safety contract;
|
|
62
|
-
6. `tools/list` for the full debug tool surface, including `close_workspace`, `write_stdin`, the non-blanket `bash` mutation policy, no kill-timeout input, the 300-second foreground-wait contract,
|
|
63
|
-
7.
|
|
64
|
-
8.
|
|
65
|
-
9.
|
|
66
|
-
10.
|
|
67
|
-
11.
|
|
68
|
-
12.
|
|
62
|
+
6. `tools/list` for the full debug tool surface, including `close_workspace`, `write_stdin`, the non-blanket `bash` mutation policy, no kill-timeout input, the 300-second foreground-wait contract, workspace resume/stale-session schema, and MCP App tool metadata;
|
|
63
|
+
7. the full MCP App template chain: `resources/list`, `resources/templates/list`, current content-hashed `resources/read`, legacy/historical template compatibility reads, `text/html;profile=mcp-app`, CSP resource domains, and an HTTP fetch of the JavaScript asset referenced by the template;
|
|
64
|
+
8. a real checkout workspace with `write`, `read`, `rename`, `delete`, foreground `bash` through `ProcessSessionManager`, and a deliberate failed `edit`;
|
|
65
|
+
9. OS temp-directory `write` → `read` → `edit` → `rename` → `delete` over the same real MCP session, plus rejection of an arbitrary path outside the workspace/temp roots;
|
|
66
|
+
10. a temporary Git repository with managed worktree creation, file modification, and `close_worktree`;
|
|
67
|
+
11. 本地 bare remote 上的 release-tag-push Hook:成功 Hook 必须先运行再允许 `v0.2.0` push,失败 Hook 必须在 remote mutation 前阻断 `v0.2.1`;
|
|
68
|
+
12. deterministic local subagent error path,不联系任何模型 provider;
|
|
69
|
+
13. debug hook recorder 覆盖全部九个 Hooks v1 lifecycle events。
|
|
69
70
|
|
|
70
71
|
`curl` must be available on `PATH` for this acceptance command. Node and Git are
|
|
71
72
|
already normal ForgeRelay development prerequisites.
|
|
72
73
|
|
|
74
|
+
## Debug ChatGPT template loading
|
|
75
|
+
|
|
76
|
+
The normal debug runtime keeps widgets off so source-only server iteration does
|
|
77
|
+
not accidentally serve stale UI assets. To debug the MCP App path, build first
|
|
78
|
+
and enable widgets explicitly:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm run build
|
|
82
|
+
FORGERELAY_DEBUG_WIDGETS=full \
|
|
83
|
+
FORGERELAY_LOG_LEVEL=debug \
|
|
84
|
+
FORGERELAY_LOG_REQUESTS=1 \
|
|
85
|
+
FORGERELAY_LOG_ASSETS=1 \
|
|
86
|
+
npm run dev
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
At `debug` level, MCP requests include the JSON-RPC method and a safe target for
|
|
90
|
+
`resources/read` and `tools/call`, while transport session IDs remain out of
|
|
91
|
+
normal `info` tool logs. Successful template callbacks also emit `app template`
|
|
92
|
+
entries identifying `current`, `legacy`, or `historical` compatibility reads. A
|
|
93
|
+
successful ChatGPT template load should produce a sequence containing
|
|
94
|
+
`resources/list`, `resources/read ui://...`, an `app template ... -> ok` entry,
|
|
95
|
+
and then an HTTP `GET /mcp-app-assets/...` request. If `resources/read` never
|
|
96
|
+
arrives, inspect the client/developer-mode connection. If it arrives and fails,
|
|
97
|
+
inspect the MCP resource registration/build artifacts. If it succeeds but the
|
|
98
|
+
asset request fails, inspect the public base URL, CSP, asset route, and browser
|
|
99
|
+
console.
|
|
100
|
+
|
|
73
101
|
## Debug configuration
|
|
74
102
|
|
|
75
103
|
The checked-in debug configuration is:
|
package/docs/gotchas.md
CHANGED
|
@@ -234,7 +234,28 @@ FORGERELAY_WIDGETS=full
|
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
Use `FORGERELAY_WIDGETS=changes` for aggregate `show_changes`, or `off` to
|
|
237
|
-
disable UI. Plain MCP clients may ignore
|
|
237
|
+
disable UI. Plain MCP clients may ignore MCP App widget metadata.
|
|
238
|
+
|
|
239
|
+
If ChatGPT shows `Failed to fetch template`, first verify the server-side template
|
|
240
|
+
chain with:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
npm run build
|
|
244
|
+
npm run debug:accept
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The acceptance runner enables full widgets and checks that the tool advertises a
|
|
248
|
+
content-hashed `ui://forgerelay/workspace-app-<hash>.html` resource, that
|
|
249
|
+
`resources/read` returns `text/html;profile=mcp-app`, and that the referenced
|
|
250
|
+
JavaScript asset is reachable. ForgeRelay also keeps the legacy
|
|
251
|
+
`ui://forgerelay/workspace-app.html` pointer and historical
|
|
252
|
+
`workspace-app-*.html` pointers readable so an older ChatGPT metadata snapshot
|
|
253
|
+
can still fetch the current template while the connection is being refreshed.
|
|
254
|
+
For a live ChatGPT trace, run the debug server with
|
|
255
|
+
`FORGERELAY_DEBUG_WIDGETS=full`, `FORGERELAY_LOG_LEVEL=debug`,
|
|
256
|
+
`FORGERELAY_LOG_REQUESTS=1`, and `FORGERELAY_LOG_ASSETS=1`; then distinguish a
|
|
257
|
+
missing `resources/read` request from a template callback failure or a failed
|
|
258
|
+
`/mcp-app-assets/` fetch.
|
|
238
259
|
|
|
239
260
|
## Data retention
|
|
240
261
|
|
package/docs/roadmap.md
CHANGED
|
@@ -82,6 +82,26 @@ Hooks v1 的目标是给用户和 Agent 一个很小、自动、可组合的生
|
|
|
82
82
|
|
|
83
83
|
0.2 不引入审批 UI、HTTP/prompt/agent handler、Git 字符串解析器或插件注册表。只有出现真实需求时再扩展 handler 类型。
|
|
84
84
|
|
|
85
|
+
### 0.2.5 — MCP App template reliability
|
|
86
|
+
|
|
87
|
+
0.2.5 收敛 ChatGPT/MCP App 模板身份与排障链路:
|
|
88
|
+
|
|
89
|
+
- 当前 UI resource URI 由实际构建出的 JavaScript/CSS 内容哈希生成,而不是仅依赖 npm 版本;
|
|
90
|
+
- `ui://forgerelay/workspace-app.html` 继续作为 legacy pointer;
|
|
91
|
+
- 历史 `workspace-app-*.html` URI 通过兼容 resource template 继续读取当前模板,避免旧 metadata snapshot 直接变成 missing resource;
|
|
92
|
+
- debug 日志区分 current / legacy / historical template read,并保留 asset request trace;
|
|
93
|
+
- 7677 acceptance 覆盖 tool metadata、resource list/template list、三类 template read 和静态 bundle HTTP fetch。
|
|
94
|
+
|
|
95
|
+
### 0.2.6 — identity and transport terminology
|
|
96
|
+
|
|
97
|
+
0.2.6 整理 ForgeRelay 中多个 `session` 含义,不改变 workspace-first 架构:
|
|
98
|
+
|
|
99
|
+
- `workspaceId` 是唯一持久的逻辑工作身份,跨请求和 transport 重连保持连续;
|
|
100
|
+
- `requestId` 只追踪单次 HTTP/JSON-RPC 请求,不持久化;
|
|
101
|
+
- MCP 协议层 session 在内部和 debug 输出中明确称为 `transportSessionId`,业务状态不得依赖它;
|
|
102
|
+
- 后台命令句柄逐步迁移为 `processId` / process handle;若公开 schema 改名需要兼容窗口,则在明确的版本边界完成;
|
|
103
|
+
- 为 stateless MCP transport 做准备,同时保留旧协议兼容 adapter,避免把 transport 生命周期重新提升成 ForgeRelay 会话模型。
|
|
104
|
+
|
|
85
105
|
## 0.3 — LSP code intelligence v1
|
|
86
106
|
|
|
87
107
|
LSP is moderate implementation complexity if ForgeRelay does not become a
|
|
@@ -90,6 +110,12 @@ language-server installer.
|
|
|
90
110
|
The first version should launch only language servers already available on the
|
|
91
111
|
user's machine or explicitly configured by the user/project.
|
|
92
112
|
|
|
113
|
+
During 0.3 development, MCP App UI hardening can land alongside the LSP work when
|
|
114
|
+
it does not distort the code-intelligence scope. In particular, evaluate a more
|
|
115
|
+
self-contained template/bootstrap bundle, reduce avoidable external chunk fetches,
|
|
116
|
+
and keep the current content-hash/compatibility-resource contract intact. This is
|
|
117
|
+
reliability work, not a requirement to move application state into the UI.
|
|
118
|
+
|
|
93
119
|
Initial operations:
|
|
94
120
|
|
|
95
121
|
- diagnostics;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Local development control plane for MCP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Akira-TL/forgerelay#readme",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"debug:accept": "node scripts/debug/accept.mjs",
|
|
42
42
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
43
43
|
"start": "node dist/cli.js serve",
|
|
44
|
-
"test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/hooks.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
44
|
+
"test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
45
45
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
46
46
|
"release:check": "node scripts/release-version.mjs check",
|
|
47
47
|
"release:tag-check": "node scripts/release-version.mjs tag",
|
package/scripts/debug/accept.mjs
CHANGED
|
@@ -38,7 +38,13 @@ await assertDebugPortFree();
|
|
|
38
38
|
rmSync(acceptanceRoot, { recursive: true, force: true });
|
|
39
39
|
mkdirSync(acceptanceRoot, { recursive: true });
|
|
40
40
|
|
|
41
|
-
const { env } = createDebugEnvironment({
|
|
41
|
+
const { env } = createDebugEnvironment({
|
|
42
|
+
ownerToken,
|
|
43
|
+
stateDir,
|
|
44
|
+
worktreeRoot,
|
|
45
|
+
hookLog,
|
|
46
|
+
widgets: "full",
|
|
47
|
+
});
|
|
42
48
|
const server = spawn(process.execPath, ["--import", "tsx", "src/cli.ts", "serve"], {
|
|
43
49
|
cwd: repoRoot,
|
|
44
50
|
env,
|
|
@@ -124,8 +130,64 @@ try {
|
|
|
124
130
|
assert.ok(openWorkspaceTool?.inputSchema?.properties?.workspaceId);
|
|
125
131
|
assert.ok(openWorkspaceTool?.inputSchema?.properties?.newWorkspace);
|
|
126
132
|
assert.ok(openWorkspaceTool?.outputSchema?.properties?.staleWorkspaces);
|
|
133
|
+
const templateUri = bashTool?._meta?.ui?.resourceUri;
|
|
134
|
+
assert.match(
|
|
135
|
+
templateUri ?? "",
|
|
136
|
+
/^ui:\/\/forgerelay\/workspace-app-[0-9a-f]{12}\.html$/,
|
|
137
|
+
JSON.stringify(bashTool ?? {}),
|
|
138
|
+
);
|
|
139
|
+
assert.deepEqual(bashTool?._meta?.ui?.visibility, ["model", "app"]);
|
|
140
|
+
assert.equal(bashTool?._meta?.["openai/outputTemplate"], templateUri);
|
|
127
141
|
pass("MCP tools/list", `${toolNames.length} tools: ${toolNames.join(", ")}`);
|
|
128
142
|
|
|
143
|
+
const resources = mcpRequest(oauth.accessToken, sessionId, {
|
|
144
|
+
jsonrpc: "2.0",
|
|
145
|
+
id: 21,
|
|
146
|
+
method: "resources/list",
|
|
147
|
+
params: {},
|
|
148
|
+
}).message.result.resources;
|
|
149
|
+
assert.ok(resources.some((resource) => resource.uri === templateUri));
|
|
150
|
+
assert.ok(resources.some((resource) => resource.uri === "ui://forgerelay/workspace-app.html"));
|
|
151
|
+
|
|
152
|
+
const resourceTemplates = mcpRequest(oauth.accessToken, sessionId, {
|
|
153
|
+
jsonrpc: "2.0",
|
|
154
|
+
id: 22,
|
|
155
|
+
method: "resources/templates/list",
|
|
156
|
+
params: {},
|
|
157
|
+
}).message.result.resourceTemplates;
|
|
158
|
+
assert.ok(resourceTemplates.some(
|
|
159
|
+
(resourceTemplate) => resourceTemplate.uriTemplate === "ui://forgerelay/workspace-app-{revision}.html",
|
|
160
|
+
));
|
|
161
|
+
|
|
162
|
+
const readTemplate = (id, uri) => mcpRequest(oauth.accessToken, sessionId, {
|
|
163
|
+
jsonrpc: "2.0",
|
|
164
|
+
id,
|
|
165
|
+
method: "resources/read",
|
|
166
|
+
params: { uri },
|
|
167
|
+
}).message.result.contents[0];
|
|
168
|
+
|
|
169
|
+
const template = readTemplate(23, templateUri);
|
|
170
|
+
assert.equal(template.uri, templateUri);
|
|
171
|
+
assert.equal(template.mimeType, "text/html;profile=mcp-app");
|
|
172
|
+
assert.match(template.text ?? "", /<script type="module" crossorigin src="[^"]+\/mcp-app-assets\//);
|
|
173
|
+
assert.ok(template._meta?.ui?.csp?.resourceDomains?.includes(debugBaseUrl));
|
|
174
|
+
const scriptUrl = template.text?.match(/<script type="module" crossorigin src="([^"]+)"/)?.[1];
|
|
175
|
+
assert.ok(scriptUrl);
|
|
176
|
+
const scriptAsset = curlRequest({ method: "GET", url: scriptUrl });
|
|
177
|
+
assert.equal(scriptAsset.status, 200, scriptAsset.body);
|
|
178
|
+
|
|
179
|
+
const legacyTemplate = readTemplate(24, "ui://forgerelay/workspace-app.html");
|
|
180
|
+
assert.equal(legacyTemplate.uri, "ui://forgerelay/workspace-app.html");
|
|
181
|
+
assert.equal(legacyTemplate.mimeType, "text/html;profile=mcp-app");
|
|
182
|
+
assert.equal(legacyTemplate.text, template.text);
|
|
183
|
+
|
|
184
|
+
const historicalUri = "ui://forgerelay/workspace-app-0.2.4.html";
|
|
185
|
+
const historicalTemplate = readTemplate(25, historicalUri);
|
|
186
|
+
assert.equal(historicalTemplate.uri, historicalUri);
|
|
187
|
+
assert.equal(historicalTemplate.mimeType, "text/html;profile=mcp-app");
|
|
188
|
+
assert.equal(historicalTemplate.text, template.text);
|
|
189
|
+
pass("MCP app template", `${templateUri} + legacy/history compatibility -> ${scriptUrl}`);
|
|
190
|
+
|
|
129
191
|
const opened = callTool(oauth.accessToken, sessionId, 3, "open_workspace", {
|
|
130
192
|
path: checkoutWorkspace,
|
|
131
193
|
});
|
|
@@ -364,7 +426,13 @@ function initializeRequest(id) {
|
|
|
364
426
|
method: "initialize",
|
|
365
427
|
params: {
|
|
366
428
|
protocolVersion: "2025-06-18",
|
|
367
|
-
capabilities: {
|
|
429
|
+
capabilities: {
|
|
430
|
+
extensions: {
|
|
431
|
+
"io.modelcontextprotocol/ui": {
|
|
432
|
+
mimeTypes: ["text/html;profile=mcp-app"],
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
368
436
|
clientInfo: { name: "forgerelay-debug-acceptance", version: "1.0.0" },
|
|
369
437
|
},
|
|
370
438
|
};
|
|
@@ -20,6 +20,7 @@ export function createDebugEnvironment({
|
|
|
20
20
|
stateDir = resolve(debugRoot, "state"),
|
|
21
21
|
worktreeRoot = resolve(debugRoot, "worktrees"),
|
|
22
22
|
hookLog = debugHookLog,
|
|
23
|
+
widgets = process.env.FORGERELAY_DEBUG_WIDGETS ?? "off",
|
|
23
24
|
} = {}) {
|
|
24
25
|
const token = ownerToken ?? process.env.FORGERELAY_DEBUG_OWNER_TOKEN ?? createDebugOwnerToken();
|
|
25
26
|
mkdirSync(debugRoot, { recursive: true });
|
|
@@ -37,7 +38,7 @@ export function createDebugEnvironment({
|
|
|
37
38
|
FORGERELAY_WORKTREE_ROOT: worktreeRoot,
|
|
38
39
|
FORGERELAY_OAUTH_OWNER_TOKEN: token,
|
|
39
40
|
FORGERELAY_TOOL_MODE: "full",
|
|
40
|
-
FORGERELAY_WIDGETS:
|
|
41
|
+
FORGERELAY_WIDGETS: widgets,
|
|
41
42
|
FORGERELAY_LOG_LEVEL: process.env.FORGERELAY_LOG_LEVEL ?? "info",
|
|
42
43
|
FORGERELAY_LOG_FORMAT: process.env.FORGERELAY_LOG_FORMAT ?? "pretty",
|
|
43
44
|
FORGERELAY_DEBUG_HOOK_RECORDER: debugHookRecorder,
|