src / toolsProvider.ts
import * as http from "http";
import * as https from "https";
import { text, tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { loadN8nConfig } from "./n8nConfig";
interface ApiResponse {
success: boolean;
status: number;
data?: unknown;
error?: string;
}
function normalizeBaseUrl(apiUrl: string, versionPath: string): string {
const trimmedApiUrl = apiUrl.replace(/\/+$/, "");
const normalizedVersionPath = versionPath.startsWith("/") ? versionPath : `/${versionPath}`;
if (trimmedApiUrl.endsWith(normalizedVersionPath)) {
return trimmedApiUrl;
}
return `${trimmedApiUrl}${normalizedVersionPath}`;
}
function buildUrl(baseUrl: string, path: string, query?: Record<string, string | number | boolean | undefined>): URL {
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const url = new URL(`${baseUrl}${normalizedPath}`);
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== undefined) {
url.searchParams.set(key, String(value));
}
}
}
return url;
}
function apiRequest(
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
path: string,
apiKey: string,
baseUrl: string,
query?: Record<string, string | number | boolean | undefined>,
body?: unknown,
timeoutMs = 30000,
): Promise<ApiResponse> {
return new Promise((resolve) => {
const url = buildUrl(baseUrl, path, query);
const isHttps = url.protocol === "https:";
const client = isHttps ? https : http;
const serializedBody = body === undefined ? undefined : JSON.stringify(body);
const request = client.request(
{
protocol: url.protocol,
hostname: url.hostname,
port: url.port,
path: `${url.pathname}${url.search}`,
method,
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"X-N8N-API-KEY": apiKey,
...(serializedBody ? { "Content-Length": Buffer.byteLength(serializedBody) } : {}),
},
timeout: timeoutMs,
},
(response) => {
let raw = "";
response.on("data", (chunk: Buffer) => {
raw += chunk.toString();
});
response.on("end", () => {
const status = response.statusCode ?? 0;
let parsed: unknown = raw;
if (raw.length > 0) {
try {
parsed = JSON.parse(raw);
} catch {
parsed = raw;
}
}
if (status >= 200 && status < 300) {
resolve({ success: true, status, data: parsed });
} else {
const msg = typeof parsed === "string" ? parsed : JSON.stringify(parsed);
resolve({ success: false, status, error: msg || "n8n API request failed" });
}
});
},
);
request.on("error", (err) => {
resolve({ success: false, status: 0, error: err.message });
});
request.on("timeout", () => {
request.destroy();
resolve({ success: false, status: 0, error: `Request timed out after ${timeoutMs}ms` });
});
if (serializedBody) {
request.write(serializedBody);
}
request.end();
});
}
function getConnectionSettings() {
const cfg = loadN8nConfig();
if (!cfg.api_url) {
return { error: "Missing api_url in n8n-config.json" } as const;
}
if (!cfg.api_key) {
return { error: "Missing api_key in n8n-config.json" } as const;
}
return {
apiKey: cfg.api_key,
baseUrl: normalizeBaseUrl(cfg.api_url, cfg.api_version_path),
} as const;
}
export async function toolsProvider(_ctl: ToolsProviderController) {
const tools: Tool[] = [];
tools.push(tool({
name: "n8n_list_workflows",
description: text`
List workflows from n8n.
Optional filters: active, tags, projectId, and pagination with limit/cursor.
`,
parameters: {
active: z.boolean().optional(),
tags: z.string().optional().describe("Comma separated tags"),
projectId: z.string().optional(),
limit: z.number().int().min(1).max(250).default(100),
cursor: z.string().optional(),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ active, tags, projectId, limit, cursor, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest("GET", "/workflows", conn.apiKey, conn.baseUrl, {
active,
tags,
projectId,
limit,
cursor,
}, undefined, timeout_ms);
},
}));
tools.push(tool({
name: "n8n_get_workflow",
description: text`Get a single workflow by workflow ID.`,
parameters: {
workflow_id: z.string(),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ workflow_id, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest("GET", `/workflows/${encodeURIComponent(workflow_id)}`, conn.apiKey, conn.baseUrl, undefined, undefined, timeout_ms);
},
}));
tools.push(tool({
name: "n8n_create_workflow",
description: text`
Create a new workflow in n8n.
Provide the full workflow object as JSON in workflow.
`,
parameters: {
workflow: z.record(z.any()).describe("Full n8n workflow JSON object"),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ workflow, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest("POST", "/workflows", conn.apiKey, conn.baseUrl, undefined, workflow, timeout_ms);
},
}));
tools.push(tool({
name: "n8n_update_workflow",
description: text`
Update an existing workflow by ID.
Provide the workflow object payload according to n8n API.
`,
parameters: {
workflow_id: z.string(),
workflow: z.record(z.any()),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ workflow_id, workflow, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest("PUT", `/workflows/${encodeURIComponent(workflow_id)}`, conn.apiKey, conn.baseUrl, undefined, workflow, timeout_ms);
},
}));
tools.push(tool({
name: "n8n_delete_workflow",
description: text`Delete a workflow by workflow ID.`,
parameters: {
workflow_id: z.string(),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ workflow_id, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest("DELETE", `/workflows/${encodeURIComponent(workflow_id)}`, conn.apiKey, conn.baseUrl, undefined, undefined, timeout_ms);
},
}));
tools.push(tool({
name: "n8n_set_workflow_active",
description: text`Set a workflow active or inactive.`,
parameters: {
workflow_id: z.string(),
active: z.boolean(),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ workflow_id, active, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest(
"PATCH",
`/workflows/${encodeURIComponent(workflow_id)}`,
conn.apiKey,
conn.baseUrl,
undefined,
{ active },
timeout_ms,
);
},
}));
tools.push(tool({
name: "n8n_list_executions",
description: text`
List executions from n8n.
Supports pagination and basic filtering.
`,
parameters: {
workflowId: z.string().optional(),
status: z.enum(["new", "running", "success", "error", "canceled", "crashed", "waiting"]).optional(),
projectId: z.string().optional(),
includeData: z.boolean().optional(),
limit: z.number().int().min(1).max(250).default(100),
cursor: z.string().optional(),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ workflowId, status, projectId, includeData, limit, cursor, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest("GET", "/executions", conn.apiKey, conn.baseUrl, {
workflowId,
status,
projectId,
includeData,
limit,
cursor,
}, undefined, timeout_ms);
},
}));
tools.push(tool({
name: "n8n_get_execution",
description: text`Get execution details by execution ID.`,
parameters: {
execution_id: z.string(),
includeData: z.boolean().optional(),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ execution_id, includeData, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest(
"GET",
`/executions/${encodeURIComponent(execution_id)}`,
conn.apiKey,
conn.baseUrl,
{ includeData },
undefined,
timeout_ms,
);
},
}));
tools.push(tool({
name: "n8n_delete_execution",
description: text`Delete execution data by execution ID.`,
parameters: {
execution_id: z.string(),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ execution_id, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest("DELETE", `/executions/${encodeURIComponent(execution_id)}`, conn.apiKey, conn.baseUrl, undefined, undefined, timeout_ms);
},
}));
tools.push(tool({
name: "n8n_api_request",
description: text`
Generic n8n API request tool.
Use this when you need an endpoint not covered by the specialized tools.
path must start with '/' and is appended to api_version_path (default /api/v1).
`,
parameters: {
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
path: z.string().describe("Example: /users or /projects/<id>"),
query: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
body: z.record(z.any()).optional(),
timeout_ms: z.number().int().positive().default(30000),
},
implementation: async ({ method, path, query, body, timeout_ms }) => {
const conn = getConnectionSettings();
if ("error" in conn) return { success: false, error: conn.error };
return apiRequest(method, path, conn.apiKey, conn.baseUrl, query, body, timeout_ms);
},
}));
return tools;
}