@kairos-sdk/core 0.4.5 → 0.5.1

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.
@@ -2,10 +2,11 @@
2
2
  import {
3
3
  PromptBuilder,
4
4
  inferWorkflowType
5
- } from "./chunk-CR2NHLOH.js";
5
+ } from "./chunk-V2IZBZGB.js";
6
6
  import {
7
7
  DEFAULT_REGISTRY,
8
8
  FileLibrary,
9
+ GuardError,
9
10
  N8nApiClient,
10
11
  N8nFieldStripper,
11
12
  N8nValidator,
@@ -15,11 +16,13 @@ import {
15
16
  TelemetryReader,
16
17
  generateUUID,
17
18
  nullLogger
18
- } from "./chunk-6IXW3WCC.js";
19
+ } from "./chunk-VPPWTMRJ.js";
19
20
 
20
21
  // src/mcp-server.ts
21
22
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
25
+ import { createServer } from "http";
23
26
  import { z } from "zod";
24
27
 
25
28
  // src/validation/node-syncer.ts
@@ -81,15 +84,44 @@ ${regularLines}`;
81
84
 
82
85
  // src/mcp-server.ts
83
86
  import { readFileSync } from "fs";
84
- import { dirname, join } from "path";
87
+ import { dirname as dirname2, join } from "path";
85
88
  import { homedir } from "os";
86
89
  import { fileURLToPath } from "url";
87
- var __dirname = dirname(fileURLToPath(import.meta.url));
90
+
91
+ // src/utils/node-catalog-cache.ts
92
+ import { readFile, writeFile, mkdir } from "fs/promises";
93
+ import { dirname } from "path";
94
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
95
+ async function readCatalogCache(cachePath) {
96
+ try {
97
+ const raw = await readFile(cachePath, "utf-8");
98
+ const cached = JSON.parse(raw);
99
+ if (Date.now() - cached.cachedAt > CACHE_TTL_MS) return null;
100
+ return cached.syncResult;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ async function writeCatalogCache(cachePath, syncResult) {
106
+ try {
107
+ await mkdir(dirname(cachePath), { recursive: true });
108
+ const payload = { cachedAt: Date.now(), syncResult };
109
+ await writeFile(cachePath, JSON.stringify(payload), "utf-8");
110
+ } catch {
111
+ }
112
+ }
113
+
114
+ // src/mcp-server.ts
115
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
88
116
  var pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
89
117
  var library = new FileLibrary();
90
- var validator = new N8nValidator();
118
+ var _validator = new N8nValidator();
119
+ function getValidator() {
120
+ return _validator;
121
+ }
91
122
  var nodeSyncer = new NodeSyncer();
92
123
  var lastSync = null;
124
+ var AUTO_SYNC_TIMEOUT_MS = 5e3;
93
125
  var stripper = new N8nFieldStripper();
94
126
  var promptBuilder = new PromptBuilder(getMcpPatternsPath());
95
127
  function getMcpTelemetry() {
@@ -120,20 +152,51 @@ function getTelemetryReader() {
120
152
  return null;
121
153
  }
122
154
  }
155
+ function getMcpMode() {
156
+ const mode = process.env["KAIROS_MCP_MODE"]?.toLowerCase();
157
+ if (mode === "readonly" || mode === "validate") return mode;
158
+ return "deploy";
159
+ }
123
160
  function isAllowed(action) {
161
+ const mode = getMcpMode();
162
+ if (mode === "readonly" || mode === "validate") return false;
124
163
  const key = `KAIROS_MCP_ALLOW_${action.toUpperCase()}`;
125
164
  return process.env[key] === "true";
126
165
  }
166
+ function mcpText(text) {
167
+ return { content: [{ type: "text", text }] };
168
+ }
169
+ function mcpError(text) {
170
+ return { content: [{ type: "text", text }], isError: true };
171
+ }
172
+ function checkMcpAuth(provided) {
173
+ const expected = process.env["KAIROS_MCP_SECRET"];
174
+ if (!expected) return null;
175
+ if (provided === expected) return null;
176
+ return mcpError(JSON.stringify({ error: "Unauthorized: missing or incorrect kairos_secret" }));
177
+ }
127
178
  function getApiClient() {
128
179
  const baseUrl = process.env["N8N_BASE_URL"];
129
180
  const apiKey = process.env["N8N_API_KEY"];
130
181
  if (!baseUrl || !apiKey) {
131
- throw new Error("N8N_BASE_URL and N8N_API_KEY environment variables are required for n8n operations");
182
+ throw new GuardError("N8N_BASE_URL and N8N_API_KEY environment variables are required for n8n operations");
132
183
  }
133
184
  return new N8nApiClient(baseUrl, apiKey, nullLogger);
134
185
  }
186
+ function getCatalogCachePath() {
187
+ const telemetry = process.env["KAIROS_TELEMETRY"];
188
+ const base = telemetry ? join(telemetry, "..") : join(homedir(), ".kairos");
189
+ return join(base, "node-catalog-cache.json");
190
+ }
135
191
  async function autoSync() {
136
192
  if (lastSync) return lastSync;
193
+ const cachePath = getCatalogCachePath();
194
+ const cached = await readCatalogCache(cachePath);
195
+ if (cached) {
196
+ lastSync = cached;
197
+ _validator = new N8nValidator(lastSync.registry);
198
+ return lastSync;
199
+ }
137
200
  const baseUrl = process.env["N8N_BASE_URL"];
138
201
  const apiKey = process.env["N8N_API_KEY"];
139
202
  if (!baseUrl || !apiKey) return null;
@@ -142,7 +205,9 @@ async function autoSync() {
142
205
  const nodeTypes = await client.getNodeTypes();
143
206
  if (nodeTypes.length === 0) return null;
144
207
  lastSync = nodeSyncer.sync(nodeTypes);
145
- validator = new N8nValidator(lastSync.registry);
208
+ _validator = new N8nValidator(lastSync.registry);
209
+ writeCatalogCache(cachePath, lastSync).catch(() => {
210
+ });
146
211
  return lastSync;
147
212
  } catch {
148
213
  return null;
@@ -161,24 +226,20 @@ server.tool(
161
226
  },
162
227
  async ({ description, name }) => {
163
228
  evictStaleSessions();
164
- const baseUrl = process.env["N8N_BASE_URL"];
165
- const apiKey = process.env["N8N_API_KEY"];
166
- if (!baseUrl || !apiKey) {
167
- return {
168
- content: [{
169
- type: "text",
170
- text: JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required. Kairos needs to sync your n8n instance's node types to generate accurate workflows." })
171
- }],
172
- isError: true
173
- };
174
- }
175
229
  const runId = generateUUID();
176
230
  const workflowType = inferWorkflowType(description);
231
+ const hasN8nCreds = !!(process.env["N8N_BASE_URL"] && process.env["N8N_API_KEY"]);
232
+ const syncPromise = autoSync();
233
+ const syncTimeout = new Promise((resolve) => setTimeout(() => resolve(null), AUTO_SYNC_TIMEOUT_MS));
177
234
  await library.initialize();
178
- const syncResult = await autoSync();
179
- const matches = await library.search(description);
180
- const telemetryReader = getTelemetryReader();
181
- const failureRates = await telemetryReader?.getFailureRates() ?? [];
235
+ const [syncResult, matches, failureRates] = await Promise.all([
236
+ Promise.race([syncPromise, syncTimeout]),
237
+ library.search(description),
238
+ (async () => {
239
+ const reader = getTelemetryReader();
240
+ return reader ? reader.getFailureRates() : [];
241
+ })()
242
+ ]);
182
243
  const request = { description, ...name ? { name } : {} };
183
244
  const built = promptBuilder.build(request, matches, failureRates, syncResult?.catalogText);
184
245
  if (mcpTelemetry) {
@@ -187,48 +248,46 @@ server.tool(
187
248
  startTime: Date.now(),
188
249
  validateAttempts: 0,
189
250
  warnedRules: promptBuilder.getWarnedRules(),
190
- workflowType
251
+ workflowType,
252
+ matchCount: matches.length
191
253
  });
192
254
  await mcpTelemetry.emit("build_start", { description, model: "mcp-decomposed", dryRun: false }, runId);
193
255
  }
194
256
  const systemText = built.system.map((block) => block.text).join("\n\n---\n\n");
195
- return {
196
- content: [{
197
- type: "text",
198
- text: JSON.stringify({
199
- kairos_run_id: runId,
200
- mode: built.mode,
201
- matchCount: matches.length,
202
- topMatchScore: matches[0]?.score ?? null,
203
- nodeCatalog: syncResult ? "synced" : "static",
204
- nodeCount: syncResult?.nodeCount ?? null,
205
- ...syncResult ? {} : { syncWarning: "Could not sync node types from your n8n instance. Using static fallback catalog \u2014 generated workflows may not match your exact n8n setup." },
206
- systemPrompt: systemText,
207
- userMessage: built.userMessage,
208
- outputFormat: {
209
- description: "Generate a JSON object with this exact structure. The workflow field contains the n8n workflow. credentialsNeeded lists services requiring credentials.",
210
- schema: {
211
- workflow: {
212
- name: "string \u2014 descriptive workflow name",
213
- nodes: "array \u2014 n8n node objects with id (UUID v4), type, typeVersion, name, position, parameters",
214
- connections: "object \u2014 keyed by source node NAME, maps to target nodes",
215
- settings: 'object \u2014 include executionOrder: "v1"'
216
- },
217
- credentialsNeeded: [{
218
- service: 'string \u2014 e.g. "Slack"',
219
- credentialType: 'string \u2014 e.g. "slackOAuth2Api"',
220
- description: "string \u2014 what the user needs to set up"
221
- }]
222
- }
223
- }
224
- }, null, 2)
225
- }]
226
- };
257
+ return mcpText(JSON.stringify({
258
+ kairos_run_id: runId,
259
+ mode: built.mode,
260
+ matchCount: matches.length,
261
+ topMatchScore: matches[0]?.score ?? null,
262
+ nodeCatalog: syncResult ? "synced" : "static",
263
+ nodeCount: syncResult?.nodeCount ?? null,
264
+ ...syncResult ? {} : {
265
+ syncWarning: hasN8nCreds ? "Could not sync node types from your n8n instance. Using static fallback catalog \u2014 generated workflows may not match your exact n8n setup." : "N8N_BASE_URL and N8N_API_KEY are not set. Using static fallback catalog \u2014 node types may not match your n8n instance. Set these env vars to enable accurate generation and deployment."
266
+ },
267
+ systemPrompt: systemText,
268
+ userMessage: built.userMessage,
269
+ outputFormat: {
270
+ description: "Generate a JSON object with this exact structure. The workflow field contains the n8n workflow. credentialsNeeded lists services requiring credentials.",
271
+ schema: {
272
+ workflow: {
273
+ name: "string \u2014 descriptive workflow name",
274
+ nodes: "array \u2014 n8n node objects with id (UUID v4), type, typeVersion, name, position, parameters",
275
+ connections: "object \u2014 keyed by source node NAME, maps to target nodes",
276
+ settings: 'object \u2014 include executionOrder: "v1"'
277
+ },
278
+ credentialsNeeded: [{
279
+ service: 'string \u2014 e.g. "Slack"',
280
+ credentialType: 'string \u2014 e.g. "slackOAuth2Api"',
281
+ description: "string \u2014 what the user needs to set up"
282
+ }]
283
+ }
284
+ }
285
+ }, null, 2));
227
286
  }
228
287
  );
229
288
  server.tool(
230
289
  "kairos_validate",
231
- "Validate n8n workflow JSON against 26 structural rules. Returns pass/fail with specific issues. If validation fails, fix the issues and call this again. Errors block deployment; warnings are advisory.",
290
+ "Validate n8n workflow JSON against 34 structural rules. Returns pass/fail with specific issues. If validation fails, fix the issues and call this again. Errors block deployment; warnings are advisory.",
232
291
  {
233
292
  workflow: z.string().describe("The workflow JSON string to validate"),
234
293
  kairos_run_id: z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
@@ -238,17 +297,9 @@ server.tool(
238
297
  try {
239
298
  parsed = JSON.parse(workflowStr);
240
299
  } catch (e) {
241
- return {
242
- content: [{
243
- type: "text",
244
- text: JSON.stringify({
245
- valid: false,
246
- error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`
247
- }, null, 2)
248
- }]
249
- };
300
+ return mcpText(JSON.stringify({ valid: false, error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }, null, 2));
250
301
  }
251
- const result = validator.validate(parsed);
302
+ const result = getValidator().validate(parsed);
252
303
  const errors = result.issues.filter((i) => i.severity === "error");
253
304
  const warnings = result.issues.filter((i) => i.severity === "warn");
254
305
  if (mcpTelemetry && kairos_run_id) {
@@ -269,27 +320,14 @@ server.tool(
269
320
  }, kairos_run_id);
270
321
  }
271
322
  }
272
- return {
273
- content: [{
274
- type: "text",
275
- text: JSON.stringify({
276
- valid: result.valid,
277
- errorCount: errors.length,
278
- warningCount: warnings.length,
279
- errors: errors.map((i) => ({
280
- rule: i.rule,
281
- message: i.message,
282
- nodeId: i.nodeId ?? null
283
- })),
284
- warnings: warnings.map((i) => ({
285
- rule: i.rule,
286
- message: i.message,
287
- nodeId: i.nodeId ?? null
288
- })),
289
- deployable: errors.length === 0
290
- }, null, 2)
291
- }]
292
- };
323
+ return mcpText(JSON.stringify({
324
+ valid: result.valid,
325
+ errorCount: errors.length,
326
+ warningCount: warnings.length,
327
+ errors: errors.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null })),
328
+ warnings: warnings.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null })),
329
+ deployable: errors.length === 0
330
+ }, null, 2));
293
331
  }
294
332
  );
295
333
  server.tool(
@@ -298,101 +336,151 @@ server.tool(
298
336
  {
299
337
  workflow: z.string().describe("The validated workflow JSON string to deploy"),
300
338
  activate: z.boolean().default(false).describe("Activate the workflow immediately after deployment"),
301
- kairos_run_id: z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
339
+ description: z.string().optional().describe("The original user intent / description for this workflow \u2014 used to improve library search quality over time"),
340
+ kairos_run_id: z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation"),
341
+ kairos_secret: z.string().optional().describe("Required when KAIROS_MCP_SECRET env var is set")
302
342
  },
303
- async ({ workflow: workflowStr, activate, kairos_run_id }) => {
343
+ async ({ workflow: workflowStr, activate, description: userDescription, kairos_run_id, kairos_secret }) => {
344
+ const authError = checkMcpAuth(kairos_secret);
345
+ if (authError) return authError;
304
346
  if (!isAllowed("deploy")) {
305
- return {
306
- content: [{
307
- type: "text",
308
- text: JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." })
309
- }],
310
- isError: true
311
- };
347
+ return mcpError(JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." }));
312
348
  }
313
349
  let parsed;
314
350
  try {
315
351
  parsed = JSON.parse(workflowStr);
316
352
  } catch (e) {
317
- return {
318
- content: [{
319
- type: "text",
320
- text: JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` })
321
- }]
322
- };
353
+ return mcpError(JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }));
323
354
  }
324
- const validation = validator.validate(parsed);
355
+ const validation = getValidator().validate(parsed);
325
356
  const errors = validation.issues.filter((i) => i.severity === "error");
326
357
  if (errors.length > 0) {
327
- return {
328
- content: [{
329
- type: "text",
330
- text: JSON.stringify({
331
- error: "Workflow has validation errors \u2014 fix them before deploying",
332
- errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
333
- }, null, 2)
334
- }]
335
- };
358
+ return mcpError(JSON.stringify({
359
+ error: "Workflow has validation errors \u2014 fix them before deploying",
360
+ errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
361
+ }, null, 2));
336
362
  }
337
363
  const client = getApiClient();
338
364
  const stripped = stripper.stripForCreate(parsed);
339
365
  const response = await client.createWorkflow(stripped);
340
366
  if (activate) {
341
367
  if (!isAllowed("activate")) {
342
- return {
343
- content: [{
344
- type: "text",
345
- text: JSON.stringify({
346
- workflowId: response.id,
347
- name: response.name,
348
- activated: false,
349
- warning: "Workflow deployed but activation is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable.",
350
- url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
351
- }, null, 2)
352
- }]
353
- };
368
+ return mcpText(JSON.stringify({
369
+ workflowId: response.id,
370
+ name: response.name,
371
+ activated: false,
372
+ warning: "Workflow deployed but activation is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable.",
373
+ url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
374
+ }, null, 2));
354
375
  }
355
376
  await client.activateWorkflow(response.id);
356
377
  }
378
+ const session = kairos_run_id ? mcpSessions.get(kairos_run_id) : void 0;
379
+ const missingSessionWarning = kairos_run_id && !session ? `
380
+
381
+ Note: kairos_run_id "${kairos_run_id}" was provided but no active session was found. This usually means kairos_deploy was called without a prior kairos_prompt call, or the session expired. Telemetry and pattern learning for this build were skipped.` : "";
357
382
  await library.initialize();
358
383
  await library.save(parsed, {
359
- description: parsed.name,
360
- generationMode: "scratch",
361
- generationAttempts: 1
384
+ description: session?.description ?? userDescription ?? parsed.name,
385
+ generationMode: session && session.matchCount > 0 ? "reference" : "scratch",
386
+ generationAttempts: session?.validateAttempts ?? 1,
387
+ n8nWorkflowId: response.id
362
388
  });
363
- if (mcpTelemetry && kairos_run_id) {
364
- const session = mcpSessions.get(kairos_run_id);
365
- if (session) {
366
- await mcpTelemetry.emit("build_complete", {
367
- description: session.description,
368
- success: true,
369
- totalAttempts: session.validateAttempts,
370
- totalDurationMs: Date.now() - session.startTime,
371
- totalTokensInput: 0,
372
- totalTokensOutput: 0,
373
- workflowName: response.name,
374
- workflowId: response.id,
375
- dryRun: false,
376
- credentialsNeeded: 0,
377
- warnedRules: session.warnedRules,
378
- workflowType: session.workflowType
379
- }, kairos_run_id);
380
- mcpSessions.delete(kairos_run_id);
381
- PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
382
- });
383
- }
389
+ if (mcpTelemetry && kairos_run_id && session) {
390
+ await mcpTelemetry.emit("build_complete", {
391
+ description: session.description,
392
+ success: true,
393
+ totalAttempts: session.validateAttempts,
394
+ totalDurationMs: Date.now() - session.startTime,
395
+ totalTokensInput: 0,
396
+ totalTokensOutput: 0,
397
+ workflowName: response.name,
398
+ workflowId: response.id,
399
+ dryRun: false,
400
+ credentialsNeeded: 0,
401
+ warnedRules: session.warnedRules,
402
+ workflowType: session.workflowType
403
+ }, kairos_run_id);
404
+ mcpSessions.delete(kairos_run_id);
405
+ PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
406
+ });
384
407
  }
385
- return {
386
- content: [{
387
- type: "text",
388
- text: JSON.stringify({
389
- workflowId: response.id,
390
- name: response.name,
391
- activated: activate,
392
- url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
393
- }, null, 2)
394
- }]
395
- };
408
+ return mcpText(JSON.stringify({
409
+ workflowId: response.id,
410
+ name: response.name,
411
+ activated: activate,
412
+ url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
413
+ }, null, 2) + missingSessionWarning);
414
+ }
415
+ );
416
+ server.tool(
417
+ "kairos_replace",
418
+ "Replace an existing n8n workflow with a new version. Validates before updating. Use kairos_prompt \u2192 kairos_validate \u2192 kairos_replace for iteration on existing workflows.",
419
+ {
420
+ workflow_id: z.string().describe("The n8n workflow ID to replace"),
421
+ workflow: z.string().describe("The validated workflow JSON string"),
422
+ description: z.string().optional().describe("The original user intent / description for this workflow \u2014 used to improve library search quality over time"),
423
+ kairos_run_id: z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation"),
424
+ kairos_secret: z.string().optional().describe("Required when KAIROS_MCP_SECRET env var is set")
425
+ },
426
+ async ({ workflow_id, workflow: workflowStr, description: userDescription, kairos_run_id, kairos_secret }) => {
427
+ const authError = checkMcpAuth(kairos_secret);
428
+ if (authError) return authError;
429
+ if (!isAllowed("deploy")) {
430
+ return mcpError(JSON.stringify({ error: "Replace is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true or KAIROS_MCP_MODE=deploy to enable." }));
431
+ }
432
+ let parsed;
433
+ try {
434
+ parsed = JSON.parse(workflowStr);
435
+ } catch (e) {
436
+ return mcpError(JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }));
437
+ }
438
+ const validation = getValidator().validate(parsed);
439
+ const errors = validation.issues.filter((i) => i.severity === "error");
440
+ if (errors.length > 0) {
441
+ return mcpError(JSON.stringify({
442
+ error: "Workflow has validation errors \u2014 fix them before replacing",
443
+ errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
444
+ }, null, 2));
445
+ }
446
+ const client = getApiClient();
447
+ const stripped = stripper.stripForUpdate(parsed);
448
+ const response = await client.updateWorkflow(workflow_id, stripped);
449
+ const session = kairos_run_id ? mcpSessions.get(kairos_run_id) : void 0;
450
+ const missingSessionWarning = kairos_run_id && !session ? `
451
+
452
+ Note: kairos_run_id "${kairos_run_id}" was provided but no active session was found.` : "";
453
+ await library.initialize();
454
+ await library.save(parsed, {
455
+ description: session?.description ?? userDescription ?? parsed.name,
456
+ generationMode: session && session.matchCount > 0 ? "reference" : "scratch",
457
+ generationAttempts: session?.validateAttempts ?? 1,
458
+ n8nWorkflowId: workflow_id
459
+ });
460
+ if (mcpTelemetry && kairos_run_id && session) {
461
+ await mcpTelemetry.emit("build_complete", {
462
+ description: session.description,
463
+ success: true,
464
+ totalAttempts: session.validateAttempts,
465
+ totalDurationMs: Date.now() - session.startTime,
466
+ totalTokensInput: 0,
467
+ totalTokensOutput: 0,
468
+ workflowName: response.name,
469
+ workflowId: response.id,
470
+ dryRun: false,
471
+ credentialsNeeded: 0,
472
+ warnedRules: session.warnedRules,
473
+ workflowType: session.workflowType
474
+ }, kairos_run_id);
475
+ mcpSessions.delete(kairos_run_id);
476
+ PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
477
+ });
478
+ }
479
+ return mcpText(JSON.stringify({
480
+ workflowId: response.id,
481
+ name: response.name,
482
+ url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
483
+ }, null, 2) + missingSessionWarning);
396
484
  }
397
485
  );
398
486
  server.tool(
@@ -405,23 +493,20 @@ server.tool(
405
493
  async ({ query, limit }) => {
406
494
  await library.initialize();
407
495
  const matches = await library.search(query);
408
- return {
409
- content: [{
410
- type: "text",
411
- text: JSON.stringify(
412
- matches.slice(0, limit).map((m) => ({
413
- score: Number(m.score.toFixed(3)),
414
- mode: m.mode,
415
- description: m.workflow.description,
416
- nodeCount: m.workflow.workflow.nodes.length,
417
- nodes: m.workflow.workflow.nodes.map((n) => n.name),
418
- failurePatterns: m.workflow.failurePatterns ?? []
419
- })),
420
- null,
421
- 2
422
- )
423
- }]
424
- };
496
+ return mcpText(JSON.stringify(
497
+ matches.slice(0, limit).map((m) => ({
498
+ id: m.workflow.id,
499
+ score: Number(m.score.toFixed(3)),
500
+ mode: m.mode,
501
+ description: m.workflow.description,
502
+ nodeCount: m.workflow.workflow.nodes.length,
503
+ nodes: m.workflow.workflow.nodes.map((n) => n.name),
504
+ n8nWorkflowId: m.workflow.n8nWorkflowId ?? null,
505
+ failurePatterns: m.workflow.failurePatterns ?? []
506
+ })),
507
+ null,
508
+ 2
509
+ ));
425
510
  }
426
511
  );
427
512
  server.tool(
@@ -432,36 +517,19 @@ server.tool(
432
517
  const baseUrl = process.env["N8N_BASE_URL"];
433
518
  const apiKey = process.env["N8N_API_KEY"];
434
519
  if (!baseUrl || !apiKey) {
435
- return {
436
- content: [{
437
- type: "text",
438
- text: JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required for sync." })
439
- }],
440
- isError: true
441
- };
520
+ return mcpError(JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required for sync." }));
442
521
  }
443
522
  lastSync = null;
444
523
  const result = await autoSync();
445
524
  if (!result) {
446
- return {
447
- content: [{
448
- type: "text",
449
- text: JSON.stringify({ error: "Failed to fetch node types from n8n. Check your credentials and that your instance is running." })
450
- }],
451
- isError: true
452
- };
525
+ return mcpError(JSON.stringify({ error: "Failed to fetch node types from n8n. Check your credentials and that your instance is running." }));
453
526
  }
454
- return {
455
- content: [{
456
- type: "text",
457
- text: JSON.stringify({
458
- synced: true,
459
- nodeCount: result.nodeCount,
460
- newNodes: result.newNodes,
461
- message: `Synced ${result.nodeCount} node types from your n8n instance (${result.newNodes} not in default catalog).`
462
- }, null, 2)
463
- }]
464
- };
527
+ return mcpText(JSON.stringify({
528
+ synced: true,
529
+ nodeCount: result.nodeCount,
530
+ newNodes: result.newNodes,
531
+ message: `Synced ${result.nodeCount} node types from your n8n instance (${result.newNodes} not in default catalog).`
532
+ }, null, 2));
465
533
  }
466
534
  );
467
535
  server.tool(
@@ -477,12 +545,72 @@ server.tool(
477
545
  if (limit !== void 0 && limit > 0) {
478
546
  analysis.topFailureRules = analysis.topFailureRules.slice(0, limit);
479
547
  }
480
- return {
481
- content: [{
482
- type: "text",
483
- text: JSON.stringify(analysis, null, 2)
484
- }]
485
- };
548
+ return mcpText(JSON.stringify(analysis, null, 2));
549
+ }
550
+ );
551
+ server.tool(
552
+ "kairos_library",
553
+ "Browse the local Kairos workflow library. Returns saved workflow metadata. Use the optional query to search, or omit it to list all entries.",
554
+ {
555
+ query: z.string().optional().describe("Optional search query \u2014 omit to list all entries"),
556
+ limit: z.number().default(20).describe("Maximum entries to return")
557
+ },
558
+ async ({ query, limit }) => {
559
+ await library.initialize();
560
+ if (query) {
561
+ const matches = await library.search(query);
562
+ return mcpText(JSON.stringify(
563
+ matches.slice(0, limit).map((m) => ({
564
+ id: m.workflow.id,
565
+ description: m.workflow.description,
566
+ score: Number(m.score.toFixed(3)),
567
+ mode: m.mode,
568
+ nodeCount: m.workflow.workflow.nodes.length,
569
+ nodes: m.workflow.workflow.nodes.map((n) => n.name),
570
+ deployCount: m.workflow.deployCount,
571
+ n8nWorkflowId: m.workflow.n8nWorkflowId ?? null,
572
+ createdAt: m.workflow.createdAt
573
+ })),
574
+ null,
575
+ 2
576
+ ));
577
+ }
578
+ const all = await library.list();
579
+ return mcpText(JSON.stringify(
580
+ all.slice(0, limit).map((w) => ({
581
+ id: w.id,
582
+ description: w.description,
583
+ nodeCount: w.workflow.nodes.length,
584
+ nodes: w.workflow.nodes.map((n) => n.name),
585
+ deployCount: w.deployCount,
586
+ n8nWorkflowId: w.n8nWorkflowId ?? null,
587
+ timesRetrieved: w.timesRetrieved ?? 0,
588
+ createdAt: w.createdAt
589
+ })),
590
+ null,
591
+ 2
592
+ ));
593
+ }
594
+ );
595
+ server.tool(
596
+ "kairos_outcome",
597
+ "Record the outcome of a workflow build against a library entry. Trains the pattern learning system to know what works and what fails over time.",
598
+ {
599
+ library_id: z.string().describe("The Kairos library entry ID (returned by kairos_deploy, kairos_replace, or kairos_library)"),
600
+ attempts: z.number().describe("Number of generation+validation attempts before success"),
601
+ first_try_pass: z.boolean().describe("Whether the first attempt passed validation"),
602
+ failed_rules: z.array(z.number()).describe("Validation rule IDs that failed during generation"),
603
+ mode: z.enum(["direct", "reference"]).describe("How the library entry was used during generation")
604
+ },
605
+ async ({ library_id, attempts, first_try_pass, failed_rules, mode }) => {
606
+ await library.initialize();
607
+ await library.recordOutcome(library_id, {
608
+ attempts,
609
+ firstTryPass: first_try_pass,
610
+ failedRules: failed_rules,
611
+ mode
612
+ });
613
+ return mcpText(JSON.stringify({ recorded: true, libraryId: library_id }));
486
614
  }
487
615
  );
488
616
  server.tool(
@@ -492,12 +620,7 @@ server.tool(
492
620
  async () => {
493
621
  const client = getApiClient();
494
622
  const workflows = await client.listWorkflows();
495
- return {
496
- content: [{
497
- type: "text",
498
- text: JSON.stringify(workflows, null, 2)
499
- }]
500
- };
623
+ return mcpText(JSON.stringify(workflows, null, 2));
501
624
  }
502
625
  );
503
626
  server.tool(
@@ -509,12 +632,7 @@ server.tool(
509
632
  async ({ workflow_id }) => {
510
633
  const client = getApiClient();
511
634
  const workflow = await client.getWorkflow(workflow_id);
512
- return {
513
- content: [{
514
- type: "text",
515
- text: JSON.stringify(workflow, null, 2)
516
- }]
517
- };
635
+ return mcpText(JSON.stringify(workflow, null, 2));
518
636
  }
519
637
  );
520
638
  server.tool(
@@ -525,22 +643,11 @@ server.tool(
525
643
  },
526
644
  async ({ workflow_id }) => {
527
645
  if (!isAllowed("activate")) {
528
- return {
529
- content: [{
530
- type: "text",
531
- text: JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." })
532
- }],
533
- isError: true
534
- };
646
+ return mcpError(JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." }));
535
647
  }
536
648
  const client = getApiClient();
537
649
  await client.activateWorkflow(workflow_id);
538
- return {
539
- content: [{
540
- type: "text",
541
- text: `Activated workflow ${workflow_id}`
542
- }]
543
- };
650
+ return mcpText(`Activated workflow ${workflow_id}`);
544
651
  }
545
652
  );
546
653
  server.tool(
@@ -552,38 +659,25 @@ server.tool(
552
659
  async ({ workflow_id }) => {
553
660
  const client = getApiClient();
554
661
  await client.deactivateWorkflow(workflow_id);
555
- return {
556
- content: [{
557
- type: "text",
558
- text: `Deactivated workflow ${workflow_id}`
559
- }]
560
- };
662
+ return mcpText(`Deactivated workflow ${workflow_id}`);
561
663
  }
562
664
  );
563
665
  server.tool(
564
666
  "kairos_delete",
565
667
  "Delete a workflow from n8n. This is irreversible.",
566
668
  {
567
- workflow_id: z.string().describe("The n8n workflow ID to delete")
669
+ workflow_id: z.string().describe("The n8n workflow ID to delete"),
670
+ kairos_secret: z.string().optional().describe("Required when KAIROS_MCP_SECRET env var is set")
568
671
  },
569
- async ({ workflow_id }) => {
672
+ async ({ workflow_id, kairos_secret }) => {
673
+ const authError = checkMcpAuth(kairos_secret);
674
+ if (authError) return authError;
570
675
  if (!isAllowed("delete")) {
571
- return {
572
- content: [{
573
- type: "text",
574
- text: JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." })
575
- }],
576
- isError: true
577
- };
676
+ return mcpError(JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." }));
578
677
  }
579
678
  const client = getApiClient();
580
679
  await client.deleteWorkflow(workflow_id);
581
- return {
582
- content: [{
583
- type: "text",
584
- text: `Deleted workflow ${workflow_id}`
585
- }]
586
- };
680
+ return mcpText(`Deleted workflow ${workflow_id}`);
587
681
  }
588
682
  );
589
683
  server.tool(
@@ -596,17 +690,36 @@ server.tool(
596
690
  async ({ workflow_id, limit }) => {
597
691
  const client = getApiClient();
598
692
  const executions = await client.getExecutions(workflow_id, { limit });
599
- return {
600
- content: [{
601
- type: "text",
602
- text: JSON.stringify(executions, null, 2)
603
- }]
604
- };
693
+ return mcpText(JSON.stringify(executions, null, 2));
605
694
  }
606
695
  );
607
696
  async function main() {
608
- const transport = new StdioServerTransport();
609
- await server.connect(transport);
697
+ if (!process.env["ANTHROPIC_API_KEY"]) {
698
+ process.stderr.write(
699
+ "[kairos-mcp] WARNING: ANTHROPIC_API_KEY is not set \u2014 kairos_prompt will fail. Set it before using workflow generation tools.\n"
700
+ );
701
+ }
702
+ const useHttp = process.argv.includes("--http");
703
+ if (useHttp) {
704
+ const port = parseInt(process.env["KAIROS_MCP_PORT"] ?? "3000", 10);
705
+ const transport = new StreamableHTTPServerTransport();
706
+ await server.connect(transport);
707
+ const httpServer = createServer(async (req, res) => {
708
+ if (req.method === "GET" || req.method === "POST" || req.method === "DELETE") {
709
+ await transport.handleRequest(req, res);
710
+ } else {
711
+ res.writeHead(405, { "Content-Type": "application/json" });
712
+ res.end(JSON.stringify({ error: "Method not allowed" }));
713
+ }
714
+ });
715
+ httpServer.listen(port, () => {
716
+ process.stderr.write(`[kairos-mcp] HTTP transport listening on port ${port}
717
+ `);
718
+ });
719
+ } else {
720
+ const transport = new StdioServerTransport();
721
+ await server.connect(transport);
722
+ }
610
723
  }
611
724
  main().catch((err) => {
612
725
  console.error("Kairos MCP server failed to start:", err);