veryfront 0.1.146 → 0.1.147

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 (42) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/internal-agents/run-stream.d.ts.map +1 -1
  3. package/esm/src/internal-agents/run-stream.js +60 -79
  4. package/esm/src/internal-agents/schema.d.ts +243 -4
  5. package/esm/src/internal-agents/schema.d.ts.map +1 -1
  6. package/esm/src/internal-agents/schema.js +219 -8
  7. package/esm/src/jobs/schemas.d.ts +4 -4
  8. package/esm/src/mcp/elicitation.d.ts +16 -0
  9. package/esm/src/mcp/elicitation.d.ts.map +1 -0
  10. package/esm/src/mcp/elicitation.js +21 -0
  11. package/esm/src/mcp/index.d.ts +3 -0
  12. package/esm/src/mcp/index.d.ts.map +1 -1
  13. package/esm/src/mcp/index.js +2 -0
  14. package/esm/src/mcp/server.d.ts +22 -0
  15. package/esm/src/mcp/server.d.ts.map +1 -1
  16. package/esm/src/mcp/server.js +163 -2
  17. package/esm/src/mcp/task-store.d.ts +27 -0
  18. package/esm/src/mcp/task-store.d.ts.map +1 -0
  19. package/esm/src/mcp/task-store.js +116 -0
  20. package/esm/src/modules/react-loader/ssr-module-loader/cache/memory.d.ts.map +1 -1
  21. package/esm/src/modules/react-loader/ssr-module-loader/cache/memory.js +4 -2
  22. package/esm/src/server/handlers/request/agent-stream.handler.d.ts.map +1 -1
  23. package/esm/src/server/handlers/request/agent-stream.handler.js +4 -3
  24. package/esm/src/tool/remote-mcp.d.ts.map +1 -1
  25. package/esm/src/tool/remote-mcp.js +60 -1
  26. package/esm/src/tool/types.d.ts +2 -0
  27. package/esm/src/tool/types.d.ts.map +1 -1
  28. package/esm/src/utils/version-constant.d.ts +1 -1
  29. package/esm/src/utils/version-constant.js +1 -1
  30. package/package.json +1 -1
  31. package/src/deno.js +1 -1
  32. package/src/src/internal-agents/run-stream.ts +61 -94
  33. package/src/src/internal-agents/schema.ts +277 -10
  34. package/src/src/mcp/elicitation.ts +42 -0
  35. package/src/src/mcp/index.ts +9 -0
  36. package/src/src/mcp/server.ts +185 -2
  37. package/src/src/mcp/task-store.ts +137 -0
  38. package/src/src/modules/react-loader/ssr-module-loader/cache/memory.ts +4 -2
  39. package/src/src/server/handlers/request/agent-stream.handler.ts +5 -3
  40. package/src/src/tool/remote-mcp.ts +86 -1
  41. package/src/src/tool/types.ts +2 -0
  42. package/src/src/utils/version-constant.ts +1 -1
@@ -79,21 +79,105 @@ export const RuntimeAgentSourceContextSchema = z.discriminatedUnion("type", [
79
79
  }),
80
80
  ]);
81
81
 
82
+ const RuntimeMessageExtensionFieldsSchema = {
83
+ name: z.string().max(256).optional(),
84
+ metadata: z.record(z.string(), z.unknown()).optional(),
85
+ createdAt: z.string().optional(),
86
+ } as const;
87
+
88
+ export const RuntimeToolFunctionCallSchema = z.object({
89
+ name: ClientToolNameSchema,
90
+ arguments: z.string().max(MAX_TOOL_PARAMETERS_BYTES),
91
+ }).strict();
92
+
93
+ export const RuntimeToolCallSchema = z.object({
94
+ id: z.string().min(1).max(128),
95
+ type: z.literal("function"),
96
+ function: RuntimeToolFunctionCallSchema,
97
+ }).strict();
98
+
99
+ export const RuntimeSystemMessageSchema = z.object({
100
+ id: z.string().min(1),
101
+ role: z.literal("system"),
102
+ content: z.string(),
103
+ ...RuntimeMessageExtensionFieldsSchema,
104
+ }).strict();
105
+
106
+ export const RuntimeUserMessageSchema = z.object({
107
+ id: z.string().min(1),
108
+ role: z.literal("user"),
109
+ content: z.string(),
110
+ ...RuntimeMessageExtensionFieldsSchema,
111
+ }).strict();
112
+
113
+ export const RuntimeAssistantMessageSchema = z.object({
114
+ id: z.string().min(1),
115
+ role: z.literal("assistant"),
116
+ content: z.string().optional(),
117
+ toolCalls: z.array(RuntimeToolCallSchema).optional(),
118
+ ...RuntimeMessageExtensionFieldsSchema,
119
+ }).strict();
120
+
121
+ export const RuntimeToolMessageSchema = z.object({
122
+ id: z.string().min(1),
123
+ role: z.literal("tool"),
124
+ toolCallId: z.string().min(1).max(128),
125
+ content: z.string(),
126
+ error: z.string().optional(),
127
+ ...RuntimeMessageExtensionFieldsSchema,
128
+ }).strict();
129
+
130
+ export const RuntimeMessageSchema = z.discriminatedUnion("role", [
131
+ RuntimeSystemMessageSchema,
132
+ RuntimeUserMessageSchema,
133
+ RuntimeAssistantMessageSchema,
134
+ RuntimeToolMessageSchema,
135
+ ]);
136
+
137
+ export const RuntimeContextSchema = z.union([
138
+ z.object({
139
+ description: z.string().max(1024),
140
+ value: z.string().max(MAX_CONTEXT_ITEM_BYTES),
141
+ }),
142
+ RuntimeContextItemSchema,
143
+ ]);
144
+
82
145
  export const RuntimeRunAgentInputSchema = z.object({
146
+ threadId: z.string().uuid(),
147
+ runId: RunIdSchema,
148
+ parentRunId: RunIdSchema.optional(),
149
+ state: z.unknown().optional(),
150
+ messages: z.array(RuntimeMessageSchema).max(MAX_RUNTIME_MESSAGES),
151
+ tools: z.array(RuntimeInjectedToolSchema).max(50).default([]),
152
+ context: z.array(RuntimeContextSchema).max(10).default([]).refine(
153
+ (value) => isWithinJsonSizeLimit(value, MAX_CONTEXT_TOTAL_BYTES),
154
+ { message: "context must be less than 64 KB total" },
155
+ ),
156
+ forwardedProps: z.record(z.string(), z.unknown()).optional().refine(
157
+ (value) => value === undefined || isWithinJsonSizeLimit(value, MAX_FORWARDED_PROPS_BYTES),
158
+ { message: "forwardedProps must be less than 64 KB" },
159
+ ),
160
+ });
161
+
162
+ export const InternalAgentCompatibilityMessageSchema = z.object({
163
+ id: z.string().min(1),
164
+ role: z.enum(["user", "assistant", "system", "tool"]),
165
+ parts: z.array(z.object({ type: z.string().min(1) }).passthrough()).default([]),
166
+ metadata: z.record(z.string(), z.unknown()).optional(),
167
+ createdAt: z.string().optional(),
168
+ });
169
+
170
+ export const InternalAgentStreamRequestSchema = z.object({
83
171
  agentId: AgentIdSchema,
84
172
  threadId: z.string().uuid(),
85
173
  runId: RunIdSchema,
86
- messages: z.array(
87
- z.object({
88
- id: z.string().min(1),
89
- role: z.enum(["user", "assistant", "system", "tool"]),
90
- parts: z.array(z.object({ type: z.string().min(1) }).passthrough()).default([]),
91
- metadata: z.record(z.string(), z.unknown()).optional(),
92
- createdAt: z.string().optional(),
93
- }),
94
- ).max(MAX_RUNTIME_MESSAGES),
174
+ parentRunId: RunIdSchema.optional(),
175
+ state: z.unknown().optional(),
176
+ messages: z.array(z.union([RuntimeMessageSchema, InternalAgentCompatibilityMessageSchema])).max(
177
+ MAX_RUNTIME_MESSAGES,
178
+ ),
95
179
  tools: z.array(RuntimeInjectedToolSchema).max(50).default([]),
96
- context: z.array(RuntimeContextItemSchema).max(10).default([]).refine(
180
+ context: z.array(RuntimeContextSchema).max(10).default([]).refine(
97
181
  (value) => isWithinJsonSizeLimit(value, MAX_CONTEXT_TOTAL_BYTES),
98
182
  { message: "context must be less than 64 KB total" },
99
183
  ),
@@ -104,6 +188,188 @@ export const RuntimeRunAgentInputSchema = z.object({
104
188
  ),
105
189
  });
106
190
 
191
+ type RuntimeMessage = z.infer<typeof RuntimeMessageSchema>;
192
+ type InternalAgentCompatibilityMessage = z.infer<typeof InternalAgentCompatibilityMessageSchema>;
193
+
194
+ function extractToolArgs(
195
+ part: Record<string, unknown>,
196
+ ): Record<string, unknown> {
197
+ const args = part.args;
198
+ if (args && typeof args === "object" && !Array.isArray(args)) {
199
+ return args as Record<string, unknown>;
200
+ }
201
+
202
+ const input = part.input;
203
+ if (input && typeof input === "object" && !Array.isArray(input)) {
204
+ return input as Record<string, unknown>;
205
+ }
206
+
207
+ return {};
208
+ }
209
+
210
+ function serializeToolArguments(args: Record<string, unknown>): string {
211
+ try {
212
+ return JSON.stringify(args);
213
+ } catch {
214
+ return "{}";
215
+ }
216
+ }
217
+
218
+ function getPartString(
219
+ part: Record<string, unknown>,
220
+ ...keys: string[]
221
+ ): string | null {
222
+ for (const key of keys) {
223
+ const value = part[key];
224
+ if (typeof value === "string" && value.length > 0) {
225
+ return value;
226
+ }
227
+ }
228
+
229
+ return null;
230
+ }
231
+
232
+ function isLegacyToolCallPart(part: Record<string, unknown>): boolean {
233
+ return getPartString(part, "type") === "tool_call";
234
+ }
235
+
236
+ function isCanonicalToolCallPart(part: Record<string, unknown>): boolean {
237
+ const type = getPartString(part, "type");
238
+
239
+ return type === "tool-call" ||
240
+ (typeof type === "string" && type.startsWith("tool-") && type !== "tool-result" &&
241
+ type !== "tool_result");
242
+ }
243
+
244
+ function getToolCallShape(
245
+ part: Record<string, unknown>,
246
+ ): z.infer<typeof RuntimeToolCallSchema> | null {
247
+ const id = getPartString(part, "toolCallId", "tool_call_id", "id");
248
+ const name = getPartString(part, "toolName", "tool_name", "name");
249
+
250
+ if (!id || !name) {
251
+ return null;
252
+ }
253
+
254
+ return {
255
+ id,
256
+ type: "function",
257
+ function: {
258
+ name,
259
+ arguments: serializeToolArguments(extractToolArgs(part)),
260
+ },
261
+ };
262
+ }
263
+
264
+ function isToolResultPart(part: Record<string, unknown>): boolean {
265
+ const type = getPartString(part, "type");
266
+ return type === "tool-result" || type === "tool_result";
267
+ }
268
+
269
+ function stringifyToolResult(result: unknown): string {
270
+ if (typeof result === "string") {
271
+ return result;
272
+ }
273
+
274
+ try {
275
+ return JSON.stringify(result);
276
+ } catch {
277
+ return String(result);
278
+ }
279
+ }
280
+
281
+ function toRuntimeMessage(
282
+ message: RuntimeMessage | InternalAgentCompatibilityMessage,
283
+ ): RuntimeMessage {
284
+ if (!("parts" in message)) {
285
+ return message;
286
+ }
287
+
288
+ const textContent = message.parts
289
+ .filter((part) => part.type === "text" && typeof part.text === "string")
290
+ .map((part) => part.text)
291
+ .join("\n");
292
+
293
+ const sharedFields = {
294
+ ...(message.metadata ? { metadata: message.metadata } : {}),
295
+ ...(message.createdAt ? { createdAt: message.createdAt } : {}),
296
+ };
297
+
298
+ switch (message.role) {
299
+ case "system":
300
+ return {
301
+ id: message.id,
302
+ role: "system",
303
+ content: textContent,
304
+ ...sharedFields,
305
+ };
306
+ case "user":
307
+ return {
308
+ id: message.id,
309
+ role: "user",
310
+ content: textContent,
311
+ ...sharedFields,
312
+ };
313
+ case "assistant": {
314
+ const toolCalls = message.parts.flatMap((part) => {
315
+ if (!isCanonicalToolCallPart(part) && !isLegacyToolCallPart(part)) {
316
+ return [];
317
+ }
318
+
319
+ const toolCall = getToolCallShape(part);
320
+ return toolCall ? [toolCall] : [];
321
+ });
322
+
323
+ return {
324
+ id: message.id,
325
+ role: "assistant",
326
+ ...(textContent ? { content: textContent } : {}),
327
+ ...(toolCalls.length ? { toolCalls } : {}),
328
+ ...sharedFields,
329
+ };
330
+ }
331
+ case "tool": {
332
+ const toolResultPart = message.parts.find(
333
+ (part) =>
334
+ isToolResultPart(part) && getPartString(part, "toolCallId", "tool_call_id") !== null,
335
+ );
336
+ const toolCallId = toolResultPart
337
+ ? getPartString(toolResultPart, "toolCallId", "tool_call_id")
338
+ : null;
339
+ const toolResult = toolResultPart && "result" in toolResultPart
340
+ ? toolResultPart.result
341
+ : toolResultPart && "output" in toolResultPart
342
+ ? toolResultPart.output
343
+ : undefined;
344
+ const toolError = toolResultPart ? getPartString(toolResultPart, "error") : null;
345
+
346
+ return {
347
+ id: message.id,
348
+ role: "tool",
349
+ toolCallId: toolCallId ?? message.id,
350
+ content: toolResult !== undefined ? stringifyToolResult(toolResult) : textContent,
351
+ ...(toolError ? { error: toolError } : {}),
352
+ ...sharedFields,
353
+ };
354
+ }
355
+ }
356
+ }
357
+
358
+ export function toRuntimeRunAgentInput(
359
+ input: z.infer<typeof InternalAgentStreamRequestSchema>,
360
+ ): z.infer<typeof RuntimeRunAgentInputSchema> {
361
+ return {
362
+ threadId: input.threadId,
363
+ runId: input.runId,
364
+ ...(input.parentRunId ? { parentRunId: input.parentRunId } : {}),
365
+ ...(input.state !== undefined ? { state: input.state } : {}),
366
+ messages: input.messages.map(toRuntimeMessage),
367
+ tools: input.tools,
368
+ context: input.context,
369
+ ...(input.forwardedProps ? { forwardedProps: input.forwardedProps } : {}),
370
+ };
371
+ }
372
+
107
373
  export const ResumeSignalSchema = z.discriminatedUnion("type", [
108
374
  z.object({
109
375
  type: z.literal("tool_result"),
@@ -120,4 +386,5 @@ export type RuntimeInjectedTool = z.infer<typeof RuntimeInjectedToolSchema>;
120
386
  export type RuntimeContextItem = z.infer<typeof RuntimeContextItemSchema>;
121
387
  export type RuntimeAgentSourceContext = z.infer<typeof RuntimeAgentSourceContextSchema>;
122
388
  export type RuntimeRunAgentInput = z.infer<typeof RuntimeRunAgentInputSchema>;
389
+ export type InternalAgentStreamRequest = z.infer<typeof InternalAgentStreamRequestSchema>;
123
390
  export type ResumeSignal = z.infer<typeof ResumeSignalSchema>;
@@ -0,0 +1,42 @@
1
+ export interface FormElicitationOptions {
2
+ message: string;
3
+ schema: Record<string, unknown>;
4
+ }
5
+
6
+ export interface UrlElicitationOptions {
7
+ message: string;
8
+ url: string;
9
+ elicitationId: string;
10
+ }
11
+
12
+ export interface ElicitationRequest {
13
+ method: "elicitation/create";
14
+ params: Record<string, unknown>;
15
+ }
16
+
17
+ export function buildFormElicitation(
18
+ options: FormElicitationOptions,
19
+ ): ElicitationRequest {
20
+ return {
21
+ method: "elicitation/create",
22
+ params: {
23
+ mode: "form",
24
+ message: options.message,
25
+ requestedSchema: options.schema,
26
+ },
27
+ };
28
+ }
29
+
30
+ export function buildUrlElicitation(
31
+ options: UrlElicitationOptions,
32
+ ): ElicitationRequest {
33
+ return {
34
+ method: "elicitation/create",
35
+ params: {
36
+ mode: "url",
37
+ message: options.message,
38
+ url: options.url,
39
+ elicitationId: options.elicitationId,
40
+ },
41
+ };
42
+ }
@@ -43,5 +43,14 @@ export {
43
43
 
44
44
  export { createMCPServer, type IntegrationLoaderConfig, MCPServer } from "./server.js";
45
45
 
46
+ export {
47
+ buildFormElicitation,
48
+ buildUrlElicitation,
49
+ type ElicitationRequest,
50
+ type FormElicitationOptions,
51
+ type UrlElicitationOptions,
52
+ } from "./elicitation.js";
46
53
  export { formatSSEEvent, formatSSEPrimingEvent, formatSSERetry } from "./sse.js";
47
54
  export { SessionManager } from "./session.js";
55
+ export { TaskStore } from "./task-store.js";
56
+ export type { Task } from "./task-store.js";
@@ -14,6 +14,7 @@ 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
16
  import { SessionManager } from "./session.js";
17
+ import { TaskStore } from "./task-store.js";
17
18
 
18
19
  const logger = baseLogger.component("mcp-server");
19
20
 
@@ -110,10 +111,29 @@ export interface IntegrationLoaderConfig {
110
111
  const MCP_SUPPORTED_VERSIONS = ["2025-11-25", "2024-11-05"];
111
112
 
112
113
  export class MCPServer {
114
+ private static LOG_LEVELS = [
115
+ "debug",
116
+ "info",
117
+ "notice",
118
+ "warning",
119
+ "error",
120
+ "critical",
121
+ "alert",
122
+ "emergency",
123
+ ] as const;
124
+ private logLevel: typeof MCPServer.LOG_LEVELS[number] = "warning";
113
125
  private config: MCPServerConfig;
114
126
  private integrationLoader?: IntegrationLoaderConfig;
115
127
  private integrationsLoaded = false;
116
128
  private sessionManager = new SessionManager();
129
+ private taskStore = new TaskStore();
130
+ private pendingTasks = new Map<string, Promise<void>>();
131
+ // TODO(#842): capabilities should be stored per-session (keyed by MCP-Session-Id)
132
+ // so concurrent clients don't overwrite each other's capability flags.
133
+ private clientCapabilities: Record<string, unknown> = {};
134
+
135
+ /** Callback for server-initiated notifications. Set by transport layer. */
136
+ onNotification?: (notification: { jsonrpc: "2.0"; method: string; params?: unknown }) => void;
117
137
 
118
138
  constructor(config: MCPServerConfig) {
119
139
  this.config = config;
@@ -123,6 +143,18 @@ export class MCPServer {
123
143
  }
124
144
  }
125
145
 
146
+ notifyToolsChanged(): void {
147
+ this.onNotification?.({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
148
+ }
149
+
150
+ notifyResourcesChanged(): void {
151
+ this.onNotification?.({ jsonrpc: "2.0", method: "notifications/resources/list_changed" });
152
+ }
153
+
154
+ notifyPromptsChanged(): void {
155
+ this.onNotification?.({ jsonrpc: "2.0", method: "notifications/prompts/list_changed" });
156
+ }
157
+
126
158
  /**
127
159
  * Configure integration tools to be loaded from the API.
128
160
  *
@@ -135,6 +167,15 @@ export class MCPServer {
135
167
  this.integrationsLoaded = false;
136
168
  }
137
169
 
170
+ clientSupportsElicitation(mode: "form" | "url"): boolean {
171
+ const raw = this.clientCapabilities.elicitation;
172
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
173
+ const elicitation = raw as Record<string, unknown>;
174
+ // Per MCP spec: empty elicitation object implies basic form support (backwards compat)
175
+ if (mode === "form" && Object.keys(elicitation).length === 0) return true;
176
+ return mode in elicitation;
177
+ }
178
+
138
179
  handleRequest(request: JSONRPCRequest, context?: ToolExecutionContext): Promise<JSONRPCResponse> {
139
180
  return withSpan(
140
181
  "mcp.handleRequest",
@@ -178,8 +219,21 @@ export class MCPServer {
178
219
  return this.initialize(params);
179
220
  case "notifications/initialized":
180
221
  return Promise.resolve({});
222
+ case "notifications/cancelled":
223
+ // TODO(#841): propagate cancellation to in-flight tool executions via AbortController
224
+ return Promise.resolve({});
181
225
  case "completion/complete":
182
226
  return this.complete(params);
227
+ case "logging/setLevel":
228
+ return this.setLogLevel(params);
229
+ case "tasks/get":
230
+ return this.getTask(params);
231
+ case "tasks/result":
232
+ return this.getTaskResult(params);
233
+ case "tasks/cancel":
234
+ return this.cancelTask(params);
235
+ case "tasks/list":
236
+ return this.listTasks();
183
237
  default:
184
238
  throw toError(
185
239
  createError({
@@ -197,6 +251,9 @@ export class MCPServer {
197
251
  ? requested
198
252
  : MCP_SUPPORTED_VERSIONS[0];
199
253
 
254
+ const clientCaps = (p.capabilities ?? {}) as Record<string, unknown>;
255
+ this.clientCapabilities = clientCaps;
256
+
200
257
  return Promise.resolve({
201
258
  protocolVersion: negotiated,
202
259
  serverInfo: {
@@ -211,6 +268,8 @@ export class MCPServer {
211
268
  resources: { subscribe: true, listChanged: true },
212
269
  prompts: { listChanged: true },
213
270
  completions: {},
271
+ logging: {},
272
+ tasks: { list: {}, cancel: {}, requests: { tools: { call: {} } } },
214
273
  },
215
274
  instructions:
216
275
  "Veryfront MCP server provides development tools. Use vf_get_errors to check for code errors, vf_get_logs for server logs, vf_scaffold for code generation, and vf_get_project_context for project structure.",
@@ -250,7 +309,13 @@ export class MCPServer {
250
309
  params: JSONRPCParams | undefined,
251
310
  context?: ToolExecutionContext,
252
311
  ): Promise<Record<string, unknown>> {
253
- const { name, arguments: args } = toParamsRecord(params);
312
+ const p = toParamsRecord(params);
313
+ const { name, arguments: args } = p;
314
+ const meta = (p._meta ?? {}) as Record<string, unknown>;
315
+ const rawToken = meta.progressToken;
316
+ const progressToken = (typeof rawToken === "string" || typeof rawToken === "number")
317
+ ? rawToken
318
+ : undefined;
254
319
 
255
320
  if (!name) {
256
321
  throw toError(createError({ type: "agent", message: "Tool name is required" }));
@@ -273,11 +338,55 @@ export class MCPServer {
273
338
  }
274
339
  }
275
340
 
341
+ const toolContext: ToolExecutionContext | undefined = progressToken !== undefined
342
+ ? { ...context, progressToken }
343
+ : context;
344
+
345
+ // Async task mode: if the caller provides a `task` field, create a task
346
+ // and run the tool in the background, returning the task immediately.
347
+ const taskParam = p.task as { ttl?: number } | undefined;
348
+ if (taskParam) {
349
+ const MIN_TTL = 1000;
350
+ const MAX_TTL = 3_600_000; // 1 hour
351
+ const rawTtl = typeof taskParam.ttl === "number" ? taskParam.ttl : 60000;
352
+ const ttl = Math.max(MIN_TTL, Math.min(MAX_TTL, rawTtl));
353
+ const task = this.taskStore.create(ttl);
354
+
355
+ // Run tool in background, update task on completion
356
+ // TODO(#842): wire AbortController so that tasks/cancel actually aborts the running tool execution
357
+ const pending = withSpan(
358
+ "mcp.callTool.async",
359
+ async () => {
360
+ try {
361
+ const result = await executeTool(toolName, args, toolContext);
362
+ this.taskStore.complete(task.taskId, {
363
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
364
+ isError: false,
365
+ });
366
+ } catch (error) {
367
+ const message = error instanceof Error ? error.message : String(error);
368
+ logger.warn("Async tool execution failed", {
369
+ tool: toolName,
370
+ taskId: task.taskId,
371
+ error: message,
372
+ });
373
+ this.taskStore.fail(task.taskId, message);
374
+ }
375
+ },
376
+ { "mcp.tool.name": toolName, "mcp.task.id": task.taskId },
377
+ ).then(() => {
378
+ this.pendingTasks.delete(task.taskId);
379
+ });
380
+ this.pendingTasks.set(task.taskId, pending);
381
+
382
+ return Promise.resolve({ task });
383
+ }
384
+
276
385
  return withSpan(
277
386
  "mcp.callTool",
278
387
  async () => {
279
388
  try {
280
- const result = await executeTool(toolName, args, context);
389
+ const result = await executeTool(toolName, args, toolContext);
281
390
  return {
282
391
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
283
392
  isError: false,
@@ -447,6 +556,75 @@ export class MCPServer {
447
556
  });
448
557
  }
449
558
 
559
+ private setLogLevel(
560
+ params: JSONRPCParams | undefined,
561
+ ): Promise<Record<string, unknown>> {
562
+ const p = toParamsRecord(params);
563
+ const level = p.level as string;
564
+ if (
565
+ !MCPServer.LOG_LEVELS.includes(
566
+ level as typeof MCPServer.LOG_LEVELS[number],
567
+ )
568
+ ) {
569
+ return Promise.reject({
570
+ code: -32602,
571
+ message: `Invalid log level: ${level}. Valid levels: ${MCPServer.LOG_LEVELS.join(", ")}`,
572
+ });
573
+ }
574
+ this.logLevel = level as typeof MCPServer.LOG_LEVELS[number];
575
+ return Promise.resolve({});
576
+ }
577
+
578
+ private getTask(params: JSONRPCParams | undefined): Promise<Record<string, unknown>> {
579
+ const { taskId } = toParamsRecord(params);
580
+ if (!taskId) {
581
+ throw new JsonRpcError(-32602, "taskId is required");
582
+ }
583
+ const task = this.taskStore.get(String(taskId));
584
+ if (!task) {
585
+ throw new JsonRpcError(-32602, `Task not found: ${taskId}`);
586
+ }
587
+ return Promise.resolve({ ...task });
588
+ }
589
+
590
+ private getTaskResult(params: JSONRPCParams | undefined): Promise<Record<string, unknown>> {
591
+ const { taskId } = toParamsRecord(params);
592
+ if (!taskId) {
593
+ throw new JsonRpcError(-32602, "taskId is required");
594
+ }
595
+ const task = this.taskStore.get(String(taskId));
596
+ if (!task) {
597
+ throw new JsonRpcError(-32602, `Task not found: ${taskId}`);
598
+ }
599
+ const result = this.taskStore.getResult(String(taskId));
600
+ if (result === undefined) {
601
+ throw new JsonRpcError(-32002, "Task result is not yet available");
602
+ }
603
+ return Promise.resolve(result as Record<string, unknown>);
604
+ }
605
+
606
+ private cancelTask(params: JSONRPCParams | undefined): Promise<Record<string, unknown>> {
607
+ const { taskId } = toParamsRecord(params);
608
+ if (!taskId) {
609
+ throw new JsonRpcError(-32602, "taskId is required");
610
+ }
611
+ const cancelled = this.taskStore.cancel(String(taskId));
612
+ if (!cancelled) {
613
+ throw new JsonRpcError(-32002, `Cannot cancel task: ${taskId}`);
614
+ }
615
+ const task = this.taskStore.get(String(taskId));
616
+ return Promise.resolve({ ...task });
617
+ }
618
+
619
+ private listTasks(): Promise<Record<string, unknown>> {
620
+ return Promise.resolve({ tasks: this.taskStore.list() });
621
+ }
622
+
623
+ /** Wait for all background task executions to settle. Useful in tests. */
624
+ waitForPendingTasks(): Promise<void> {
625
+ return Promise.all(this.pendingTasks.values()).then(() => {});
626
+ }
627
+
450
628
  createHTTPHandler(): (request: dntShim.Request) => Promise<dntShim.Response> {
451
629
  return async (request: dntShim.Request) => {
452
630
  const requestOrigin = request.headers.get("Origin");
@@ -623,6 +801,11 @@ export class MCPServer {
623
801
  };
624
802
  }
625
803
  await syncIntegrationConfig(apiBaseUrl, apiToken, integrationConfigs);
804
+ try {
805
+ this.notifyToolsChanged();
806
+ } catch (_) {
807
+ // Notification delivery failure is non-fatal — sync already succeeded
808
+ }
626
809
  return true;
627
810
  }
628
811
  }