veryfront 0.1.144 → 0.1.145

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 (71) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/agent/ag-ui-handler.d.ts +77 -0
  3. package/esm/src/agent/ag-ui-handler.d.ts.map +1 -0
  4. package/esm/src/agent/ag-ui-handler.js +331 -0
  5. package/esm/src/agent/index.d.ts +2 -1
  6. package/esm/src/agent/index.d.ts.map +1 -1
  7. package/esm/src/agent/index.js +2 -1
  8. package/esm/src/agent/runtime/index.d.ts +2 -0
  9. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  10. package/esm/src/agent/runtime/index.js +15 -7
  11. package/esm/src/agent/runtime/resume-session.d.ts +61 -0
  12. package/esm/src/agent/runtime/resume-session.d.ts.map +1 -0
  13. package/esm/src/agent/runtime/resume-session.js +232 -0
  14. package/esm/src/agent/runtime/tool-helpers.d.ts +4 -2
  15. package/esm/src/agent/runtime/tool-helpers.d.ts.map +1 -1
  16. package/esm/src/agent/runtime/tool-helpers.js +57 -4
  17. package/esm/src/agent/types.d.ts +2 -1
  18. package/esm/src/agent/types.d.ts.map +1 -1
  19. package/esm/src/internal-agents/session-manager.d.ts +8 -24
  20. package/esm/src/internal-agents/session-manager.d.ts.map +1 -1
  21. package/esm/src/internal-agents/session-manager.js +55 -178
  22. package/esm/src/mcp/index.d.ts +2 -0
  23. package/esm/src/mcp/index.d.ts.map +1 -1
  24. package/esm/src/mcp/index.js +2 -0
  25. package/esm/src/mcp/server.d.ts +1 -0
  26. package/esm/src/mcp/server.d.ts.map +1 -1
  27. package/esm/src/mcp/server.js +47 -8
  28. package/esm/src/mcp/session.d.ts +9 -0
  29. package/esm/src/mcp/session.d.ts.map +1 -0
  30. package/esm/src/mcp/session.js +25 -0
  31. package/esm/src/mcp/sse.d.ts +8 -0
  32. package/esm/src/mcp/sse.d.ts.map +1 -0
  33. package/esm/src/mcp/sse.js +17 -0
  34. package/esm/src/platform/cloud/resolver.d.ts.map +1 -1
  35. package/esm/src/platform/cloud/resolver.js +13 -3
  36. package/esm/src/provider/index.d.ts +2 -0
  37. package/esm/src/provider/index.d.ts.map +1 -1
  38. package/esm/src/provider/index.js +1 -0
  39. package/esm/src/provider/veryfront-cloud/context.d.ts +10 -0
  40. package/esm/src/provider/veryfront-cloud/context.d.ts.map +1 -0
  41. package/esm/src/provider/veryfront-cloud/context.js +11 -0
  42. package/esm/src/tool/index.d.ts +3 -1
  43. package/esm/src/tool/index.d.ts.map +1 -1
  44. package/esm/src/tool/index.js +1 -0
  45. package/esm/src/tool/remote-mcp.d.ts +14 -0
  46. package/esm/src/tool/remote-mcp.d.ts.map +1 -0
  47. package/esm/src/tool/remote-mcp.js +176 -0
  48. package/esm/src/tool/types.d.ts +12 -0
  49. package/esm/src/tool/types.d.ts.map +1 -1
  50. package/esm/src/utils/version-constant.d.ts +1 -1
  51. package/esm/src/utils/version-constant.js +1 -1
  52. package/package.json +1 -1
  53. package/src/deno.js +1 -1
  54. package/src/src/agent/ag-ui-handler.ts +451 -0
  55. package/src/src/agent/index.ts +21 -1
  56. package/src/src/agent/runtime/index.ts +28 -3
  57. package/src/src/agent/runtime/resume-session.ts +319 -0
  58. package/src/src/agent/runtime/tool-helpers.ts +89 -6
  59. package/src/src/agent/types.ts +2 -1
  60. package/src/src/internal-agents/session-manager.ts +69 -243
  61. package/src/src/mcp/index.ts +3 -0
  62. package/src/src/mcp/server.ts +54 -9
  63. package/src/src/mcp/session.ts +31 -0
  64. package/src/src/mcp/sse.ts +19 -0
  65. package/src/src/platform/cloud/resolver.ts +15 -3
  66. package/src/src/provider/index.ts +5 -0
  67. package/src/src/provider/veryfront-cloud/context.ts +28 -0
  68. package/src/src/tool/index.ts +9 -1
  69. package/src/src/tool/remote-mcp.ts +261 -0
  70. package/src/src/tool/types.ts +17 -0
  71. package/src/src/utils/version-constant.ts +1 -1
@@ -1,4 +1,18 @@
1
1
  import * as dntShim from "../../_dnt.shims.js";
2
+ import {
3
+ RunAlreadyExistsError,
4
+ RunCancelledError,
5
+ RunNotActiveError,
6
+ RunResumeSessionManager,
7
+ type RunResumeSessionManagerOptions,
8
+ type RunSessionStatus,
9
+ type SubmitResumeValueOutcome,
10
+ WaitConflictError,
11
+ WaitNotPendingError,
12
+ } from "../agent/runtime/resume-session.js";
13
+
14
+ export { RunNotActiveError };
15
+
2
16
  function stableJsonStringify(value: unknown, depth = 0): string {
3
17
  if (depth > 64) {
4
18
  return JSON.stringify(value);
@@ -23,77 +37,48 @@ function createToolResultKey(result: unknown, isError: boolean): string {
23
37
  return `${isError ? "1" : "0"}:${stableJsonStringify(result)}`;
24
38
  }
25
39
 
26
- export class AgentRunCancelledError extends Error {
40
+ export class AgentRunCancelledError extends RunCancelledError {
27
41
  constructor(message = "Run cancelled") {
28
42
  super(message);
29
43
  this.name = "AgentRunCancelledError";
30
44
  }
31
45
  }
32
46
 
33
- export class AgentRunAlreadyExistsError extends Error {
47
+ export class AgentRunAlreadyExistsError extends RunAlreadyExistsError {
34
48
  constructor(runId: string) {
35
- super(`Run "${runId}" is already active`);
49
+ super(runId);
36
50
  this.name = "AgentRunAlreadyExistsError";
37
51
  }
38
52
  }
39
53
 
40
- export class RunNotActiveError extends Error {
41
- constructor(runId: string) {
42
- super(`Run "${runId}" is not active`);
43
- this.name = "RunNotActiveError";
44
- }
45
- }
46
-
47
- export class ToolResultNotWaitingError extends Error {
54
+ export class ToolResultNotWaitingError extends WaitNotPendingError {
48
55
  constructor(runId: string, toolCallId: string) {
49
- super(`Run "${runId}" is not waiting for tool call "${toolCallId}"`);
56
+ super(runId, toolCallId);
50
57
  this.name = "ToolResultNotWaitingError";
51
58
  }
52
59
  }
53
60
 
54
- export class ToolResultConflictError extends Error {
61
+ export class ToolResultConflictError extends WaitConflictError {
55
62
  constructor(runId: string, toolCallId: string) {
56
- super(`Conflicting tool result for run "${runId}" and tool call "${toolCallId}"`);
63
+ super(runId, toolCallId);
57
64
  this.name = "ToolResultConflictError";
58
65
  }
59
66
  }
60
67
 
61
68
  const DEFAULT_WAITING_FOR_TOOL_TTL_MS = 5 * 60 * 1000;
62
69
  const DEFAULT_SESSION_TTL_MS = 15 * 60 * 1000;
63
- const DEFAULT_MAX_CONCURRENT_SESSIONS = 100;
64
70
  const AGENT_RUN_SESSION_MANAGER_GLOBAL_KEY = "__veryfrontAgentRunSessionManager" as const;
65
71
 
66
- type SessionStatus = "running" | "waiting" | "completed" | "cancelled" | "failed";
67
-
68
72
  interface SubmittedToolResult {
69
73
  result: unknown;
70
74
  isError: boolean;
71
75
  key: string;
72
76
  }
73
-
74
- interface WaitingToolState {
75
- toolCallId: string;
76
- resolve: (value: SubmittedToolResult) => void;
77
- reject: (reason?: unknown) => void;
78
- }
79
-
80
- interface AgentRunSession {
81
- runId: string;
82
- status: SessionStatus;
83
- abortController: AbortController;
84
- waitingTool: WaitingToolState | null;
85
- submittedResults: Map<string, SubmittedToolResult>;
86
- waitingTimeoutId: number | null;
87
- sessionTimeoutId: number | null;
88
- }
89
-
90
- export interface SubmitToolResultOutcome {
91
- accepted: true;
92
- duplicate?: true;
93
- }
77
+ export type SessionStatus = RunSessionStatus;
78
+ export type SubmitToolResultOutcome = SubmitResumeValueOutcome;
94
79
 
95
80
  export class AgentRunSessionManager {
96
- private readonly sessions = new Map<string, AgentRunSession>();
81
+ private readonly sessions: RunResumeSessionManager<SubmittedToolResult>;
97
82
 
98
83
  constructor(
99
84
  private readonly options: {
@@ -103,250 +88,91 @@ export class AgentRunSessionManager {
103
88
  setTimeoutFn?: typeof dntShim.setTimeout;
104
89
  clearTimeoutFn?: typeof clearTimeout;
105
90
  } = {},
106
- ) {}
107
-
108
- private get waitingForToolTtlMs(): number {
109
- return this.options.waitingForToolTtlMs ?? DEFAULT_WAITING_FOR_TOOL_TTL_MS;
110
- }
111
-
112
- private get sessionTtlMs(): number | null {
113
- return this.options.sessionTtlMs ?? null;
114
- }
115
-
116
- private get setTimeoutFn(): typeof dntShim.setTimeout {
117
- return this.options.setTimeoutFn ?? dntShim.dntGlobalThis.setTimeout.bind(dntShim.dntGlobalThis);
118
- }
119
-
120
- private get clearTimeoutFn(): typeof clearTimeout {
121
- return this.options.clearTimeoutFn ?? globalThis.clearTimeout.bind(dntShim.dntGlobalThis);
122
- }
123
-
124
- private clearWaitingTimeout(session: AgentRunSession): void {
125
- if (session.waitingTimeoutId === null) {
126
- return;
127
- }
128
-
129
- this.clearTimeoutFn(session.waitingTimeoutId);
130
- session.waitingTimeoutId = null;
131
- }
132
-
133
- private clearSessionTimeout(session: AgentRunSession): void {
134
- if (session.sessionTimeoutId === null) {
135
- return;
136
- }
137
-
138
- this.clearTimeoutFn(session.sessionTimeoutId);
139
- session.sessionTimeoutId = null;
140
- }
141
-
142
- private scheduleSessionTimeout(session: AgentRunSession): void {
143
- if (this.sessionTtlMs === null) {
144
- return;
145
- }
146
-
147
- this.clearSessionTimeout(session);
148
- session.sessionTimeoutId = this.setTimeoutFn(() => {
149
- this.cancelRun(session.runId);
150
- }, this.sessionTtlMs) as unknown as number;
151
- }
152
-
153
- private scheduleWaitingTimeout(session: AgentRunSession): void {
154
- this.clearWaitingTimeout(session);
155
- session.waitingTimeoutId = this.setTimeoutFn(() => {
156
- this.cancelRun(session.runId);
157
- }, this.waitingForToolTtlMs) as unknown as number;
158
- }
159
-
160
- private touchSession(session: AgentRunSession): void {
161
- if (
162
- session.status === "running" || session.status === "waiting"
163
- ) {
164
- this.scheduleSessionTimeout(session);
165
- }
166
- }
167
-
168
- private finalizeSession(
169
- session: AgentRunSession,
170
- status: Exclude<SessionStatus, "running" | "waiting">,
171
- ): void {
172
- session.status = status;
173
- this.clearWaitingTimeout(session);
174
- this.clearSessionTimeout(session);
175
- session.waitingTool = null;
176
- this.sessions.delete(session.runId);
177
- }
178
-
179
- private get maxConcurrentSessions(): number {
180
- return this.options.maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
91
+ ) {
92
+ const managerOptions: RunResumeSessionManagerOptions<SubmittedToolResult> = {
93
+ waitingTtlMs: this.options.waitingForToolTtlMs ?? DEFAULT_WAITING_FOR_TOOL_TTL_MS,
94
+ sessionTtlMs: this.options.sessionTtlMs ?? null,
95
+ maxConcurrentSessions: this.options.maxConcurrentSessions,
96
+ setTimeoutFn: this.options.setTimeoutFn,
97
+ clearTimeoutFn: this.options.clearTimeoutFn,
98
+ getConflictKey: (value) => value.key,
99
+ };
100
+ this.sessions = new RunResumeSessionManager(managerOptions);
181
101
  }
182
102
 
183
103
  startRun(input: { runId: string; threadId: string }): AbortSignal {
184
- const existing = this.sessions.get(input.runId);
185
- if (existing && (existing.status === "running" || existing.status === "waiting")) {
186
- throw new AgentRunAlreadyExistsError(input.runId);
187
- }
188
-
189
- if (this.sessions.size >= this.maxConcurrentSessions) {
190
- throw new Error(
191
- `Maximum concurrent sessions (${this.maxConcurrentSessions}) reached`,
192
- );
104
+ try {
105
+ return this.sessions.startRun(input);
106
+ } catch (error) {
107
+ if (error instanceof RunAlreadyExistsError) {
108
+ throw new AgentRunAlreadyExistsError(input.runId);
109
+ }
110
+ throw error;
193
111
  }
194
-
195
- const session: AgentRunSession = {
196
- runId: input.runId,
197
- status: "running",
198
- abortController: new AbortController(),
199
- waitingTool: null,
200
- submittedResults: new Map(),
201
- waitingTimeoutId: null,
202
- sessionTimeoutId: null,
203
- };
204
-
205
- this.sessions.set(input.runId, session);
206
- this.touchSession(session);
207
- return session.abortController.signal;
208
112
  }
209
113
 
210
114
  async waitForToolResult(runId: string, toolCallId: string): Promise<{
211
115
  result: unknown;
212
116
  isError: boolean;
213
117
  }> {
214
- const session = this.sessions.get(runId);
215
- if (!session || session.status === "completed" || session.status === "failed") {
216
- throw new RunNotActiveError(runId);
217
- }
218
-
219
- if (session.abortController.signal.aborted || session.status === "cancelled") {
220
- throw new AgentRunCancelledError();
221
- }
222
-
223
- const existingResult = session.submittedResults.get(toolCallId);
224
- if (existingResult) {
225
- session.status = "running";
226
- this.touchSession(session);
227
- return { result: existingResult.result, isError: existingResult.isError };
228
- }
229
-
230
- if (session.waitingTool && session.waitingTool.toolCallId !== toolCallId) {
231
- throw new ToolResultNotWaitingError(runId, toolCallId);
118
+ try {
119
+ const value = await this.sessions.waitForSignal(runId, toolCallId);
120
+ return { result: value.result, isError: value.isError };
121
+ } catch (error) {
122
+ if (error instanceof RunCancelledError) {
123
+ throw new AgentRunCancelledError();
124
+ }
125
+ if (error instanceof WaitNotPendingError) {
126
+ throw new ToolResultNotWaitingError(runId, toolCallId);
127
+ }
128
+ throw error;
232
129
  }
233
-
234
- session.status = "waiting";
235
- this.scheduleWaitingTimeout(session);
236
- this.touchSession(session);
237
-
238
- return await new Promise<{ result: unknown; isError: boolean }>((resolve, reject) => {
239
- const abortHandler = () => {
240
- this.clearWaitingTimeout(session);
241
- session.waitingTool = null;
242
- session.status = "cancelled";
243
- reject(new AgentRunCancelledError());
244
- };
245
-
246
- session.abortController.signal.addEventListener("abort", abortHandler, { once: true });
247
- session.waitingTool = {
248
- toolCallId,
249
- resolve: (value) => {
250
- session.abortController.signal.removeEventListener("abort", abortHandler);
251
- this.clearWaitingTimeout(session);
252
- session.waitingTool = null;
253
- session.status = "running";
254
- this.touchSession(session);
255
- resolve({ result: value.result, isError: value.isError });
256
- },
257
- reject: (reason) => {
258
- session.abortController.signal.removeEventListener("abort", abortHandler);
259
- this.clearWaitingTimeout(session);
260
- session.waitingTool = null;
261
- reject(reason);
262
- },
263
- };
264
- });
265
130
  }
266
131
 
267
132
  submitToolResult(
268
133
  runId: string,
269
134
  input: { toolCallId: string; result: unknown; isError?: boolean },
270
135
  ): SubmitToolResultOutcome {
271
- const session = this.sessions.get(runId);
272
- if (!session) {
273
- throw new RunNotActiveError(runId);
274
- }
275
-
276
136
  const normalized: SubmittedToolResult = {
277
137
  result: input.result,
278
138
  isError: Boolean(input.isError),
279
139
  key: createToolResultKey(input.result, Boolean(input.isError)),
280
140
  };
281
141
 
282
- const existingResult = session.submittedResults.get(input.toolCallId);
283
- if (existingResult) {
284
- if (existingResult.key === normalized.key) {
285
- return { accepted: true, duplicate: true };
142
+ try {
143
+ return this.sessions.submitSignal(runId, {
144
+ waitKey: input.toolCallId,
145
+ value: normalized,
146
+ });
147
+ } catch (error) {
148
+ if (error instanceof WaitConflictError) {
149
+ throw new ToolResultConflictError(runId, input.toolCallId);
286
150
  }
287
-
288
- throw new ToolResultConflictError(runId, input.toolCallId);
289
- }
290
-
291
- if (
292
- session.status === "completed" || session.status === "failed" ||
293
- session.status === "cancelled"
294
- ) {
295
- throw new RunNotActiveError(runId);
296
- }
297
-
298
- if (!session.waitingTool || session.waitingTool.toolCallId !== input.toolCallId) {
299
- throw new ToolResultNotWaitingError(runId, input.toolCallId);
151
+ if (error instanceof WaitNotPendingError) {
152
+ throw new ToolResultNotWaitingError(runId, input.toolCallId);
153
+ }
154
+ throw error;
300
155
  }
301
-
302
- session.submittedResults.set(input.toolCallId, normalized);
303
- this.touchSession(session);
304
- session.waitingTool.resolve(normalized);
305
- return { accepted: true };
306
156
  }
307
157
 
308
158
  cancelRun(runId: string): boolean {
309
- const session = this.sessions.get(runId);
310
- if (!session) {
311
- return false;
312
- }
313
-
314
- if (
315
- session.status === "completed" || session.status === "failed" ||
316
- session.status === "cancelled"
317
- ) {
318
- return false;
319
- }
320
-
321
- const waitingTool = session.waitingTool;
322
- session.abortController.abort(new AgentRunCancelledError());
323
- waitingTool?.reject(new AgentRunCancelledError());
324
- this.finalizeSession(session, "cancelled");
325
- return true;
159
+ return this.sessions.cancelRun(runId);
326
160
  }
327
161
 
328
162
  completeRun(runId: string): void {
329
- const session = this.sessions.get(runId);
330
- if (!session) return;
331
- this.finalizeSession(session, "completed");
163
+ this.sessions.completeRun(runId);
332
164
  }
333
165
 
334
166
  failRun(runId: string): void {
335
- const session = this.sessions.get(runId);
336
- if (!session) return;
337
- this.finalizeSession(session, "failed");
167
+ this.sessions.failRun(runId);
338
168
  }
339
169
 
340
170
  getRunStatus(runId: string): SessionStatus | null {
341
- return this.sessions.get(runId)?.status ?? null;
171
+ return this.sessions.getRunStatus(runId);
342
172
  }
343
173
 
344
174
  reset(): void {
345
- for (const session of this.sessions.values()) {
346
- this.clearWaitingTimeout(session);
347
- this.clearSessionTimeout(session);
348
- }
349
- this.sessions.clear();
175
+ this.sessions.reset();
350
176
  }
351
177
  }
352
178
 
@@ -42,3 +42,6 @@ export {
42
42
  } from "./registry.js";
43
43
 
44
44
  export { createMCPServer, type IntegrationLoaderConfig, MCPServer } from "./server.js";
45
+
46
+ export { formatSSEEvent, formatSSEPrimingEvent, formatSSERetry } from "./sse.js";
47
+ export { SessionManager } from "./session.js";
@@ -13,6 +13,7 @@ import { validateContentType } from "../security/input-validation/limits.js";
13
13
  import { VeryfrontError } from "../security/input-validation/errors.js";
14
14
  import type { IntegrationRuntimeConfig } from "../integrations/types.js";
15
15
  import { logger as baseLogger } from "../utils/index.js";
16
+ import { SessionManager } from "./session.js";
16
17
 
17
18
  const logger = baseLogger.component("mcp-server");
18
19
 
@@ -112,6 +113,7 @@ export class MCPServer {
112
113
  private config: MCPServerConfig;
113
114
  private integrationLoader?: IntegrationLoaderConfig;
114
115
  private integrationsLoaded = false;
116
+ private sessionManager = new SessionManager();
115
117
 
116
118
  constructor(config: MCPServerConfig) {
117
119
  this.config = config;
@@ -448,21 +450,37 @@ export class MCPServer {
448
450
  createHTTPHandler(): (request: dntShim.Request) => Promise<dntShim.Response> {
449
451
  return async (request: dntShim.Request) => {
450
452
  const requestOrigin = request.headers.get("Origin");
453
+
454
+ // CORS preflight
451
455
  if (request.method === "OPTIONS") {
452
456
  return new dntShim.Response(null, { status: 204, headers: this.getCORSHeaders(requestOrigin) });
453
457
  }
454
458
 
459
+ // Origin validation (DNS rebinding protection)
455
460
  if (requestOrigin && this.config.cors?.enabled && this.config.cors.origins?.length) {
456
461
  if (!this.config.cors.origins.includes(requestOrigin)) {
457
462
  return createJSONRPCErrorResponse(403, -32600, "Forbidden: Origin not allowed");
458
463
  }
459
464
  }
460
465
 
466
+ // Auth check (applies to all methods including DELETE)
461
467
  if (this.config.auth?.type && this.config.auth.type !== "none") {
462
468
  const authorized = await this.validateAuth(request);
463
469
  if (!authorized) return new dntShim.Response("Unauthorized", { status: 401 });
464
470
  }
465
471
 
472
+ // DELETE = terminate session
473
+ if (request.method === "DELETE") {
474
+ const sessionId = request.headers.get("MCP-Session-Id");
475
+ if (sessionId) this.sessionManager.terminate(sessionId);
476
+ return new dntShim.Response(null, { status: 200, headers: this.getCORSHeaders(requestOrigin) });
477
+ }
478
+
479
+ // Only POST allowed for JSON-RPC messages
480
+ if (request.method !== "POST") {
481
+ return new dntShim.Response("Method Not Allowed", { status: 405 });
482
+ }
483
+
466
484
  // Enforce request body size limit (fast path via Content-Length header)
467
485
  const contentLength = request.headers.get("content-length");
468
486
  if (contentLength && Number(contentLength) > MAX_REQUEST_BODY_SIZE) {
@@ -488,15 +506,41 @@ export class MCPServer {
488
506
  return createJSONRPCErrorResponse(400, -32700, "Parse error");
489
507
  }
490
508
 
491
- // Extract end-user identity from request headers for per-user token flows
509
+ // Session management: initialize creates session, everything else requires it
510
+ const responseHeaders: Record<string, string> = {
511
+ ...this.getCORSHeaders(requestOrigin),
512
+ };
513
+
514
+ if (rpcRequest.method === "initialize") {
515
+ const context = this.extractRequestContext(request);
516
+ const rpcResponse = await this.handleRequest(rpcRequest, context);
517
+ const sessionId = this.sessionManager.create();
518
+ responseHeaders["MCP-Session-Id"] = sessionId;
519
+ return createJSONResponse(rpcResponse, { headers: responseHeaders });
520
+ }
521
+
522
+ // Post-init: require session ID when sessions are active
523
+ if (this.sessionManager.size > 0) {
524
+ const sessionId = request.headers.get("MCP-Session-Id");
525
+ if (!sessionId) {
526
+ return createJSONRPCErrorResponse(400, -32600, "Missing MCP-Session-Id header");
527
+ }
528
+ if (!this.sessionManager.isValid(sessionId)) {
529
+ return createJSONRPCErrorResponse(404, -32600, "Session not found or expired");
530
+ }
531
+ }
532
+
533
+ // Notifications have no id member — return 202 Accepted
534
+ // Note: id:0 is a valid request ID per JSON-RPC 2.0, so check for undefined
535
+ if (rpcRequest.id === undefined) {
536
+ const context = this.extractRequestContext(request);
537
+ await this.handleRequest(rpcRequest, context);
538
+ return new dntShim.Response(null, { status: 202, headers: responseHeaders });
539
+ }
540
+
492
541
  const context = this.extractRequestContext(request);
493
542
  const rpcResponse = await this.handleRequest(rpcRequest, context);
494
-
495
- return createJSONResponse(rpcResponse, {
496
- headers: {
497
- ...this.getCORSHeaders(requestOrigin),
498
- },
499
- });
543
+ return createJSONResponse(rpcResponse, { headers: responseHeaders });
500
544
  };
501
545
  }
502
546
 
@@ -553,8 +597,9 @@ export class MCPServer {
553
597
 
554
598
  return {
555
599
  "Access-Control-Allow-Origin": matchedOrigin,
556
- "Access-Control-Allow-Methods": "POST, OPTIONS",
557
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-End-User-Id, X-Project-Id",
600
+ "Access-Control-Allow-Methods": "POST, GET, DELETE, OPTIONS",
601
+ "Access-Control-Allow-Headers":
602
+ "Content-Type, Authorization, MCP-Session-Id, X-End-User-Id, X-Project-Id",
558
603
  "Vary": "Origin",
559
604
  };
560
605
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Manages MCP sessions for the Streamable HTTP transport.
3
+ * Sessions are created during initialization and validated on subsequent requests.
4
+ */
5
+ import * as dntShim from "../../_dnt.shims.js";
6
+
7
+ export class SessionManager {
8
+ private sessions = new Set<string>();
9
+
10
+ create(): string {
11
+ const id = dntShim.crypto.randomUUID();
12
+ this.sessions.add(id);
13
+ return id;
14
+ }
15
+
16
+ isValid(id: string): boolean {
17
+ return this.sessions.has(id);
18
+ }
19
+
20
+ terminate(id: string): void {
21
+ this.sessions.delete(id);
22
+ }
23
+
24
+ get size(): number {
25
+ return this.sessions.size;
26
+ }
27
+
28
+ clear(): void {
29
+ this.sessions.clear();
30
+ }
31
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Stateless SSE formatting utilities per the Server-Sent Events standard.
3
+ * Used by the Streamable HTTP transport for MCP.
4
+ */
5
+
6
+ export function formatSSEEvent(data: unknown, id?: string): string {
7
+ let event = "";
8
+ if (id) event += `id: ${id}\n`;
9
+ event += `data: ${JSON.stringify(data)}\n\n`;
10
+ return event;
11
+ }
12
+
13
+ export function formatSSERetry(ms: number): string {
14
+ return `retry: ${ms}\n\n`;
15
+ }
16
+
17
+ export function formatSSEPrimingEvent(id: string): string {
18
+ return `id: ${id}\ndata: \n\n`;
19
+ }
@@ -2,6 +2,7 @@ import { getRuntimeConfig, isRuntimeConfigInitialized } from "../../config/index
2
2
  import { getApiBaseUrlEnv } from "../../config/env.js";
3
3
  import { getCurrentRequestContext } from "../adapters/fs/veryfront/multi-project-adapter.js";
4
4
  import { getEnv } from "../compat/process.js";
5
+ import { getCurrentVeryfrontCloudContext } from "../../provider/veryfront-cloud/context.js";
5
6
 
6
7
  export const DEFAULT_VERYFRONT_CLOUD_MODEL = "veryfront-cloud/anthropic/claude-sonnet-4-6";
7
8
  export const DEFAULT_VERYFRONT_CLOUD_EMBEDDING_MODEL =
@@ -39,19 +40,28 @@ function normalizeCloudModelString(value: string | undefined, fallback: string):
39
40
  return resolved.startsWith("veryfront-cloud/") ? resolved : `veryfront-cloud/${resolved}`;
40
41
  }
41
42
 
43
+ function normalizeServiceLayer(value: string | undefined): string | undefined {
44
+ const normalized = value?.trim().toLowerCase();
45
+ return normalized?.length ? normalized : undefined;
46
+ }
47
+
42
48
  function getResolvedVeryfrontCloudContext(): Omit<VeryfrontCloudBootstrap, "apiBaseUrl"> {
43
49
  const requestContext = getCurrentRequestContext();
50
+ const scopedContext = getCurrentVeryfrontCloudContext();
44
51
  const runtimeBootstrap = getRuntimeBootstrap();
45
52
 
46
53
  return {
47
54
  apiToken: requestContext?.token ??
55
+ scopedContext?.apiToken ??
48
56
  getEnv("VERYFRONT_API_TOKEN") ??
49
57
  runtimeBootstrap.apiToken,
50
58
  projectSlug: requestContext?.projectSlug ??
59
+ scopedContext?.projectSlug ??
51
60
  getEnv("VERYFRONT_PROJECT_SLUG") ??
52
61
  runtimeBootstrap.projectSlug,
53
- serviceLayer: getEnv("VERYFRONT_SERVICE_LAYER")?.trim().toLowerCase(),
54
- hasRequestContext: requestContext !== null,
62
+ serviceLayer: normalizeServiceLayer(scopedContext?.serviceLayer) ??
63
+ normalizeServiceLayer(getEnv("VERYFRONT_SERVICE_LAYER")),
64
+ hasRequestContext: requestContext !== null || scopedContext !== undefined,
55
65
  usesVeryfrontFs: runtimeBootstrap.usesVeryfrontFs,
56
66
  };
57
67
  }
@@ -65,8 +75,10 @@ export function getVeryfrontCloudProjectSlug(): string | undefined {
65
75
  }
66
76
 
67
77
  export function getVeryfrontCloudBootstrap(): VeryfrontCloudBootstrap {
78
+ const scopedContext = getCurrentVeryfrontCloudContext();
79
+
68
80
  return {
69
- apiBaseUrl: getApiBaseUrlEnv(),
81
+ apiBaseUrl: scopedContext?.apiBaseUrl?.trim() || getApiBaseUrlEnv(),
70
82
  ...getResolvedVeryfrontCloudContext(),
71
83
  };
72
84
  }
@@ -28,3 +28,8 @@ export {
28
28
  resolveModel,
29
29
  } from "./model-registry.js";
30
30
  export type { ModelProviderFactory } from "./model-registry.js";
31
+ export {
32
+ runWithVeryfrontCloudContext,
33
+ runWithVeryfrontCloudContextAsync,
34
+ } from "./veryfront-cloud/context.js";
35
+ export type { VeryfrontCloudContext } from "./veryfront-cloud/context.js";
@@ -0,0 +1,28 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ export interface VeryfrontCloudContext {
4
+ apiBaseUrl?: string;
5
+ apiToken?: string;
6
+ projectSlug?: string;
7
+ serviceLayer?: string;
8
+ }
9
+
10
+ const veryfrontCloudContextStorage = new AsyncLocalStorage<VeryfrontCloudContext>();
11
+
12
+ export function runWithVeryfrontCloudContext<T>(
13
+ context: VeryfrontCloudContext,
14
+ fn: () => T,
15
+ ): T {
16
+ return veryfrontCloudContextStorage.run(context, fn);
17
+ }
18
+
19
+ export function runWithVeryfrontCloudContextAsync<T>(
20
+ context: VeryfrontCloudContext,
21
+ fn: () => Promise<T>,
22
+ ): Promise<T> {
23
+ return veryfrontCloudContextStorage.run(context, fn);
24
+ }
25
+
26
+ export function getCurrentVeryfrontCloudContext(): VeryfrontCloudContext | undefined {
27
+ return veryfrontCloudContextStorage.getStore();
28
+ }
@@ -45,10 +45,18 @@
45
45
  import "../../_dnt.polyfills.js";
46
46
 
47
47
 
48
- export type { Tool, ToolConfig, ToolDefinition, ToolExecutionContext } from "./types.js";
48
+ export type {
49
+ RemoteToolSource,
50
+ Tool,
51
+ ToolConfig,
52
+ ToolDefinition,
53
+ ToolExecutionContext,
54
+ } from "./types.js";
49
55
 
50
56
  export { dynamicTool, tool } from "./factory.js";
51
57
  export type { DynamicToolConfig } from "./factory.js";
58
+ export { createRemoteMCPToolSource } from "./remote-mcp.js";
59
+ export type { RemoteMCPToolSourceConfig } from "./remote-mcp.js";
52
60
 
53
61
  export { toolRegistry } from "./registry.js";
54
62