@akira-tl/forgerelay 0.2.4 → 0.2.6
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 +17 -0
- package/dist/artifact-tools.js +2 -3
- package/dist/logger.js +77 -10
- package/dist/mcp/server-instructions.js +4 -4
- package/dist/mcp-app-template.js +45 -0
- package/dist/mcp-sessions.js +24 -22
- package/dist/process-sessions.js +135 -108
- package/dist/roots.js +1 -1
- package/dist/server.js +165 -88
- package/docs/chatgpt-coding-workflow.md +3 -2
- package/docs/configuration.md +11 -6
- package/docs/debugging.md +35 -7
- package/docs/gotchas.md +22 -1
- package/docs/roadmap.md +26 -0
- package/docs/security.md +3 -2
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +79 -3
- package/scripts/debug/runtime.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.6] - 2026-08-10
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Running shell commands now expose canonical `processId` handles; `write_stdin` accepts `processId`, while the former process `sessionId` remains a deprecated compatibility alias throughout 0.2.x.
|
|
12
|
+
- ForgeRelay now names protocol-level MCP state as a transport session in internal/debug terminology. Workspace identity remains `workspaceId`, one-request tracing remains `requestId`, and third-party provider session identifiers are unchanged.
|
|
13
|
+
- Process elapsed time now uses a monotonic clock, preventing negative `wallTimeMs` values when the operating-system wall clock is adjusted while a long-running command or release Hook is executing.
|
|
14
|
+
|
|
15
|
+
## [0.2.5] - 2026-08-10
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
- `~/...` 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.
|
|
22
|
+
- Successful MCP session shutdown and idle-cleanup lifecycle logs moved to `debug`; individual session close failures remain visible as warnings.
|
|
23
|
+
|
|
7
24
|
## [0.2.4] - 2026-08-09
|
|
8
25
|
|
|
9
26
|
### Added
|
package/dist/artifact-tools.js
CHANGED
|
@@ -7,7 +7,7 @@ import * as z from "zod/v4";
|
|
|
7
7
|
import { ArtifactError } from "./artifact-error.js";
|
|
8
8
|
import { runToolWithHooks } from "./hooks.js";
|
|
9
9
|
import { describeIncomingArtifactValue, IncomingArtifactAdapterRegistry, } from "./incoming-artifacts.js";
|
|
10
|
-
import { logEvent,
|
|
10
|
+
import { logEvent, workspaceLogLabel } from "./logger.js";
|
|
11
11
|
const ARTIFACT_WRITE_ANNOTATIONS = {
|
|
12
12
|
readOnlyHint: false,
|
|
13
13
|
destructiveHint: false,
|
|
@@ -47,7 +47,7 @@ export function registerArtifactTools(server, { config, workspaces, hooks, incom
|
|
|
47
47
|
},
|
|
48
48
|
_meta: { "openai/fileParams": ["file"] },
|
|
49
49
|
annotations: ARTIFACT_WRITE_ANNOTATIONS,
|
|
50
|
-
}, async (input
|
|
50
|
+
}, async (input) => {
|
|
51
51
|
const workspace = workspaces.getWorkspace(input.workspaceId);
|
|
52
52
|
return runToolWithHooks(hooks, {
|
|
53
53
|
tool: "download_artifact",
|
|
@@ -61,7 +61,6 @@ export function registerArtifactTools(server, { config, workspaces, hooks, incom
|
|
|
61
61
|
changedPaths: (result) => [result.structuredContent.path],
|
|
62
62
|
operation: () => executeArtifactTool(config, input, {
|
|
63
63
|
workspace: workspaceLogLabel(workspace.root, workspace.id),
|
|
64
|
-
session: sessionIdPrefix(extra?.sessionId),
|
|
65
64
|
}, async () => {
|
|
66
65
|
const downloaded = await downloadIncomingArtifact({
|
|
67
66
|
registry: incomingRegistry,
|
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
|
}
|
|
@@ -53,9 +61,11 @@ export function requestIp(req, trustProxy) {
|
|
|
53
61
|
export function requestPath(req) {
|
|
54
62
|
return req.path || req.url.split("?")[0] || req.url;
|
|
55
63
|
}
|
|
56
|
-
export function
|
|
57
|
-
return
|
|
64
|
+
export function transportSessionIdPrefix(transportSessionId) {
|
|
65
|
+
return transportSessionId ? transportSessionId.slice(0, 8) : undefined;
|
|
58
66
|
}
|
|
67
|
+
/** @deprecated Use transportSessionIdPrefix. */
|
|
68
|
+
export const sessionIdPrefix = transportSessionIdPrefix;
|
|
59
69
|
export function workspaceLogLabel(root, workspaceId) {
|
|
60
70
|
const shortWorkspaceId = workspaceId.startsWith("ws_")
|
|
61
71
|
? `ws_${workspaceId.slice(3, 11)}`
|
|
@@ -70,12 +80,16 @@ export function formatPrettyLogEntry(entry, options = {}) {
|
|
|
70
80
|
const level = logLevel(entry.level);
|
|
71
81
|
const time = formatTimestamp(entry.ts);
|
|
72
82
|
const source = stringField(entry.workspace) ?? stringField(entry.workspaceId) ?? "forgerelay";
|
|
73
|
-
const
|
|
83
|
+
const transportSession = level === "debug"
|
|
84
|
+
? stringField(entry.transportSessionIdPrefix)
|
|
85
|
+
?? stringField(entry.session)
|
|
86
|
+
?? stringField(entry.sessionIdPrefix)
|
|
87
|
+
: undefined;
|
|
74
88
|
const prefix = [
|
|
75
89
|
style("gray", time, options),
|
|
76
90
|
`[${style(LEVEL_STYLE[level], level.toUpperCase(), options)}]`,
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
formatPrettySource(source, options),
|
|
92
|
+
transportSession ? style("gray", `transport:${transportSession}`, options) : undefined,
|
|
79
93
|
style("gray", "|", options),
|
|
80
94
|
].filter((value) => Boolean(value)).join(" ");
|
|
81
95
|
return `${prefix} ${formatPrettyMessage(entry, options)}`;
|
|
@@ -92,12 +106,24 @@ function formatPrettyMessage(entry, options) {
|
|
|
92
106
|
return formatHookMessage(entry, options);
|
|
93
107
|
case "http_request":
|
|
94
108
|
return formatHttpMessage(entry, options);
|
|
109
|
+
case "mcp_request":
|
|
110
|
+
return formatMcpRequestMessage(entry);
|
|
111
|
+
case "mcp_app_template_read":
|
|
112
|
+
return formatAppTemplateMessage(entry, options, false);
|
|
113
|
+
case "mcp_app_template_read_failed":
|
|
114
|
+
return formatAppTemplateMessage(entry, options, true);
|
|
115
|
+
case "mcp_transport_session_created":
|
|
95
116
|
case "mcp_session_created":
|
|
96
|
-
return `session ${
|
|
117
|
+
return `transport session ${transportSessionPrefix(entry) ?? "unknown"} created`;
|
|
118
|
+
case "mcp_transport_session_closed":
|
|
97
119
|
case "mcp_session_closed":
|
|
98
|
-
return `session ${
|
|
120
|
+
return `transport session ${transportSessionPrefix(entry) ?? "unknown"} closed`;
|
|
121
|
+
case "mcp_transport_sessions_closed":
|
|
122
|
+
case "mcp_sessions_closed":
|
|
123
|
+
return `${numberField(entry.count) ?? 0} transport sessions closed`;
|
|
124
|
+
case "mcp_transport_session_close_failed":
|
|
99
125
|
case "mcp_session_close_failed":
|
|
100
|
-
return `session ${
|
|
126
|
+
return `transport session ${transportSessionPrefix(entry) ?? "unknown"} close -> ${style("red", "error", options)}`;
|
|
101
127
|
case "auth_denied":
|
|
102
128
|
return `auth denied${entry.reason ? `: ${String(entry.reason)}` : ""}`;
|
|
103
129
|
case "mcp_request_error":
|
|
@@ -121,8 +147,8 @@ function toolTarget(entry, tool) {
|
|
|
121
147
|
}
|
|
122
148
|
function toolResult(entry, tool, options) {
|
|
123
149
|
if (entry.running === true) {
|
|
124
|
-
const
|
|
125
|
-
return style("yellow",
|
|
150
|
+
const processId = entry.processId ?? entry.processSessionId;
|
|
151
|
+
return style("yellow", processId === undefined ? "running" : `running process:${String(processId)}`, options);
|
|
126
152
|
}
|
|
127
153
|
const exitCode = numberField(entry.exitCode) ?? exitCodeFromError(entry.error);
|
|
128
154
|
if (isShellTool(tool)) {
|
|
@@ -161,6 +187,42 @@ function formatHttpMessage(entry, options) {
|
|
|
161
187
|
const statusStyle = status !== undefined && status >= 400 ? "red" : "green";
|
|
162
188
|
return `http ${method} ${path} -> ${style(statusStyle, statusText, options)}`;
|
|
163
189
|
}
|
|
190
|
+
function formatMcpRequestMessage(entry) {
|
|
191
|
+
const method = stringField(entry.rpcMethod) ?? stringField(entry.httpMethod) ?? "request";
|
|
192
|
+
const target = stringField(entry.rpcTarget);
|
|
193
|
+
return target ? `mcp ${method} ${target}` : `mcp ${method}`;
|
|
194
|
+
}
|
|
195
|
+
function formatAppTemplateMessage(entry, options, failed) {
|
|
196
|
+
const requestedUri = stringField(entry.requestedUri) ?? "unknown";
|
|
197
|
+
const compatibility = stringField(entry.compatibility) ?? "unknown";
|
|
198
|
+
if (failed) {
|
|
199
|
+
const error = stringField(entry.error);
|
|
200
|
+
return `app template ${compatibility} ${requestedUri} -> ${style("red", error ? `error: ${error}` : "error", options)}`;
|
|
201
|
+
}
|
|
202
|
+
const currentUri = stringField(entry.currentUri);
|
|
203
|
+
const target = compatibility === "current" || !currentUri
|
|
204
|
+
? requestedUri
|
|
205
|
+
: `${requestedUri} => ${currentUri}`;
|
|
206
|
+
return `app template ${compatibility} ${target} -> ${style("green", "ok", options)}`;
|
|
207
|
+
}
|
|
208
|
+
function formatPrettySource(source, options) {
|
|
209
|
+
const separator = source.lastIndexOf("/");
|
|
210
|
+
if (separator <= 0 || separator === source.length - 1) {
|
|
211
|
+
return style(["cyan", "underline"], source, options);
|
|
212
|
+
}
|
|
213
|
+
const project = source.slice(0, separator);
|
|
214
|
+
const workspace = source.slice(separator + 1);
|
|
215
|
+
const projectColor = WORKSPACE_PROJECT_COLORS[stableColorIndex(project)];
|
|
216
|
+
return `${style([projectColor, "bold"], project, options)}/${style(["cyan", "underline"], workspace, options)}`;
|
|
217
|
+
}
|
|
218
|
+
function stableColorIndex(value) {
|
|
219
|
+
let hash = 2166136261;
|
|
220
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
221
|
+
hash ^= value.charCodeAt(index);
|
|
222
|
+
hash = Math.imul(hash, 16777619);
|
|
223
|
+
}
|
|
224
|
+
return (hash >>> 0) % WORKSPACE_PROJECT_COLORS.length;
|
|
225
|
+
}
|
|
164
226
|
function formatGenericMessage(entry) {
|
|
165
227
|
const event = String(entry.event ?? "log");
|
|
166
228
|
const detail = [entry.reason, entry.error]
|
|
@@ -181,6 +243,11 @@ function logLevel(value) {
|
|
|
181
243
|
function stringField(value) {
|
|
182
244
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
183
245
|
}
|
|
246
|
+
function transportSessionPrefix(entry) {
|
|
247
|
+
return stringField(entry.transportSessionIdPrefix)
|
|
248
|
+
?? stringField(entry.sessionIdPrefix)
|
|
249
|
+
?? stringField(entry.session);
|
|
250
|
+
}
|
|
184
251
|
function numberField(value) {
|
|
185
252
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
186
253
|
}
|
|
@@ -34,7 +34,7 @@ export function buildToolDescriptions(config) {
|
|
|
34
34
|
rename: `Rename or move one file or directory inside an open workspace or the OS temp directory without overwriting an existing destination. Source and destination must both remain inside the permitted file roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
35
35
|
delete: `Delete one file or directory inside an open workspace or the OS temp directory. Non-empty directories require recursive=true. An allowed root itself cannot be deleted. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
36
36
|
applyPatch: `Apply one Codex-style patch inside an open workspace or the OS temp directory. Supports adding, overwriting, updating, deleting, and moving files. Workspace paths must remain relative; absolute paths are accepted only inside the OS temp directory. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
37
|
-
shell: `Run a shell command inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace filesystem containment does not make shell execution a sandbox. ForgeRelay waits up to 300 seconds for bash, then returns a running process
|
|
37
|
+
shell: `Run a shell command inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace filesystem containment does not make shell execution a sandbox. ForgeRelay waits up to 300 seconds for bash, then returns a running process with a processId without killing it; use ${toolNames.writeStdin} with that processId to poll, keep waiting, interact, or send Ctrl-C. Completed background commands are also reported with a later tool result for the same workspaceId. ${shellMutationPolicy} Call ${toolNames.openWorkspace} first and pass workspaceId. This capability should only be exposed behind strong authentication.`,
|
|
38
38
|
shellCommand: "Shell command to run with the local user's authority.",
|
|
39
39
|
};
|
|
40
40
|
}
|
|
@@ -62,9 +62,9 @@ function toolSurfaceInstructions(config) {
|
|
|
62
62
|
return `In codex tool mode, workspace file and command operations use ${toolNames.read}, ${toolNames.rename}, ${toolNames.delete}, apply_patch, exec_command, and ${toolNames.writeStdin}.`;
|
|
63
63
|
}
|
|
64
64
|
if (config.toolMode === "full") {
|
|
65
|
-
return `In full tool mode, dedicated ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} inspection tools are available alongside the core workspace tools. ${toolNames.writeStdin} is available for running bash
|
|
65
|
+
return `In full tool mode, dedicated ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} inspection tools are available alongside the core workspace tools. ${toolNames.writeStdin} is available for running bash processes.`;
|
|
66
66
|
}
|
|
67
|
-
return `In minimal tool mode, dedicated ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} inspection tools are disabled; the core workspace tools remain available, including ${toolNames.writeStdin} for running bash
|
|
67
|
+
return `In minimal tool mode, dedicated ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} inspection tools are disabled; the core workspace tools remain available, including ${toolNames.writeStdin} for running bash processes.`;
|
|
68
68
|
}
|
|
69
69
|
function selectedWorkflowInstructions(config) {
|
|
70
70
|
if (config.workflowInstructions === false)
|
|
@@ -80,7 +80,7 @@ function defaultWorkflowInstructions(config) {
|
|
|
80
80
|
const inspection = config.toolMode === "full"
|
|
81
81
|
? `Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection.`
|
|
82
82
|
: `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
|
|
83
|
-
return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds, git inspection, package scripts, generators, formatters, and commands that are better executed by the shell. If ${toolNames.shell} returns a running
|
|
83
|
+
return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds, git inspection, package scripts, generators, formatters, and commands that are better executed by the shell. If ${toolNames.shell} returns a running process with a processId, use ${toolNames.writeStdin} only when you need to poll, wait, interact, or interrupt it; otherwise you may continue other work and consume its completion notice from a later tool result.`);
|
|
84
84
|
}
|
|
85
85
|
function joinInstructions(...parts) {
|
|
86
86
|
return parts
|
|
@@ -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/mcp-sessions.js
CHANGED
|
@@ -1,56 +1,58 @@
|
|
|
1
|
-
export class
|
|
2
|
-
|
|
1
|
+
export class McpTransportRegistry {
|
|
2
|
+
transports = new Map();
|
|
3
3
|
now;
|
|
4
4
|
constructor(options = {}) {
|
|
5
5
|
this.now = options.now ?? Date.now;
|
|
6
6
|
}
|
|
7
7
|
get size() {
|
|
8
|
-
return this.
|
|
8
|
+
return this.transports.size;
|
|
9
9
|
}
|
|
10
|
-
register(
|
|
11
|
-
this.
|
|
10
|
+
register(transportSessionId, transport) {
|
|
11
|
+
this.transports.set(transportSessionId, {
|
|
12
12
|
transport,
|
|
13
13
|
lastActivityAt: this.now(),
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
-
get(
|
|
17
|
-
const entry = this.
|
|
16
|
+
get(transportSessionId) {
|
|
17
|
+
const entry = this.transports.get(transportSessionId);
|
|
18
18
|
if (!entry)
|
|
19
19
|
return undefined;
|
|
20
20
|
entry.lastActivityAt = this.now();
|
|
21
21
|
return entry.transport;
|
|
22
22
|
}
|
|
23
|
-
remove(
|
|
24
|
-
return this.
|
|
23
|
+
remove(transportSessionId) {
|
|
24
|
+
return this.transports.delete(transportSessionId);
|
|
25
25
|
}
|
|
26
26
|
async closeIdle(idleTimeoutMs) {
|
|
27
27
|
const cutoff = this.now() - idleTimeoutMs;
|
|
28
|
-
const
|
|
29
|
-
for (const [
|
|
28
|
+
const idleTransports = [];
|
|
29
|
+
for (const [transportSessionId, entry] of this.transports) {
|
|
30
30
|
if (entry.lastActivityAt > cutoff)
|
|
31
31
|
continue;
|
|
32
|
-
this.
|
|
33
|
-
|
|
32
|
+
this.transports.delete(transportSessionId);
|
|
33
|
+
idleTransports.push({ transportSessionId, transport: entry.transport });
|
|
34
34
|
}
|
|
35
|
-
return
|
|
35
|
+
return closeTransports(idleTransports);
|
|
36
36
|
}
|
|
37
37
|
async closeAll() {
|
|
38
|
-
const
|
|
39
|
-
|
|
38
|
+
const transports = Array.from(this.transports, ([transportSessionId, entry]) => ({
|
|
39
|
+
transportSessionId,
|
|
40
40
|
transport: entry.transport,
|
|
41
41
|
}));
|
|
42
|
-
this.
|
|
43
|
-
return
|
|
42
|
+
this.transports.clear();
|
|
43
|
+
return closeTransports(transports);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
async function
|
|
47
|
-
return Promise.all(
|
|
46
|
+
async function closeTransports(transports) {
|
|
47
|
+
return Promise.all(transports.map(async ({ transportSessionId, transport }) => {
|
|
48
48
|
try {
|
|
49
49
|
await transport.close();
|
|
50
|
-
return {
|
|
50
|
+
return { transportSessionId };
|
|
51
51
|
}
|
|
52
52
|
catch (error) {
|
|
53
|
-
return {
|
|
53
|
+
return { transportSessionId, error };
|
|
54
54
|
}
|
|
55
55
|
}));
|
|
56
56
|
}
|
|
57
|
+
/** @deprecated Use McpTransportRegistry. */
|
|
58
|
+
export { McpTransportRegistry as McpSessionRegistry };
|