@kairos-sdk/core 0.4.0 → 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.
@@ -1,16 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ PromptBuilder,
4
+ inferWorkflowType
5
+ } from "./chunk-EVOAYH2K.js";
2
6
  import {
3
7
  DEFAULT_REGISTRY,
4
8
  FileLibrary,
9
+ GuardError,
5
10
  N8nApiClient,
6
11
  N8nFieldStripper,
7
12
  N8nValidator,
8
13
  NodeRegistry,
9
14
  PatternAnalyzer,
10
- PromptBuilder,
15
+ TelemetryCollector,
11
16
  TelemetryReader,
17
+ generateUUID,
12
18
  nullLogger
13
- } from "./chunk-NJ6QZBIC.js";
19
+ } from "./chunk-KIFT5LA7.js";
14
20
 
15
21
  // src/mcp-server.ts
16
22
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -77,15 +83,41 @@ ${regularLines}`;
77
83
  // src/mcp-server.ts
78
84
  import { readFileSync } from "fs";
79
85
  import { dirname, join } from "path";
86
+ import { homedir } from "os";
80
87
  import { fileURLToPath } from "url";
81
88
  var __dirname = dirname(fileURLToPath(import.meta.url));
82
89
  var pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
83
90
  var library = new FileLibrary();
84
- var validator = new N8nValidator();
91
+ var _validator = new N8nValidator();
92
+ function getValidator() {
93
+ return _validator;
94
+ }
85
95
  var nodeSyncer = new NodeSyncer();
86
96
  var lastSync = null;
97
+ var AUTO_SYNC_TIMEOUT_MS = 5e3;
87
98
  var stripper = new N8nFieldStripper();
88
- var promptBuilder = new PromptBuilder();
99
+ var promptBuilder = new PromptBuilder(getMcpPatternsPath());
100
+ function getMcpTelemetry() {
101
+ const val = process.env["KAIROS_TELEMETRY"];
102
+ if (!val || val === "false") return null;
103
+ return val === "true" ? new TelemetryCollector() : new TelemetryCollector(val);
104
+ }
105
+ function getMcpPatternsPath() {
106
+ const val = process.env["KAIROS_TELEMETRY"];
107
+ if (val && val !== "false" && val !== "true") {
108
+ return join(val, "..", "patterns.json");
109
+ }
110
+ return join(homedir(), ".kairos", "patterns.json");
111
+ }
112
+ var mcpTelemetry = getMcpTelemetry();
113
+ var mcpSessions = /* @__PURE__ */ new Map();
114
+ var SESSION_TTL_MS = 60 * 60 * 1e3;
115
+ function evictStaleSessions() {
116
+ const cutoff = Date.now() - SESSION_TTL_MS;
117
+ for (const [id, session] of mcpSessions) {
118
+ if (session.startTime < cutoff) mcpSessions.delete(id);
119
+ }
120
+ }
89
121
  function getTelemetryReader() {
90
122
  try {
91
123
  return new TelemetryReader();
@@ -97,11 +129,23 @@ function isAllowed(action) {
97
129
  const key = `KAIROS_MCP_ALLOW_${action.toUpperCase()}`;
98
130
  return process.env[key] === "true";
99
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
+ }
100
144
  function getApiClient() {
101
145
  const baseUrl = process.env["N8N_BASE_URL"];
102
146
  const apiKey = process.env["N8N_API_KEY"];
103
147
  if (!baseUrl || !apiKey) {
104
- 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");
105
149
  }
106
150
  return new N8nApiClient(baseUrl, apiKey, nullLogger);
107
151
  }
@@ -115,7 +159,7 @@ async function autoSync() {
115
159
  const nodeTypes = await client.getNodeTypes();
116
160
  if (nodeTypes.length === 0) return null;
117
161
  lastSync = nodeSyncer.sync(nodeTypes);
118
- validator = new N8nValidator(lastSync.registry);
162
+ _validator = new N8nValidator(lastSync.registry);
119
163
  return lastSync;
120
164
  } catch {
121
165
  return null;
@@ -133,103 +177,110 @@ server.tool(
133
177
  name: z.string().optional().describe("Optional workflow name override")
134
178
  },
135
179
  async ({ description, name }) => {
180
+ evictStaleSessions();
136
181
  const baseUrl = process.env["N8N_BASE_URL"];
137
182
  const apiKey = process.env["N8N_API_KEY"];
138
183
  if (!baseUrl || !apiKey) {
139
- return {
140
- content: [{
141
- type: "text",
142
- 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." })
143
- }],
144
- isError: true
145
- };
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." }));
146
185
  }
186
+ const runId = generateUUID();
187
+ const workflowType = inferWorkflowType(description);
188
+ const syncPromise = autoSync();
189
+ const syncTimeout = new Promise((resolve) => setTimeout(() => resolve(null), AUTO_SYNC_TIMEOUT_MS));
147
190
  await library.initialize();
148
- const syncResult = await autoSync();
149
- const matches = await library.search(description);
150
- const telemetryReader = getTelemetryReader();
151
- 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
+ ]);
152
199
  const request = { description, ...name ? { name } : {} };
153
200
  const built = promptBuilder.build(request, matches, failureRates, syncResult?.catalogText);
201
+ if (mcpTelemetry) {
202
+ mcpSessions.set(runId, {
203
+ description,
204
+ startTime: Date.now(),
205
+ validateAttempts: 0,
206
+ warnedRules: promptBuilder.getWarnedRules(),
207
+ workflowType
208
+ });
209
+ await mcpTelemetry.emit("build_start", { description, model: "mcp-decomposed", dryRun: false }, runId);
210
+ }
154
211
  const systemText = built.system.map((block) => block.text).join("\n\n---\n\n");
155
- return {
156
- content: [{
157
- type: "text",
158
- text: JSON.stringify({
159
- mode: built.mode,
160
- matchCount: matches.length,
161
- topMatchScore: matches[0]?.score ?? null,
162
- nodeCatalog: syncResult ? "synced" : "static",
163
- nodeCount: syncResult?.nodeCount ?? null,
164
- ...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." },
165
- systemPrompt: systemText,
166
- userMessage: built.userMessage,
167
- outputFormat: {
168
- description: "Generate a JSON object with this exact structure. The workflow field contains the n8n workflow. credentialsNeeded lists services requiring credentials.",
169
- schema: {
170
- workflow: {
171
- name: "string \u2014 descriptive workflow name",
172
- nodes: "array \u2014 n8n node objects with id (UUID v4), type, typeVersion, name, position, parameters",
173
- connections: "object \u2014 keyed by source node NAME, maps to target nodes",
174
- settings: 'object \u2014 include executionOrder: "v1"'
175
- },
176
- credentialsNeeded: [{
177
- service: 'string \u2014 e.g. "Slack"',
178
- credentialType: 'string \u2014 e.g. "slackOAuth2Api"',
179
- description: "string \u2014 what the user needs to set up"
180
- }]
181
- }
182
- }
183
- }, null, 2)
184
- }]
185
- };
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));
186
239
  }
187
240
  );
188
241
  server.tool(
189
242
  "kairos_validate",
190
- "Validate n8n workflow JSON against 23 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.",
191
244
  {
192
- workflow: z.string().describe("The workflow JSON string to validate")
245
+ workflow: z.string().describe("The workflow JSON string to validate"),
246
+ kairos_run_id: z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
193
247
  },
194
- async ({ workflow: workflowStr }) => {
248
+ async ({ workflow: workflowStr, kairos_run_id }) => {
195
249
  let parsed;
196
250
  try {
197
251
  parsed = JSON.parse(workflowStr);
198
252
  } catch (e) {
199
- return {
200
- content: [{
201
- type: "text",
202
- text: JSON.stringify({
203
- valid: false,
204
- error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`
205
- }, null, 2)
206
- }]
207
- };
253
+ return mcpText(JSON.stringify({ valid: false, error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }, null, 2));
208
254
  }
209
- const result = validator.validate(parsed);
255
+ const result = getValidator().validate(parsed);
210
256
  const errors = result.issues.filter((i) => i.severity === "error");
211
257
  const warnings = result.issues.filter((i) => i.severity === "warn");
212
- return {
213
- content: [{
214
- type: "text",
215
- text: JSON.stringify({
216
- valid: result.valid,
217
- errorCount: errors.length,
218
- warningCount: warnings.length,
219
- errors: errors.map((i) => ({
220
- rule: i.rule,
221
- message: i.message,
222
- nodeId: i.nodeId ?? null
223
- })),
224
- warnings: warnings.map((i) => ({
225
- rule: i.rule,
226
- message: i.message,
227
- nodeId: i.nodeId ?? null
228
- })),
229
- deployable: errors.length === 0
230
- }, null, 2)
231
- }]
232
- };
258
+ if (mcpTelemetry && kairos_run_id) {
259
+ const session = mcpSessions.get(kairos_run_id);
260
+ if (session) {
261
+ session.validateAttempts++;
262
+ await mcpTelemetry.emit("generation_attempt", {
263
+ description: session.description,
264
+ attempt: session.validateAttempts,
265
+ temperature: 0,
266
+ durationMs: 0,
267
+ tokensInput: 0,
268
+ tokensOutput: 0,
269
+ validationPassed: result.valid,
270
+ issueCount: result.issues.length,
271
+ issues: result.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null })),
272
+ workflowType: session.workflowType
273
+ }, kairos_run_id);
274
+ }
275
+ }
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));
233
284
  }
234
285
  );
235
286
  server.tool(
@@ -237,79 +288,147 @@ server.tool(
237
288
  "Deploy a validated workflow to n8n. Pass the workflow JSON that passed kairos_validate. Strips server-assigned fields automatically. Requires N8N_BASE_URL and N8N_API_KEY.",
238
289
  {
239
290
  workflow: z.string().describe("The validated workflow JSON string to deploy"),
240
- activate: z.boolean().default(false).describe("Activate the workflow immediately after deployment")
291
+ activate: z.boolean().default(false).describe("Activate the workflow immediately after deployment"),
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")
241
294
  },
242
- async ({ workflow: workflowStr, activate }) => {
295
+ async ({ workflow: workflowStr, activate, kairos_run_id, kairos_secret }) => {
296
+ const authError = checkMcpAuth(kairos_secret);
297
+ if (authError) return authError;
243
298
  if (!isAllowed("deploy")) {
244
- return {
245
- content: [{
246
- type: "text",
247
- text: JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." })
248
- }],
249
- isError: true
250
- };
299
+ return mcpError(JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." }));
251
300
  }
252
301
  let parsed;
253
302
  try {
254
303
  parsed = JSON.parse(workflowStr);
255
304
  } catch (e) {
256
- return {
257
- content: [{
258
- type: "text",
259
- text: JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` })
260
- }]
261
- };
305
+ return mcpError(JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }));
262
306
  }
263
- const validation = validator.validate(parsed);
307
+ const validation = getValidator().validate(parsed);
264
308
  const errors = validation.issues.filter((i) => i.severity === "error");
265
309
  if (errors.length > 0) {
266
- return {
267
- content: [{
268
- type: "text",
269
- text: JSON.stringify({
270
- error: "Workflow has validation errors \u2014 fix them before deploying",
271
- errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
272
- }, null, 2)
273
- }]
274
- };
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));
275
314
  }
276
315
  const client = getApiClient();
277
316
  const stripped = stripper.stripForCreate(parsed);
278
317
  const response = await client.createWorkflow(stripped);
279
318
  if (activate) {
280
319
  if (!isAllowed("activate")) {
281
- return {
282
- content: [{
283
- type: "text",
284
- text: JSON.stringify({
285
- workflowId: response.id,
286
- name: response.name,
287
- activated: false,
288
- warning: "Workflow deployed but activation is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable.",
289
- url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
290
- }, null, 2)
291
- }]
292
- };
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));
293
327
  }
294
328
  await client.activateWorkflow(response.id);
295
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.` : "";
296
334
  await library.initialize();
297
335
  await library.save(parsed, {
298
- description: parsed.name,
336
+ description: session?.description ?? parsed.name,
299
337
  generationMode: "scratch",
300
- generationAttempts: 1
338
+ generationAttempts: session?.validateAttempts ?? 1,
339
+ n8nWorkflowId: response.id
301
340
  });
302
- return {
303
- content: [{
304
- type: "text",
305
- text: JSON.stringify({
306
- workflowId: response.id,
307
- name: response.name,
308
- activated: activate,
309
- url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
310
- }, null, 2)
311
- }]
312
- };
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
+ });
359
+ }
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);
313
432
  }
314
433
  );
315
434
  server.tool(
@@ -322,23 +441,20 @@ server.tool(
322
441
  async ({ query, limit }) => {
323
442
  await library.initialize();
324
443
  const matches = await library.search(query);
325
- return {
326
- content: [{
327
- type: "text",
328
- text: JSON.stringify(
329
- matches.slice(0, limit).map((m) => ({
330
- score: Number(m.score.toFixed(3)),
331
- mode: m.mode,
332
- description: m.workflow.description,
333
- nodeCount: m.workflow.workflow.nodes.length,
334
- nodes: m.workflow.workflow.nodes.map((n) => n.name),
335
- failurePatterns: m.workflow.failurePatterns ?? []
336
- })),
337
- null,
338
- 2
339
- )
340
- }]
341
- };
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
+ ));
342
458
  }
343
459
  );
344
460
  server.tool(
@@ -349,36 +465,19 @@ server.tool(
349
465
  const baseUrl = process.env["N8N_BASE_URL"];
350
466
  const apiKey = process.env["N8N_API_KEY"];
351
467
  if (!baseUrl || !apiKey) {
352
- return {
353
- content: [{
354
- type: "text",
355
- text: JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required for sync." })
356
- }],
357
- isError: true
358
- };
468
+ return mcpError(JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required for sync." }));
359
469
  }
360
470
  lastSync = null;
361
471
  const result = await autoSync();
362
472
  if (!result) {
363
- return {
364
- content: [{
365
- type: "text",
366
- text: JSON.stringify({ error: "Failed to fetch node types from n8n. Check your credentials and that your instance is running." })
367
- }],
368
- isError: true
369
- };
473
+ return mcpError(JSON.stringify({ error: "Failed to fetch node types from n8n. Check your credentials and that your instance is running." }));
370
474
  }
371
- return {
372
- content: [{
373
- type: "text",
374
- text: JSON.stringify({
375
- synced: true,
376
- nodeCount: result.nodeCount,
377
- newNodes: result.newNodes,
378
- message: `Synced ${result.nodeCount} node types from your n8n instance (${result.newNodes} not in default catalog).`
379
- }, null, 2)
380
- }]
381
- };
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));
382
481
  }
383
482
  );
384
483
  server.tool(
@@ -394,12 +493,72 @@ server.tool(
394
493
  if (limit !== void 0 && limit > 0) {
395
494
  analysis.topFailureRules = analysis.topFailureRules.slice(0, limit);
396
495
  }
397
- return {
398
- content: [{
399
- type: "text",
400
- text: JSON.stringify(analysis, null, 2)
401
- }]
402
- };
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 }));
403
562
  }
404
563
  );
405
564
  server.tool(
@@ -409,12 +568,7 @@ server.tool(
409
568
  async () => {
410
569
  const client = getApiClient();
411
570
  const workflows = await client.listWorkflows();
412
- return {
413
- content: [{
414
- type: "text",
415
- text: JSON.stringify(workflows, null, 2)
416
- }]
417
- };
571
+ return mcpText(JSON.stringify(workflows, null, 2));
418
572
  }
419
573
  );
420
574
  server.tool(
@@ -426,12 +580,7 @@ server.tool(
426
580
  async ({ workflow_id }) => {
427
581
  const client = getApiClient();
428
582
  const workflow = await client.getWorkflow(workflow_id);
429
- return {
430
- content: [{
431
- type: "text",
432
- text: JSON.stringify(workflow, null, 2)
433
- }]
434
- };
583
+ return mcpText(JSON.stringify(workflow, null, 2));
435
584
  }
436
585
  );
437
586
  server.tool(
@@ -442,22 +591,11 @@ server.tool(
442
591
  },
443
592
  async ({ workflow_id }) => {
444
593
  if (!isAllowed("activate")) {
445
- return {
446
- content: [{
447
- type: "text",
448
- text: JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." })
449
- }],
450
- isError: true
451
- };
594
+ return mcpError(JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." }));
452
595
  }
453
596
  const client = getApiClient();
454
597
  await client.activateWorkflow(workflow_id);
455
- return {
456
- content: [{
457
- type: "text",
458
- text: `Activated workflow ${workflow_id}`
459
- }]
460
- };
598
+ return mcpText(`Activated workflow ${workflow_id}`);
461
599
  }
462
600
  );
463
601
  server.tool(
@@ -469,38 +607,25 @@ server.tool(
469
607
  async ({ workflow_id }) => {
470
608
  const client = getApiClient();
471
609
  await client.deactivateWorkflow(workflow_id);
472
- return {
473
- content: [{
474
- type: "text",
475
- text: `Deactivated workflow ${workflow_id}`
476
- }]
477
- };
610
+ return mcpText(`Deactivated workflow ${workflow_id}`);
478
611
  }
479
612
  );
480
613
  server.tool(
481
614
  "kairos_delete",
482
615
  "Delete a workflow from n8n. This is irreversible.",
483
616
  {
484
- 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")
485
619
  },
486
- async ({ workflow_id }) => {
620
+ async ({ workflow_id, kairos_secret }) => {
621
+ const authError = checkMcpAuth(kairos_secret);
622
+ if (authError) return authError;
487
623
  if (!isAllowed("delete")) {
488
- return {
489
- content: [{
490
- type: "text",
491
- text: JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." })
492
- }],
493
- isError: true
494
- };
624
+ return mcpError(JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." }));
495
625
  }
496
626
  const client = getApiClient();
497
627
  await client.deleteWorkflow(workflow_id);
498
- return {
499
- content: [{
500
- type: "text",
501
- text: `Deleted workflow ${workflow_id}`
502
- }]
503
- };
628
+ return mcpText(`Deleted workflow ${workflow_id}`);
504
629
  }
505
630
  );
506
631
  server.tool(
@@ -513,15 +638,15 @@ server.tool(
513
638
  async ({ workflow_id, limit }) => {
514
639
  const client = getApiClient();
515
640
  const executions = await client.getExecutions(workflow_id, { limit });
516
- return {
517
- content: [{
518
- type: "text",
519
- text: JSON.stringify(executions, null, 2)
520
- }]
521
- };
641
+ return mcpText(JSON.stringify(executions, null, 2));
522
642
  }
523
643
  );
524
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
+ }
525
650
  const transport = new StdioServerTransport();
526
651
  await server.connect(transport);
527
652
  }