Merge pull request #2 from 041Agency/run_webhook

Add run_webhook tool for executing workflows via webhooks
This commit is contained in:
Leonard Sellem
2025-03-28 09:54:07 +01:00
committed by GitHub
7 changed files with 281 additions and 4 deletions

View File

@@ -55,6 +55,8 @@ Configure the following environment variables:
|----------|-------------|---------| |----------|-------------|---------|
| `N8N_API_URL` | URL of the n8n API | `http://localhost:5678/api/v1` | | `N8N_API_URL` | URL of the n8n API | `http://localhost:5678/api/v1` |
| `N8N_API_KEY` | API key for authenticating with n8n | `n8n_api_...` | | `N8N_API_KEY` | API key for authenticating with n8n | `n8n_api_...` |
| `N8N_WEBHOOK_USERNAME` | Username for webhook authentication | `username` |
| `N8N_WEBHOOK_PASSWORD` | Password for webhook authentication | `password` |
| `DEBUG` | Enable debug logging (optional) | `true` or `false` | | `DEBUG` | Enable debug logging (optional) | `true` or `false` |
### Generating an n8n API Key ### Generating an n8n API Key
@@ -100,6 +102,26 @@ install_local_mcp_server path/to/n8n-mcp-server
The server provides the following tools: The server provides the following tools:
### Using Webhooks
This MCP server supports executing workflows through n8n webhooks. To use this functionality:
1. Create a webhook-triggered workflow in n8n.
2. Set up Basic Authentication on your webhook node.
3. Use the `run_webhook` tool to trigger the workflow, passing just the workflow name.
Example:
```javascript
const result = await useRunWebhook({
workflowName: "hello-world", // Will call <n8n-url>/webhook/hello-world
data: {
prompt: "Hello from AI assistant!"
}
});
```
The webhook authentication is handled automatically using the `N8N_WEBHOOK_USERNAME` and `N8N_WEBHOOK_PASSWORD` environment variables.
### Workflow Management ### Workflow Management
- `workflow_list`: List all workflows - `workflow_list`: List all workflows
@@ -112,7 +134,8 @@ The server provides the following tools:
### Execution Management ### Execution Management
- `execution_run`: Execute a workflow - `execution_run`: Execute a workflow via the API
- `run_webhook`: Execute a workflow via a webhook
- `execution_get`: Get details of a specific execution - `execution_get`: Get details of a specific execution
- `execution_list`: List executions for a workflow - `execution_list`: List executions for a workflow
- `execution_stop`: Stop a running execution - `execution_stop`: Stop a running execution

View File

@@ -8,6 +8,73 @@ Execution tools allow AI assistants to execute n8n workflows and manage executio
## Available Tools ## Available Tools
### run_webhook
Executes a workflow via webhook with optional input data.
**Input Schema:**
```json
{
"type": "object",
"properties": {
"workflowName": {
"type": "string",
"description": "Name of the workflow to execute (e.g., \"hello-world\")"
},
"data": {
"type": "object",
"description": "Input data to pass to the webhook"
},
"headers": {
"type": "object",
"description": "Additional headers to send with the request"
}
},
"required": ["workflowName"]
}
```
**Example Usage:**
```javascript
// Execute webhook with data
const webhookResult = await useRunWebhook({
workflowName: "hello-world",
data: {
prompt: "Good morning!"
}
});
// Execute webhook with additional headers
const webhookWithHeaders = await useRunWebhook({
workflowName: "hello-world",
data: {
prompt: "Hello with custom header"
},
headers: {
"X-Custom-Header": "CustomValue"
}
});
```
**Response:**
```javascript
{
"status": 200,
"statusText": "OK",
"data": {
// Response data from the webhook
}
}
```
**Note:**
- Authentication for the webhook is automatically handled using the environment variables `N8N_WEBHOOK_USERNAME` and `N8N_WEBHOOK_PASSWORD`.
- The tool automatically prefixes the `workflowName` with `webhook/` to create the full webhook path. For example, if you provide `hello-world` as the workflow name, the tool will call `{baseUrl}/webhook/hello-world`.
### execution_run ### execution_run
Executes a workflow with optional input data. Executes a workflow with optional input data.

View File

@@ -13,6 +13,8 @@ import { ErrorCode } from '../errors/error-codes.js';
export const ENV_VARS = { export const ENV_VARS = {
N8N_API_URL: 'N8N_API_URL', N8N_API_URL: 'N8N_API_URL',
N8N_API_KEY: 'N8N_API_KEY', N8N_API_KEY: 'N8N_API_KEY',
N8N_WEBHOOK_USERNAME: 'N8N_WEBHOOK_USERNAME',
N8N_WEBHOOK_PASSWORD: 'N8N_WEBHOOK_PASSWORD',
DEBUG: 'DEBUG', DEBUG: 'DEBUG',
}; };
@@ -20,6 +22,8 @@ export const ENV_VARS = {
export interface EnvConfig { export interface EnvConfig {
n8nApiUrl: string; n8nApiUrl: string;
n8nApiKey: string; n8nApiKey: string;
n8nWebhookUsername: string;
n8nWebhookPassword: string;
debug: boolean; debug: boolean;
} }
@@ -39,6 +43,8 @@ export function loadEnvironmentVariables(): void {
export function getEnvConfig(): EnvConfig { export function getEnvConfig(): EnvConfig {
const n8nApiUrl = process.env[ENV_VARS.N8N_API_URL]; const n8nApiUrl = process.env[ENV_VARS.N8N_API_URL];
const n8nApiKey = process.env[ENV_VARS.N8N_API_KEY]; const n8nApiKey = process.env[ENV_VARS.N8N_API_KEY];
const n8nWebhookUsername = process.env[ENV_VARS.N8N_WEBHOOK_USERNAME];
const n8nWebhookPassword = process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD];
const debug = process.env[ENV_VARS.DEBUG]?.toLowerCase() === 'true'; const debug = process.env[ENV_VARS.DEBUG]?.toLowerCase() === 'true';
// Validate required environment variables // Validate required environment variables
@@ -56,6 +62,20 @@ export function getEnvConfig(): EnvConfig {
); );
} }
if (!n8nWebhookUsername) {
throw new McpError(
ErrorCode.InitializationError,
`Missing required environment variable: ${ENV_VARS.N8N_WEBHOOK_USERNAME}`
);
}
if (!n8nWebhookPassword) {
throw new McpError(
ErrorCode.InitializationError,
`Missing required environment variable: ${ENV_VARS.N8N_WEBHOOK_PASSWORD}`
);
}
// Validate URL format // Validate URL format
try { try {
new URL(n8nApiUrl); new URL(n8nApiUrl);
@@ -69,6 +89,8 @@ export function getEnvConfig(): EnvConfig {
return { return {
n8nApiUrl, n8nApiUrl,
n8nApiKey, n8nApiKey,
n8nWebhookUsername,
n8nWebhookPassword,
debug, debug,
}; };
} }

View File

@@ -110,7 +110,8 @@ function setupToolCallRequestHandler(server: Server): void {
const { const {
ListExecutionsHandler, ListExecutionsHandler,
GetExecutionHandler, GetExecutionHandler,
DeleteExecutionHandler DeleteExecutionHandler,
RunWebhookHandler
} = await import('../tools/execution/index.js'); } = await import('../tools/execution/index.js');
// Route the tool call to the appropriate handler // Route the tool call to the appropriate handler
@@ -144,6 +145,9 @@ function setupToolCallRequestHandler(server: Server): void {
} else if (toolName === 'delete_execution') { } else if (toolName === 'delete_execution') {
const handler = new DeleteExecutionHandler(); const handler = new DeleteExecutionHandler();
result = await handler.execute(args); result = await handler.execute(args);
} else if (toolName === 'run_webhook') {
const handler = new RunWebhookHandler();
result = await handler.execute(args);
} else { } else {
throw new Error(`Unknown tool: ${toolName}`); throw new Error(`Unknown tool: ${toolName}`);
} }

View File

@@ -11,7 +11,8 @@ import { N8nApiError, getErrorMessage } from '../../errors/index.js';
import { import {
ListExecutionsHandler, ListExecutionsHandler,
GetExecutionHandler, GetExecutionHandler,
DeleteExecutionHandler DeleteExecutionHandler,
RunWebhookHandler
} from './index.js'; } from './index.js';
/** /**
@@ -37,6 +38,9 @@ export default async function executionHandler(
case 'delete_execution': case 'delete_execution':
return await new DeleteExecutionHandler().execute(args); return await new DeleteExecutionHandler().execute(args);
case 'run_webhook':
return await new RunWebhookHandler().execute(args);
default: default:
throw new McpError( throw new McpError(
ErrorCode.NotImplemented, ErrorCode.NotImplemented,

View File

@@ -8,6 +8,7 @@ import { ToolDefinition } from '../../types/index.js';
import { getListExecutionsToolDefinition } from './list.js'; import { getListExecutionsToolDefinition } from './list.js';
import { getGetExecutionToolDefinition } from './get.js'; import { getGetExecutionToolDefinition } from './get.js';
import { getDeleteExecutionToolDefinition } from './delete.js'; import { getDeleteExecutionToolDefinition } from './delete.js';
import { getRunWebhookToolDefinition } from './run.js';
/** /**
* Set up execution management tools * Set up execution management tools
@@ -18,7 +19,8 @@ export async function setupExecutionTools(): Promise<ToolDefinition[]> {
return [ return [
getListExecutionsToolDefinition(), getListExecutionsToolDefinition(),
getGetExecutionToolDefinition(), getGetExecutionToolDefinition(),
getDeleteExecutionToolDefinition() getDeleteExecutionToolDefinition(),
getRunWebhookToolDefinition()
]; ];
} }
@@ -26,3 +28,4 @@ export async function setupExecutionTools(): Promise<ToolDefinition[]> {
export { ListExecutionsHandler } from './list.js'; export { ListExecutionsHandler } from './list.js';
export { GetExecutionHandler } from './get.js'; export { GetExecutionHandler } from './get.js';
export { DeleteExecutionHandler } from './delete.js'; export { DeleteExecutionHandler } from './delete.js';
export { RunWebhookHandler } from './run.js';

154
src/tools/execution/run.ts Normal file
View File

@@ -0,0 +1,154 @@
/**
* Run Execution via Webhook Tool Handler
*
* This module provides a tool for running n8n workflows via webhooks.
*/
import axios from 'axios';
import { z } from 'zod';
import { ToolCallResult } from '../../types/index.js';
import { BaseExecutionToolHandler } from './base-handler.js';
import { N8nApiError } from '../../errors/index.js';
import { getEnvConfig } from '../../config/environment.js';
import { URL } from 'url';
/**
* Webhook execution input schema
*/
const runWebhookSchema = z.object({
workflowName: z.string().describe('Name of the workflow to execute (e.g., "hello-world")'),
data: z.record(z.any()).optional().describe('Input data to pass to the webhook'),
headers: z.record(z.string()).optional().describe('Additional headers to send with the request')
});
/**
* Type for webhook execution parameters
*/
type RunWebhookParams = z.infer<typeof runWebhookSchema>;
/**
* Handler for the run_webhook tool
*/
export class RunWebhookHandler extends BaseExecutionToolHandler {
/**
* Tool definition for execution via webhook
*/
public static readonly inputSchema = runWebhookSchema;
/**
* Extract N8N base URL from N8N API URL by removing /api/v1
* @returns N8N base URL
*/
private getN8nBaseUrl(): string {
const config = getEnvConfig();
const apiUrl = new URL(config.n8nApiUrl);
// Remove /api/v1 if it exists in the path
let path = apiUrl.pathname;
if (path.endsWith('/api/v1') || path.endsWith('/api/v1/')) {
path = path.replace(/\/api\/v1\/?$/, '');
}
// Create a new URL with the base path
apiUrl.pathname = path;
return apiUrl.toString();
}
/**
* Validate and execute webhook call
*
* @param args Tool arguments
* @returns Tool call result
*/
async execute(args: Record<string, any>): Promise<ToolCallResult> {
return this.handleExecution(async (args) => {
// Parse and validate arguments
const params = runWebhookSchema.parse(args);
// Get environment config for auth credentials
const config = getEnvConfig();
try {
// Get the webhook URL with the proper prefix
const baseUrl = this.getN8nBaseUrl();
const webhookPath = `webhook/${params.workflowName}`;
const webhookUrl = new URL(webhookPath, baseUrl).toString();
// Prepare request config with basic auth from environment
const requestConfig: any = {
headers: {
'Content-Type': 'application/json',
...(params.headers || {})
},
auth: {
username: config.n8nWebhookUsername,
password: config.n8nWebhookPassword
}
};
// Make the request to the webhook
const response = await axios.post(
webhookUrl,
params.data || {},
requestConfig
);
// Return the webhook response
return this.formatSuccess({
status: response.status,
statusText: response.statusText,
data: response.data
}, 'Webhook executed successfully');
} catch (error) {
// Handle error from the webhook request
if (axios.isAxiosError(error)) {
let errorMessage = `Webhook execution failed: ${error.message}`;
if (error.response) {
errorMessage = `Webhook execution failed with status ${error.response.status}: ${error.response.statusText}`;
if (error.response.data) {
return this.formatError(new N8nApiError(
`${errorMessage}\n\n${JSON.stringify(error.response.data, null, 2)}`,
error.response.status
));
}
}
return this.formatError(new N8nApiError(errorMessage, error.response?.status || 500));
}
throw error; // Re-throw non-axios errors for the handler to catch
}
}, args);
}
}
/**
* Get the tool definition for run_webhook
*
* @returns Tool definition object
*/
export function getRunWebhookToolDefinition() {
return {
name: 'run_webhook',
description: 'Execute a workflow via webhook with optional input data',
inputSchema: {
type: 'object',
properties: {
workflowName: {
type: 'string',
description: 'Name of the workflow to execute (e.g., "hello-world")'
},
data: {
type: 'object',
description: 'Input data to pass to the webhook'
},
headers: {
type: 'object',
description: 'Additional headers to send with the request'
}
},
required: ['workflowName']
}
};
}