@kairos-sdk/core 0.4.5 → 0.5.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.
@@ -2,10 +2,11 @@
2
2
  import {
3
3
  PromptBuilder,
4
4
  inferWorkflowType
5
- } from "./chunk-CR2NHLOH.js";
5
+ } from "./chunk-EVOAYH2K.js";
6
6
  import {
7
7
  DEFAULT_REGISTRY,
8
8
  FileLibrary,
9
+ GuardError,
9
10
  N8nApiClient,
10
11
  N8nFieldStripper,
11
12
  N8nValidator,
@@ -15,7 +16,7 @@ import {
15
16
  TelemetryReader,
16
17
  generateUUID,
17
18
  nullLogger
18
- } from "./chunk-6IXW3WCC.js";
19
+ } from "./chunk-KIFT5LA7.js";
19
20
 
20
21
  // src/mcp-server.ts
21
22
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -87,9 +88,13 @@ import { fileURLToPath } from "url";
87
88
  var __dirname = dirname(fileURLToPath(import.meta.url));
88
89
  var pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
89
90
  var library = new FileLibrary();
90
- var validator = new N8nValidator();
91
+ var _validator = new N8nValidator();
92
+ function getValidator() {
93
+ return _validator;
94
+ }
91
95
  var nodeSyncer = new NodeSyncer();
92
96
  var lastSync = null;
97
+ var AUTO_SYNC_TIMEOUT_MS = 5e3;
93
98
  var stripper = new N8nFieldStripper();
94
99
  var promptBuilder = new PromptBuilder(getMcpPatternsPath());
95
100
  function getMcpTelemetry() {
@@ -124,11 +129,23 @@ function isAllowed(action) {
124
129
  const key = `KAIROS_MCP_ALLOW_${action.toUpperCase()}`;
125
130
  return process.env[key] === "true";
126
131
  }
132
+ function mcpText(text) {
133
+ return { content: [{ type: "text", text }] };
134
+ }
135
+ function mcpError(text) {
136
+ return { content: [{ type: "text", text }], isError: true };
137
+ }
138
+ function checkMcpAuth(provided) {
139
+ const expected = process.env["KAIROS_MCP_SECRET"];
140
+ if (!expected) return null;
141
+ if (provided === expected) return null;
142
+ return mcpError(JSON.stringify({ error: "Unauthorized: missing or incorrect kairos_secret" }));
143
+ }
127
144
  function getApiClient() {
128
145
  const baseUrl = process.env["N8N_BASE_URL"];
129
146
  const apiKey = process.env["N8N_API_KEY"];
130
147
  if (!baseUrl || !apiKey) {
131
- throw new Error("N8N_BASE_URL and N8N_API_KEY environment variables are required for n8n operations");
148
+ throw new GuardError("N8N_BASE_URL and N8N_API_KEY environment variables are required for n8n operations");
132
149
  }
133
150
  return new N8nApiClient(baseUrl, apiKey, nullLogger);
134
151
  }
@@ -142,7 +159,7 @@ async function autoSync() {
142
159
  const nodeTypes = await client.getNodeTypes();
143
160
  if (nodeTypes.length === 0) return null;
144
161
  lastSync = nodeSyncer.sync(nodeTypes);
145
- validator = new N8nValidator(lastSync.registry);
162
+ _validator = new N8nValidator(lastSync.registry);
146
163
  return lastSync;
147
164
  } catch {
148
165
  return null;
@@ -164,21 +181,21 @@ server.tool(
164
181
  const baseUrl = process.env["N8N_BASE_URL"];
165
182
  const apiKey = process.env["N8N_API_KEY"];
166
183
  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
- };
184
+ return mcpError(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." }));
174
185
  }
175
186
  const runId = generateUUID();
176
187
  const workflowType = inferWorkflowType(description);
188
+ const syncPromise = autoSync();
189
+ const syncTimeout = new Promise((resolve) => setTimeout(() => resolve(null), AUTO_SYNC_TIMEOUT_MS));
177
190
  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() ?? [];
191
+ const [syncResult, matches, failureRates] = await Promise.all([
192
+ Promise.race([syncPromise, syncTimeout]),
193
+ library.search(description),
194
+ (async () => {
195
+ const reader = getTelemetryReader();
196
+ return reader ? reader.getFailureRates() : [];
197
+ })()
198
+ ]);
182
199
  const request = { description, ...name ? { name } : {} };
183
200
  const built = promptBuilder.build(request, matches, failureRates, syncResult?.catalogText);
184
201
  if (mcpTelemetry) {
@@ -192,43 +209,38 @@ server.tool(
192
209
  await mcpTelemetry.emit("build_start", { description, model: "mcp-decomposed", dryRun: false }, runId);
193
210
  }
194
211
  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
- };
212
+ return mcpText(JSON.stringify({
213
+ kairos_run_id: runId,
214
+ mode: built.mode,
215
+ matchCount: matches.length,
216
+ topMatchScore: matches[0]?.score ?? null,
217
+ nodeCatalog: syncResult ? "synced" : "static",
218
+ nodeCount: syncResult?.nodeCount ?? null,
219
+ ...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." },
220
+ systemPrompt: systemText,
221
+ userMessage: built.userMessage,
222
+ outputFormat: {
223
+ description: "Generate a JSON object with this exact structure. The workflow field contains the n8n workflow. credentialsNeeded lists services requiring credentials.",
224
+ schema: {
225
+ workflow: {
226
+ name: "string \u2014 descriptive workflow name",
227
+ nodes: "array \u2014 n8n node objects with id (UUID v4), type, typeVersion, name, position, parameters",
228
+ connections: "object \u2014 keyed by source node NAME, maps to target nodes",
229
+ settings: 'object \u2014 include executionOrder: "v1"'
230
+ },
231
+ credentialsNeeded: [{
232
+ service: 'string \u2014 e.g. "Slack"',
233
+ credentialType: 'string \u2014 e.g. "slackOAuth2Api"',
234
+ description: "string \u2014 what the user needs to set up"
235
+ }]
236
+ }
237
+ }
238
+ }, null, 2));
227
239
  }
228
240
  );
229
241
  server.tool(
230
242
  "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.",
243
+ "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
244
  {
233
245
  workflow: z.string().describe("The workflow JSON string to validate"),
234
246
  kairos_run_id: z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
@@ -238,17 +250,9 @@ server.tool(
238
250
  try {
239
251
  parsed = JSON.parse(workflowStr);
240
252
  } 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
- };
253
+ return mcpText(JSON.stringify({ valid: false, error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }, null, 2));
250
254
  }
251
- const result = validator.validate(parsed);
255
+ const result = getValidator().validate(parsed);
252
256
  const errors = result.issues.filter((i) => i.severity === "error");
253
257
  const warnings = result.issues.filter((i) => i.severity === "warn");
254
258
  if (mcpTelemetry && kairos_run_id) {
@@ -269,27 +273,14 @@ server.tool(
269
273
  }, kairos_run_id);
270
274
  }
271
275
  }
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
- };
276
+ return mcpText(JSON.stringify({
277
+ valid: result.valid,
278
+ errorCount: errors.length,
279
+ warningCount: warnings.length,
280
+ errors: errors.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null })),
281
+ warnings: warnings.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null })),
282
+ deployable: errors.length === 0
283
+ }, null, 2));
293
284
  }
294
285
  );
295
286
  server.tool(
@@ -298,101 +289,146 @@ server.tool(
298
289
  {
299
290
  workflow: z.string().describe("The validated workflow JSON string to deploy"),
300
291
  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")
292
+ kairos_run_id: z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation"),
293
+ kairos_secret: z.string().optional().describe("Required when KAIROS_MCP_SECRET env var is set")
302
294
  },
303
- async ({ workflow: workflowStr, activate, kairos_run_id }) => {
295
+ async ({ workflow: workflowStr, activate, kairos_run_id, kairos_secret }) => {
296
+ const authError = checkMcpAuth(kairos_secret);
297
+ if (authError) return authError;
304
298
  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
- };
299
+ return mcpError(JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." }));
312
300
  }
313
301
  let parsed;
314
302
  try {
315
303
  parsed = JSON.parse(workflowStr);
316
304
  } 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
- };
305
+ return mcpError(JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }));
323
306
  }
324
- const validation = validator.validate(parsed);
307
+ const validation = getValidator().validate(parsed);
325
308
  const errors = validation.issues.filter((i) => i.severity === "error");
326
309
  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
- };
310
+ return mcpError(JSON.stringify({
311
+ error: "Workflow has validation errors \u2014 fix them before deploying",
312
+ errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
313
+ }, null, 2));
336
314
  }
337
315
  const client = getApiClient();
338
316
  const stripped = stripper.stripForCreate(parsed);
339
317
  const response = await client.createWorkflow(stripped);
340
318
  if (activate) {
341
319
  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
- };
320
+ return mcpText(JSON.stringify({
321
+ workflowId: response.id,
322
+ name: response.name,
323
+ activated: false,
324
+ warning: "Workflow deployed but activation is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable.",
325
+ url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
326
+ }, null, 2));
354
327
  }
355
328
  await client.activateWorkflow(response.id);
356
329
  }
330
+ const session = kairos_run_id ? mcpSessions.get(kairos_run_id) : void 0;
331
+ const missingSessionWarning = kairos_run_id && !session ? `
332
+
333
+ 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
334
  await library.initialize();
358
335
  await library.save(parsed, {
359
- description: parsed.name,
336
+ description: session?.description ?? parsed.name,
360
337
  generationMode: "scratch",
361
- generationAttempts: 1
338
+ generationAttempts: session?.validateAttempts ?? 1,
339
+ n8nWorkflowId: response.id
362
340
  });
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
- }
341
+ if (mcpTelemetry && kairos_run_id && session) {
342
+ await mcpTelemetry.emit("build_complete", {
343
+ description: session.description,
344
+ success: true,
345
+ totalAttempts: session.validateAttempts,
346
+ totalDurationMs: Date.now() - session.startTime,
347
+ totalTokensInput: 0,
348
+ totalTokensOutput: 0,
349
+ workflowName: response.name,
350
+ workflowId: response.id,
351
+ dryRun: false,
352
+ credentialsNeeded: 0,
353
+ warnedRules: session.warnedRules,
354
+ workflowType: session.workflowType
355
+ }, kairos_run_id);
356
+ mcpSessions.delete(kairos_run_id);
357
+ PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
358
+ });
384
359
  }
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
- };
360
+ return mcpText(JSON.stringify({
361
+ workflowId: response.id,
362
+ name: response.name,
363
+ activated: activate,
364
+ url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
365
+ }, null, 2) + missingSessionWarning);
366
+ }
367
+ );
368
+ server.tool(
369
+ "kairos_replace",
370
+ "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.",
371
+ {
372
+ workflow_id: z.string().describe("The n8n workflow ID to replace"),
373
+ workflow: z.string().describe("The validated workflow JSON string"),
374
+ kairos_run_id: z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation"),
375
+ kairos_secret: z.string().optional().describe("Required when KAIROS_MCP_SECRET env var is set")
376
+ },
377
+ async ({ workflow_id, workflow: workflowStr, kairos_run_id, kairos_secret }) => {
378
+ const authError = checkMcpAuth(kairos_secret);
379
+ if (authError) return authError;
380
+ let parsed;
381
+ try {
382
+ parsed = JSON.parse(workflowStr);
383
+ } catch (e) {
384
+ return mcpError(JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }));
385
+ }
386
+ const validation = getValidator().validate(parsed);
387
+ const errors = validation.issues.filter((i) => i.severity === "error");
388
+ if (errors.length > 0) {
389
+ return mcpError(JSON.stringify({
390
+ error: "Workflow has validation errors \u2014 fix them before replacing",
391
+ errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
392
+ }, null, 2));
393
+ }
394
+ const client = getApiClient();
395
+ const stripped = stripper.stripForUpdate(parsed);
396
+ const response = await client.updateWorkflow(workflow_id, stripped);
397
+ const session = kairos_run_id ? mcpSessions.get(kairos_run_id) : void 0;
398
+ const missingSessionWarning = kairos_run_id && !session ? `
399
+
400
+ Note: kairos_run_id "${kairos_run_id}" was provided but no active session was found.` : "";
401
+ await library.initialize();
402
+ await library.save(parsed, {
403
+ description: session?.description ?? parsed.name,
404
+ generationMode: "scratch",
405
+ generationAttempts: session?.validateAttempts ?? 1,
406
+ n8nWorkflowId: workflow_id
407
+ });
408
+ if (mcpTelemetry && kairos_run_id && session) {
409
+ await mcpTelemetry.emit("build_complete", {
410
+ description: session.description,
411
+ success: true,
412
+ totalAttempts: session.validateAttempts,
413
+ totalDurationMs: Date.now() - session.startTime,
414
+ totalTokensInput: 0,
415
+ totalTokensOutput: 0,
416
+ workflowName: response.name,
417
+ workflowId: response.id,
418
+ dryRun: false,
419
+ credentialsNeeded: 0,
420
+ warnedRules: session.warnedRules,
421
+ workflowType: session.workflowType
422
+ }, kairos_run_id);
423
+ mcpSessions.delete(kairos_run_id);
424
+ PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
425
+ });
426
+ }
427
+ return mcpText(JSON.stringify({
428
+ workflowId: response.id,
429
+ name: response.name,
430
+ url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
431
+ }, null, 2) + missingSessionWarning);
396
432
  }
397
433
  );
398
434
  server.tool(
@@ -405,23 +441,20 @@ server.tool(
405
441
  async ({ query, limit }) => {
406
442
  await library.initialize();
407
443
  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
- };
444
+ return mcpText(JSON.stringify(
445
+ matches.slice(0, limit).map((m) => ({
446
+ id: m.workflow.id,
447
+ score: Number(m.score.toFixed(3)),
448
+ mode: m.mode,
449
+ description: m.workflow.description,
450
+ nodeCount: m.workflow.workflow.nodes.length,
451
+ nodes: m.workflow.workflow.nodes.map((n) => n.name),
452
+ n8nWorkflowId: m.workflow.n8nWorkflowId ?? null,
453
+ failurePatterns: m.workflow.failurePatterns ?? []
454
+ })),
455
+ null,
456
+ 2
457
+ ));
425
458
  }
426
459
  );
427
460
  server.tool(
@@ -432,36 +465,19 @@ server.tool(
432
465
  const baseUrl = process.env["N8N_BASE_URL"];
433
466
  const apiKey = process.env["N8N_API_KEY"];
434
467
  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
- };
468
+ return mcpError(JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required for sync." }));
442
469
  }
443
470
  lastSync = null;
444
471
  const result = await autoSync();
445
472
  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
- };
473
+ return mcpError(JSON.stringify({ error: "Failed to fetch node types from n8n. Check your credentials and that your instance is running." }));
453
474
  }
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
- };
475
+ return mcpText(JSON.stringify({
476
+ synced: true,
477
+ nodeCount: result.nodeCount,
478
+ newNodes: result.newNodes,
479
+ message: `Synced ${result.nodeCount} node types from your n8n instance (${result.newNodes} not in default catalog).`
480
+ }, null, 2));
465
481
  }
466
482
  );
467
483
  server.tool(
@@ -477,12 +493,72 @@ server.tool(
477
493
  if (limit !== void 0 && limit > 0) {
478
494
  analysis.topFailureRules = analysis.topFailureRules.slice(0, limit);
479
495
  }
480
- return {
481
- content: [{
482
- type: "text",
483
- text: JSON.stringify(analysis, null, 2)
484
- }]
485
- };
496
+ return mcpText(JSON.stringify(analysis, null, 2));
497
+ }
498
+ );
499
+ server.tool(
500
+ "kairos_library",
501
+ "Browse the local Kairos workflow library. Returns saved workflow metadata. Use the optional query to search, or omit it to list all entries.",
502
+ {
503
+ query: z.string().optional().describe("Optional search query \u2014 omit to list all entries"),
504
+ limit: z.number().default(20).describe("Maximum entries to return")
505
+ },
506
+ async ({ query, limit }) => {
507
+ await library.initialize();
508
+ if (query) {
509
+ const matches = await library.search(query);
510
+ return mcpText(JSON.stringify(
511
+ matches.slice(0, limit).map((m) => ({
512
+ id: m.workflow.id,
513
+ description: m.workflow.description,
514
+ score: Number(m.score.toFixed(3)),
515
+ mode: m.mode,
516
+ nodeCount: m.workflow.workflow.nodes.length,
517
+ nodes: m.workflow.workflow.nodes.map((n) => n.name),
518
+ deployCount: m.workflow.deployCount,
519
+ n8nWorkflowId: m.workflow.n8nWorkflowId ?? null,
520
+ createdAt: m.workflow.createdAt
521
+ })),
522
+ null,
523
+ 2
524
+ ));
525
+ }
526
+ const all = await library.list();
527
+ return mcpText(JSON.stringify(
528
+ all.slice(0, limit).map((w) => ({
529
+ id: w.id,
530
+ description: w.description,
531
+ nodeCount: w.workflow.nodes.length,
532
+ nodes: w.workflow.nodes.map((n) => n.name),
533
+ deployCount: w.deployCount,
534
+ n8nWorkflowId: w.n8nWorkflowId ?? null,
535
+ timesRetrieved: w.timesRetrieved ?? 0,
536
+ createdAt: w.createdAt
537
+ })),
538
+ null,
539
+ 2
540
+ ));
541
+ }
542
+ );
543
+ server.tool(
544
+ "kairos_outcome",
545
+ "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.",
546
+ {
547
+ library_id: z.string().describe("The Kairos library entry ID (returned by kairos_deploy, kairos_replace, or kairos_library)"),
548
+ attempts: z.number().describe("Number of generation+validation attempts before success"),
549
+ first_try_pass: z.boolean().describe("Whether the first attempt passed validation"),
550
+ failed_rules: z.array(z.number()).describe("Validation rule IDs that failed during generation"),
551
+ mode: z.enum(["direct", "reference"]).describe("How the library entry was used during generation")
552
+ },
553
+ async ({ library_id, attempts, first_try_pass, failed_rules, mode }) => {
554
+ await library.initialize();
555
+ await library.recordOutcome(library_id, {
556
+ attempts,
557
+ firstTryPass: first_try_pass,
558
+ failedRules: failed_rules,
559
+ mode
560
+ });
561
+ return mcpText(JSON.stringify({ recorded: true, libraryId: library_id }));
486
562
  }
487
563
  );
488
564
  server.tool(
@@ -492,12 +568,7 @@ server.tool(
492
568
  async () => {
493
569
  const client = getApiClient();
494
570
  const workflows = await client.listWorkflows();
495
- return {
496
- content: [{
497
- type: "text",
498
- text: JSON.stringify(workflows, null, 2)
499
- }]
500
- };
571
+ return mcpText(JSON.stringify(workflows, null, 2));
501
572
  }
502
573
  );
503
574
  server.tool(
@@ -509,12 +580,7 @@ server.tool(
509
580
  async ({ workflow_id }) => {
510
581
  const client = getApiClient();
511
582
  const workflow = await client.getWorkflow(workflow_id);
512
- return {
513
- content: [{
514
- type: "text",
515
- text: JSON.stringify(workflow, null, 2)
516
- }]
517
- };
583
+ return mcpText(JSON.stringify(workflow, null, 2));
518
584
  }
519
585
  );
520
586
  server.tool(
@@ -525,22 +591,11 @@ server.tool(
525
591
  },
526
592
  async ({ workflow_id }) => {
527
593
  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
- };
594
+ return mcpError(JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." }));
535
595
  }
536
596
  const client = getApiClient();
537
597
  await client.activateWorkflow(workflow_id);
538
- return {
539
- content: [{
540
- type: "text",
541
- text: `Activated workflow ${workflow_id}`
542
- }]
543
- };
598
+ return mcpText(`Activated workflow ${workflow_id}`);
544
599
  }
545
600
  );
546
601
  server.tool(
@@ -552,38 +607,25 @@ server.tool(
552
607
  async ({ workflow_id }) => {
553
608
  const client = getApiClient();
554
609
  await client.deactivateWorkflow(workflow_id);
555
- return {
556
- content: [{
557
- type: "text",
558
- text: `Deactivated workflow ${workflow_id}`
559
- }]
560
- };
610
+ return mcpText(`Deactivated workflow ${workflow_id}`);
561
611
  }
562
612
  );
563
613
  server.tool(
564
614
  "kairos_delete",
565
615
  "Delete a workflow from n8n. This is irreversible.",
566
616
  {
567
- workflow_id: z.string().describe("The n8n workflow ID to delete")
617
+ workflow_id: z.string().describe("The n8n workflow ID to delete"),
618
+ kairos_secret: z.string().optional().describe("Required when KAIROS_MCP_SECRET env var is set")
568
619
  },
569
- async ({ workflow_id }) => {
620
+ async ({ workflow_id, kairos_secret }) => {
621
+ const authError = checkMcpAuth(kairos_secret);
622
+ if (authError) return authError;
570
623
  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
- };
624
+ return mcpError(JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." }));
578
625
  }
579
626
  const client = getApiClient();
580
627
  await client.deleteWorkflow(workflow_id);
581
- return {
582
- content: [{
583
- type: "text",
584
- text: `Deleted workflow ${workflow_id}`
585
- }]
586
- };
628
+ return mcpText(`Deleted workflow ${workflow_id}`);
587
629
  }
588
630
  );
589
631
  server.tool(
@@ -596,15 +638,15 @@ server.tool(
596
638
  async ({ workflow_id, limit }) => {
597
639
  const client = getApiClient();
598
640
  const executions = await client.getExecutions(workflow_id, { limit });
599
- return {
600
- content: [{
601
- type: "text",
602
- text: JSON.stringify(executions, null, 2)
603
- }]
604
- };
641
+ return mcpText(JSON.stringify(executions, null, 2));
605
642
  }
606
643
  );
607
644
  async function main() {
645
+ if (!process.env["ANTHROPIC_API_KEY"]) {
646
+ process.stderr.write(
647
+ "[kairos-mcp] WARNING: ANTHROPIC_API_KEY is not set \u2014 kairos_prompt will fail. Set it before using workflow generation tools.\n"
648
+ );
649
+ }
608
650
  const transport = new StdioServerTransport();
609
651
  await server.connect(transport);
610
652
  }