@ai-sdk/harness-acp 1.0.38 → 1.0.40

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.
@@ -3,7 +3,9 @@ import type {
3
3
  HarnessV1CredentialForwarding,
4
4
  HarnessV1PermissionMode,
5
5
  HarnessV1RequestTransformation,
6
+ HarnessV1StreamPart,
6
7
  } from '@ai-sdk/harness';
8
+ import type { ToolResultPart } from '@ai-sdk/provider-utils';
7
9
  import type { ACPToolCall } from '../acp-tool-call';
8
10
 
9
11
  export type ACPSerializablePrimitive = string | number | boolean | null;
@@ -88,9 +90,11 @@ export type ACPModelMapping =
88
90
  export type ACPCredentialBrokering = ({
89
91
  env,
90
92
  sandboxEnv,
93
+ headers,
91
94
  }: {
92
95
  env: Readonly<Record<string, string>>;
93
96
  sandboxEnv?: Readonly<Record<string, string>>;
97
+ headers?: Readonly<Record<string, string>>;
94
98
  }) => ReadonlyArray<HarnessV1RequestTransformation>;
95
99
 
96
100
  export type ACPPermissionModeTarget =
@@ -122,6 +126,10 @@ export type ACPInstructionMapping =
122
126
  readonly type: 'launch-env-json';
123
127
  readonly variable: string;
124
128
  readonly path: ReadonlyArray<string>;
129
+ }
130
+ | {
131
+ readonly type: 'filesystem';
132
+ readonly path: string;
125
133
  };
126
134
 
127
135
  export type ACPOutputSchemaMapping = {
@@ -129,6 +137,31 @@ export type ACPOutputSchemaMapping = {
129
137
  readonly path: ReadonlyArray<string>;
130
138
  };
131
139
 
140
+ /**
141
+ * Transport used for the harness-owned MCP server that exposes host tools to
142
+ * an ACP implementation.
143
+ */
144
+ export type ACPHostToolMCPTransport = 'stdio' | 'http';
145
+
146
+ export type ACPAskUserQuestionsSettings = {
147
+ readonly requestMethod: string;
148
+ readonly isNativeToolCall?: (options: {
149
+ nativeToolCall: ACPToolCall;
150
+ }) => boolean;
151
+ readonly fromNativeRequest: (options: {
152
+ nativeRequest: unknown;
153
+ nativeToolCall?: ACPToolCall;
154
+ }) => Extract<HarnessV1StreamPart, { type: 'tool-call' }> | null;
155
+ readonly toNativeResponse: (options: {
156
+ nativeRequest: unknown;
157
+ toolResult: ToolResultPart;
158
+ }) => unknown;
159
+ readonly matchesNativeRequest?: (options: {
160
+ previousNativeRequest: unknown;
161
+ nativeRequest: unknown;
162
+ }) => boolean;
163
+ };
164
+
132
165
  export type ACPV1Settings = {
133
166
  readonly version?: 'v1';
134
167
  readonly harnessId: string;
@@ -179,6 +212,15 @@ export type ACPV1Settings = {
179
212
  * below the ACP session prompt's `_meta` field.
180
213
  */
181
214
  readonly outputSchemaMapping?: ACPOutputSchemaMapping;
215
+ /**
216
+ * Transport used for the harness-owned MCP server that exposes host tools to
217
+ * the ACP implementation. Defaults to `stdio`. Set this to `http` for
218
+ * implementations that only accept HTTP or SSE MCP servers from the client,
219
+ * which requires the implementation to advertise
220
+ * `agentCapabilities.mcpCapabilities.http`.
221
+ */
222
+ readonly hostToolMcpTransport?: ACPHostToolMCPTransport;
223
+ readonly askUserQuestions?: ACPAskUserQuestionsSettings;
182
224
  readonly permissionModeMapping?: ACPPermissionModeMapping;
183
225
  readonly session?: {
184
226
  readonly meta?: Readonly<Record<string, ACPSerializableValue>>;
@@ -1,74 +1,35 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import type { HarnessV1Skill } from '@ai-sdk/harness';
4
- import { writeSkills, type WriteSkillsResult } from '@ai-sdk/harness/utils';
5
- import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
6
4
 
7
- const ACP_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
+ export const ACP_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
6
  export const DEFAULT_ACP_SKILLS_DIRECTORY = '.agents/skills';
9
7
 
10
8
  export function resolveACPPrivateSessionDirectory({
11
9
  sandboxHomeDir,
12
- sessionWorkDir,
13
10
  harnessId,
14
11
  sessionId,
15
12
  }: {
16
13
  sandboxHomeDir: string;
17
- sessionWorkDir: string;
18
14
  harnessId: string;
19
15
  sessionId: string;
20
16
  }): string {
21
17
  const sessionKey = createHash('sha256').update(sessionId).digest('hex');
22
- const rootDir = path.posix.join(
18
+ return path.posix.join(
23
19
  sandboxHomeDir,
24
20
  '.ai-sdk',
25
21
  'harness-acp',
26
22
  harnessId,
27
23
  sessionKey,
28
24
  );
29
- assertOutsideSessionWorkDir({ rootDir, sessionWorkDir });
30
- return rootDir;
31
- }
32
-
33
- export async function materializeACPSkills({
34
- sandbox,
35
- rootDir,
36
- sessionWorkDir,
37
- skills,
38
- abortSignal,
39
- }: {
40
- sandbox: Experimental_SandboxSession;
41
- rootDir: string;
42
- sessionWorkDir: string;
43
- skills: ReadonlyArray<HarnessV1Skill>;
44
- abortSignal?: AbortSignal;
45
- }): Promise<WriteSkillsResult> {
46
- validateACPSkills({ skills });
47
- assertOutsideSessionWorkDir({ rootDir, sessionWorkDir });
48
-
49
- return writeSkills({
50
- sandbox,
51
- rootDir,
52
- skills,
53
- abortSignal,
54
- skillNamePattern: ACP_SKILL_NAME_PATTERN,
55
- invalidSkillNameMessage: ({ name }) =>
56
- `Invalid ACP skill name ${JSON.stringify(name)}: expected a kebab-case slug.`,
57
- invalidSkillFilePathMessage: ({ skillName, filePath }) =>
58
- `Invalid ACP skill file path ${JSON.stringify(filePath)} for skill ${JSON.stringify(
59
- skillName,
60
- )}: expected a relative POSIX path without traversal.`,
61
- });
62
25
  }
63
26
 
64
27
  export function resolveACPSkillsDirectory({
65
28
  implementationHomeDir,
66
29
  skillsDirectory = DEFAULT_ACP_SKILLS_DIRECTORY,
67
- sessionWorkDir,
68
30
  }: {
69
31
  implementationHomeDir: string;
70
32
  skillsDirectory?: string;
71
- sessionWorkDir: string;
72
33
  }): string {
73
34
  const containsTraversal = skillsDirectory
74
35
  .split(/[\\/]/)
@@ -90,12 +51,10 @@ export function resolveACPSkillsDirectory({
90
51
  );
91
52
  }
92
53
 
93
- const rootDir = path.posix.join(implementationHomeDir, normalizedDirectory);
94
- assertOutsideSessionWorkDir({ rootDir, sessionWorkDir });
95
- return rootDir;
54
+ return path.posix.join(implementationHomeDir, normalizedDirectory);
96
55
  }
97
56
 
98
- function validateACPSkills({
57
+ export function validateACPSkills({
99
58
  skills,
100
59
  }: {
101
60
  skills: ReadonlyArray<HarnessV1Skill>;
@@ -174,23 +133,3 @@ function validateACPAttachedFilePath({
174
133
  }
175
134
  return normalizedPath;
176
135
  }
177
-
178
- function assertOutsideSessionWorkDir({
179
- rootDir,
180
- sessionWorkDir,
181
- }: {
182
- rootDir: string;
183
- sessionWorkDir: string;
184
- }): void {
185
- const relative = path.posix.relative(sessionWorkDir, rootDir);
186
- const isInside =
187
- relative === '' ||
188
- (!relative.startsWith('../') && !path.posix.isAbsolute(relative));
189
- if (isInside) {
190
- throw new Error(
191
- `ACP skill directory ${JSON.stringify(rootDir)} must be outside sessionWorkDir ${JSON.stringify(
192
- sessionWorkDir,
193
- )}.`,
194
- );
195
- }
196
- }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import type {
3
3
  ACPAuthentication,
4
+ ACPHostToolMCPTransport,
4
5
  ACPProfileValue,
5
6
  ACPSerializableValue,
6
7
  } from '../acp-v1-settings';
@@ -33,6 +34,11 @@ const profileValueSchema: z.ZodType<ACPProfileValue> = z.lazy(() =>
33
34
  ]),
34
35
  );
35
36
 
37
+ const hostToolMcpTransportSchema: z.ZodType<ACPHostToolMCPTransport> = z.enum([
38
+ 'stdio',
39
+ 'http',
40
+ ]);
41
+
36
42
  const serializableRecordSchema = z.record(z.string(), serializableValueSchema);
37
43
  const profileRecordSchema = z.record(z.string(), profileValueSchema);
38
44
 
@@ -60,6 +66,8 @@ export type ACPBridgeConfiguration = {
60
66
  readonly providerEnvironment?: Readonly<Record<string, string>>;
61
67
  readonly sessionMeta?: Readonly<Record<string, ACPSerializableValue>>;
62
68
  readonly clientCapabilities?: Readonly<Record<string, ACPSerializableValue>>;
69
+ readonly askUserQuestionsRequestMethod?: string;
70
+ readonly hostToolMcpTransport?: ACPHostToolMCPTransport;
63
71
  };
64
72
 
65
73
  const bridgeConfigurationSchema: z.ZodType<ACPBridgeConfiguration> = z.object({
@@ -68,6 +76,8 @@ const bridgeConfigurationSchema: z.ZodType<ACPBridgeConfiguration> = z.object({
68
76
  providerEnvironment: z.record(z.string(), z.string()).optional(),
69
77
  sessionMeta: serializableRecordSchema.optional(),
70
78
  clientCapabilities: serializableRecordSchema.optional(),
79
+ askUserQuestionsRequestMethod: z.string().optional(),
80
+ hostToolMcpTransport: hostToolMcpTransportSchema.optional(),
71
81
  });
72
82
 
73
83
  export function createACPBridgeEnvironment({
@@ -76,6 +86,8 @@ export function createACPBridgeEnvironment({
76
86
  providerEnvironment,
77
87
  sessionMeta,
78
88
  clientCapabilities,
89
+ askUserQuestionsRequestMethod,
90
+ hostToolMcpTransport,
79
91
  }: ACPBridgeConfiguration): Record<string, string> {
80
92
  return {
81
93
  [ACP_BRIDGE_CONFIGURATION_ENV]: JSON.stringify({
@@ -84,6 +96,10 @@ export function createACPBridgeEnvironment({
84
96
  ...(providerEnvironment == null ? {} : { providerEnvironment }),
85
97
  ...(sessionMeta == null ? {} : { sessionMeta }),
86
98
  ...(clientCapabilities == null ? {} : { clientCapabilities }),
99
+ ...(askUserQuestionsRequestMethod == null
100
+ ? {}
101
+ : { askUserQuestionsRequestMethod }),
102
+ ...(hostToolMcpTransport == null ? {} : { hostToolMcpTransport }),
87
103
  }),
88
104
  };
89
105
  }
@@ -51,6 +51,8 @@ export function createEmitStreamEvent({
51
51
  order: number;
52
52
  }) => void;
53
53
  removeHostToolCorrelationInvocation: (options: { token: string }) => void;
54
+ suppressToolCall: (options: { toolCallId: string }) => void;
55
+ getToolCall: (options: { toolCallId: string }) => ACPToolCall | undefined;
54
56
  } {
55
57
  const translator = createACPStreamTranslator({
56
58
  emit,
@@ -100,5 +102,9 @@ export function createEmitStreamEvent({
100
102
  hostToolResult: translator.hostToolResult,
101
103
  registerHostToolCorrelationInvocation: correlation.registerInvocation,
102
104
  removeHostToolCorrelationInvocation: correlation.removeInvocation,
105
+ suppressToolCall: correlation.suppressToolCall,
106
+ getToolCall: ({ toolCallId }) =>
107
+ correlation.getToolCall({ toolCallId }) ??
108
+ translator.getToolCall({ toolCallId }),
103
109
  };
104
110
  }
@@ -2,6 +2,7 @@ import type {
2
2
  ActiveSessionMessage,
3
3
  ToolCallUpdate,
4
4
  } from '@agentclientprotocol/sdk';
5
+ import type { ACPToolCall } from '../../acp-tool-call';
5
6
 
6
7
  type SessionUpdateMessage = Extract<
7
8
  ActiveSessionMessage,
@@ -55,6 +56,8 @@ export function createHostToolCorrelation({
55
56
  order: number;
56
57
  }): void;
57
58
  claimHostToolPermission(options: { toolCall: ToolCallUpdate }): boolean;
59
+ suppressToolCall(options: { toolCallId: string }): void;
60
+ getToolCall(options: { toolCallId: string }): ACPToolCall | undefined;
58
61
  flush(): void;
59
62
  removeInvocation(options: { token: string }): void;
60
63
  close(): void;
@@ -63,6 +66,7 @@ export function createHostToolCorrelation({
63
66
  const candidates = new Map<string, CorrelationCandidate>();
64
67
  const suppressedToolCallIds = new Set<string>();
65
68
  const releasedToolCallIds = new Set<string>();
69
+ const observedToolCalls = new Map<string, ACPToolCall>();
66
70
  let nextCandidateOrder = 0;
67
71
  let buffered: BufferedUpdate[] = [];
68
72
  let flushTimer: ReturnType<typeof setTimeout> | undefined;
@@ -155,6 +159,13 @@ export function createHostToolCorrelation({
155
159
  }
156
160
 
157
161
  const toolCallId = message.update.toolCallId;
162
+ observedToolCalls.set(
163
+ toolCallId,
164
+ mergeObservedToolCall({
165
+ previous: observedToolCalls.get(toolCallId),
166
+ update: message.update,
167
+ }),
168
+ );
158
169
  if (suppressedToolCallIds.has(toolCallId)) {
159
170
  if (isTerminalToolUpdate(message)) {
160
171
  suppressedToolCallIds.delete(toolCallId);
@@ -242,6 +253,8 @@ export function createHostToolCorrelation({
242
253
  suppressToolCallUpdate({ toolCallId: toolCall.toolCallId });
243
254
  return true;
244
255
  },
256
+ suppressToolCall: suppressToolCallUpdate,
257
+ getToolCall: ({ toolCallId }) => observedToolCalls.get(toolCallId),
245
258
  flush,
246
259
  removeInvocation: ({ token }) => {
247
260
  deleteInvocation({ invocations, token });
@@ -257,10 +270,31 @@ export function createHostToolCorrelation({
257
270
  candidates.clear();
258
271
  suppressedToolCallIds.clear();
259
272
  releasedToolCallIds.clear();
273
+ observedToolCalls.clear();
260
274
  },
261
275
  };
262
276
  }
263
277
 
278
+ function mergeObservedToolCall({
279
+ previous,
280
+ update,
281
+ }: {
282
+ previous: ACPToolCall | undefined;
283
+ update: ToolCallUpdate;
284
+ }): ACPToolCall {
285
+ return {
286
+ toolCallId: update.toolCallId,
287
+ title: update.title ?? previous?.title ?? `Tool ${update.toolCallId}`,
288
+ ...(update.kind == null ? {} : { kind: update.kind }),
289
+ ...(update.status == null ? {} : { status: update.status }),
290
+ ...(update.content == null ? {} : { content: update.content }),
291
+ ...(update.locations == null ? {} : { locations: update.locations }),
292
+ ...(update.rawInput === undefined ? {} : { rawInput: update.rawInput }),
293
+ ...(update.rawOutput === undefined ? {} : { rawOutput: update.rawOutput }),
294
+ ...(update._meta === undefined ? {} : { _meta: update._meta }),
295
+ };
296
+ }
297
+
264
298
  function hasPortableEvidence({
265
299
  candidate,
266
300
  invocation,
@@ -0,0 +1,69 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { execPath } from 'node:process';
3
+ import type * as acp from '@agentclientprotocol/sdk';
4
+ import { HarnessBridgeCapabilityUnsupportedError } from '@ai-sdk/harness/bridge';
5
+ import type { ACPHostToolMCPTransport } from '../acp-v1-settings';
6
+ import type { HostToolRelay } from './host-tool-relay';
7
+
8
+ /*
9
+ * Builds the ACP `session/new` definition for the harness-owned MCP server
10
+ * that exposes host tools. The stdio transport launches `host-tool-mcp.mjs`,
11
+ * which bridges back to the relay over its private HTTP protocol. The http
12
+ * transport points the ACP implementation straight at the relay's own MCP
13
+ * endpoint, which is required by implementations that reject client-supplied
14
+ * stdio MCP servers.
15
+ */
16
+ export function createHostToolMcpServerDefinition({
17
+ mcpTransport,
18
+ relay,
19
+ serverName,
20
+ catalogPath,
21
+ initialization,
22
+ harnessId,
23
+ }: {
24
+ mcpTransport: ACPHostToolMCPTransport;
25
+ relay: Pick<HostToolRelay, 'url' | 'mcpUrl' | 'credential'>;
26
+ serverName: string;
27
+ catalogPath: string;
28
+ initialization: Pick<acp.InitializeResponse, 'agentCapabilities'>;
29
+ harnessId: string;
30
+ }): acp.McpServer {
31
+ if (mcpTransport === 'stdio') {
32
+ return {
33
+ name: serverName,
34
+ command: execPath,
35
+ args: [fileURLToPath(new URL('./host-tool-mcp.mjs', import.meta.url))],
36
+ env: [
37
+ {
38
+ name: 'AI_SDK_ACP_HOST_TOOLS_FILE',
39
+ value: catalogPath,
40
+ },
41
+ {
42
+ name: 'AI_SDK_ACP_HOST_TOOL_RELAY_URL',
43
+ value: relay.url,
44
+ },
45
+ {
46
+ name: 'AI_SDK_ACP_HOST_TOOL_RELAY_CREDENTIAL',
47
+ value: relay.credential,
48
+ },
49
+ ],
50
+ };
51
+ }
52
+ if (initialization.agentCapabilities?.mcpCapabilities?.http !== true) {
53
+ throw new HarnessBridgeCapabilityUnsupportedError({
54
+ harnessId,
55
+ message:
56
+ 'This harness exposes host tools through an HTTP MCP server, but ' +
57
+ 'the ACP agent does not advertise support for HTTP MCP servers.',
58
+ });
59
+ }
60
+ if (relay.mcpUrl == null) {
61
+ throw new Error('The host tool MCP HTTP endpoint is unavailable.');
62
+ }
63
+ return {
64
+ type: 'http',
65
+ name: serverName,
66
+ url: relay.mcpUrl,
67
+ headers: [{ name: 'Authorization', value: `Bearer ${relay.credential}` }],
68
+ };
69
+ }
@@ -0,0 +1,182 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
5
+ import type { HarnessV1BridgeToolWire } from '@ai-sdk/harness';
6
+ import {
7
+ createHostToolMCPServer,
8
+ type HostToolMCPInvocationResult,
9
+ type HostToolMCPServer,
10
+ } from './host-tool-mcp-server';
11
+
12
+ export const HOST_TOOL_MCP_ENDPOINT_PATH = '/mcp';
13
+
14
+ export type HostToolMCPHttpEndpoint = {
15
+ handleRequest(options: {
16
+ request: IncomingMessage;
17
+ response: ServerResponse;
18
+ body?: unknown;
19
+ }): Promise<void>;
20
+ updateCatalog(options: {
21
+ revision: number;
22
+ tools: ReadonlyArray<HarnessV1BridgeToolWire>;
23
+ }): Promise<void>;
24
+ close(): Promise<void>;
25
+ };
26
+
27
+ type EndpointSession = {
28
+ readonly transport: StreamableHTTPServerTransport;
29
+ readonly hostToolServer: HostToolMCPServer;
30
+ };
31
+
32
+ /*
33
+ * The ACP host tool catalog is shared by every MCP session opened against this
34
+ * endpoint. Each session needs its own MCP `Server` instance because a server
35
+ * can be connected to a single transport at a time, so catalog updates are
36
+ * fanned out to all live sessions and also retained for sessions opened later.
37
+ */
38
+ export function createHostToolMCPHttpEndpoint({
39
+ tools,
40
+ revision,
41
+ invoke,
42
+ onListTools,
43
+ }: {
44
+ tools: ReadonlyArray<HarnessV1BridgeToolWire>;
45
+ revision: number;
46
+ invoke: (options: {
47
+ toolName: string;
48
+ input: Readonly<Record<string, unknown>>;
49
+ catalogRevision: number;
50
+ }) => Promise<HostToolMCPInvocationResult>;
51
+ onListTools: (options: { revision: number }) => Promise<void>;
52
+ }): HostToolMCPHttpEndpoint {
53
+ let catalog: {
54
+ revision: number;
55
+ tools: ReadonlyArray<HarnessV1BridgeToolWire>;
56
+ } = { revision, tools: [...tools] };
57
+ const sessions = new Map<string, EndpointSession>();
58
+ let closed = false;
59
+
60
+ async function openSession({
61
+ request,
62
+ response,
63
+ body,
64
+ }: {
65
+ request: IncomingMessage;
66
+ response: ServerResponse;
67
+ body: unknown;
68
+ }): Promise<void> {
69
+ const hostToolServer = createHostToolMCPServer({
70
+ tools: catalog.tools,
71
+ revision: catalog.revision,
72
+ invoke,
73
+ onListTools,
74
+ });
75
+ const transport = new StreamableHTTPServerTransport({
76
+ sessionIdGenerator: () => randomUUID(),
77
+ onsessioninitialized: sessionId => {
78
+ sessions.set(sessionId, { transport, hostToolServer });
79
+ },
80
+ onsessionclosed: sessionId => {
81
+ sessions.delete(sessionId);
82
+ },
83
+ });
84
+ transport.onclose = () => {
85
+ const { sessionId } = transport;
86
+ if (sessionId != null) sessions.delete(sessionId);
87
+ };
88
+ await hostToolServer.server.connect(transport);
89
+ await transport.handleRequest(request, response, body);
90
+ }
91
+
92
+ return {
93
+ handleRequest: async ({ request, response, body }) => {
94
+ if (closed) {
95
+ respondWithJSONRPCError({
96
+ response,
97
+ status: 503,
98
+ code: -32000,
99
+ message: 'The host tool MCP endpoint is closed.',
100
+ });
101
+ return;
102
+ }
103
+ const sessionId = readSessionId({ request });
104
+ if (sessionId != null) {
105
+ const session = sessions.get(sessionId);
106
+ if (session == null) {
107
+ respondWithJSONRPCError({
108
+ response,
109
+ status: 404,
110
+ code: -32001,
111
+ message: 'Unknown host tool MCP session.',
112
+ });
113
+ return;
114
+ }
115
+ await session.transport.handleRequest(request, response, body);
116
+ return;
117
+ }
118
+ if (request.method !== 'POST' || !isInitializeRequest(body)) {
119
+ respondWithJSONRPCError({
120
+ response,
121
+ status: 400,
122
+ code: -32000,
123
+ message:
124
+ 'Host tool MCP requests without a session id must be an initialize request.',
125
+ });
126
+ return;
127
+ }
128
+ await openSession({ request, response, body });
129
+ },
130
+ updateCatalog: async ({ revision: nextRevision, tools: nextTools }) => {
131
+ catalog = { revision: nextRevision, tools: [...nextTools] };
132
+ await Promise.all(
133
+ [...sessions.values()].map(session =>
134
+ session.hostToolServer.updateCatalog({
135
+ revision: nextRevision,
136
+ tools: nextTools,
137
+ }),
138
+ ),
139
+ );
140
+ },
141
+ close: async () => {
142
+ if (closed) return;
143
+ closed = true;
144
+ const live = [...sessions.values()];
145
+ sessions.clear();
146
+ await Promise.all(
147
+ live.map(session => session.hostToolServer.server.close()),
148
+ );
149
+ },
150
+ };
151
+ }
152
+
153
+ function readSessionId({
154
+ request,
155
+ }: {
156
+ request: IncomingMessage;
157
+ }): string | undefined {
158
+ const value = request.headers['mcp-session-id'];
159
+ const sessionId = Array.isArray(value) ? value[0] : value;
160
+ return sessionId == null || sessionId.length === 0 ? undefined : sessionId;
161
+ }
162
+
163
+ function respondWithJSONRPCError({
164
+ response,
165
+ status,
166
+ code,
167
+ message,
168
+ }: {
169
+ response: ServerResponse;
170
+ status: number;
171
+ code: number;
172
+ message: string;
173
+ }): void {
174
+ response.writeHead(status, { 'content-type': 'application/json' });
175
+ response.end(
176
+ JSON.stringify({
177
+ jsonrpc: '2.0',
178
+ error: { code, message },
179
+ id: null,
180
+ }),
181
+ );
182
+ }