Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a532bde9cd | ||
|
|
ecd9133437 |
77
README.md
77
README.md
@@ -53,9 +53,9 @@ Configure the following environment variables:
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `N8N_API_URL` | URL of the n8n API | `http://localhost:5678/api/v1` |
|
||||
| `N8N_API_URL` | Full URL of the n8n API, including `/api/v1` | `http://localhost:5678/api/v1` |
|
||||
| `N8N_API_KEY` | API key for authenticating with n8n | `n8n_api_...` |
|
||||
| `N8N_WEBHOOK_USERNAME` | Username for webhook authentication | `username` |
|
||||
| `N8N_WEBHOOK_USERNAME` | Username for webhook authentication (if using webhooks) | `username` |
|
||||
| `N8N_WEBHOOK_PASSWORD` | Password for webhook authentication | `password` |
|
||||
| `DEBUG` | Enable debug logging (optional) | `true` or `false` |
|
||||
|
||||
@@ -84,19 +84,47 @@ n8n-mcp-server
|
||||
|
||||
### Integrating with AI Assistants
|
||||
|
||||
To use this MCP server with AI assistants, you need to register it with your AI assistant platform. The exact method depends on the platform you're using.
|
||||
After building the server (`npm run build`), you need to configure your AI assistant (like VS Code with the Claude extension or the Claude Desktop app) to run it. This typically involves editing a JSON configuration file.
|
||||
|
||||
For example, with the MCP installer:
|
||||
**Example Configuration (e.g., in VS Code `settings.json` or Claude Desktop `claude_desktop_config.json`):**
|
||||
|
||||
```bash
|
||||
npx @anaisbetts/mcp-installer
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
// Give your server a unique name
|
||||
"n8n-local": {
|
||||
// Use 'node' to execute the built JavaScript file
|
||||
"command": "node",
|
||||
// Provide the *absolute path* to the built index.js file
|
||||
"args": [
|
||||
"/path/to/your/cloned/n8n-mcp-server/build/index.js"
|
||||
// On Windows, use double backslashes:
|
||||
// "C:\\path\\to\\your\\cloned\\n8n-mcp-server\\build\\index.js"
|
||||
],
|
||||
// Environment variables needed by the server
|
||||
"env": {
|
||||
"N8N_API_URL": "http://your-n8n-instance:5678/api/v1", // Replace with your n8n URL
|
||||
"N8N_API_KEY": "YOUR_N8N_API_KEY", // Replace with your key
|
||||
// Add webhook credentials only if you plan to use webhook tools
|
||||
// "N8N_WEBHOOK_USERNAME": "your_webhook_user",
|
||||
// "N8N_WEBHOOK_PASSWORD": "your_webhook_password"
|
||||
},
|
||||
// Ensure the server is enabled
|
||||
"disabled": false,
|
||||
// Default autoApprove settings
|
||||
"autoApprove": []
|
||||
}
|
||||
// ... other servers might be configured here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then register the server:
|
||||
**Key Points:**
|
||||
|
||||
```
|
||||
install_local_mcp_server path/to/n8n-mcp-server
|
||||
```
|
||||
* Replace `/path/to/your/cloned/n8n-mcp-server/` with the actual absolute path where you cloned and built the repository.
|
||||
* Use the correct path separator for your operating system (forward slashes `/` for macOS/Linux, double backslashes `\\` for Windows).
|
||||
* Ensure you provide the correct `N8N_API_URL` (including `/api/v1`) and `N8N_API_KEY`.
|
||||
* The server needs to be built (`npm run build`) before the assistant can run the `build/index.js` file.
|
||||
|
||||
## Available Tools
|
||||
|
||||
@@ -124,30 +152,29 @@ The webhook authentication is handled automatically using the `N8N_WEBHOOK_USERN
|
||||
|
||||
### Workflow Management
|
||||
|
||||
- `workflow_list`: List all workflows
|
||||
- `workflow_get`: Get details of a specific workflow
|
||||
- `workflow_create`: Create a new workflow
|
||||
- `workflow_update`: Update an existing workflow
|
||||
- `workflow_delete`: Delete a workflow
|
||||
- `workflow_activate`: Activate a workflow
|
||||
- `workflow_deactivate`: Deactivate a workflow
|
||||
- `list_workflows`: List all workflows
|
||||
- `get_workflow`: Get details of a specific workflow
|
||||
- `create_workflow`: Create a new workflow
|
||||
- `update_workflow`: Update an existing workflow
|
||||
- `delete_workflow`: Delete a workflow
|
||||
- `activate_workflow`: Activate a workflow
|
||||
- `deactivate_workflow`: Deactivate a workflow
|
||||
|
||||
### Execution Management
|
||||
|
||||
- `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_list`: List executions for a workflow
|
||||
- `execution_stop`: Stop a running execution
|
||||
- `get_execution`: Get details of a specific execution
|
||||
- `list_executions`: List executions for a workflow
|
||||
- `delete_execution`: Delete a specific execution
|
||||
|
||||
## Resources
|
||||
|
||||
The server provides the following resources:
|
||||
|
||||
- `n8n://workflows/list`: List of all workflows
|
||||
- `n8n://workflow/{id}`: Details of a specific workflow
|
||||
- `n8n://executions/{workflowId}`: List of executions for a workflow
|
||||
- `n8n://execution/{id}`: Details of a specific execution
|
||||
- `n8n://workflows`: List of all workflows (static resource)
|
||||
- `n8n://workflows/{id}`: Details of a specific workflow (dynamic resource template)
|
||||
- `n8n://executions/{id}`: Details of a specific execution (dynamic resource template)
|
||||
- `n8n://execution-stats`: Summary statistics of recent executions (static resource)
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
module.exports = (api) => {
|
||||
// Check if running in test environment (NODE_ENV is set to 'test' by run-tests.js)
|
||||
const isTest = api.env('test');
|
||||
|
||||
const presets = [
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: [
|
||||
['@babel/plugin-transform-modules-commonjs']
|
||||
]
|
||||
];
|
||||
|
||||
const plugins = []; // Keep only one declaration
|
||||
|
||||
// Only add the CJS transform plugin if NOT in test environment
|
||||
if (!isTest) {
|
||||
plugins.push(['@babel/plugin-transform-modules-commonjs']);
|
||||
}
|
||||
|
||||
// For Jest (test environment), ensure node_modules are not completely ignored
|
||||
// if needed, but rely on transformIgnorePatterns in jest.config.cjs primarily.
|
||||
// This is more of a fallback if transformIgnorePatterns isn't sufficient.
|
||||
const ignore = isTest ? [] : [/node_modules/];
|
||||
|
||||
return {
|
||||
presets,
|
||||
plugins,
|
||||
ignore, // Add ignore configuration back
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
module.exports = {
|
||||
// Use commonjs style export
|
||||
preset: 'ts-jest',
|
||||
preset: 'ts-jest/presets/default-esm', // Use ESM preset for ts-jest
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'babel-jest',
|
||||
// Use ts-jest transformer with ESM support and point to tests tsconfig
|
||||
'^.+\\.tsx?$': ['ts-jest', { useESM: true, tsconfig: 'tests/tsconfig.json' }],
|
||||
},
|
||||
// Allow src and test folders to resolve imports properly
|
||||
extensionsToTreatAsEsm: ['.ts'], // Treat .ts as ESM
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
// Recommended mapper for ts-jest ESM support: map extensionless paths to .js
|
||||
'^(\\.{1,2}/.*)$': '$1.js',
|
||||
},
|
||||
// Handle the modelcontextprotocol SDK
|
||||
// Handle the modelcontextprotocol SDK and other potential ESM dependencies
|
||||
transformIgnorePatterns: [
|
||||
"node_modules/(?!(@modelcontextprotocol)/)"
|
||||
"/node_modules/(?!(@modelcontextprotocol/sdk|axios|another-esm-dep)/)" // Adjust as needed
|
||||
],
|
||||
collectCoverage: true,
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"start": "node build/index.js",
|
||||
"dev": "tsc -w",
|
||||
"lint": "eslint --ext .ts src/",
|
||||
"test": "node --experimental-vm-modules run-tests.js",
|
||||
"test:watch": "node --experimental-vm-modules run-tests.js --watch",
|
||||
"test:coverage": "node --experimental-vm-modules run-tests.js --coverage",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs",
|
||||
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs --watch",
|
||||
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs --coverage",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* This module provides a client for interacting with the n8n API.
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { EnvConfig } from '../config/environment.js';
|
||||
import { handleAxiosError, N8nApiError } from '../errors/index.js';
|
||||
import axios, { AxiosInstance, AxiosResponse } from 'axios'; // Import AxiosResponse
|
||||
import { EnvConfig } from '../config/environment.js'; // Already has .js
|
||||
import { handleAxiosError, N8nApiError } from '../errors/index.js'; // Already has .js
|
||||
import { Workflow, Execution, ExecutionRunData } from '../types/index.js'; // Already has .js
|
||||
|
||||
/**
|
||||
* n8n API Client class for making requests to the n8n API
|
||||
@@ -86,9 +87,9 @@ export class N8nApiClient {
|
||||
*
|
||||
* @returns Array of workflow objects
|
||||
*/
|
||||
async getWorkflows(): Promise<any[]> {
|
||||
async getWorkflows(): Promise<Workflow[]> { // Use specific type
|
||||
try {
|
||||
const response = await this.axiosInstance.get('/workflows');
|
||||
const response: AxiosResponse<{ data: Workflow[] }> = await this.axiosInstance.get('/workflows');
|
||||
return response.data.data || [];
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, 'Failed to fetch workflows');
|
||||
@@ -101,9 +102,9 @@ export class N8nApiClient {
|
||||
* @param id Workflow ID
|
||||
* @returns Workflow object
|
||||
*/
|
||||
async getWorkflow(id: string): Promise<any> {
|
||||
async getWorkflow(id: string): Promise<Workflow> { // Use specific type
|
||||
try {
|
||||
const response = await this.axiosInstance.get(`/workflows/${id}`);
|
||||
const response: AxiosResponse<Workflow> = await this.axiosInstance.get(`/workflows/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, `Failed to fetch workflow ${id}`);
|
||||
@@ -115,9 +116,9 @@ export class N8nApiClient {
|
||||
*
|
||||
* @returns Array of execution objects
|
||||
*/
|
||||
async getExecutions(): Promise<any[]> {
|
||||
async getExecutions(): Promise<Execution[]> { // Use specific type
|
||||
try {
|
||||
const response = await this.axiosInstance.get('/executions');
|
||||
const response: AxiosResponse<{ data: Execution[] }> = await this.axiosInstance.get('/executions');
|
||||
return response.data.data || [];
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, 'Failed to fetch executions');
|
||||
@@ -130,9 +131,9 @@ export class N8nApiClient {
|
||||
* @param id Execution ID
|
||||
* @returns Execution object
|
||||
*/
|
||||
async getExecution(id: string): Promise<any> {
|
||||
async getExecution(id: string): Promise<Execution> { // Use specific type
|
||||
try {
|
||||
const response = await this.axiosInstance.get(`/executions/${id}`);
|
||||
const response: AxiosResponse<Execution> = await this.axiosInstance.get(`/executions/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, `Failed to fetch execution ${id}`);
|
||||
@@ -144,11 +145,12 @@ export class N8nApiClient {
|
||||
*
|
||||
* @param id Workflow ID
|
||||
* @param data Optional data to pass to the workflow
|
||||
* @returns Execution result
|
||||
* @returns Execution result (structure might vary)
|
||||
*/
|
||||
async executeWorkflow(id: string, data?: Record<string, any>): Promise<any> {
|
||||
async executeWorkflow(id: string, data?: Record<string, any>): Promise<ExecutionRunData> { // Use specific type
|
||||
try {
|
||||
const response = await this.axiosInstance.post(`/workflows/${id}/execute`, data || {});
|
||||
// Assuming the response data directly matches ExecutionRunData or similar
|
||||
const response: AxiosResponse<ExecutionRunData> = await this.axiosInstance.post(`/workflows/${id}/execute`, data || {});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, `Failed to execute workflow ${id}`);
|
||||
@@ -161,7 +163,7 @@ export class N8nApiClient {
|
||||
* @param workflow Workflow object to create
|
||||
* @returns Created workflow
|
||||
*/
|
||||
async createWorkflow(workflow: Record<string, any>): Promise<any> {
|
||||
async createWorkflow(workflow: Partial<Workflow>): Promise<Workflow> { // Use specific types
|
||||
try {
|
||||
// Make sure settings property is present
|
||||
if (!workflow.settings) {
|
||||
@@ -181,15 +183,16 @@ export class N8nApiClient {
|
||||
delete workflowToCreate.id; // Remove id property if it exists
|
||||
delete workflowToCreate.createdAt; // Remove createdAt property if it exists
|
||||
delete workflowToCreate.updatedAt; // Remove updatedAt property if it exists
|
||||
delete workflowToCreate.tags; // Remove tags property as it's read-only
|
||||
delete workflowToCreate.tags; // Remove tags property if it exists and is read-only
|
||||
|
||||
// Log request for debugging
|
||||
console.error('[DEBUG] Creating workflow with data:', JSON.stringify(workflowToCreate, null, 2));
|
||||
// Removed debug log
|
||||
// console.error('[DEBUG] Creating workflow with data:', JSON.stringify(workflowToCreate, null, 2));
|
||||
|
||||
const response = await this.axiosInstance.post('/workflows', workflowToCreate);
|
||||
const response: AxiosResponse<Workflow> = await this.axiosInstance.post('/workflows', workflowToCreate);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Create workflow error:', error);
|
||||
// Removed error log, handleAxiosError should suffice
|
||||
// console.error('[ERROR] Create workflow error:', error);
|
||||
throw handleAxiosError(error, 'Failed to create workflow');
|
||||
}
|
||||
}
|
||||
@@ -201,9 +204,9 @@ export class N8nApiClient {
|
||||
* @param workflow Updated workflow object
|
||||
* @returns Updated workflow
|
||||
*/
|
||||
async updateWorkflow(id: string, workflow: Record<string, any>): Promise<any> {
|
||||
async updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow> { // Use specific types
|
||||
try {
|
||||
const response = await this.axiosInstance.put(`/workflows/${id}`, workflow);
|
||||
const response: AxiosResponse<Workflow> = await this.axiosInstance.put(`/workflows/${id}`, workflow);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, `Failed to update workflow ${id}`);
|
||||
@@ -214,11 +217,12 @@ export class N8nApiClient {
|
||||
* Delete a workflow
|
||||
*
|
||||
* @param id Workflow ID
|
||||
* @returns Deleted workflow
|
||||
* @returns Success indicator
|
||||
*/
|
||||
async deleteWorkflow(id: string): Promise<any> {
|
||||
async deleteWorkflow(id: string): Promise<{ success: boolean }> { // Use specific type
|
||||
try {
|
||||
const response = await this.axiosInstance.delete(`/workflows/${id}`);
|
||||
// Assuming API returns { success: true } or similar on successful delete
|
||||
const response: AxiosResponse<{ success: boolean }> = await this.axiosInstance.delete(`/workflows/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, `Failed to delete workflow ${id}`);
|
||||
@@ -231,9 +235,10 @@ export class N8nApiClient {
|
||||
* @param id Workflow ID
|
||||
* @returns Activated workflow
|
||||
*/
|
||||
async activateWorkflow(id: string): Promise<any> {
|
||||
async activateWorkflow(id: string): Promise<Workflow> { // Use specific type
|
||||
try {
|
||||
const response = await this.axiosInstance.post(`/workflows/${id}/activate`);
|
||||
// Assuming API returns the updated workflow object
|
||||
const response: AxiosResponse<Workflow> = await this.axiosInstance.post(`/workflows/${id}/activate`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, `Failed to activate workflow ${id}`);
|
||||
@@ -246,9 +251,10 @@ export class N8nApiClient {
|
||||
* @param id Workflow ID
|
||||
* @returns Deactivated workflow
|
||||
*/
|
||||
async deactivateWorkflow(id: string): Promise<any> {
|
||||
async deactivateWorkflow(id: string): Promise<Workflow> { // Use specific type
|
||||
try {
|
||||
const response = await this.axiosInstance.post(`/workflows/${id}/deactivate`);
|
||||
// Assuming API returns the updated workflow object
|
||||
const response: AxiosResponse<Workflow> = await this.axiosInstance.post(`/workflows/${id}/deactivate`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, `Failed to deactivate workflow ${id}`);
|
||||
@@ -259,11 +265,12 @@ export class N8nApiClient {
|
||||
* Delete an execution
|
||||
*
|
||||
* @param id Execution ID
|
||||
* @returns Deleted execution or success message
|
||||
* @returns Success indicator
|
||||
*/
|
||||
async deleteExecution(id: string): Promise<any> {
|
||||
async deleteExecution(id: string): Promise<{ success: boolean }> { // Use specific type
|
||||
try {
|
||||
const response = await this.axiosInstance.delete(`/executions/${id}`);
|
||||
// Assuming API returns { success: true } or similar on successful delete
|
||||
const response: AxiosResponse<{ success: boolean }> = await this.axiosInstance.delete(`/executions/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleAxiosError(error, `Failed to delete execution ${id}`);
|
||||
|
||||
@@ -1,152 +1,27 @@
|
||||
/**
|
||||
* n8n API Client Interface
|
||||
* n8n API Client Export
|
||||
*
|
||||
* This module defines interfaces and types for the n8n API client.
|
||||
* This module primarily exports the N8nApiClient class.
|
||||
* The N8nApiService wrapper was removed as it provided little additional value.
|
||||
*/
|
||||
|
||||
import { N8nApiClient } from './client.js';
|
||||
import { EnvConfig } from '../config/environment.js';
|
||||
import { Workflow, Execution } from '../types/index.js';
|
||||
|
||||
// Re-export the client class
|
||||
export { N8nApiClient } from './client.js';
|
||||
|
||||
// Keep the factory function for consistency, but have it return N8nApiClient directly
|
||||
/**
|
||||
* n8n API service - provides functions for interacting with n8n API
|
||||
*/
|
||||
export class N8nApiService {
|
||||
private client: N8nApiClient;
|
||||
|
||||
/**
|
||||
* Create a new n8n API service
|
||||
* Create a new n8n API client instance
|
||||
*
|
||||
* @param config Environment configuration
|
||||
* @returns n8n API client instance
|
||||
*/
|
||||
constructor(config: EnvConfig) {
|
||||
this.client = new N8nApiClient(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check connectivity to the n8n API
|
||||
*/
|
||||
async checkConnectivity(): Promise<void> {
|
||||
return this.client.checkConnectivity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all workflows from n8n
|
||||
*
|
||||
* @returns Array of workflow objects
|
||||
*/
|
||||
async getWorkflows(): Promise<Workflow[]> {
|
||||
return this.client.getWorkflows();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific workflow by ID
|
||||
*
|
||||
* @param id Workflow ID
|
||||
* @returns Workflow object
|
||||
*/
|
||||
async getWorkflow(id: string): Promise<Workflow> {
|
||||
return this.client.getWorkflow(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a workflow by ID
|
||||
*
|
||||
* @param id Workflow ID
|
||||
* @param data Optional data to pass to the workflow
|
||||
* @returns Execution result
|
||||
*/
|
||||
async executeWorkflow(id: string, data?: Record<string, any>): Promise<any> {
|
||||
return this.client.executeWorkflow(id, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new workflow
|
||||
*
|
||||
* @param workflow Workflow object to create
|
||||
* @returns Created workflow
|
||||
*/
|
||||
async createWorkflow(workflow: Record<string, any>): Promise<Workflow> {
|
||||
return this.client.createWorkflow(workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing workflow
|
||||
*
|
||||
* @param id Workflow ID
|
||||
* @param workflow Updated workflow object
|
||||
* @returns Updated workflow
|
||||
*/
|
||||
async updateWorkflow(id: string, workflow: Record<string, any>): Promise<Workflow> {
|
||||
return this.client.updateWorkflow(id, workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a workflow
|
||||
*
|
||||
* @param id Workflow ID
|
||||
* @returns Deleted workflow or success message
|
||||
*/
|
||||
async deleteWorkflow(id: string): Promise<any> {
|
||||
return this.client.deleteWorkflow(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a workflow
|
||||
*
|
||||
* @param id Workflow ID
|
||||
* @returns Activated workflow
|
||||
*/
|
||||
async activateWorkflow(id: string): Promise<Workflow> {
|
||||
return this.client.activateWorkflow(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a workflow
|
||||
*
|
||||
* @param id Workflow ID
|
||||
* @returns Deactivated workflow
|
||||
*/
|
||||
async deactivateWorkflow(id: string): Promise<Workflow> {
|
||||
return this.client.deactivateWorkflow(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all workflow executions
|
||||
*
|
||||
* @returns Array of execution objects
|
||||
*/
|
||||
async getExecutions(): Promise<Execution[]> {
|
||||
return this.client.getExecutions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific execution by ID
|
||||
*
|
||||
* @param id Execution ID
|
||||
* @returns Execution object
|
||||
*/
|
||||
async getExecution(id: string): Promise<Execution> {
|
||||
return this.client.getExecution(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an execution
|
||||
*
|
||||
* @param id Execution ID
|
||||
* @returns Deleted execution or success message
|
||||
*/
|
||||
async deleteExecution(id: string): Promise<any> {
|
||||
return this.client.deleteExecution(id);
|
||||
}
|
||||
export function createN8nApiClient(config: EnvConfig): N8nApiClient {
|
||||
return new N8nApiClient(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new n8n API service
|
||||
*
|
||||
* @param config Environment configuration
|
||||
* @returns n8n API service
|
||||
*/
|
||||
export function createApiService(config: EnvConfig): N8nApiService {
|
||||
return new N8nApiService(config);
|
||||
}
|
||||
// Export the type alias for convenience if needed elsewhere,
|
||||
// though direct use of N8nApiClient is preferred.
|
||||
export type N8nApiService = N8nApiClient;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { ErrorCode } from '../errors/error-codes.js';
|
||||
import { ErrorCode } from '../errors/error-codes.js'; // Already has .js
|
||||
|
||||
// Environment variable names
|
||||
export const ENV_VARS = {
|
||||
|
||||
@@ -13,14 +13,16 @@ import {
|
||||
ListToolsRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { getEnvConfig } from './environment.js';
|
||||
import { setupWorkflowTools } from '../tools/workflow/index.js';
|
||||
import { setupExecutionTools } from '../tools/execution/index.js';
|
||||
import { getEnvConfig, EnvConfig } from './environment.js'; // Import EnvConfig
|
||||
import { setupWorkflowTools, ListWorkflowsHandler, GetWorkflowHandler, CreateWorkflowHandler, UpdateWorkflowHandler, DeleteWorkflowHandler, ActivateWorkflowHandler, DeactivateWorkflowHandler } from '../tools/workflow/index.js';
|
||||
import { setupExecutionTools, ListExecutionsHandler, GetExecutionHandler, DeleteExecutionHandler, RunWebhookHandler } from '../tools/execution/index.js';
|
||||
import { setupResourceHandlers } from '../resources/index.js';
|
||||
import { createApiService } from '../api/n8n-client.js';
|
||||
// Update imports to use N8nApiClient and its factory
|
||||
import { createN8nApiClient, N8nApiClient } from '../api/n8n-client.js';
|
||||
import { McpError, ErrorCode } from '../errors/index.js';
|
||||
|
||||
// Import types
|
||||
import { ToolCallResult } from '../types/index.js';
|
||||
import { ToolCallResult, BaseToolHandler } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Configure and return an MCP server instance with all tools and resources
|
||||
@@ -31,13 +33,13 @@ export async function configureServer(): Promise<Server> {
|
||||
// Get validated environment configuration
|
||||
const envConfig = getEnvConfig();
|
||||
|
||||
// Create n8n API service
|
||||
const apiService = createApiService(envConfig);
|
||||
// Create n8n API client instance
|
||||
const apiClient = createN8nApiClient(envConfig); // Use new factory function name
|
||||
|
||||
// Verify n8n API connectivity
|
||||
try {
|
||||
console.error('Verifying n8n API connectivity...');
|
||||
await apiService.checkConnectivity();
|
||||
await apiClient.checkConnectivity(); // Use apiClient instance
|
||||
console.error(`Successfully connected to n8n API at ${envConfig.n8nApiUrl}`);
|
||||
} catch (error) {
|
||||
console.error('ERROR: Failed to connect to n8n API:', error instanceof Error ? error.message : error);
|
||||
@@ -58,9 +60,10 @@ export async function configureServer(): Promise<Server> {
|
||||
}
|
||||
);
|
||||
|
||||
// Set up all request handlers
|
||||
// Set up all request handlers, passing the single apiClient instance where needed
|
||||
setupToolListRequestHandler(server);
|
||||
setupToolCallRequestHandler(server);
|
||||
setupToolCallRequestHandler(server, apiClient); // Pass apiClient
|
||||
// Pass envConfig to resource handlers as originally intended
|
||||
setupResourceHandlers(server, envConfig);
|
||||
|
||||
return server;
|
||||
@@ -87,72 +90,44 @@ function setupToolListRequestHandler(server: Server): void {
|
||||
* Set up the tool call request handler for the server
|
||||
*
|
||||
* @param server MCP server instance
|
||||
* @param apiClient The shared N8nApiClient instance
|
||||
*/
|
||||
function setupToolCallRequestHandler(server: Server): void {
|
||||
// Update function signature to accept N8nApiClient
|
||||
function setupToolCallRequestHandler(server: Server, apiClient: N8nApiClient): void {
|
||||
|
||||
// Map tool names to their handler classes - Update constructor signature type
|
||||
// The constructor now expects N8nApiClient (which is aliased as N8nApiService)
|
||||
const toolHandlerMap: Record<string, new (apiClient: N8nApiClient) => BaseToolHandler> = {
|
||||
'list_workflows': ListWorkflowsHandler,
|
||||
'get_workflow': GetWorkflowHandler,
|
||||
'create_workflow': CreateWorkflowHandler,
|
||||
'update_workflow': UpdateWorkflowHandler,
|
||||
'delete_workflow': DeleteWorkflowHandler,
|
||||
'activate_workflow': ActivateWorkflowHandler,
|
||||
'deactivate_workflow': DeactivateWorkflowHandler,
|
||||
'list_executions': ListExecutionsHandler,
|
||||
'get_execution': GetExecutionHandler,
|
||||
'delete_execution': DeleteExecutionHandler,
|
||||
'run_webhook': RunWebhookHandler,
|
||||
// Add other tools here
|
||||
};
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const toolName = request.params.name;
|
||||
const args = request.params.arguments || {};
|
||||
|
||||
let result: ToolCallResult;
|
||||
|
||||
try {
|
||||
// Import handlers
|
||||
const {
|
||||
ListWorkflowsHandler,
|
||||
GetWorkflowHandler,
|
||||
CreateWorkflowHandler,
|
||||
UpdateWorkflowHandler,
|
||||
DeleteWorkflowHandler,
|
||||
ActivateWorkflowHandler,
|
||||
DeactivateWorkflowHandler
|
||||
} = await import('../tools/workflow/index.js');
|
||||
const HandlerClass = toolHandlerMap[toolName];
|
||||
|
||||
const {
|
||||
ListExecutionsHandler,
|
||||
GetExecutionHandler,
|
||||
DeleteExecutionHandler,
|
||||
RunWebhookHandler
|
||||
} = await import('../tools/execution/index.js');
|
||||
|
||||
// Route the tool call to the appropriate handler
|
||||
if (toolName === 'list_workflows') {
|
||||
const handler = new ListWorkflowsHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'get_workflow') {
|
||||
const handler = new GetWorkflowHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'create_workflow') {
|
||||
const handler = new CreateWorkflowHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'update_workflow') {
|
||||
const handler = new UpdateWorkflowHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'delete_workflow') {
|
||||
const handler = new DeleteWorkflowHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'activate_workflow') {
|
||||
const handler = new ActivateWorkflowHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'deactivate_workflow') {
|
||||
const handler = new DeactivateWorkflowHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'list_executions') {
|
||||
const handler = new ListExecutionsHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'get_execution') {
|
||||
const handler = new GetExecutionHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'delete_execution') {
|
||||
const handler = new DeleteExecutionHandler();
|
||||
result = await handler.execute(args);
|
||||
} else if (toolName === 'run_webhook') {
|
||||
const handler = new RunWebhookHandler();
|
||||
result = await handler.execute(args);
|
||||
} else {
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
if (!HandlerClass) {
|
||||
throw new McpError(ErrorCode.NotImplemented, `Unknown tool: ${toolName}`); // Use NotImplemented
|
||||
}
|
||||
|
||||
// Converting to MCP SDK expected format
|
||||
// Pass the apiClient instance to the constructor
|
||||
const handler = new HandlerClass(apiClient);
|
||||
const result: ToolCallResult = await handler.execute(args);
|
||||
|
||||
// Return result in MCP SDK expected format
|
||||
return {
|
||||
content: result.content,
|
||||
isError: result.isError,
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* for the n8n MCP Server.
|
||||
*/
|
||||
|
||||
import { McpError as SdkMcpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { ErrorCode } from './error-codes.js';
|
||||
import { McpError as SdkMcpError } from '@modelcontextprotocol/sdk/types.js'; // Add .js back
|
||||
import { ErrorCode } from './error-codes.js'; // Add .js back
|
||||
|
||||
// Re-export McpError from SDK
|
||||
export { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { EnvConfig } from '../config/environment.js';
|
||||
import { createApiService } from '../api/n8n-client.js';
|
||||
import { EnvConfig } from '../config/environment.js'; // Re-add EnvConfig import
|
||||
import { createN8nApiClient, N8nApiClient } from '../api/n8n-client.js'; // Use correct factory import
|
||||
import { McpError, ErrorCode } from '../errors/index.js';
|
||||
|
||||
// Import static resource handlers
|
||||
@@ -46,12 +46,13 @@ import {
|
||||
* @param server MCP server instance
|
||||
* @param envConfig Environment configuration
|
||||
*/
|
||||
// Revert function signature to accept EnvConfig
|
||||
export function setupResourceHandlers(server: Server, envConfig: EnvConfig): void {
|
||||
// Set up static resources
|
||||
setupStaticResources(server, envConfig);
|
||||
setupStaticResources(server, envConfig); // Pass envConfig
|
||||
|
||||
// Set up dynamic resources
|
||||
setupDynamicResources(server, envConfig);
|
||||
setupDynamicResources(server, envConfig); // Pass envConfig
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,8 +61,10 @@ export function setupResourceHandlers(server: Server, envConfig: EnvConfig): voi
|
||||
* @param server MCP server instance
|
||||
* @param envConfig Environment configuration
|
||||
*/
|
||||
// Revert function signature to accept EnvConfig
|
||||
function setupStaticResources(server: Server, envConfig: EnvConfig): void {
|
||||
const apiService = createApiService(envConfig);
|
||||
// Create apiClient internally for now
|
||||
const apiClient = createN8nApiClient(envConfig); // Use correct factory function
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
// Return all available static resources
|
||||
@@ -80,8 +83,10 @@ function setupStaticResources(server: Server, envConfig: EnvConfig): void {
|
||||
* @param server MCP server instance
|
||||
* @param envConfig Environment configuration
|
||||
*/
|
||||
// Revert function signature to accept EnvConfig
|
||||
function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
||||
const apiService = createApiService(envConfig);
|
||||
// Create apiClient internally for now
|
||||
const apiClient = createN8nApiClient(envConfig); // Use correct factory function
|
||||
|
||||
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
||||
// Return all available dynamic resource templates
|
||||
@@ -100,7 +105,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
||||
try {
|
||||
// Handle static resources
|
||||
if (uri === getWorkflowsResourceUri()) {
|
||||
const content = await getWorkflowsResource(apiService);
|
||||
const content = await getWorkflowsResource(apiClient); // Use apiClient instance
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
@@ -113,7 +118,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
||||
}
|
||||
|
||||
if (uri === getExecutionStatsResourceUri()) {
|
||||
const content = await getExecutionStatsResource(apiService);
|
||||
const content = await getExecutionStatsResource(apiClient); // Use apiClient instance
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
@@ -128,7 +133,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
||||
// Handle dynamic resources
|
||||
const workflowId = extractWorkflowIdFromUri(uri);
|
||||
if (workflowId) {
|
||||
const content = await getWorkflowResource(apiService, workflowId);
|
||||
const content = await getWorkflowResource(apiClient, workflowId); // Use apiClient instance
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
@@ -142,7 +147,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
||||
|
||||
const executionId = extractExecutionIdFromUri(uri);
|
||||
if (executionId) {
|
||||
const content = await getExecutionResource(apiService, executionId);
|
||||
const content = await getExecutionResource(apiClient, executionId); // Use apiClient instance
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
* This module provides a base handler for execution-related tools.
|
||||
*/
|
||||
|
||||
import { ToolCallResult } from '../../types/index.js';
|
||||
import { N8nApiError } from '../../errors/index.js';
|
||||
import { createApiService } from '../../api/n8n-client.js';
|
||||
import { getEnvConfig } from '../../config/environment.js';
|
||||
import { ToolCallResult, BaseToolHandler } from '../../types/index.js'; // Already has .js
|
||||
import { N8nApiError } from '../../errors/index.js'; // Already has .js
|
||||
import { N8nApiService } from '../../api/n8n-client.js'; // Already has .js
|
||||
|
||||
/**
|
||||
* Base class for execution tool handlers
|
||||
*/
|
||||
export abstract class BaseExecutionToolHandler {
|
||||
protected apiService = createApiService(getEnvConfig());
|
||||
export abstract class BaseExecutionToolHandler implements BaseToolHandler { // Implement BaseToolHandler
|
||||
protected apiService: N8nApiService; // Declare apiService property
|
||||
|
||||
/**
|
||||
* Constructor to inject the API service
|
||||
* @param apiService Instance of N8nApiService
|
||||
*/
|
||||
constructor(apiService: N8nApiService) {
|
||||
this.apiService = apiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and execute the tool
|
||||
|
||||
@@ -9,6 +9,11 @@ import { ToolCallResult, ToolDefinition } from '../../types/index.js';
|
||||
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { ErrorCode } from '../../errors/error-codes.js';
|
||||
|
||||
// Define specific type for delete arguments
|
||||
interface DeleteExecutionArgs {
|
||||
executionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the delete_execution tool
|
||||
*/
|
||||
@@ -19,8 +24,8 @@ export class DeleteExecutionHandler extends BaseExecutionToolHandler {
|
||||
* @param args Tool arguments (executionId)
|
||||
* @returns Result of the deletion operation
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
return this.handleExecution(async () => {
|
||||
async execute(args: DeleteExecutionArgs): Promise<ToolCallResult> { // Use specific args type
|
||||
return this.handleExecution(async (args) => { // Pass args to handler
|
||||
// Validate required parameters
|
||||
if (!args.executionId) {
|
||||
throw new McpError(
|
||||
@@ -39,7 +44,7 @@ export class DeleteExecutionHandler extends BaseExecutionToolHandler {
|
||||
{ id: executionId, deleted: true },
|
||||
`Successfully deleted execution with ID: ${executionId}`
|
||||
);
|
||||
}, args);
|
||||
}, args); // Pass args to handleExecution
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { ErrorCode } from '../../errors/error-codes.js';
|
||||
import { formatExecutionDetails } from '../../utils/execution-formatter.js';
|
||||
|
||||
// Define specific type for get arguments
|
||||
interface GetExecutionArgs {
|
||||
executionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the get_execution tool
|
||||
*/
|
||||
@@ -20,8 +25,8 @@ export class GetExecutionHandler extends BaseExecutionToolHandler {
|
||||
* @param args Tool arguments (executionId)
|
||||
* @returns Execution details
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
return this.handleExecution(async () => {
|
||||
async execute(args: GetExecutionArgs): Promise<ToolCallResult> { // Use specific args type
|
||||
return this.handleExecution(async (args) => { // Pass args to handler
|
||||
// Validate required parameters
|
||||
if (!args.executionId) {
|
||||
throw new McpError(
|
||||
@@ -40,7 +45,7 @@ export class GetExecutionHandler extends BaseExecutionToolHandler {
|
||||
formattedExecution,
|
||||
`Execution Details for ID: ${args.executionId}`
|
||||
);
|
||||
}, args);
|
||||
}, args); // Pass args to handleExecution
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,9 +5,18 @@
|
||||
*/
|
||||
|
||||
import { BaseExecutionToolHandler } from './base-handler.js';
|
||||
import { ToolCallResult, ToolDefinition, Execution } from '../../types/index.js';
|
||||
import { ToolCallResult, ToolDefinition, Execution } from '../../types/index.js'; // Import Execution type
|
||||
import { formatExecutionSummary, summarizeExecutions } from '../../utils/execution-formatter.js';
|
||||
|
||||
// Define specific type for list arguments based on ToolDefinition
|
||||
interface ListExecutionsArgs {
|
||||
workflowId?: string;
|
||||
status?: 'success' | 'error' | 'waiting' | 'canceled'; // Use specific statuses if known
|
||||
limit?: number;
|
||||
lastId?: string; // Note: n8n API might not support cursor pagination with lastId easily
|
||||
includeSummary?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the list_executions tool
|
||||
*/
|
||||
@@ -15,14 +24,16 @@ export class ListExecutionsHandler extends BaseExecutionToolHandler {
|
||||
/**
|
||||
* Execute the tool
|
||||
*
|
||||
* @param args Tool arguments (workflowId, status, limit, lastId)
|
||||
* @param args Tool arguments (workflowId, status, limit, lastId, includeSummary)
|
||||
* @returns List of executions
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
return this.handleExecution(async () => {
|
||||
const executions = await this.apiService.getExecutions();
|
||||
async execute(args: ListExecutionsArgs): Promise<ToolCallResult> { // Use specific args type
|
||||
return this.handleExecution(async (args) => { // Pass args to handler
|
||||
// Fetch all executions first (n8n API might require filtering via query params)
|
||||
// TODO: Update apiService.getExecutions to accept filter parameters if possible
|
||||
const executions: Execution[] = await this.apiService.getExecutions();
|
||||
|
||||
// Apply filters if provided
|
||||
// Apply filters locally for now
|
||||
let filteredExecutions = executions;
|
||||
|
||||
// Filter by workflow ID if provided
|
||||
@@ -39,8 +50,12 @@ export class ListExecutionsHandler extends BaseExecutionToolHandler {
|
||||
);
|
||||
}
|
||||
|
||||
// Apply limit if provided
|
||||
// TODO: Implement pagination using lastId if the API supports it.
|
||||
// This usually requires sorting and finding the index, or specific API params.
|
||||
|
||||
// Apply limit if provided (after filtering and potential pagination)
|
||||
const limit = args.limit && args.limit > 0 ? args.limit : filteredExecutions.length;
|
||||
// Ensure limit is applied correctly after potential pagination logic
|
||||
filteredExecutions = filteredExecutions.slice(0, limit);
|
||||
|
||||
// Format the executions for display
|
||||
@@ -48,25 +63,30 @@ export class ListExecutionsHandler extends BaseExecutionToolHandler {
|
||||
formatExecutionSummary(execution)
|
||||
);
|
||||
|
||||
// Generate summary if requested
|
||||
// Generate summary if requested (based on the initially fetched, unfiltered list)
|
||||
let summary = undefined;
|
||||
if (args.includeSummary) {
|
||||
// Summarize based on the *original* list before filtering/limiting for accurate stats
|
||||
summary = summarizeExecutions(executions);
|
||||
}
|
||||
|
||||
// Prepare response data
|
||||
const responseData = {
|
||||
// Return the filtered and limited list
|
||||
executions: formattedExecutions,
|
||||
summary: summary,
|
||||
total: formattedExecutions.length,
|
||||
filtered: args.workflowId || args.status ? true : false
|
||||
count: formattedExecutions.length, // Count of returned executions
|
||||
// Indicate if filters were applied
|
||||
filtersApplied: args.workflowId || args.status ? true : false,
|
||||
// Note: totalAvailable might be misleading if pagination isn't fully implemented
|
||||
totalAvailable: executions.length,
|
||||
};
|
||||
|
||||
return this.formatSuccess(
|
||||
responseData,
|
||||
`Found ${formattedExecutions.length} execution(s)`
|
||||
`Found ${formattedExecutions.length} execution(s)` + (responseData.filtersApplied ? ' matching filters.' : '.')
|
||||
);
|
||||
}, args);
|
||||
}, args); // Pass args to handleExecution
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +98,7 @@ export class ListExecutionsHandler extends BaseExecutionToolHandler {
|
||||
export function getListExecutionsToolDefinition(): ToolDefinition {
|
||||
return {
|
||||
name: 'list_executions',
|
||||
description: 'Retrieve a list of workflow executions from n8n',
|
||||
description: 'Retrieve a list of workflow executions from n8n, with optional filtering.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -88,19 +108,23 @@ export function getListExecutionsToolDefinition(): ToolDefinition {
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Optional status to filter by (success, error, waiting, or canceled)',
|
||||
description: 'Optional status to filter by (e.g., success, error, waiting)',
|
||||
// Consider using enum if statuses are fixed:
|
||||
// enum: ['success', 'error', 'waiting', 'canceled']
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of executions to return',
|
||||
},
|
||||
lastId: {
|
||||
type: 'string',
|
||||
description: 'ID of the last execution for pagination',
|
||||
description: 'Maximum number of executions to return (default: all matching)',
|
||||
},
|
||||
// lastId is hard to implement without API support for cursor pagination
|
||||
// lastId: {
|
||||
// type: 'string',
|
||||
// description: 'ID of the last execution for pagination (if supported)',
|
||||
// },
|
||||
includeSummary: {
|
||||
type: 'boolean',
|
||||
description: 'Include summary statistics about executions',
|
||||
description: 'Include summary statistics about all executions (before filtering/limiting)',
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* This module provides a tool for running n8n workflows via webhooks.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosRequestConfig } from 'axios'; // Import AxiosRequestConfig
|
||||
import { z } from 'zod';
|
||||
import { ToolCallResult } from '../../types/index.js';
|
||||
import { ToolCallResult, ToolDefinition } from '../../types/index.js'; // Import ToolDefinition
|
||||
import { BaseExecutionToolHandler } from './base-handler.js';
|
||||
import { N8nApiError } from '../../errors/index.js';
|
||||
import { getEnvConfig } from '../../config/environment.js';
|
||||
@@ -33,7 +33,8 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
||||
/**
|
||||
* Tool definition for execution via webhook
|
||||
*/
|
||||
public static readonly inputSchema = runWebhookSchema;
|
||||
// Note: Static properties on classes aren't directly usable for instance methods in TS
|
||||
// The schema is used within the execute method instead.
|
||||
|
||||
/**
|
||||
* Extract N8N base URL from N8N API URL by removing /api/v1
|
||||
@@ -60,9 +61,10 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
||||
* @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
|
||||
async execute(args: RunWebhookParams): Promise<ToolCallResult> { // Use specific args type
|
||||
return this.handleExecution(async (args) => { // Pass args to handler
|
||||
// Parse and validate arguments using the Zod schema
|
||||
// This ensures args conforms to RunWebhookParams
|
||||
const params = runWebhookSchema.parse(args);
|
||||
|
||||
// Get environment config for auth credentials
|
||||
@@ -71,11 +73,13 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
||||
try {
|
||||
// Get the webhook URL with the proper prefix
|
||||
const baseUrl = this.getN8nBaseUrl();
|
||||
const webhookPath = `webhook/${params.workflowName}`;
|
||||
// Ensure workflowName doesn't contain slashes that could break the URL path
|
||||
const safeWorkflowName = params.workflowName.replace(/\//g, '');
|
||||
const webhookPath = `webhook/${safeWorkflowName}`;
|
||||
const webhookUrl = new URL(webhookPath, baseUrl).toString();
|
||||
|
||||
// Prepare request config with basic auth from environment
|
||||
const requestConfig: any = {
|
||||
const requestConfig: AxiosRequestConfig = { // Use AxiosRequestConfig type
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(params.headers || {})
|
||||
@@ -97,7 +101,7 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
||||
return this.formatSuccess({
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data
|
||||
data: response.data // Assuming response.data is JSON-serializable
|
||||
}, 'Webhook executed successfully');
|
||||
} catch (error) {
|
||||
// Handle error from the webhook request
|
||||
@@ -106,20 +110,29 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
||||
|
||||
if (error.response) {
|
||||
errorMessage = `Webhook execution failed with status ${error.response.status}: ${error.response.statusText}`;
|
||||
if (error.response.data) {
|
||||
// Attempt to stringify response data safely
|
||||
let responseDataStr = '';
|
||||
try {
|
||||
responseDataStr = JSON.stringify(error.response.data, null, 2);
|
||||
} catch (stringifyError) {
|
||||
responseDataStr = '[Could not stringify response data]';
|
||||
}
|
||||
// Add explicit check for error.response before accessing status
|
||||
const statusCode = error.response?.status || 500;
|
||||
return this.formatError(new N8nApiError(
|
||||
`${errorMessage}\n\n${JSON.stringify(error.response.data, null, 2)}`,
|
||||
error.response.status
|
||||
`${errorMessage}\n\n${responseDataStr}`,
|
||||
statusCode
|
||||
));
|
||||
}
|
||||
|
||||
// Cast error.response to any before accessing status
|
||||
return this.formatError(new N8nApiError(errorMessage, (error.response as any)?.status || 500));
|
||||
}
|
||||
|
||||
return this.formatError(new N8nApiError(errorMessage, error.response?.status || 500));
|
||||
// Re-throw non-axios errors for the base handler to catch
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw error; // Re-throw non-axios errors for the handler to catch
|
||||
}
|
||||
}, args);
|
||||
}, args); // Pass args to handleExecution
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +141,7 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
||||
*
|
||||
* @returns Tool definition object
|
||||
*/
|
||||
export function getRunWebhookToolDefinition() {
|
||||
export function getRunWebhookToolDefinition(): ToolDefinition { // Add return type
|
||||
return {
|
||||
name: 'run_webhook',
|
||||
description: 'Execute a workflow via webhook with optional input data',
|
||||
@@ -141,11 +154,14 @@ export function getRunWebhookToolDefinition() {
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Input data to pass to the webhook'
|
||||
description: 'Input data (JSON object) to pass to the webhook',
|
||||
// Indicate that properties can be anything for an object
|
||||
additionalProperties: true,
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: 'Additional headers to send with the request'
|
||||
description: 'Additional headers (key-value pairs) to send with the request',
|
||||
additionalProperties: { type: 'string' },
|
||||
}
|
||||
},
|
||||
required: ['workflowName']
|
||||
|
||||
@@ -16,11 +16,11 @@ export class ActivateWorkflowHandler extends BaseWorkflowToolHandler {
|
||||
* Execute the tool
|
||||
*
|
||||
* @param args Tool arguments containing workflowId
|
||||
* @returns Activation confirmation
|
||||
* @returns Activated workflow details
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
async execute(args: { workflowId: string }): Promise<ToolCallResult> { // Use specific type for args
|
||||
return this.handleExecution(async (args) => {
|
||||
const { workflowId } = args;
|
||||
const { workflowId } = args; // Destructuring remains the same
|
||||
|
||||
if (!workflowId) {
|
||||
throw new N8nApiError('Missing required parameter: workflowId');
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
* This module provides a base handler for workflow-related tools.
|
||||
*/
|
||||
|
||||
import { ToolCallResult } from '../../types/index.js';
|
||||
import { N8nApiError } from '../../errors/index.js';
|
||||
import { createApiService } from '../../api/n8n-client.js';
|
||||
import { getEnvConfig } from '../../config/environment.js';
|
||||
import { ToolCallResult, BaseToolHandler } from '../../types/index.js'; // Already has .js
|
||||
import { N8nApiError } from '../../errors/index.js'; // Already has .js
|
||||
import { N8nApiService } from '../../api/n8n-client.js'; // Already has .js
|
||||
|
||||
/**
|
||||
* Base class for workflow tool handlers
|
||||
*/
|
||||
export abstract class BaseWorkflowToolHandler {
|
||||
protected apiService = createApiService(getEnvConfig());
|
||||
export abstract class BaseWorkflowToolHandler implements BaseToolHandler { // Implement BaseToolHandler
|
||||
protected apiService: N8nApiService; // Declare apiService property
|
||||
|
||||
/**
|
||||
* Constructor to inject the API service
|
||||
* @param apiService Instance of N8nApiService
|
||||
*/
|
||||
constructor(apiService: N8nApiService) {
|
||||
this.apiService = apiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and execute the tool
|
||||
|
||||
@@ -5,9 +5,18 @@
|
||||
*/
|
||||
|
||||
import { BaseWorkflowToolHandler } from './base-handler.js';
|
||||
import { ToolCallResult, ToolDefinition } from '../../types/index.js';
|
||||
import { ToolCallResult, ToolDefinition, Workflow, N8nNode, N8nConnection } from '../../types/index.js'; // Import specific types
|
||||
import { N8nApiError } from '../../errors/index.js';
|
||||
|
||||
// Define specific type for create arguments based on ToolDefinition
|
||||
interface CreateWorkflowArgs {
|
||||
name: string;
|
||||
nodes?: N8nNode[];
|
||||
connections?: N8nConnection;
|
||||
active?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the create_workflow tool
|
||||
*/
|
||||
@@ -18,43 +27,44 @@ export class CreateWorkflowHandler extends BaseWorkflowToolHandler {
|
||||
* @param args Tool arguments containing workflow details
|
||||
* @returns Created workflow information
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
async execute(args: CreateWorkflowArgs): Promise<ToolCallResult> { // Use specific args type
|
||||
return this.handleExecution(async (args) => {
|
||||
const { name, nodes, connections, active, tags } = args;
|
||||
|
||||
if (!name) {
|
||||
// This check might be redundant if 'name' is required in schema, but good for safety
|
||||
throw new N8nApiError('Missing required parameter: name');
|
||||
}
|
||||
|
||||
// Validate nodes if provided
|
||||
// Basic validation (more robust validation could use Zod or similar)
|
||||
if (nodes && !Array.isArray(nodes)) {
|
||||
throw new N8nApiError('Parameter "nodes" must be an array');
|
||||
}
|
||||
|
||||
// Validate connections if provided
|
||||
if (connections && typeof connections !== 'object') {
|
||||
throw new N8nApiError('Parameter "connections" must be an object');
|
||||
}
|
||||
if (tags && !Array.isArray(tags)) {
|
||||
throw new N8nApiError('Parameter "tags" must be an array of strings');
|
||||
}
|
||||
|
||||
// Prepare workflow object
|
||||
const workflowData: Record<string, any> = {
|
||||
// Prepare workflow object using Partial<Workflow> for the API call
|
||||
const workflowData: Partial<Workflow> = {
|
||||
name,
|
||||
active: active === true, // Default to false if not specified
|
||||
active: active === true, // Default to false if not specified or undefined
|
||||
nodes: nodes || [], // Default to empty array if not provided
|
||||
connections: connections || {}, // Default to empty object if not provided
|
||||
tags: tags || [], // Default to empty array if not provided
|
||||
};
|
||||
|
||||
// Add optional fields if provided
|
||||
if (nodes) workflowData.nodes = nodes;
|
||||
if (connections) workflowData.connections = connections;
|
||||
if (tags) workflowData.tags = tags;
|
||||
|
||||
// Create the workflow
|
||||
const workflow = await this.apiService.createWorkflow(workflowData);
|
||||
const createdWorkflow = await this.apiService.createWorkflow(workflowData);
|
||||
|
||||
// Return summary of the created workflow
|
||||
return this.formatSuccess(
|
||||
{
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
active: workflow.active
|
||||
id: createdWorkflow.id,
|
||||
name: createdWorkflow.name,
|
||||
active: createdWorkflow.active
|
||||
},
|
||||
`Workflow created successfully`
|
||||
);
|
||||
@@ -80,18 +90,18 @@ export function getCreateWorkflowToolDefinition(): ToolDefinition {
|
||||
},
|
||||
nodes: {
|
||||
type: 'array',
|
||||
description: 'Array of node objects that define the workflow',
|
||||
description: 'Array of node objects (N8nNode structure) defining the workflow',
|
||||
items: {
|
||||
type: 'object',
|
||||
type: 'object', // Ideally, reference a detailed N8nNode schema here
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
type: 'object',
|
||||
description: 'Connection mappings between nodes',
|
||||
description: 'Connection mappings between nodes (N8nConnection structure)',
|
||||
},
|
||||
active: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the workflow should be active upon creation',
|
||||
description: 'Whether the workflow should be active upon creation (defaults to false)',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
|
||||
@@ -16,11 +16,11 @@ export class DeactivateWorkflowHandler extends BaseWorkflowToolHandler {
|
||||
* Execute the tool
|
||||
*
|
||||
* @param args Tool arguments containing workflowId
|
||||
* @returns Deactivation confirmation
|
||||
* @returns Deactivated workflow details
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
async execute(args: { workflowId: string }): Promise<ToolCallResult> { // Use specific type for args
|
||||
return this.handleExecution(async (args) => {
|
||||
const { workflowId } = args;
|
||||
const { workflowId } = args; // Destructuring remains the same
|
||||
|
||||
if (!workflowId) {
|
||||
throw new N8nApiError('Missing required parameter: workflowId');
|
||||
|
||||
@@ -16,11 +16,11 @@ export class DeleteWorkflowHandler extends BaseWorkflowToolHandler {
|
||||
* Execute the tool
|
||||
*
|
||||
* @param args Tool arguments containing workflowId
|
||||
* @returns Deletion confirmation
|
||||
* @returns Success message
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
async execute(args: { workflowId: string }): Promise<ToolCallResult> { // Use specific type for args
|
||||
return this.handleExecution(async (args) => {
|
||||
const { workflowId } = args;
|
||||
const { workflowId } = args; // Destructuring remains the same
|
||||
|
||||
if (!workflowId) {
|
||||
throw new N8nApiError('Missing required parameter: workflowId');
|
||||
|
||||
@@ -18,9 +18,9 @@ export class GetWorkflowHandler extends BaseWorkflowToolHandler {
|
||||
* @param args Tool arguments containing workflowId
|
||||
* @returns Workflow details
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
async execute(args: { workflowId: string }): Promise<ToolCallResult> { // Use specific type for args
|
||||
return this.handleExecution(async (args) => {
|
||||
const { workflowId } = args;
|
||||
const { workflowId } = args; // Destructuring remains the same
|
||||
|
||||
if (!workflowId) {
|
||||
throw new N8nApiError('Missing required parameter: workflowId');
|
||||
|
||||
@@ -14,12 +14,17 @@ export class ListWorkflowsHandler extends BaseWorkflowToolHandler {
|
||||
/**
|
||||
* Execute the tool
|
||||
*
|
||||
* @param args Tool arguments
|
||||
* @param args Tool arguments (expecting optional 'active' boolean)
|
||||
* @returns List of workflows
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
return this.handleExecution(async () => {
|
||||
const workflows = await this.apiService.getWorkflows();
|
||||
async execute(args: { active?: boolean }): Promise<ToolCallResult> { // Use specific type for args
|
||||
return this.handleExecution(async (args) => { // Pass args to the handler
|
||||
let workflows: Workflow[] = await this.apiService.getWorkflows(); // Add type annotation
|
||||
|
||||
// Apply filtering if the 'active' argument is provided
|
||||
if (args && typeof args.active === 'boolean') {
|
||||
workflows = workflows.filter((workflow: Workflow) => workflow.active === args.active);
|
||||
}
|
||||
|
||||
// Format the workflows for display
|
||||
const formattedWorkflows = workflows.map((workflow: Workflow) => ({
|
||||
@@ -31,9 +36,9 @@ export class ListWorkflowsHandler extends BaseWorkflowToolHandler {
|
||||
|
||||
return this.formatSuccess(
|
||||
formattedWorkflows,
|
||||
`Found ${formattedWorkflows.length} workflow(s)`
|
||||
`Found ${formattedWorkflows.length} workflow(s)` + (typeof args?.active === 'boolean' ? ` (filtered by active=${args.active})` : '')
|
||||
);
|
||||
}, args);
|
||||
}, args); // Pass args to handleExecution
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,16 @@
|
||||
*/
|
||||
|
||||
import { BaseWorkflowToolHandler } from './base-handler.js';
|
||||
import { ToolCallResult, ToolDefinition } from '../../types/index.js';
|
||||
import { ToolCallResult, ToolDefinition, Workflow, N8nNode, N8nConnection } from '../../types/index.js'; // Import specific types
|
||||
import { N8nApiError } from '../../errors/index.js';
|
||||
|
||||
// Define specific type for update arguments
|
||||
// Intersect with Partial<Workflow> to allow any workflow property update
|
||||
// Requires workflowId to identify the workflow
|
||||
interface UpdateWorkflowArgs extends Partial<Workflow> {
|
||||
workflowId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the update_workflow tool
|
||||
*/
|
||||
@@ -18,51 +25,73 @@ export class UpdateWorkflowHandler extends BaseWorkflowToolHandler {
|
||||
* @param args Tool arguments containing workflow updates
|
||||
* @returns Updated workflow information
|
||||
*/
|
||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
||||
async execute(args: UpdateWorkflowArgs): Promise<ToolCallResult> { // Use specific args type
|
||||
return this.handleExecution(async (args) => {
|
||||
const { workflowId, name, nodes, connections, active, tags } = args;
|
||||
const { workflowId, name, nodes, connections, active, tags, settings } = args; // Destructure known properties
|
||||
|
||||
if (!workflowId) {
|
||||
throw new N8nApiError('Missing required parameter: workflowId');
|
||||
}
|
||||
|
||||
// Validate nodes if provided
|
||||
// Basic validation (more robust validation could use Zod or similar)
|
||||
if (nodes && !Array.isArray(nodes)) {
|
||||
throw new N8nApiError('Parameter "nodes" must be an array');
|
||||
}
|
||||
|
||||
// Validate connections if provided
|
||||
if (connections && typeof connections !== 'object') {
|
||||
throw new N8nApiError('Parameter "connections" must be an object');
|
||||
}
|
||||
if (tags && !Array.isArray(tags)) {
|
||||
throw new N8nApiError('Parameter "tags" must be an array of strings');
|
||||
}
|
||||
if (settings && typeof settings !== 'object') {
|
||||
throw new N8nApiError('Parameter "settings" must be an object');
|
||||
}
|
||||
|
||||
// Get the current workflow to update
|
||||
const currentWorkflow = await this.apiService.getWorkflow(workflowId);
|
||||
// Get the current workflow to compare changes (optional, but good for summary)
|
||||
let currentWorkflow: Workflow | null = null;
|
||||
try {
|
||||
currentWorkflow = await this.apiService.getWorkflow(workflowId);
|
||||
} catch (error) {
|
||||
// Handle case where workflow to update doesn't exist
|
||||
if (error instanceof N8nApiError && error.message.includes('not found')) { // Adjust error check as needed
|
||||
throw new N8nApiError(`Workflow with ID "${workflowId}" not found.`);
|
||||
}
|
||||
throw error; // Re-throw other errors
|
||||
}
|
||||
|
||||
// Prepare update object with changes
|
||||
const workflowData: Record<string, any> = { ...currentWorkflow };
|
||||
// Prepare update object with only the provided changes
|
||||
const workflowUpdateData: Partial<Workflow> = {};
|
||||
if (name !== undefined) workflowUpdateData.name = name;
|
||||
if (nodes !== undefined) workflowUpdateData.nodes = nodes;
|
||||
if (connections !== undefined) workflowUpdateData.connections = connections;
|
||||
if (active !== undefined) workflowUpdateData.active = active;
|
||||
if (tags !== undefined) workflowUpdateData.tags = tags;
|
||||
if (settings !== undefined) workflowUpdateData.settings = settings;
|
||||
// Add other updatable fields from Workflow interface if needed
|
||||
|
||||
// Update fields if provided
|
||||
if (name !== undefined) workflowData.name = name;
|
||||
if (nodes !== undefined) workflowData.nodes = nodes;
|
||||
if (connections !== undefined) workflowData.connections = connections;
|
||||
if (active !== undefined) workflowData.active = active;
|
||||
if (tags !== undefined) workflowData.tags = tags;
|
||||
// Check if there are any actual changes to send
|
||||
if (Object.keys(workflowUpdateData).length === 0) {
|
||||
return this.formatSuccess(
|
||||
{ id: workflowId, name: currentWorkflow.name, active: currentWorkflow.active },
|
||||
`No update parameters provided for workflow ${workflowId}. No changes made.`
|
||||
);
|
||||
}
|
||||
|
||||
// Update the workflow
|
||||
const updatedWorkflow = await this.apiService.updateWorkflow(workflowId, workflowData);
|
||||
const updatedWorkflow = await this.apiService.updateWorkflow(workflowId, workflowUpdateData);
|
||||
|
||||
// Build a summary of changes
|
||||
// Build a summary of changes (optional)
|
||||
const changesArray = [];
|
||||
if (name !== undefined && name !== currentWorkflow.name) changesArray.push(`name: "${currentWorkflow.name}" → "${name}"`);
|
||||
if (active !== undefined && active !== currentWorkflow.active) changesArray.push(`active: ${currentWorkflow.active} → ${active}`);
|
||||
if (nodes !== undefined) changesArray.push('nodes updated');
|
||||
if (connections !== undefined) changesArray.push('connections updated');
|
||||
if (tags !== undefined) changesArray.push('tags updated');
|
||||
if (settings !== undefined) changesArray.push('settings updated');
|
||||
|
||||
const changesSummary = changesArray.length > 0
|
||||
? `Changes: ${changesArray.join(', ')}`
|
||||
: 'No changes were made';
|
||||
: 'No effective changes were made (values might be the same as current)';
|
||||
|
||||
return this.formatSuccess(
|
||||
{
|
||||
@@ -84,7 +113,7 @@ export class UpdateWorkflowHandler extends BaseWorkflowToolHandler {
|
||||
export function getUpdateWorkflowToolDefinition(): ToolDefinition {
|
||||
return {
|
||||
name: 'update_workflow',
|
||||
description: 'Update an existing workflow in n8n',
|
||||
description: 'Update an existing workflow in n8n. Provide only the fields you want to change.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -98,14 +127,14 @@ export function getUpdateWorkflowToolDefinition(): ToolDefinition {
|
||||
},
|
||||
nodes: {
|
||||
type: 'array',
|
||||
description: 'Updated array of node objects that define the workflow',
|
||||
description: 'Updated array of node objects (N8nNode structure) defining the workflow',
|
||||
items: {
|
||||
type: 'object',
|
||||
type: 'object', // Ideally, reference a detailed N8nNode schema here
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
type: 'object',
|
||||
description: 'Updated connection mappings between nodes',
|
||||
description: 'Updated connection mappings between nodes (N8nConnection structure)',
|
||||
},
|
||||
active: {
|
||||
type: 'boolean',
|
||||
@@ -118,8 +147,12 @@ export function getUpdateWorkflowToolDefinition(): ToolDefinition {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
type: 'object',
|
||||
description: 'Updated workflow settings (WorkflowSettings structure)',
|
||||
},
|
||||
required: ['workflowId'],
|
||||
},
|
||||
required: ['workflowId'], // Only ID is strictly required to identify the workflow
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,31 +25,107 @@ export interface ToolCallResult {
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
// Type for n8n workflow object
|
||||
// Base interface for tool handlers
|
||||
export interface BaseToolHandler {
|
||||
execute(args: Record<string, any>): Promise<ToolCallResult>;
|
||||
}
|
||||
|
||||
// --- n8n Specific Types ---
|
||||
|
||||
// Interface for n8n Node Parameters
|
||||
export interface NodeParameter {
|
||||
// Define common parameter properties if known, otherwise keep flexible
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Interface for n8n Node
|
||||
export interface N8nNode {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
typeVersion: number;
|
||||
position: [number, number];
|
||||
parameters: NodeParameter;
|
||||
credentials?: Record<string, any>;
|
||||
notes?: string;
|
||||
disabled?: boolean;
|
||||
[key: string]: any; // Allow other properties
|
||||
}
|
||||
|
||||
// Interface for n8n Connection Data
|
||||
export interface ConnectionData {
|
||||
// Define specific properties if known
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Interface for n8n Connection
|
||||
export interface N8nConnection {
|
||||
[outputNodeId: string]: {
|
||||
[outputType: string]: Array<{
|
||||
node: string; // Input node ID
|
||||
type: string; // Input type
|
||||
data?: ConnectionData;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
// Interface for n8n Workflow Settings
|
||||
export interface WorkflowSettings {
|
||||
saveExecutionProgress?: boolean;
|
||||
saveManualExecutions?: boolean;
|
||||
saveDataErrorExecution?: string; // e.g., "all", "none"
|
||||
saveDataSuccessExecution?: string; // e.g., "all", "none"
|
||||
executionTimeout?: number;
|
||||
timezone?: string;
|
||||
errorWorkflow?: string;
|
||||
[key: string]: any; // Allow other settings
|
||||
}
|
||||
|
||||
// Enhanced Type for n8n workflow object
|
||||
export interface Workflow {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
nodes: any[];
|
||||
connections: any;
|
||||
nodes: N8nNode[]; // Use specific Node type
|
||||
connections: N8nConnection; // Use specific Connection type
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
settings?: WorkflowSettings; // Use specific Settings type
|
||||
staticData?: Record<string, any> | null;
|
||||
tags?: string[]; // Assuming tags are strings
|
||||
pinData?: Record<string, any>;
|
||||
[key: string]: any; // Keep for flexibility if needed
|
||||
}
|
||||
|
||||
// Interface for n8n Execution Error Details
|
||||
export interface ExecutionError {
|
||||
message?: string;
|
||||
stack?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Type for n8n execution object
|
||||
// Interface for n8n Execution Run Data
|
||||
export interface ExecutionRunData {
|
||||
// Define specific properties if known structure exists
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Enhanced Type for n8n execution object
|
||||
export interface Execution {
|
||||
id: string;
|
||||
workflowId: string;
|
||||
finished: boolean;
|
||||
mode: string;
|
||||
mode: string; // e.g., 'manual', 'webhook', 'trigger'
|
||||
startedAt: string;
|
||||
stoppedAt: string;
|
||||
status: string;
|
||||
data: {
|
||||
resultData: {
|
||||
runData: any;
|
||||
stoppedAt: string | null; // Can be null if running
|
||||
status: 'waiting' | 'running' | 'success' | 'error' | 'unknown'; // More specific statuses
|
||||
data?: { // Make data optional
|
||||
resultData?: { // Make resultData optional
|
||||
runData?: ExecutionRunData; // Use specific RunData type
|
||||
error?: ExecutionError; // Add error details
|
||||
};
|
||||
};
|
||||
[key: string]: any;
|
||||
workflowData?: Partial<Workflow>; // Workflow data might be partial
|
||||
waitTill?: string | null;
|
||||
[key: string]: any; // Keep for flexibility
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* in a consistent, user-friendly manner.
|
||||
*/
|
||||
|
||||
import { Execution } from '../types/index.js';
|
||||
import { Execution, ExecutionRunData, ExecutionError } from '../types/index.js'; // Import specific types
|
||||
|
||||
/**
|
||||
* Format basic execution information for display
|
||||
@@ -13,9 +13,10 @@ import { Execution } from '../types/index.js';
|
||||
* @param execution Execution object
|
||||
* @returns Formatted execution summary
|
||||
*/
|
||||
export function formatExecutionSummary(execution: Execution): Record<string, any> {
|
||||
export function formatExecutionSummary(execution: Execution): Record<string, any> { // Keep return flexible for now
|
||||
// Calculate duration
|
||||
const startedAt = new Date(execution.startedAt);
|
||||
// Use current time if stoppedAt is null (execution still running)
|
||||
const stoppedAt = execution.stoppedAt ? new Date(execution.stoppedAt) : new Date();
|
||||
const durationMs = stoppedAt.getTime() - startedAt.getTime();
|
||||
const durationSeconds = Math.round(durationMs / 1000);
|
||||
@@ -40,49 +41,56 @@ export function formatExecutionSummary(execution: Execution): Record<string, any
|
||||
* @param execution Execution object
|
||||
* @returns Formatted execution details
|
||||
*/
|
||||
export function formatExecutionDetails(execution: Execution): Record<string, any> {
|
||||
export function formatExecutionDetails(execution: Execution): Record<string, any> { // Keep return flexible
|
||||
const summary = formatExecutionSummary(execution);
|
||||
|
||||
// Extract node results
|
||||
const nodeResults: Record<string, any> = {};
|
||||
if (execution.data?.resultData?.runData) {
|
||||
for (const [nodeName, nodeData] of Object.entries(execution.data.resultData.runData)) {
|
||||
const runData: ExecutionRunData | undefined = execution.data?.resultData?.runData;
|
||||
|
||||
if (runData) {
|
||||
for (const [nodeName, nodeDataArray] of Object.entries(runData)) {
|
||||
try {
|
||||
// Get the last output
|
||||
const lastOutput = Array.isArray(nodeData) && nodeData.length > 0
|
||||
? nodeData[nodeData.length - 1]
|
||||
// Get the last output object from the node's execution history array
|
||||
const lastOutput = Array.isArray(nodeDataArray) && nodeDataArray.length > 0
|
||||
? nodeDataArray[nodeDataArray.length - 1]
|
||||
: null;
|
||||
|
||||
// Check if the last output has the expected structure
|
||||
if (lastOutput && lastOutput.data && Array.isArray(lastOutput.data.main)) {
|
||||
// Extract the output data
|
||||
const outputData = lastOutput.data.main.length > 0
|
||||
? lastOutput.data.main[0]
|
||||
// Extract the output data items
|
||||
const outputItems = lastOutput.data.main.length > 0
|
||||
? lastOutput.data.main[0] // Assuming the first element contains the items array
|
||||
: [];
|
||||
|
||||
nodeResults[nodeName] = {
|
||||
status: lastOutput.status,
|
||||
items: outputData.length,
|
||||
data: outputData.slice(0, 3), // Limit to first 3 items to avoid overwhelming response
|
||||
items: Array.isArray(outputItems) ? outputItems.length : 0, // Ensure items is an array
|
||||
// Limit data preview to avoid overwhelming response
|
||||
dataPreview: Array.isArray(outputItems) ? outputItems.slice(0, 3) : [],
|
||||
};
|
||||
} else {
|
||||
nodeResults[nodeName] = { status: lastOutput?.status || 'unknown', items: 0, dataPreview: [] };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error parsing node output for ${nodeName}:`, error);
|
||||
nodeResults[nodeName] = { error: 'Failed to parse node output' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract error details if present
|
||||
const errorDetails: ExecutionError | undefined = execution.data?.resultData?.error;
|
||||
|
||||
// Add node results and error information to the summary
|
||||
return {
|
||||
...summary,
|
||||
mode: execution.mode,
|
||||
nodeResults: nodeResults,
|
||||
// Include error information if present
|
||||
error: execution.data?.resultData && 'error' in execution.data.resultData
|
||||
? {
|
||||
message: (execution.data.resultData as any).error?.message,
|
||||
stack: (execution.data.resultData as any).error?.stack,
|
||||
}
|
||||
: undefined,
|
||||
error: errorDetails ? { // Use the defined ExecutionError type
|
||||
message: errorDetails.message,
|
||||
stack: errorDetails.stack, // Include stack if available
|
||||
} : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,7 +100,7 @@ export function formatExecutionDetails(execution: Execution): Record<string, any
|
||||
* @param status Execution status string
|
||||
* @returns Status indicator emoji
|
||||
*/
|
||||
export function getStatusIndicator(status: string): string {
|
||||
export function getStatusIndicator(status: Execution['status']): string { // Use specific status type
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return '✅'; // Success
|
||||
@@ -100,10 +108,11 @@ export function getStatusIndicator(status: string): string {
|
||||
return '❌'; // Error
|
||||
case 'waiting':
|
||||
return '⏳'; // Waiting
|
||||
case 'canceled':
|
||||
return '🛑'; // Canceled
|
||||
// Add other potential statuses if known, e.g., 'canceled'
|
||||
// case 'canceled':
|
||||
// return '🛑';
|
||||
default:
|
||||
return '⏱️'; // In progress or unknown
|
||||
return '⏱️'; // Running or unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +123,7 @@ export function getStatusIndicator(status: string): string {
|
||||
* @param limit Maximum number of executions to include
|
||||
* @returns Summary of execution results
|
||||
*/
|
||||
export function summarizeExecutions(executions: Execution[], limit: number = 10): Record<string, any> {
|
||||
export function summarizeExecutions(executions: Execution[], limit: number = 10): Record<string, any> { // Keep return flexible
|
||||
const limitedExecutions = executions.slice(0, limit);
|
||||
|
||||
// Group executions by status
|
||||
@@ -132,7 +141,7 @@ export function summarizeExecutions(executions: Execution[], limit: number = 10)
|
||||
return {
|
||||
total: totalCount,
|
||||
byStatus: Object.entries(byStatus).map(([status, count]) => ({
|
||||
status: `${getStatusIndicator(status)} ${status}`,
|
||||
status: `${getStatusIndicator(status as Execution['status'])} ${status}`, // Cast status
|
||||
count,
|
||||
percentage: totalCount > 0 ? Math.round((count / totalCount) * 100) : 0
|
||||
})),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* in a consistent, user-friendly manner for MCP resources.
|
||||
*/
|
||||
|
||||
import { Workflow, Execution } from '../types/index.js';
|
||||
import { Workflow, Execution, N8nNode } from '../types/index.js'; // Import N8nNode
|
||||
import { formatExecutionSummary, summarizeExecutions } from './execution-formatter.js';
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ import { formatExecutionSummary, summarizeExecutions } from './execution-formatt
|
||||
* @param workflow Workflow object
|
||||
* @returns Formatted workflow summary
|
||||
*/
|
||||
export function formatWorkflowSummary(workflow: Workflow): Record<string, any> {
|
||||
export function formatWorkflowSummary(workflow: Workflow): Record<string, any> { // Keep return flexible
|
||||
return {
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
@@ -31,21 +31,23 @@ export function formatWorkflowSummary(workflow: Workflow): Record<string, any> {
|
||||
* @param workflow Workflow object
|
||||
* @returns Formatted workflow details
|
||||
*/
|
||||
export function formatWorkflowDetails(workflow: Workflow): Record<string, any> {
|
||||
export function formatWorkflowDetails(workflow: Workflow): Record<string, any> { // Keep return flexible
|
||||
const summary = formatWorkflowSummary(workflow);
|
||||
|
||||
// Add additional details
|
||||
return {
|
||||
...summary,
|
||||
nodes: workflow.nodes.map(node => ({
|
||||
nodes: workflow.nodes.map((node: N8nNode) => ({ // Use N8nNode type
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
parameters: node.parameters,
|
||||
parameters: node.parameters, // Keep parameters as is for now
|
||||
disabled: node.disabled,
|
||||
notes: node.notes,
|
||||
})),
|
||||
connections: workflow.connections,
|
||||
staticData: workflow.staticData,
|
||||
connections: workflow.connections, // Keep connections as is for now
|
||||
staticData: workflow.staticData, // Keep staticData as is
|
||||
settings: workflow.settings,
|
||||
tags: workflow.tags,
|
||||
// Exclude potentially sensitive or unuseful information
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Axios mock utilities for n8n MCP Server tests
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals'; // Import jest
|
||||
import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
export interface MockResponse {
|
||||
@@ -27,39 +28,36 @@ export const createMockAxiosResponse = (options: Partial<MockResponse> = {}): Ax
|
||||
*/
|
||||
export const createMockAxiosInstance = () => {
|
||||
const mockRequests: Record<string, any[]> = {};
|
||||
const mockResponses: Record<string, MockResponse[]> = {};
|
||||
const mockResponses: Record<string, (MockResponse | Error)[]> = {}; // Allow Error type
|
||||
|
||||
const mockInstance = {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
get: jest.fn<any>(), // Add type hint for mock function
|
||||
post: jest.fn<any>(), // Add type hint for mock function
|
||||
put: jest.fn<any>(), // Add type hint for mock function
|
||||
delete: jest.fn<any>(), // Add type hint for mock function
|
||||
interceptors: {
|
||||
request: {
|
||||
use: jest.fn(),
|
||||
use: jest.fn<any>(), // Add type hint for mock function
|
||||
},
|
||||
response: {
|
||||
use: jest.fn(),
|
||||
use: jest.fn<any>(), // Add type hint for mock function
|
||||
},
|
||||
},
|
||||
defaults: {},
|
||||
|
||||
// Helper method to add mock response
|
||||
addMockResponse(method: string, url: string, response: MockResponse | Error) {
|
||||
if (!mockResponses[`${method}:${url}`]) {
|
||||
mockResponses[`${method}:${url}`] = [];
|
||||
}
|
||||
|
||||
if (response instanceof Error) {
|
||||
mockResponses[`${method}:${url}`].push(response as any);
|
||||
} else {
|
||||
mockResponses[`${method}:${url}`].push(response);
|
||||
const key = `${method}:${url}`;
|
||||
if (!mockResponses[key]) {
|
||||
mockResponses[key] = [];
|
||||
}
|
||||
mockResponses[key].push(response);
|
||||
},
|
||||
|
||||
// Helper method to get request history
|
||||
getRequestHistory(method: string, url: string) {
|
||||
return mockRequests[`${method}:${url}`] || [];
|
||||
const key = `${method}:${url}`;
|
||||
return mockRequests[key] || [];
|
||||
},
|
||||
|
||||
// Reset all mocks
|
||||
@@ -76,12 +74,14 @@ export const createMockAxiosInstance = () => {
|
||||
mockInstance.post.mockReset();
|
||||
mockInstance.put.mockReset();
|
||||
mockInstance.delete.mockReset();
|
||||
mockInstance.interceptors.request.use.mockReset();
|
||||
mockInstance.interceptors.response.use.mockReset();
|
||||
}
|
||||
};
|
||||
|
||||
// Setup method implementations
|
||||
['get', 'post', 'put', 'delete'].forEach(method => {
|
||||
mockInstance[method].mockImplementation(async (url: string, data?: any) => {
|
||||
['get', 'post', 'put', 'delete'].forEach((method) => { // Remove explicit type annotation
|
||||
(mockInstance as any)[method].mockImplementation(async (url: string, data?: any) => { // Keep cast for dynamic access
|
||||
const requestKey = `${method}:${url}`;
|
||||
|
||||
if (!mockRequests[requestKey]) {
|
||||
@@ -91,14 +91,16 @@ export const createMockAxiosInstance = () => {
|
||||
mockRequests[requestKey].push(data);
|
||||
|
||||
if (mockResponses[requestKey] && mockResponses[requestKey].length > 0) {
|
||||
const response = mockResponses[requestKey].shift();
|
||||
const response = mockResponses[requestKey].shift(); // shift() can return undefined
|
||||
|
||||
if (response instanceof Error) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
if (response) { // Check if response is defined
|
||||
return createMockAxiosResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`No mock response defined for ${method.toUpperCase()} ${url}`);
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ export const createMockWorkflow = (overrides: Partial<Workflow> = {}): Workflow
|
||||
id: 'start',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1, // Added missing property
|
||||
parameters: {},
|
||||
position: [100, 300],
|
||||
},
|
||||
|
||||
@@ -3,10 +3,17 @@
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "node"],
|
||||
"esModuleInterop": true,
|
||||
"rootDir": ".."
|
||||
// Remove rootDir override as base config now handles it
|
||||
// "rootDir": "..",
|
||||
"outDir": "../build/tests", // Optional: specify separate output for tests if needed
|
||||
"composite": true // Keep composite for potential future use
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
"**/*.ts", // Keep existing test files
|
||||
"../src/**/*.ts" // Add back source files referenced by tests
|
||||
],
|
||||
"exclude": [ // Add exclude for build output if needed
|
||||
"../node_modules",
|
||||
"../build"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,33 +2,40 @@
|
||||
* N8nApiClient unit tests
|
||||
*/
|
||||
|
||||
import '@jest/globals';
|
||||
import axios from 'axios';
|
||||
import { N8nApiClient } from '../../../src/api/client.js';
|
||||
import { EnvConfig } from '../../../src/config/environment.js';
|
||||
import { N8nApiError } from '../../../src/errors/index.js';
|
||||
import { createMockAxiosInstance, createMockAxiosResponse } from '../../mocks/axios-mock.js';
|
||||
import { mockApiResponses } from '../../mocks/n8n-fixtures.js';
|
||||
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; // Explicit import
|
||||
import axios, { AxiosInstance } from 'axios'; // Import AxiosInstance type
|
||||
import { N8nApiClient } from '../../../src/api/client.js'; // Add .js
|
||||
import { EnvConfig } from '../../../src/config/environment.js'; // Add .js
|
||||
import { N8nApiError } from '../../../src/errors/index.js'; // Add .js
|
||||
import { createMockAxiosInstance, createMockAxiosResponse } from '../../mocks/axios-mock.js'; // Add .js
|
||||
import { mockApiResponses } from '../../mocks/n8n-fixtures.js'; // Add .js
|
||||
|
||||
// We will spy on axios.create instead of mocking the whole module
|
||||
// jest.mock('axios');
|
||||
// const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
// Mock axios
|
||||
jest.mock('axios', () => ({
|
||||
create: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('N8nApiClient', () => {
|
||||
// Mock configuration
|
||||
const mockConfig: EnvConfig = {
|
||||
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
||||
n8nApiKey: 'test-api-key',
|
||||
n8nWebhookUsername: 'test-user', // Added missing property
|
||||
n8nWebhookPassword: 'test-password', // Added missing property
|
||||
debug: false,
|
||||
};
|
||||
|
||||
// Define a type for the mock axios instance based on axios-mock.ts
|
||||
type MockAxiosInstance = ReturnType<typeof createMockAxiosInstance>;
|
||||
|
||||
// Mock axios instance
|
||||
let mockAxios;
|
||||
let mockAxios: MockAxiosInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create the mock instance
|
||||
mockAxios = createMockAxiosInstance();
|
||||
(axios.create as jest.Mock).mockReturnValue(mockAxios);
|
||||
// Spy on axios.create and mock its return value
|
||||
jest.spyOn(axios, 'create').mockReturnValue(mockAxios as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -42,7 +49,7 @@ describe('N8nApiClient', () => {
|
||||
new N8nApiClient(mockConfig);
|
||||
|
||||
// Assert
|
||||
expect(axios.create).toHaveBeenCalledWith({
|
||||
expect(axios.create).toHaveBeenCalledWith({ // Check the spy
|
||||
baseURL: mockConfig.n8nApiUrl,
|
||||
headers: {
|
||||
'X-N8N-API-KEY': mockConfig.n8nApiKey,
|
||||
@@ -80,6 +87,7 @@ describe('N8nApiClient', () => {
|
||||
const client = new N8nApiClient(mockConfig);
|
||||
mockAxios.addMockResponse('get', '/workflows', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: { data: [] },
|
||||
});
|
||||
|
||||
@@ -92,6 +100,7 @@ describe('N8nApiClient', () => {
|
||||
const client = new N8nApiClient(mockConfig);
|
||||
mockAxios.addMockResponse('get', '/workflows', {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error', // Added statusText
|
||||
data: { message: 'Server error' },
|
||||
});
|
||||
|
||||
@@ -116,6 +125,7 @@ describe('N8nApiClient', () => {
|
||||
const mockWorkflows = mockApiResponses.workflows.list;
|
||||
mockAxios.addMockResponse('get', '/workflows', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockWorkflows,
|
||||
});
|
||||
|
||||
@@ -132,6 +142,7 @@ describe('N8nApiClient', () => {
|
||||
const client = new N8nApiClient(mockConfig);
|
||||
mockAxios.addMockResponse('get', '/workflows', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: {},
|
||||
});
|
||||
|
||||
@@ -160,6 +171,7 @@ describe('N8nApiClient', () => {
|
||||
const mockWorkflow = mockApiResponses.workflows.single(workflowId);
|
||||
mockAxios.addMockResponse('get', `/workflows/${workflowId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockWorkflow,
|
||||
});
|
||||
|
||||
@@ -193,6 +205,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('post', `/workflows/${workflowId}/execute`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -214,6 +227,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('post', '/workflows', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -236,6 +250,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('put', `/workflows/${workflowId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -257,6 +272,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('delete', `/workflows/${workflowId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -278,6 +294,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('post', `/workflows/${workflowId}/activate`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -299,6 +316,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('post', `/workflows/${workflowId}/deactivate`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -318,6 +336,7 @@ describe('N8nApiClient', () => {
|
||||
const mockExecutions = mockApiResponses.executions.list;
|
||||
mockAxios.addMockResponse('get', '/executions', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockExecutions,
|
||||
});
|
||||
|
||||
@@ -338,6 +357,7 @@ describe('N8nApiClient', () => {
|
||||
const mockExecution = mockApiResponses.executions.single(executionId);
|
||||
mockAxios.addMockResponse('get', `/executions/${executionId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockExecution,
|
||||
});
|
||||
|
||||
@@ -359,6 +379,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('delete', `/executions/${executionId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Simple HTTP client tests without complex dependencies
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
// Create a simple HTTP client class to test
|
||||
class SimpleHttpClient {
|
||||
constructor(private baseUrl: string, private apiKey: string) {}
|
||||
|
||||
getBaseUrl(): string {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
getApiKey(): string {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
buildAuthHeader(): Record<string, string> {
|
||||
return {
|
||||
'X-N8N-API-KEY': this.apiKey
|
||||
};
|
||||
}
|
||||
|
||||
formatUrl(path: string): string {
|
||||
return `${this.baseUrl}${path.startsWith('/') ? path : '/' + path}`;
|
||||
}
|
||||
}
|
||||
|
||||
describe('SimpleHttpClient', () => {
|
||||
it('should store baseUrl and apiKey properly', () => {
|
||||
const baseUrl = 'https://n8n.example.com/api/v1';
|
||||
const apiKey = 'test-api-key';
|
||||
const client = new SimpleHttpClient(baseUrl, apiKey);
|
||||
|
||||
expect(client.getBaseUrl()).toBe(baseUrl);
|
||||
expect(client.getApiKey()).toBe(apiKey);
|
||||
});
|
||||
|
||||
it('should create proper auth headers', () => {
|
||||
const client = new SimpleHttpClient('https://n8n.example.com/api/v1', 'test-api-key');
|
||||
const headers = client.buildAuthHeader();
|
||||
|
||||
expect(headers).toEqual({ 'X-N8N-API-KEY': 'test-api-key' });
|
||||
});
|
||||
|
||||
it('should format URLs correctly', () => {
|
||||
const baseUrl = 'https://n8n.example.com/api/v1';
|
||||
const client = new SimpleHttpClient(baseUrl, 'test-api-key');
|
||||
|
||||
expect(client.formatUrl('workflows')).toBe(`${baseUrl}/workflows`);
|
||||
expect(client.formatUrl('/workflows')).toBe(`${baseUrl}/workflows`);
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,10 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
import { getEnvConfig, loadEnvironmentVariables, ENV_VARS } from '../../../src/config/environment.js';
|
||||
import { getEnvConfig, loadEnvironmentVariables, ENV_VARS } from '../../../src/config/environment.js'; // Add .js
|
||||
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { ErrorCode } from '../../../src/errors/error-codes.js';
|
||||
import { mockEnv } from '../../test-setup.js';
|
||||
import { ErrorCode } from '../../../src/errors/error-codes.js'; // Add .js
|
||||
import { mockEnv } from '../../test-setup.js'; // Add .js
|
||||
|
||||
describe('Environment Configuration', () => {
|
||||
const originalEnv = process.env;
|
||||
@@ -36,6 +36,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
|
||||
// Execute
|
||||
const config = getEnvConfig();
|
||||
@@ -44,6 +46,8 @@ describe('Environment Configuration', () => {
|
||||
expect(config).toEqual({
|
||||
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
||||
n8nApiKey: 'test-api-key',
|
||||
n8nWebhookUsername: 'test-user',
|
||||
n8nWebhookPassword: 'test-pass',
|
||||
debug: false,
|
||||
});
|
||||
});
|
||||
@@ -52,6 +56,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
process.env[ENV_VARS.DEBUG] = 'true';
|
||||
|
||||
// Execute
|
||||
@@ -65,6 +71,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
process.env[ENV_VARS.DEBUG] = 'TRUE';
|
||||
|
||||
// Execute
|
||||
@@ -78,6 +86,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
process.env[ENV_VARS.DEBUG] = 'yes';
|
||||
|
||||
// Execute
|
||||
@@ -117,6 +127,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'invalid-url';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
|
||||
// Execute & Assert
|
||||
expect(() => getEnvConfig()).toThrow(
|
||||
@@ -131,6 +143,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'http://localhost:5678/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
|
||||
// Execute
|
||||
const config = getEnvConfig();
|
||||
@@ -143,6 +157,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1/';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
|
||||
// Execute
|
||||
const config = getEnvConfig();
|
||||
@@ -155,14 +171,18 @@ describe('Environment Configuration', () => {
|
||||
describe('with mockEnv helper', () => {
|
||||
// Using the mockEnv helper from test-setup
|
||||
mockEnv({
|
||||
[ENV_VARS.N8N_API_URL]: 'https://n8n.example.com/api/v1',
|
||||
[ENV_VARS.N8N_API_KEY]: 'test-api-key',
|
||||
[ENV_VARS.N8N_API_URL]: 'https://mock.n8n.com/api/v1',
|
||||
[ENV_VARS.N8N_API_KEY]: 'mock-api-key',
|
||||
[ENV_VARS.N8N_WEBHOOK_USERNAME]: 'mock-user', // Add webhook user
|
||||
[ENV_VARS.N8N_WEBHOOK_PASSWORD]: 'mock-pass', // Add webhook pass
|
||||
});
|
||||
|
||||
it('should use the mocked environment variables', () => {
|
||||
const config = getEnvConfig();
|
||||
expect(config.n8nApiUrl).toBe('https://n8n.example.com/api/v1');
|
||||
expect(config.n8nApiKey).toBe('test-api-key');
|
||||
expect(config.n8nApiUrl).toBe('https://mock.n8n.com/api/v1');
|
||||
expect(config.n8nApiKey).toBe('mock-api-key');
|
||||
expect(config.n8nWebhookUsername).toBe('mock-user');
|
||||
expect(config.n8nWebhookPassword).toBe('mock-pass');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Simple environment configuration tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
// Simple environment validation function to test
|
||||
function validateEnvironment(env: Record<string, string | undefined>): {
|
||||
n8nApiUrl: string;
|
||||
n8nApiKey: string;
|
||||
debug: boolean;
|
||||
} {
|
||||
// Check required variables
|
||||
if (!env.N8N_API_URL) {
|
||||
throw new Error('Missing required environment variable: N8N_API_URL');
|
||||
}
|
||||
|
||||
if (!env.N8N_API_KEY) {
|
||||
throw new Error('Missing required environment variable: N8N_API_KEY');
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(env.N8N_API_URL);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid URL format for N8N_API_URL: ${env.N8N_API_URL}`);
|
||||
}
|
||||
|
||||
// Return parsed config
|
||||
return {
|
||||
n8nApiUrl: env.N8N_API_URL,
|
||||
n8nApiKey: env.N8N_API_KEY,
|
||||
debug: env.DEBUG?.toLowerCase() === 'true'
|
||||
};
|
||||
}
|
||||
|
||||
describe('Environment Configuration', () => {
|
||||
describe('validateEnvironment', () => {
|
||||
it('should return a valid config when all required variables are present', () => {
|
||||
const env = {
|
||||
N8N_API_URL: 'https://n8n.example.com/api/v1',
|
||||
N8N_API_KEY: 'test-api-key'
|
||||
};
|
||||
|
||||
const config = validateEnvironment(env);
|
||||
|
||||
expect(config).toEqual({
|
||||
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
||||
n8nApiKey: 'test-api-key',
|
||||
debug: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should set debug to true when DEBUG=true', () => {
|
||||
const env = {
|
||||
N8N_API_URL: 'https://n8n.example.com/api/v1',
|
||||
N8N_API_KEY: 'test-api-key',
|
||||
DEBUG: 'true'
|
||||
};
|
||||
|
||||
const config = validateEnvironment(env);
|
||||
|
||||
expect(config.debug).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw an error when N8N_API_URL is missing', () => {
|
||||
const env = {
|
||||
N8N_API_KEY: 'test-api-key'
|
||||
};
|
||||
|
||||
expect(() => validateEnvironment(env)).toThrow(
|
||||
'Missing required environment variable: N8N_API_URL'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when N8N_API_KEY is missing', () => {
|
||||
const env = {
|
||||
N8N_API_URL: 'https://n8n.example.com/api/v1'
|
||||
};
|
||||
|
||||
expect(() => validateEnvironment(env)).toThrow(
|
||||
'Missing required environment variable: N8N_API_KEY'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when N8N_API_URL is not a valid URL', () => {
|
||||
const env = {
|
||||
N8N_API_URL: 'invalid-url',
|
||||
N8N_API_KEY: 'test-api-key'
|
||||
};
|
||||
|
||||
expect(() => validateEnvironment(env)).toThrow(
|
||||
'Invalid URL format for N8N_API_URL: invalid-url'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,38 +1,38 @@
|
||||
/**
|
||||
* Simple test for URI Template functionality
|
||||
* Tests for dynamic workflow resource URI functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
// Simple functions to test without complex imports
|
||||
function getWorkflowResourceTemplateUri() {
|
||||
return 'n8n://workflows/{id}';
|
||||
}
|
||||
|
||||
function extractWorkflowIdFromUri(uri: string): string | null {
|
||||
const regex = /^n8n:\/\/workflows\/([^/]+)$/;
|
||||
const match = uri.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
// Import the actual functions from the source file with .js extension
|
||||
import {
|
||||
getWorkflowResourceTemplateUri,
|
||||
extractWorkflowIdFromUri
|
||||
} from '../../../../src/resources/dynamic/workflow.js';
|
||||
|
||||
describe('Workflow Resource URI Functions', () => {
|
||||
describe('getWorkflowResourceTemplateUri', () => {
|
||||
it('should return the correct URI template', () => {
|
||||
// Test the actual imported function
|
||||
expect(getWorkflowResourceTemplateUri()).toBe('n8n://workflows/{id}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractWorkflowIdFromUri', () => {
|
||||
it('should extract workflow ID from valid URI', () => {
|
||||
// Test the actual imported function
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows/123abc')).toBe('123abc');
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows/workflow-name-with-dashes')).toBe('workflow-name-with-dashes');
|
||||
});
|
||||
|
||||
it('should return null for invalid URI formats', () => {
|
||||
// Test the actual imported function
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows/')).toBeNull();
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows')).toBeNull();
|
||||
expect(extractWorkflowIdFromUri('n8n://workflow/123')).toBeNull();
|
||||
expect(extractWorkflowIdFromUri('n8n://workflow/123')).toBeNull(); // Should fail based on regex
|
||||
expect(extractWorkflowIdFromUri('invalid://workflows/123')).toBeNull();
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows/123/extra')).toBeNull(); // Should fail based on regex
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Add tests for getWorkflowResource function (requires mocking apiService)
|
||||
});
|
||||
|
||||
154
tests/unit/tools/workflow/list.test.ts
Normal file
154
tests/unit/tools/workflow/list.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* ListWorkflowsHandler unit tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
||||
import { ListWorkflowsHandler, getListWorkflowsToolDefinition } from '../../../../src/tools/workflow/list.js'; // Add .js back
|
||||
import { createMockWorkflows } from '../../../mocks/n8n-fixtures.js'; // Add .js back
|
||||
// Import BaseWorkflowToolHandler to check constructor call, but don't mock it directly
|
||||
import { BaseWorkflowToolHandler } from '../../../../src/tools/workflow/base-handler.js'; // Add .js back
|
||||
|
||||
// Generate mock data
|
||||
const mockWorkflows = createMockWorkflows();
|
||||
|
||||
// Mock dependencies of the BaseWorkflowToolHandler
|
||||
const mockGetWorkflows = jest.fn(); // Define typed mock function variable
|
||||
const mockApiService = {
|
||||
getWorkflows: mockGetWorkflows,
|
||||
// Add other methods used by BaseWorkflowToolHandler if necessary
|
||||
};
|
||||
const mockEnvConfig = {
|
||||
/* mock necessary env config properties */
|
||||
n8nApiUrl: 'http://mock-n8n.com',
|
||||
n8nApiKey: 'mock-key',
|
||||
n8nWebhookUsername: 'test-user', // Added webhook user
|
||||
n8nWebhookPassword: 'test-pass', // Added webhook pass
|
||||
};
|
||||
|
||||
// Add .js extension back to mock paths
|
||||
jest.mock('../../../../src/config/environment.js', () => ({
|
||||
getEnvConfig: jest.fn(() => mockEnvConfig),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../src/api/n8n-client.js', () => ({
|
||||
createApiService: jest.fn(() => mockApiService),
|
||||
}));
|
||||
|
||||
|
||||
import { Workflow, ToolCallResult } from '../../../../src/types/index.js'; // Add .js back
|
||||
|
||||
|
||||
describe('ListWorkflows Tool', () => {
|
||||
let handler: ListWorkflowsHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
jest.clearAllMocks();
|
||||
// Instantiate the handler, passing the mocked apiService (cast to any)
|
||||
handler = new ListWorkflowsHandler(mockApiService as any);
|
||||
// Check that dependencies were called by the base constructor
|
||||
// Use the mocked functions directly now, casting the require result
|
||||
const configMock = jest.requireMock('../../../../src/config/environment.js') as any; // Add .js back
|
||||
const apiMock = jest.requireMock('../../../../src/api/n8n-client.js') as any; // Add .js back
|
||||
expect(configMock.getEnvConfig).toHaveBeenCalledTimes(1);
|
||||
expect(apiMock.createApiService).toHaveBeenCalledWith(mockEnvConfig);
|
||||
});
|
||||
|
||||
describe('getListWorkflowsToolDefinition', () => {
|
||||
it('should return the correct tool definition', () => {
|
||||
// Execute
|
||||
const definition = getListWorkflowsToolDefinition();
|
||||
|
||||
// Assert
|
||||
expect(definition.name).toBe('list_workflows');
|
||||
expect(definition.description).toBeTruthy();
|
||||
expect(definition.inputSchema).toBeDefined();
|
||||
expect(definition.inputSchema.properties).toHaveProperty('active');
|
||||
expect(definition.inputSchema.required).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should fetch all workflows and format them when no filter is provided', async () => {
|
||||
// Arrange
|
||||
// @ts-ignore - Suppress persistent TS2345 error
|
||||
mockGetWorkflows.mockResolvedValue(mockWorkflows);
|
||||
const expectedFormatted = mockWorkflows.map((wf: Workflow) => ({ // Add type annotation
|
||||
id: wf.id,
|
||||
name: wf.name,
|
||||
active: wf.active,
|
||||
updatedAt: wf.updatedAt,
|
||||
}));
|
||||
|
||||
// Act
|
||||
const result = await handler.execute({});
|
||||
|
||||
// Assert
|
||||
expect(mockApiService.getWorkflows).toHaveBeenCalledTimes(1);
|
||||
// Check the actual result, which uses the real base class formatters
|
||||
expect(result.isError).toBe(false);
|
||||
expect(result.content[0].text).toContain(`Found ${expectedFormatted.length} workflow(s)`);
|
||||
expect(result.content[0].text).toContain(JSON.stringify(expectedFormatted, null, 2));
|
||||
});
|
||||
|
||||
it('should filter workflows by active=true when provided', async () => {
|
||||
// Arrange
|
||||
// @ts-ignore - Suppress persistent TS2345 error
|
||||
mockGetWorkflows.mockResolvedValue(mockWorkflows);
|
||||
const activeWorkflows = mockWorkflows.filter(wf => wf.active === true);
|
||||
const expectedFormatted = activeWorkflows.map((wf: Workflow) => ({ // Add type annotation
|
||||
id: wf.id,
|
||||
name: wf.name,
|
||||
active: wf.active,
|
||||
updatedAt: wf.updatedAt,
|
||||
}));
|
||||
|
||||
// Act
|
||||
const result = await handler.execute({ active: true });
|
||||
|
||||
// Assert
|
||||
expect(mockApiService.getWorkflows).toHaveBeenCalledTimes(1);
|
||||
expect(result.isError).toBe(false);
|
||||
expect(result.content[0].text).toContain(`Found ${expectedFormatted.length} workflow(s) (filtered by active=true)`);
|
||||
expect(result.content[0].text).toContain(JSON.stringify(expectedFormatted, null, 2));
|
||||
});
|
||||
|
||||
it('should filter workflows by active=false when provided', async () => {
|
||||
// Arrange
|
||||
// @ts-ignore - Suppress persistent TS2345 error
|
||||
mockGetWorkflows.mockResolvedValue(mockWorkflows);
|
||||
const inactiveWorkflows = mockWorkflows.filter(wf => wf.active === false);
|
||||
const expectedFormatted = inactiveWorkflows.map((wf: Workflow) => ({ // Add type annotation
|
||||
id: wf.id,
|
||||
name: wf.name,
|
||||
active: wf.active,
|
||||
updatedAt: wf.updatedAt,
|
||||
}));
|
||||
|
||||
// Act
|
||||
const result = await handler.execute({ active: false });
|
||||
|
||||
// Assert
|
||||
expect(mockApiService.getWorkflows).toHaveBeenCalledTimes(1);
|
||||
expect(result.isError).toBe(false);
|
||||
expect(result.content[0].text).toContain(`Found ${expectedFormatted.length} workflow(s) (filtered by active=false)`);
|
||||
expect(result.content[0].text).toContain(JSON.stringify(expectedFormatted, null, 2));
|
||||
});
|
||||
|
||||
it('should handle errors during API call', async () => {
|
||||
// Arrange
|
||||
const apiError = new Error('API Failure');
|
||||
// @ts-ignore - Suppress persistent TS2345 error
|
||||
mockGetWorkflows.mockRejectedValue(apiError);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute({});
|
||||
|
||||
// Assert
|
||||
expect(mockApiService.getWorkflows).toHaveBeenCalledTimes(1);
|
||||
// Check the actual error result formatted by the real base class method
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Error executing workflow tool: API Failure');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* ListWorkflowsHandler unit tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { getListWorkflowsToolDefinition } from '../../../../src/tools/workflow/list.js';
|
||||
import { mockApiResponses } from '../../../mocks/n8n-fixtures.js';
|
||||
|
||||
// Since this is an integration test, we'll test the definition directly
|
||||
// rather than mocking the complex handler implementation
|
||||
jest.mock('../../../../src/tools/workflow/base-handler.js');
|
||||
|
||||
describe('getListWorkflowsToolDefinition', () => {
|
||||
it('should return the correct tool definition', () => {
|
||||
// Execute
|
||||
const definition = getListWorkflowsToolDefinition();
|
||||
|
||||
// Assert
|
||||
expect(definition.name).toBe('list_workflows');
|
||||
expect(definition.description).toBeTruthy();
|
||||
expect(definition.inputSchema).toBeDefined();
|
||||
expect(definition.inputSchema.properties).toHaveProperty('active');
|
||||
expect(definition.inputSchema.required).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* Simple workflow tool tests without complex dependencies
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
// Mock workflow data
|
||||
const mockWorkflows = [
|
||||
{
|
||||
id: '1234abc',
|
||||
name: 'Test Workflow 1',
|
||||
active: true,
|
||||
createdAt: '2025-03-01T12:00:00.000Z',
|
||||
updatedAt: '2025-03-02T14:30:00.000Z',
|
||||
nodes: []
|
||||
},
|
||||
{
|
||||
id: '5678def',
|
||||
name: 'Test Workflow 2',
|
||||
active: false,
|
||||
createdAt: '2025-03-01T12:00:00.000Z',
|
||||
updatedAt: '2025-03-12T10:15:00.000Z',
|
||||
nodes: []
|
||||
}
|
||||
];
|
||||
|
||||
// Simple function to test tool definition
|
||||
function getListWorkflowsToolDefinition() {
|
||||
return {
|
||||
name: 'list_workflows',
|
||||
description: 'List all workflows with optional filtering by status',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
active: {
|
||||
type: 'boolean',
|
||||
description: 'Filter workflows by active status'
|
||||
}
|
||||
},
|
||||
required: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Simple function to test workflow filtering
|
||||
function filterWorkflows(workflows, filter) {
|
||||
if (filter && typeof filter.active === 'boolean') {
|
||||
return workflows.filter(workflow => workflow.active === filter.active);
|
||||
}
|
||||
return workflows;
|
||||
}
|
||||
|
||||
describe('Workflow Tools', () => {
|
||||
describe('getListWorkflowsToolDefinition', () => {
|
||||
it('should return the correct tool definition', () => {
|
||||
const definition = getListWorkflowsToolDefinition();
|
||||
|
||||
expect(definition.name).toBe('list_workflows');
|
||||
expect(definition.description).toBeTruthy();
|
||||
expect(definition.inputSchema).toBeDefined();
|
||||
expect(definition.inputSchema.properties).toHaveProperty('active');
|
||||
expect(definition.inputSchema.required).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterWorkflows', () => {
|
||||
it('should return all workflows when no filter is provided', () => {
|
||||
const result = filterWorkflows(mockWorkflows, {});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual(mockWorkflows);
|
||||
});
|
||||
|
||||
it('should filter workflows by active status when active is true', () => {
|
||||
const result = filterWorkflows(mockWorkflows, { active: true });
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('1234abc');
|
||||
expect(result[0].active).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter workflows by active status when active is false', () => {
|
||||
const result = filterWorkflows(mockWorkflows, { active: false });
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('5678def');
|
||||
expect(result[0].active).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,10 +11,9 @@
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"rootDir": "src",
|
||||
"rootDir": ".", // Set rootDir to encompass both src and tests
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM"
|
||||
"ES2020" // Removed "DOM" as it's likely unused in a Node.js server
|
||||
],
|
||||
"types": [
|
||||
"node"
|
||||
@@ -28,4 +27,5 @@
|
||||
"build",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
// Remove project reference
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user