@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.
- package/README.md +28 -11
- package/dist/{chunk-4TS6GW6O.js → chunk-GVZKMS53.js} +27 -16
- package/dist/chunk-GVZKMS53.js.map +1 -0
- package/dist/{chunk-6CLI43FI.js → chunk-MYAGTDQ2.js} +109 -13
- package/dist/chunk-MYAGTDQ2.js.map +1 -0
- package/dist/{chunk-CR2NHLOH.js → chunk-V2IZBZGB.js} +57 -11
- package/dist/chunk-V2IZBZGB.js.map +1 -0
- package/dist/{chunk-6IXW3WCC.js → chunk-VPPWTMRJ.js} +533 -77
- package/dist/chunk-VPPWTMRJ.js.map +1 -0
- package/dist/cli.cjs +790 -145
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +54 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +740 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +5 -5
- package/dist/mcp-server.cjs +960 -347
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +374 -261
- package/dist/mcp-server.js.map +1 -1
- package/dist/{reader-CpUcHhKW.d.ts → reader-B5mV20H6.d.cts} +34 -4
- package/dist/{reader-CpUcHhKW.d.cts → reader-B5mV20H6.d.ts} +34 -4
- package/dist/standalone.cjs +603 -85
- package/dist/standalone.cjs.map +1 -1
- package/dist/standalone.d.cts +2 -1
- package/dist/standalone.d.ts +2 -1
- package/dist/standalone.js +3 -3
- package/package.json +15 -5
- package/dist/chunk-4TS6GW6O.js.map +0 -1
- package/dist/chunk-6CLI43FI.js.map +0 -1
- package/dist/chunk-6IXW3WCC.js.map +0 -1
- package/dist/chunk-CR2NHLOH.js.map +0 -1
package/dist/mcp-server.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
PromptBuilder,
|
|
4
4
|
inferWorkflowType
|
|
5
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
609
|
-
|
|
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);
|