@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.
- package/README.md +21 -10
- package/dist/chunk-5GAY7CSJ.js +411 -0
- package/dist/chunk-5GAY7CSJ.js.map +1 -0
- package/dist/chunk-6FOFWVMG.js +1 -0
- package/dist/chunk-6FOFWVMG.js.map +1 -0
- package/dist/chunk-EVOAYH2K.js +569 -0
- package/dist/chunk-EVOAYH2K.js.map +1 -0
- package/dist/{chunk-N6LRD2FN.js → chunk-HBGZTUUZ.js} +81 -380
- package/dist/chunk-HBGZTUUZ.js.map +1 -0
- package/dist/{chunk-NJ6QZBIC.js → chunk-KIFT5LA7.js} +971 -572
- package/dist/chunk-KIFT5LA7.js.map +1 -0
- package/dist/cli.cjs +1341 -236
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +83 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1259 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -540
- package/dist/index.d.ts +3 -540
- package/dist/index.js +9 -5
- package/dist/mcp-server.cjs +1473 -402
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +357 -232
- package/dist/mcp-server.js.map +1 -1
- package/dist/reader-B5mV20H6.d.cts +596 -0
- package/dist/reader-B5mV20H6.d.ts +596 -0
- package/dist/standalone.cjs +2978 -0
- package/dist/standalone.cjs.map +1 -0
- package/dist/standalone.d.cts +106 -0
- package/dist/standalone.d.ts +106 -0
- package/dist/standalone.js +58 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +9 -1
- package/dist/chunk-N6LRD2FN.js.map +0 -1
- package/dist/chunk-NJ6QZBIC.js.map +0 -1
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
15
|
+
TelemetryCollector,
|
|
11
16
|
TelemetryReader,
|
|
17
|
+
generateUUID,
|
|
12
18
|
nullLogger
|
|
13
|
-
} from "./chunk-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 =
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
}
|