keystone-cli 0.5.0 → 0.6.0

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 (47) hide show
  1. package/README.md +55 -8
  2. package/package.json +5 -3
  3. package/src/cli.ts +33 -192
  4. package/src/db/memory-db.test.ts +54 -0
  5. package/src/db/memory-db.ts +122 -0
  6. package/src/db/sqlite-setup.ts +49 -0
  7. package/src/db/workflow-db.test.ts +41 -10
  8. package/src/db/workflow-db.ts +84 -28
  9. package/src/expression/evaluator.test.ts +19 -0
  10. package/src/expression/evaluator.ts +134 -39
  11. package/src/parser/schema.ts +41 -0
  12. package/src/runner/audit-verification.test.ts +23 -0
  13. package/src/runner/auto-heal.test.ts +64 -0
  14. package/src/runner/debug-repl.test.ts +74 -0
  15. package/src/runner/debug-repl.ts +225 -0
  16. package/src/runner/foreach-executor.ts +327 -0
  17. package/src/runner/llm-adapter.test.ts +27 -14
  18. package/src/runner/llm-adapter.ts +90 -112
  19. package/src/runner/llm-executor.test.ts +91 -6
  20. package/src/runner/llm-executor.ts +26 -6
  21. package/src/runner/mcp-client.audit.test.ts +69 -0
  22. package/src/runner/mcp-client.test.ts +12 -3
  23. package/src/runner/mcp-client.ts +199 -19
  24. package/src/runner/mcp-manager.ts +19 -8
  25. package/src/runner/mcp-server.test.ts +8 -5
  26. package/src/runner/mcp-server.ts +31 -17
  27. package/src/runner/optimization-runner.ts +305 -0
  28. package/src/runner/reflexion.test.ts +87 -0
  29. package/src/runner/shell-executor.test.ts +12 -0
  30. package/src/runner/shell-executor.ts +9 -6
  31. package/src/runner/step-executor.test.ts +46 -1
  32. package/src/runner/step-executor.ts +154 -60
  33. package/src/runner/stream-utils.test.ts +65 -0
  34. package/src/runner/stream-utils.ts +186 -0
  35. package/src/runner/workflow-runner.test.ts +4 -4
  36. package/src/runner/workflow-runner.ts +436 -251
  37. package/src/templates/agents/keystone-architect.md +6 -4
  38. package/src/templates/full-feature-demo.yaml +4 -4
  39. package/src/types/assets.d.ts +14 -0
  40. package/src/types/status.ts +1 -1
  41. package/src/ui/dashboard.tsx +38 -26
  42. package/src/utils/auth-manager.ts +3 -1
  43. package/src/utils/logger.test.ts +76 -0
  44. package/src/utils/logger.ts +39 -0
  45. package/src/utils/prompt.ts +75 -0
  46. package/src/utils/redactor.test.ts +86 -4
  47. package/src/utils/redactor.ts +48 -13
@@ -1,6 +1,9 @@
1
1
  import { type ChildProcess, spawn } from 'node:child_process';
2
+ import { lookup } from 'node:dns/promises';
3
+ import { isIP } from 'node:net';
2
4
  import { type Interface, createInterface } from 'node:readline';
3
5
  import pkg from '../../package.json' with { type: 'json' };
6
+ import { ConsoleLogger, type Logger } from '../utils/logger.ts';
4
7
 
5
8
  // MCP Protocol version - update when upgrading to newer MCP spec
6
9
  export const MCP_PROTOCOL_VERSION = '2024-11-05';
@@ -8,6 +11,142 @@ export const MCP_PROTOCOL_VERSION = '2024-11-05';
8
11
  // Maximum buffer size for incoming messages (10MB) to prevent memory exhaustion
9
12
  const MAX_BUFFER_SIZE = 10 * 1024 * 1024;
10
13
 
14
+ /**
15
+ * Validate a URL to prevent SSRF attacks
16
+ * Blocks private IP ranges, localhost, and other internal addresses
17
+ * @param url The URL to validate
18
+ * @param options.allowInsecure If true, skips all security checks (use only for development/testing)
19
+ * @throws Error if the URL is potentially dangerous
20
+ */
21
+ function isPrivateIpAddress(address: string): boolean {
22
+ const normalized = address.toLowerCase();
23
+ const parseMappedIpv4 = (mapped: string): string | null => {
24
+ const rest = mapped.replace(/^::ffff:/i, '');
25
+ if (rest.includes('.')) {
26
+ return rest;
27
+ }
28
+ const parts = rest.split(':');
29
+ if (parts.length !== 2) {
30
+ return null;
31
+ }
32
+ const high = Number.parseInt(parts[0], 16);
33
+ const low = Number.parseInt(parts[1], 16);
34
+ if (Number.isNaN(high) || Number.isNaN(low)) {
35
+ return null;
36
+ }
37
+ const a = (high >> 8) & 0xff;
38
+ const b = high & 0xff;
39
+ const c = (low >> 8) & 0xff;
40
+ const d = low & 0xff;
41
+ return `${a}.${b}.${c}.${d}`;
42
+ };
43
+
44
+ // IPv4 checks
45
+ const ipv4Match = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
46
+ if (ipv4Match) {
47
+ const [, a, b] = ipv4Match.map(Number);
48
+ return (
49
+ a === 10 || // 10.0.0.0/8
50
+ (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12
51
+ (a === 192 && b === 168) || // 192.168.0.0/16
52
+ (a === 169 && b === 254) || // 169.254.0.0/16 (link-local)
53
+ a === 127 // 127.0.0.0/8
54
+ );
55
+ }
56
+
57
+ // IPv6-mapped IPv4 (::ffff:127.0.0.1 or ::ffff:7f00:1)
58
+ if (normalized.startsWith('::ffff:')) {
59
+ const mappedIpv4 = parseMappedIpv4(normalized);
60
+ if (mappedIpv4) {
61
+ return isPrivateIpAddress(mappedIpv4);
62
+ }
63
+ }
64
+
65
+ // IPv6 checks (best-effort, without full CIDR parsing)
66
+ const ipv6 = normalized.replace(/^\[|\]$/g, '');
67
+ return (
68
+ ipv6 === '::1' || // Loopback
69
+ ipv6.startsWith('fe80:') || // Link-local
70
+ ipv6.startsWith('fc') || // Unique local (fc00::/7)
71
+ ipv6.startsWith('fd') // Unique local (fc00::/7)
72
+ );
73
+ }
74
+
75
+ export async function validateRemoteUrl(
76
+ url: string,
77
+ options: { allowInsecure?: boolean } = {}
78
+ ): Promise<void> {
79
+ let parsed: URL;
80
+ try {
81
+ parsed = new URL(url);
82
+ } catch {
83
+ throw new Error(`Invalid MCP server URL: ${url}`);
84
+ }
85
+
86
+ // Skip all security checks if allowInsecure is set (for development/testing)
87
+ if (options.allowInsecure) {
88
+ return;
89
+ }
90
+
91
+ // Require HTTPS in production
92
+ if (parsed.protocol !== 'https:') {
93
+ throw new Error(
94
+ `SSRF Protection: MCP remote URL must use HTTPS. Got: ${parsed.protocol}. Set allowInsecure option to true if you trust this server.`
95
+ );
96
+ }
97
+
98
+ const hostname = parsed.hostname.toLowerCase();
99
+
100
+ // Block localhost variants
101
+ if (
102
+ hostname === 'localhost' ||
103
+ hostname === '127.0.0.1' ||
104
+ hostname === '::1' ||
105
+ hostname === '0.0.0.0' ||
106
+ hostname.endsWith('.localhost')
107
+ ) {
108
+ throw new Error(`SSRF Protection: Cannot connect to localhost/loopback address: ${hostname}`);
109
+ }
110
+
111
+ // Block private IP ranges (IPv4/IPv6) for literal IPs
112
+ if (isIP(hostname)) {
113
+ if (isPrivateIpAddress(hostname)) {
114
+ throw new Error(
115
+ `SSRF Protection: Cannot connect to private/internal IP address: ${hostname}`
116
+ );
117
+ }
118
+ }
119
+
120
+ // Block cloud metadata endpoints
121
+ if (
122
+ hostname === '169.254.169.254' || // AWS/GCP/Azure metadata
123
+ hostname === 'metadata.google.internal' ||
124
+ hostname.endsWith('.internal')
125
+ ) {
126
+ throw new Error(`SSRF Protection: Cannot connect to cloud metadata endpoint: ${hostname}`);
127
+ }
128
+
129
+ // Resolve DNS to prevent hostnames that map to private IPs (DNS rebinding)
130
+ if (!isIP(hostname)) {
131
+ try {
132
+ const resolved = await lookup(hostname, { all: true });
133
+ for (const record of resolved) {
134
+ if (isPrivateIpAddress(record.address)) {
135
+ throw new Error(
136
+ `SSRF Protection: Hostname "${hostname}" resolves to private/internal address: ${record.address}`
137
+ );
138
+ }
139
+ }
140
+ } catch (error) {
141
+ throw new Error(
142
+ `SSRF Protection: Failed to resolve hostname "${hostname}": ${
143
+ error instanceof Error ? error.message : String(error)
144
+ }`
145
+ );
146
+ }
147
+ }
148
+ }
149
+
11
150
  interface MCPTool {
12
151
  name: string;
13
152
  description?: string;
@@ -32,17 +171,20 @@ interface MCPTransport {
32
171
  send(message: unknown): Promise<void>;
33
172
  onMessage(callback: (message: MCPResponse) => void): void;
34
173
  close(): void;
174
+ setLogger(logger: Logger): void;
35
175
  }
36
176
 
37
177
  class StdConfigTransport implements MCPTransport {
38
178
  private process: ChildProcess;
39
179
  private rl: Interface;
180
+ private logger: Logger = new ConsoleLogger();
40
181
 
41
182
  constructor(command: string, args: string[] = [], env: Record<string, string> = {}) {
42
183
  // Filter out sensitive environment variables from the host process
43
184
  // unless they are explicitly provided in the 'env' argument
44
185
  const safeEnv: Record<string, string> = {};
45
- const sensitivePattern = /(?:key|token|secret|password|credential|auth|private)/i;
186
+ const sensitivePattern =
187
+ /(?:key|token|secret|password|credential|auth|private|cookie|session|signature)/i;
46
188
 
47
189
  for (const [key, value] of Object.entries(process.env)) {
48
190
  if (value && !sensitivePattern.test(key)) {
@@ -64,6 +206,10 @@ class StdConfigTransport implements MCPTransport {
64
206
  });
65
207
  }
66
208
 
209
+ setLogger(logger: Logger): void {
210
+ this.logger = logger;
211
+ }
212
+
67
213
  async send(message: unknown): Promise<void> {
68
214
  this.process.stdin?.write(`${JSON.stringify(message)}\n`);
69
215
  }
@@ -72,8 +218,8 @@ class StdConfigTransport implements MCPTransport {
72
218
  this.rl.on('line', (line) => {
73
219
  // Safety check for extremely long lines that might have bypassed readline's internal limits
74
220
  if (line.length > MAX_BUFFER_SIZE) {
75
- process.stderr.write(
76
- `[MCP Error] Received line exceeding maximum size (${line.length} bytes), ignoring.\n`
221
+ this.logger.error(
222
+ `[MCP Error] Received line exceeding maximum size (${line.length} bytes), ignoring.`
77
223
  );
78
224
  return;
79
225
  }
@@ -84,7 +230,7 @@ class StdConfigTransport implements MCPTransport {
84
230
  } catch (e) {
85
231
  // Log non-JSON lines to stderr so they show up in the terminal
86
232
  if (line.trim()) {
87
- process.stderr.write(`[MCP Server Output] ${line}\n`);
233
+ this.logger.log(`[MCP Server Output] ${line}`);
88
234
  }
89
235
  }
90
236
  });
@@ -104,12 +250,17 @@ class SSETransport implements MCPTransport {
104
250
  private abortController: AbortController | null = null;
105
251
  private sessionId?: string;
106
252
  private activeReaders: Set<ReadableStreamDefaultReader<Uint8Array>> = new Set();
253
+ private logger: Logger = new ConsoleLogger();
107
254
 
108
255
  constructor(url: string, headers: Record<string, string> = {}) {
109
256
  this.url = url;
110
257
  this.headers = headers;
111
258
  }
112
259
 
260
+ setLogger(logger: Logger): void {
261
+ this.logger = logger;
262
+ }
263
+
113
264
  async connect(timeout = 60000): Promise<void> {
114
265
  this.abortController = new AbortController();
115
266
 
@@ -179,6 +330,18 @@ class SSETransport implements MCPTransport {
179
330
  const dispatchEvent = () => {
180
331
  if (currentEvent.data) {
181
332
  if (currentEvent.event === 'endpoint') {
333
+ // Validate endpoint to prevent SSRF - only allow relative paths
334
+ const endpointValue = currentEvent.data;
335
+ if (
336
+ endpointValue &&
337
+ (endpointValue.startsWith('http://') ||
338
+ endpointValue.startsWith('https://') ||
339
+ endpointValue.startsWith('//'))
340
+ ) {
341
+ throw new Error(
342
+ `SSE endpoint must be a relative path, got absolute URL: ${endpointValue.substring(0, 50)}`
343
+ );
344
+ }
182
345
  this.endpoint = currentEvent.data;
183
346
  if (this.endpoint) {
184
347
  this.endpoint = new URL(this.endpoint, this.url).href;
@@ -414,15 +577,25 @@ class SSETransport implements MCPTransport {
414
577
  export class MCPClient {
415
578
  private transport: MCPTransport;
416
579
  private messageId = 0;
417
- private pendingRequests = new Map<number, (response: MCPResponse) => void>();
580
+ private pendingRequests = new Map<
581
+ number,
582
+ {
583
+ resolve: (response: MCPResponse) => void;
584
+ reject: (error: Error) => void;
585
+ timeoutId: ReturnType<typeof setTimeout>;
586
+ }
587
+ >();
418
588
  private timeout: number;
589
+ private logger: Logger;
419
590
 
420
591
  constructor(
421
592
  transportOrCommand: MCPTransport | string,
422
593
  timeoutOrArgs: number | string[] = [],
423
594
  env: Record<string, string> = {},
424
- timeout = 60000
595
+ timeout = 60000,
596
+ logger: Logger = new ConsoleLogger()
425
597
  ) {
598
+ this.logger = logger;
426
599
  if (typeof transportOrCommand === 'string') {
427
600
  this.transport = new StdConfigTransport(transportOrCommand, timeoutOrArgs as string[], env);
428
601
  this.timeout = timeout;
@@ -430,13 +603,15 @@ export class MCPClient {
430
603
  this.transport = transportOrCommand;
431
604
  this.timeout = timeoutOrArgs as number;
432
605
  }
606
+ this.transport.setLogger(this.logger);
433
607
 
434
608
  this.transport.onMessage((response) => {
435
609
  if (response.id !== undefined && this.pendingRequests.has(response.id)) {
436
- const resolve = this.pendingRequests.get(response.id);
437
- if (resolve) {
610
+ const pending = this.pendingRequests.get(response.id);
611
+ if (pending) {
438
612
  this.pendingRequests.delete(response.id);
439
- resolve(response);
613
+ clearTimeout(pending.timeoutId);
614
+ pending.resolve(response);
440
615
  }
441
616
  }
442
617
  });
@@ -446,20 +621,27 @@ export class MCPClient {
446
621
  command: string,
447
622
  args: string[] = [],
448
623
  env: Record<string, string> = {},
449
- timeout = 60000
624
+ timeout = 60000,
625
+ logger: Logger = new ConsoleLogger()
450
626
  ): Promise<MCPClient> {
451
627
  const transport = new StdConfigTransport(command, args, env);
452
- return new MCPClient(transport, timeout);
628
+ return new MCPClient(transport, timeout, {}, 0, logger);
453
629
  }
454
630
 
455
631
  static async createRemote(
456
632
  url: string,
457
633
  headers: Record<string, string> = {},
458
- timeout = 60000
634
+ timeout = 60000,
635
+ options: { allowInsecure?: boolean; logger?: Logger } = {}
459
636
  ): Promise<MCPClient> {
637
+ // Validate URL to prevent SSRF attacks
638
+ await validateRemoteUrl(url, options);
639
+
640
+ const logger = options.logger || new ConsoleLogger();
460
641
  const transport = new SSETransport(url, headers);
642
+ transport.setLogger(logger);
461
643
  await transport.connect(timeout);
462
- return new MCPClient(transport, timeout);
644
+ return new MCPClient(transport, timeout, {}, 0, logger);
463
645
  }
464
646
 
465
647
  private async request(
@@ -482,10 +664,7 @@ export class MCPClient {
482
664
  }
483
665
  }, this.timeout);
484
666
 
485
- this.pendingRequests.set(id, (response) => {
486
- clearTimeout(timeoutId);
487
- resolve(response);
488
- });
667
+ this.pendingRequests.set(id, { resolve, reject, timeoutId });
489
668
 
490
669
  this.transport.send(message).catch((err) => {
491
670
  clearTimeout(timeoutId);
@@ -524,8 +703,9 @@ export class MCPClient {
524
703
 
525
704
  stop() {
526
705
  // Reject all pending requests to prevent hanging callers
527
- for (const [id, resolve] of this.pendingRequests) {
528
- resolve({ id, error: { code: -1, message: 'MCP client stopped' } });
706
+ for (const [, pending] of this.pendingRequests) {
707
+ clearTimeout(pending.timeoutId);
708
+ pending.reject(new Error('MCP client stopped'));
529
709
  }
530
710
  this.pendingRequests.clear();
531
711
  this.transport.close();
@@ -1,6 +1,6 @@
1
1
  import { ConfigLoader } from '../utils/config-loader';
2
+ import { ConsoleLogger, type Logger } from '../utils/logger.ts';
2
3
  import { MCPClient } from './mcp-client';
3
- import type { Logger } from './workflow-runner';
4
4
 
5
5
  export interface MCPServerConfig {
6
6
  name: string;
@@ -20,9 +20,16 @@ export class MCPManager {
20
20
  private clients: Map<string, MCPClient> = new Map();
21
21
  private connectionPromises: Map<string, Promise<MCPClient | undefined>> = new Map();
22
22
  private sharedServers: Map<string, MCPServerConfig> = new Map();
23
+ private logger: Logger;
23
24
 
24
- constructor() {
25
+ constructor(logger: Logger = new ConsoleLogger()) {
26
+ this.logger = logger;
25
27
  this.loadGlobalConfig();
28
+
29
+ // Ensure cleanup on process exit
30
+ process.on('exit', () => {
31
+ this.stopAll();
32
+ });
26
33
  }
27
34
 
28
35
  private loadGlobalConfig() {
@@ -39,14 +46,15 @@ export class MCPManager {
39
46
 
40
47
  async getClient(
41
48
  serverRef: string | MCPServerConfig,
42
- logger: Logger = console
49
+ logger?: Logger
43
50
  ): Promise<MCPClient | undefined> {
51
+ const activeLogger = logger || this.logger;
44
52
  let config: MCPServerConfig;
45
53
 
46
54
  if (typeof serverRef === 'string') {
47
55
  const shared = this.sharedServers.get(serverRef);
48
56
  if (!shared) {
49
- logger.error(` ✗ Global MCP server not found: ${serverRef}`);
57
+ activeLogger.error(` ✗ Global MCP server not found: ${serverRef}`);
50
58
  return undefined;
51
59
  }
52
60
  config = shared;
@@ -68,7 +76,7 @@ export class MCPManager {
68
76
 
69
77
  // Start a new connection and cache the promise
70
78
  const connectionPromise = (async () => {
71
- logger.log(` 🔌 Connecting to MCP server: ${config.name} (${config.type || 'local'})`);
79
+ activeLogger.log(` 🔌 Connecting to MCP server: ${config.name} (${config.type || 'local'})`);
72
80
 
73
81
  let client: MCPClient;
74
82
  try {
@@ -91,7 +99,9 @@ export class MCPManager {
91
99
  headers.Authorization = `Bearer ${token}`;
92
100
  }
93
101
 
94
- client = await MCPClient.createRemote(config.url, headers, config.timeout);
102
+ client = await MCPClient.createRemote(config.url, headers, config.timeout, {
103
+ logger: activeLogger,
104
+ });
95
105
  } else {
96
106
  if (!config.command) throw new Error('Local MCP server missing command');
97
107
 
@@ -118,7 +128,8 @@ export class MCPManager {
118
128
  config.command,
119
129
  config.args || [],
120
130
  env,
121
- config.timeout
131
+ config.timeout,
132
+ activeLogger
122
133
  );
123
134
  }
124
135
 
@@ -126,7 +137,7 @@ export class MCPManager {
126
137
  this.clients.set(key, client);
127
138
  return client;
128
139
  } catch (error) {
129
- logger.error(
140
+ activeLogger.error(
130
141
  ` ✗ Failed to connect to MCP server ${config.name}: ${error instanceof Error ? error.message : String(error)}`
131
142
  );
132
143
  return undefined;
@@ -166,7 +166,7 @@ describe('MCPServer', () => {
166
166
  expect(JSON.parse(response?.result?.content?.[0]?.text || '{}').status).toBe('success');
167
167
 
168
168
  // Verify DB was updated
169
- const steps = db.getStepsByRun(runId);
169
+ const steps = await db.getStepsByRun(runId);
170
170
  expect(steps[0].status).toBe('success');
171
171
  expect(steps[0].output).toBeDefined();
172
172
  if (steps[0].output) {
@@ -301,10 +301,13 @@ describe('MCPServer', () => {
301
301
  expect(status.hint).toContain('still running');
302
302
  });
303
303
 
304
- it('should call get_run_status tool for completed workflow', async () => {
305
- const runId = 'completed-test-run';
304
+ it('should call get_run_status tool for success workflow', async () => {
305
+ const runId = 'success-test-run';
306
306
  await db.createRun(runId, 'test-wf', {});
307
- await db.updateRunStatus(runId, 'completed', { output: 'done' });
307
+ await db.updateRunStatus(runId, 'success', { output: 'done' });
308
+
309
+ // Wait for the async run to finish and update DB
310
+ await new Promise((resolve) => setTimeout(resolve, 200));
308
311
 
309
312
  const response = await handleMessage({
310
313
  jsonrpc: '2.0',
@@ -314,7 +317,7 @@ describe('MCPServer', () => {
314
317
  });
315
318
 
316
319
  const status = JSON.parse(response?.result?.content?.[0]?.text || '{}');
317
- expect(status.status).toBe('completed');
320
+ expect(status.status).toBe('success');
318
321
  expect(status.outputs).toEqual({ output: 'done' });
319
322
  expect(status.hint).toBeUndefined();
320
323
  });
@@ -3,6 +3,7 @@ import type { Readable, Writable } from 'node:stream';
3
3
  import pkg from '../../package.json' with { type: 'json' };
4
4
  import { WorkflowDb } from '../db/workflow-db';
5
5
  import { WorkflowParser } from '../parser/workflow-parser';
6
+ import { ConsoleLogger, type Logger } from '../utils/logger';
6
7
  import { generateMermaidGraph } from '../utils/mermaid';
7
8
  import { WorkflowRegistry } from '../utils/workflow-registry';
8
9
  import { WorkflowSuspendedError } from './step-executor';
@@ -19,11 +20,18 @@ export class MCPServer {
19
20
  private db: WorkflowDb;
20
21
  private input: Readable;
21
22
  private output: Writable;
22
-
23
- constructor(db?: WorkflowDb, input: Readable = process.stdin, output: Writable = process.stdout) {
23
+ private logger: Logger;
24
+
25
+ constructor(
26
+ db?: WorkflowDb,
27
+ input: Readable = process.stdin,
28
+ output: Writable = process.stdout,
29
+ logger: Logger = new ConsoleLogger()
30
+ ) {
24
31
  this.db = db || new WorkflowDb();
25
32
  this.input = input;
26
33
  this.output = output;
34
+ this.logger = logger;
27
35
  }
28
36
 
29
37
  async start() {
@@ -43,7 +51,7 @@ export class MCPServer {
43
51
  this.output.write(`${JSON.stringify(response)}\n`);
44
52
  }
45
53
  } catch (error) {
46
- console.error('Error handling MCP message:', error);
54
+ this.logger.error(`Error handling MCP message: ${error}`);
47
55
  }
48
56
  });
49
57
 
@@ -54,7 +62,7 @@ export class MCPServer {
54
62
 
55
63
  // Handle stream errors
56
64
  this.input.on('error', (err: Error) => {
57
- console.error('stdin error:', err);
65
+ this.logger.error(`stdin error: ${err}`);
58
66
  });
59
67
  });
60
68
  }
@@ -223,6 +231,8 @@ export class MCPServer {
223
231
  log: (msg: string) => logs.push(msg),
224
232
  error: (msg: string) => logs.push(`ERROR: ${msg}`),
225
233
  warn: (msg: string) => logs.push(`WARN: ${msg}`),
234
+ info: (msg: string) => logs.push(`INFO: ${msg}`),
235
+ debug: (msg: string) => logs.push(`DEBUG: ${msg}`),
226
236
  };
227
237
 
228
238
  const runner = new WorkflowRunner(workflow, {
@@ -307,13 +317,13 @@ export class MCPServer {
307
317
  // --- Tool: get_run_logs ---
308
318
  if (toolParams.name === 'get_run_logs') {
309
319
  const { run_id } = toolParams.arguments as { run_id: string };
310
- const run = this.db.getRun(run_id);
320
+ const run = await this.db.getRun(run_id);
311
321
 
312
322
  if (!run) {
313
323
  throw new Error(`Run ID ${run_id} not found`);
314
324
  }
315
325
 
316
- const steps = this.db.getStepsByRun(run_id);
326
+ const steps = await this.db.getStepsByRun(run_id);
317
327
  const summary = {
318
328
  workflow: run.workflow_name,
319
329
  status: run.status,
@@ -360,7 +370,7 @@ export class MCPServer {
360
370
  // --- Tool: answer_human_input ---
361
371
  if (toolParams.name === 'answer_human_input') {
362
372
  const { run_id, input } = toolParams.arguments as { run_id: string; input: string };
363
- const run = this.db.getRun(run_id);
373
+ const run = await this.db.getRun(run_id);
364
374
  if (!run) {
365
375
  throw new Error(`Run ID ${run_id} not found`);
366
376
  }
@@ -370,7 +380,7 @@ export class MCPServer {
370
380
  }
371
381
 
372
382
  // Find the pending or suspended step
373
- const steps = this.db.getStepsByRun(run_id);
383
+ const steps = await this.db.getStepsByRun(run_id);
374
384
  const pendingStep = steps.find(
375
385
  (s) => s.status === 'pending' || s.status === 'suspended'
376
386
  );
@@ -403,6 +413,8 @@ export class MCPServer {
403
413
  log: (msg: string) => logs.push(msg),
404
414
  error: (msg: string) => logs.push(`ERROR: ${msg}`),
405
415
  warn: (msg: string) => logs.push(`WARN: ${msg}`),
416
+ info: (msg: string) => logs.push(`INFO: ${msg}`),
417
+ debug: (msg: string) => logs.push(`DEBUG: ${msg}`),
406
418
  };
407
419
 
408
420
  const runner = new WorkflowRunner(workflow, {
@@ -497,6 +509,8 @@ export class MCPServer {
497
509
  log: () => {},
498
510
  error: () => {},
499
511
  warn: () => {},
512
+ info: () => {},
513
+ debug: () => {},
500
514
  };
501
515
 
502
516
  const runner = new WorkflowRunner(workflow, {
@@ -507,18 +521,18 @@ export class MCPServer {
507
521
 
508
522
  const runId = runner.getRunId();
509
523
 
510
- // Start the workflow asynchronously - don't await
524
+ // Start the workflow asynchronously
511
525
  runner.run().then(
512
- (outputs) => {
513
- // Update DB with success on completion (RunStatus uses 'completed')
514
- this.db.updateRunStatus(runId, 'completed', outputs);
526
+ async (outputs) => {
527
+ // Update DB with success on completion
528
+ await this.db.updateRunStatus(runId, 'success', outputs);
515
529
  },
516
- (error) => {
530
+ async (error) => {
517
531
  // Update DB with failure
518
532
  if (error instanceof WorkflowSuspendedError) {
519
- this.db.updateRunStatus(runId, 'paused');
533
+ await this.db.updateRunStatus(runId, 'paused');
520
534
  } else {
521
- this.db.updateRunStatus(
535
+ await this.db.updateRunStatus(
522
536
  runId,
523
537
  'failed',
524
538
  undefined,
@@ -554,7 +568,7 @@ export class MCPServer {
554
568
  // --- Tool: get_run_status ---
555
569
  if (toolParams.name === 'get_run_status') {
556
570
  const { run_id } = toolParams.arguments as { run_id: string };
557
- const run = this.db.getRun(run_id);
571
+ const run = await this.db.getRun(run_id);
558
572
 
559
573
  if (!run) {
560
574
  throw new Error(`Run ID ${run_id} not found`);
@@ -567,7 +581,7 @@ export class MCPServer {
567
581
  };
568
582
 
569
583
  // Include outputs if completed successfully
570
- if (run.status === 'completed' && run.outputs) {
584
+ if (run.status === 'success' && run.outputs) {
571
585
  response.outputs = JSON.parse(run.outputs);
572
586
  }
573
587