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.
- package/esm/deno.js +1 -1
- package/esm/src/agent/ag-ui-handler.d.ts +77 -0
- package/esm/src/agent/ag-ui-handler.d.ts.map +1 -0
- package/esm/src/agent/ag-ui-handler.js +331 -0
- package/esm/src/agent/index.d.ts +2 -1
- package/esm/src/agent/index.d.ts.map +1 -1
- package/esm/src/agent/index.js +2 -1
- package/esm/src/agent/runtime/index.d.ts +2 -0
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +15 -7
- package/esm/src/agent/runtime/resume-session.d.ts +61 -0
- package/esm/src/agent/runtime/resume-session.d.ts.map +1 -0
- package/esm/src/agent/runtime/resume-session.js +232 -0
- package/esm/src/agent/runtime/tool-helpers.d.ts +4 -2
- package/esm/src/agent/runtime/tool-helpers.d.ts.map +1 -1
- package/esm/src/agent/runtime/tool-helpers.js +57 -4
- package/esm/src/agent/types.d.ts +2 -1
- package/esm/src/agent/types.d.ts.map +1 -1
- package/esm/src/internal-agents/session-manager.d.ts +8 -24
- package/esm/src/internal-agents/session-manager.d.ts.map +1 -1
- package/esm/src/internal-agents/session-manager.js +55 -178
- package/esm/src/mcp/index.d.ts +2 -0
- package/esm/src/mcp/index.d.ts.map +1 -1
- package/esm/src/mcp/index.js +2 -0
- package/esm/src/mcp/server.d.ts +1 -0
- package/esm/src/mcp/server.d.ts.map +1 -1
- package/esm/src/mcp/server.js +47 -8
- package/esm/src/mcp/session.d.ts +9 -0
- package/esm/src/mcp/session.d.ts.map +1 -0
- package/esm/src/mcp/session.js +25 -0
- package/esm/src/mcp/sse.d.ts +8 -0
- package/esm/src/mcp/sse.d.ts.map +1 -0
- package/esm/src/mcp/sse.js +17 -0
- package/esm/src/platform/cloud/resolver.d.ts.map +1 -1
- package/esm/src/platform/cloud/resolver.js +13 -3
- package/esm/src/provider/index.d.ts +2 -0
- package/esm/src/provider/index.d.ts.map +1 -1
- package/esm/src/provider/index.js +1 -0
- package/esm/src/provider/veryfront-cloud/context.d.ts +10 -0
- package/esm/src/provider/veryfront-cloud/context.d.ts.map +1 -0
- package/esm/src/provider/veryfront-cloud/context.js +11 -0
- package/esm/src/tool/index.d.ts +3 -1
- package/esm/src/tool/index.d.ts.map +1 -1
- package/esm/src/tool/index.js +1 -0
- package/esm/src/tool/remote-mcp.d.ts +14 -0
- package/esm/src/tool/remote-mcp.d.ts.map +1 -0
- package/esm/src/tool/remote-mcp.js +176 -0
- package/esm/src/tool/types.d.ts +12 -0
- package/esm/src/tool/types.d.ts.map +1 -1
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
- package/src/deno.js +1 -1
- package/src/src/agent/ag-ui-handler.ts +451 -0
- package/src/src/agent/index.ts +21 -1
- package/src/src/agent/runtime/index.ts +28 -3
- package/src/src/agent/runtime/resume-session.ts +319 -0
- package/src/src/agent/runtime/tool-helpers.ts +89 -6
- package/src/src/agent/types.ts +2 -1
- package/src/src/internal-agents/session-manager.ts +69 -243
- package/src/src/mcp/index.ts +3 -0
- package/src/src/mcp/server.ts +54 -9
- package/src/src/mcp/session.ts +31 -0
- package/src/src/mcp/sse.ts +19 -0
- package/src/src/platform/cloud/resolver.ts +15 -3
- package/src/src/provider/index.ts +5 -0
- package/src/src/provider/veryfront-cloud/context.ts +28 -0
- package/src/src/tool/index.ts +9 -1
- package/src/src/tool/remote-mcp.ts +261 -0
- package/src/src/tool/types.ts +17 -0
- 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
|
|
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
|
|
47
|
+
export class AgentRunAlreadyExistsError extends RunAlreadyExistsError {
|
|
34
48
|
constructor(runId: string) {
|
|
35
|
-
super(
|
|
49
|
+
super(runId);
|
|
36
50
|
this.name = "AgentRunAlreadyExistsError";
|
|
37
51
|
}
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
export class
|
|
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(
|
|
56
|
+
super(runId, toolCallId);
|
|
50
57
|
this.name = "ToolResultNotWaitingError";
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
export class ToolResultConflictError extends
|
|
61
|
+
export class ToolResultConflictError extends WaitConflictError {
|
|
55
62
|
constructor(runId: string, toolCallId: string) {
|
|
56
|
-
super(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
throw
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
171
|
+
return this.sessions.getRunStatus(runId);
|
|
342
172
|
}
|
|
343
173
|
|
|
344
174
|
reset(): void {
|
|
345
|
-
|
|
346
|
-
this.clearWaitingTimeout(session);
|
|
347
|
-
this.clearSessionTimeout(session);
|
|
348
|
-
}
|
|
349
|
-
this.sessions.clear();
|
|
175
|
+
this.sessions.reset();
|
|
350
176
|
}
|
|
351
177
|
}
|
|
352
178
|
|
package/src/src/mcp/index.ts
CHANGED
|
@@ -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";
|
package/src/src/mcp/server.ts
CHANGED
|
@@ -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
|
-
//
|
|
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":
|
|
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:
|
|
54
|
-
|
|
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
|
+
}
|
package/src/src/tool/index.ts
CHANGED
|
@@ -45,10 +45,18 @@
|
|
|
45
45
|
import "../../_dnt.polyfills.js";
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
export type {
|
|
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
|
|