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 |
|
| 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_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` |
|
| `N8N_WEBHOOK_PASSWORD` | Password for webhook authentication | `password` |
|
||||||
| `DEBUG` | Enable debug logging (optional) | `true` or `false` |
|
| `DEBUG` | Enable debug logging (optional) | `true` or `false` |
|
||||||
|
|
||||||
@@ -84,19 +84,47 @@ n8n-mcp-server
|
|||||||
|
|
||||||
### Integrating with AI Assistants
|
### 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
|
```json
|
||||||
npx @anaisbetts/mcp-installer
|
{
|
||||||
|
"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:**
|
||||||
|
|
||||||
```
|
* Replace `/path/to/your/cloned/n8n-mcp-server/` with the actual absolute path where you cloned and built the repository.
|
||||||
install_local_mcp_server path/to/n8n-mcp-server
|
* 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
|
## Available Tools
|
||||||
|
|
||||||
@@ -124,30 +152,29 @@ The webhook authentication is handled automatically using the `N8N_WEBHOOK_USERN
|
|||||||
|
|
||||||
### Workflow Management
|
### Workflow Management
|
||||||
|
|
||||||
- `workflow_list`: List all workflows
|
- `list_workflows`: List all workflows
|
||||||
- `workflow_get`: Get details of a specific workflow
|
- `get_workflow`: Get details of a specific workflow
|
||||||
- `workflow_create`: Create a new workflow
|
- `create_workflow`: Create a new workflow
|
||||||
- `workflow_update`: Update an existing workflow
|
- `update_workflow`: Update an existing workflow
|
||||||
- `workflow_delete`: Delete a workflow
|
- `delete_workflow`: Delete a workflow
|
||||||
- `workflow_activate`: Activate a workflow
|
- `activate_workflow`: Activate a workflow
|
||||||
- `workflow_deactivate`: Deactivate a workflow
|
- `deactivate_workflow`: Deactivate a workflow
|
||||||
|
|
||||||
### Execution Management
|
### Execution Management
|
||||||
|
|
||||||
- `execution_run`: Execute a workflow via the API
|
|
||||||
- `run_webhook`: Execute a workflow via a webhook
|
- `run_webhook`: Execute a workflow via a webhook
|
||||||
- `execution_get`: Get details of a specific execution
|
- `get_execution`: Get details of a specific execution
|
||||||
- `execution_list`: List executions for a workflow
|
- `list_executions`: List executions for a workflow
|
||||||
- `execution_stop`: Stop a running execution
|
- `delete_execution`: Delete a specific execution
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
The server provides the following resources:
|
The server provides the following resources:
|
||||||
|
|
||||||
- `n8n://workflows/list`: List of all workflows
|
- `n8n://workflows`: List of all workflows (static resource)
|
||||||
- `n8n://workflow/{id}`: Details of a specific workflow
|
- `n8n://workflows/{id}`: Details of a specific workflow (dynamic resource template)
|
||||||
- `n8n://executions/{workflowId}`: List of executions for a workflow
|
- `n8n://executions/{id}`: Details of a specific execution (dynamic resource template)
|
||||||
- `n8n://execution/{id}`: Details of a specific execution
|
- `n8n://execution-stats`: Summary statistics of recent executions (static resource)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
module.exports = {
|
module.exports = (api) => {
|
||||||
presets: [
|
// 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-env', { targets: { node: 'current' } }],
|
||||||
'@babel/preset-typescript',
|
'@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 = {
|
module.exports = {
|
||||||
// Use commonjs style export
|
preset: 'ts-jest/presets/default-esm', // Use ESM preset for ts-jest
|
||||||
preset: 'ts-jest',
|
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
transform: {
|
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: {
|
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: [
|
transformIgnorePatterns: [
|
||||||
"node_modules/(?!(@modelcontextprotocol)/)"
|
"/node_modules/(?!(@modelcontextprotocol/sdk|axios|another-esm-dep)/)" // Adjust as needed
|
||||||
],
|
],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageDirectory: 'coverage',
|
coverageDirectory: 'coverage',
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
"start": "node build/index.js",
|
"start": "node build/index.js",
|
||||||
"dev": "tsc -w",
|
"dev": "tsc -w",
|
||||||
"lint": "eslint --ext .ts src/",
|
"lint": "eslint --ext .ts src/",
|
||||||
"test": "node --experimental-vm-modules run-tests.js",
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs",
|
||||||
"test:watch": "node --experimental-vm-modules run-tests.js --watch",
|
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs --watch",
|
||||||
"test:coverage": "node --experimental-vm-modules run-tests.js --coverage",
|
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs --coverage",
|
||||||
"prepare": "npm run build"
|
"prepare": "npm run build"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
* This module provides a client for interacting with the n8n API.
|
* This module provides a client for interacting with the n8n API.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance, AxiosResponse } from 'axios'; // Import AxiosResponse
|
||||||
import { EnvConfig } from '../config/environment.js';
|
import { EnvConfig } from '../config/environment.js'; // Already has .js
|
||||||
import { handleAxiosError, N8nApiError } from '../errors/index.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
|
* n8n API Client class for making requests to the n8n API
|
||||||
@@ -86,9 +87,9 @@ export class N8nApiClient {
|
|||||||
*
|
*
|
||||||
* @returns Array of workflow objects
|
* @returns Array of workflow objects
|
||||||
*/
|
*/
|
||||||
async getWorkflows(): Promise<any[]> {
|
async getWorkflows(): Promise<Workflow[]> { // Use specific type
|
||||||
try {
|
try {
|
||||||
const response = await this.axiosInstance.get('/workflows');
|
const response: AxiosResponse<{ data: Workflow[] }> = await this.axiosInstance.get('/workflows');
|
||||||
return response.data.data || [];
|
return response.data.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, 'Failed to fetch workflows');
|
throw handleAxiosError(error, 'Failed to fetch workflows');
|
||||||
@@ -101,9 +102,9 @@ export class N8nApiClient {
|
|||||||
* @param id Workflow ID
|
* @param id Workflow ID
|
||||||
* @returns Workflow object
|
* @returns Workflow object
|
||||||
*/
|
*/
|
||||||
async getWorkflow(id: string): Promise<any> {
|
async getWorkflow(id: string): Promise<Workflow> { // Use specific type
|
||||||
try {
|
try {
|
||||||
const response = await this.axiosInstance.get(`/workflows/${id}`);
|
const response: AxiosResponse<Workflow> = await this.axiosInstance.get(`/workflows/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, `Failed to fetch workflow ${id}`);
|
throw handleAxiosError(error, `Failed to fetch workflow ${id}`);
|
||||||
@@ -115,9 +116,9 @@ export class N8nApiClient {
|
|||||||
*
|
*
|
||||||
* @returns Array of execution objects
|
* @returns Array of execution objects
|
||||||
*/
|
*/
|
||||||
async getExecutions(): Promise<any[]> {
|
async getExecutions(): Promise<Execution[]> { // Use specific type
|
||||||
try {
|
try {
|
||||||
const response = await this.axiosInstance.get('/executions');
|
const response: AxiosResponse<{ data: Execution[] }> = await this.axiosInstance.get('/executions');
|
||||||
return response.data.data || [];
|
return response.data.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, 'Failed to fetch executions');
|
throw handleAxiosError(error, 'Failed to fetch executions');
|
||||||
@@ -130,9 +131,9 @@ export class N8nApiClient {
|
|||||||
* @param id Execution ID
|
* @param id Execution ID
|
||||||
* @returns Execution object
|
* @returns Execution object
|
||||||
*/
|
*/
|
||||||
async getExecution(id: string): Promise<any> {
|
async getExecution(id: string): Promise<Execution> { // Use specific type
|
||||||
try {
|
try {
|
||||||
const response = await this.axiosInstance.get(`/executions/${id}`);
|
const response: AxiosResponse<Execution> = await this.axiosInstance.get(`/executions/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, `Failed to fetch execution ${id}`);
|
throw handleAxiosError(error, `Failed to fetch execution ${id}`);
|
||||||
@@ -144,11 +145,12 @@ export class N8nApiClient {
|
|||||||
*
|
*
|
||||||
* @param id Workflow ID
|
* @param id Workflow ID
|
||||||
* @param data Optional data to pass to the workflow
|
* @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 {
|
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;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, `Failed to execute workflow ${id}`);
|
throw handleAxiosError(error, `Failed to execute workflow ${id}`);
|
||||||
@@ -161,7 +163,7 @@ export class N8nApiClient {
|
|||||||
* @param workflow Workflow object to create
|
* @param workflow Workflow object to create
|
||||||
* @returns Created workflow
|
* @returns Created workflow
|
||||||
*/
|
*/
|
||||||
async createWorkflow(workflow: Record<string, any>): Promise<any> {
|
async createWorkflow(workflow: Partial<Workflow>): Promise<Workflow> { // Use specific types
|
||||||
try {
|
try {
|
||||||
// Make sure settings property is present
|
// Make sure settings property is present
|
||||||
if (!workflow.settings) {
|
if (!workflow.settings) {
|
||||||
@@ -181,15 +183,16 @@ export class N8nApiClient {
|
|||||||
delete workflowToCreate.id; // Remove id property if it exists
|
delete workflowToCreate.id; // Remove id property if it exists
|
||||||
delete workflowToCreate.createdAt; // Remove createdAt property if it exists
|
delete workflowToCreate.createdAt; // Remove createdAt property if it exists
|
||||||
delete workflowToCreate.updatedAt; // Remove updatedAt 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
|
// Removed debug log
|
||||||
console.error('[DEBUG] Creating workflow with data:', JSON.stringify(workflowToCreate, null, 2));
|
// 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;
|
return response.data;
|
||||||
} catch (error) {
|
} 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');
|
throw handleAxiosError(error, 'Failed to create workflow');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,9 +204,9 @@ export class N8nApiClient {
|
|||||||
* @param workflow Updated workflow object
|
* @param workflow Updated workflow object
|
||||||
* @returns Updated workflow
|
* @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 {
|
try {
|
||||||
const response = await this.axiosInstance.put(`/workflows/${id}`, workflow);
|
const response: AxiosResponse<Workflow> = await this.axiosInstance.put(`/workflows/${id}`, workflow);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, `Failed to update workflow ${id}`);
|
throw handleAxiosError(error, `Failed to update workflow ${id}`);
|
||||||
@@ -214,11 +217,12 @@ export class N8nApiClient {
|
|||||||
* Delete a workflow
|
* Delete a workflow
|
||||||
*
|
*
|
||||||
* @param id Workflow ID
|
* @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 {
|
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;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, `Failed to delete workflow ${id}`);
|
throw handleAxiosError(error, `Failed to delete workflow ${id}`);
|
||||||
@@ -231,9 +235,10 @@ export class N8nApiClient {
|
|||||||
* @param id Workflow ID
|
* @param id Workflow ID
|
||||||
* @returns Activated workflow
|
* @returns Activated workflow
|
||||||
*/
|
*/
|
||||||
async activateWorkflow(id: string): Promise<any> {
|
async activateWorkflow(id: string): Promise<Workflow> { // Use specific type
|
||||||
try {
|
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;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, `Failed to activate workflow ${id}`);
|
throw handleAxiosError(error, `Failed to activate workflow ${id}`);
|
||||||
@@ -246,9 +251,10 @@ export class N8nApiClient {
|
|||||||
* @param id Workflow ID
|
* @param id Workflow ID
|
||||||
* @returns Deactivated workflow
|
* @returns Deactivated workflow
|
||||||
*/
|
*/
|
||||||
async deactivateWorkflow(id: string): Promise<any> {
|
async deactivateWorkflow(id: string): Promise<Workflow> { // Use specific type
|
||||||
try {
|
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;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, `Failed to deactivate workflow ${id}`);
|
throw handleAxiosError(error, `Failed to deactivate workflow ${id}`);
|
||||||
@@ -259,11 +265,12 @@ export class N8nApiClient {
|
|||||||
* Delete an execution
|
* Delete an execution
|
||||||
*
|
*
|
||||||
* @param id Execution ID
|
* @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 {
|
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;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleAxiosError(error, `Failed to delete execution ${id}`);
|
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 { N8nApiClient } from './client.js';
|
||||||
import { EnvConfig } from '../config/environment.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
|
* Create a new n8n API client instance
|
||||||
*/
|
|
||||||
export class N8nApiService {
|
|
||||||
private client: N8nApiClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new n8n API service
|
|
||||||
*
|
|
||||||
* @param config Environment configuration
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new n8n API service
|
|
||||||
*
|
*
|
||||||
* @param config Environment configuration
|
* @param config Environment configuration
|
||||||
* @returns n8n API service
|
* @returns n8n API client instance
|
||||||
*/
|
*/
|
||||||
export function createApiService(config: EnvConfig): N8nApiService {
|
export function createN8nApiClient(config: EnvConfig): N8nApiClient {
|
||||||
return new N8nApiService(config);
|
return new N8nApiClient(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 dotenv from 'dotenv';
|
||||||
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
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
|
// Environment variable names
|
||||||
export const ENV_VARS = {
|
export const ENV_VARS = {
|
||||||
|
|||||||
@@ -13,14 +13,16 @@ import {
|
|||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
ReadResourceRequestSchema,
|
ReadResourceRequestSchema,
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { getEnvConfig } from './environment.js';
|
import { getEnvConfig, EnvConfig } from './environment.js'; // Import EnvConfig
|
||||||
import { setupWorkflowTools } from '../tools/workflow/index.js';
|
import { setupWorkflowTools, ListWorkflowsHandler, GetWorkflowHandler, CreateWorkflowHandler, UpdateWorkflowHandler, DeleteWorkflowHandler, ActivateWorkflowHandler, DeactivateWorkflowHandler } from '../tools/workflow/index.js';
|
||||||
import { setupExecutionTools } from '../tools/execution/index.js';
|
import { setupExecutionTools, ListExecutionsHandler, GetExecutionHandler, DeleteExecutionHandler, RunWebhookHandler } from '../tools/execution/index.js';
|
||||||
import { setupResourceHandlers } from '../resources/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 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
|
* 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
|
// Get validated environment configuration
|
||||||
const envConfig = getEnvConfig();
|
const envConfig = getEnvConfig();
|
||||||
|
|
||||||
// Create n8n API service
|
// Create n8n API client instance
|
||||||
const apiService = createApiService(envConfig);
|
const apiClient = createN8nApiClient(envConfig); // Use new factory function name
|
||||||
|
|
||||||
// Verify n8n API connectivity
|
// Verify n8n API connectivity
|
||||||
try {
|
try {
|
||||||
console.error('Verifying n8n API connectivity...');
|
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}`);
|
console.error(`Successfully connected to n8n API at ${envConfig.n8nApiUrl}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('ERROR: Failed to connect to n8n API:', error instanceof Error ? error.message : error);
|
console.error('ERROR: Failed to connect to n8n API:', error instanceof Error ? error.message : error);
|
||||||
@@ -58,10 +60,11 @@ 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);
|
setupToolListRequestHandler(server);
|
||||||
setupToolCallRequestHandler(server);
|
setupToolCallRequestHandler(server, apiClient); // Pass apiClient
|
||||||
setupResourceHandlers(server, envConfig);
|
// Pass envConfig to resource handlers as originally intended
|
||||||
|
setupResourceHandlers(server, envConfig);
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
@@ -87,72 +90,44 @@ function setupToolListRequestHandler(server: Server): void {
|
|||||||
* Set up the tool call request handler for the server
|
* Set up the tool call request handler for the server
|
||||||
*
|
*
|
||||||
* @param server MCP server instance
|
* @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) => {
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
const toolName = request.params.name;
|
const toolName = request.params.name;
|
||||||
const args = request.params.arguments || {};
|
const args = request.params.arguments || {};
|
||||||
|
|
||||||
let result: ToolCallResult;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Import handlers
|
const HandlerClass = toolHandlerMap[toolName];
|
||||||
const {
|
|
||||||
ListWorkflowsHandler,
|
if (!HandlerClass) {
|
||||||
GetWorkflowHandler,
|
throw new McpError(ErrorCode.NotImplemented, `Unknown tool: ${toolName}`); // Use NotImplemented
|
||||||
CreateWorkflowHandler,
|
|
||||||
UpdateWorkflowHandler,
|
|
||||||
DeleteWorkflowHandler,
|
|
||||||
ActivateWorkflowHandler,
|
|
||||||
DeactivateWorkflowHandler
|
|
||||||
} = await import('../tools/workflow/index.js');
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
return {
|
||||||
content: result.content,
|
content: result.content,
|
||||||
isError: result.isError,
|
isError: result.isError,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
* for the n8n MCP Server.
|
* for the n8n MCP Server.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpError as SdkMcpError } from '@modelcontextprotocol/sdk/types.js';
|
import { McpError as SdkMcpError } from '@modelcontextprotocol/sdk/types.js'; // Add .js back
|
||||||
import { ErrorCode } from './error-codes.js';
|
import { ErrorCode } from './error-codes.js'; // Add .js back
|
||||||
|
|
||||||
// Re-export McpError from SDK
|
// Re-export McpError from SDK
|
||||||
export { McpError } from '@modelcontextprotocol/sdk/types.js';
|
export { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
ListResourceTemplatesRequestSchema,
|
ListResourceTemplatesRequestSchema,
|
||||||
ReadResourceRequestSchema,
|
ReadResourceRequestSchema,
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { EnvConfig } from '../config/environment.js';
|
import { EnvConfig } from '../config/environment.js'; // Re-add EnvConfig import
|
||||||
import { createApiService } from '../api/n8n-client.js';
|
import { createN8nApiClient, N8nApiClient } from '../api/n8n-client.js'; // Use correct factory import
|
||||||
import { McpError, ErrorCode } from '../errors/index.js';
|
import { McpError, ErrorCode } from '../errors/index.js';
|
||||||
|
|
||||||
// Import static resource handlers
|
// Import static resource handlers
|
||||||
@@ -46,12 +46,13 @@ import {
|
|||||||
* @param server MCP server instance
|
* @param server MCP server instance
|
||||||
* @param envConfig Environment configuration
|
* @param envConfig Environment configuration
|
||||||
*/
|
*/
|
||||||
export function setupResourceHandlers(server: Server, envConfig: EnvConfig): void {
|
// Revert function signature to accept EnvConfig
|
||||||
|
export function setupResourceHandlers(server: Server, envConfig: EnvConfig): void {
|
||||||
// Set up static resources
|
// Set up static resources
|
||||||
setupStaticResources(server, envConfig);
|
setupStaticResources(server, envConfig); // Pass envConfig
|
||||||
|
|
||||||
// Set up dynamic resources
|
// 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 server MCP server instance
|
||||||
* @param envConfig Environment configuration
|
* @param envConfig Environment configuration
|
||||||
*/
|
*/
|
||||||
function setupStaticResources(server: Server, envConfig: EnvConfig): void {
|
// Revert function signature to accept EnvConfig
|
||||||
const apiService = createApiService(envConfig);
|
function setupStaticResources(server: Server, envConfig: EnvConfig): void {
|
||||||
|
// Create apiClient internally for now
|
||||||
|
const apiClient = createN8nApiClient(envConfig); // Use correct factory function
|
||||||
|
|
||||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
// Return all available static resources
|
// Return all available static resources
|
||||||
@@ -80,8 +83,10 @@ function setupStaticResources(server: Server, envConfig: EnvConfig): void {
|
|||||||
* @param server MCP server instance
|
* @param server MCP server instance
|
||||||
* @param envConfig Environment configuration
|
* @param envConfig Environment configuration
|
||||||
*/
|
*/
|
||||||
function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
// Revert function signature to accept EnvConfig
|
||||||
const apiService = createApiService(envConfig);
|
function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
||||||
|
// Create apiClient internally for now
|
||||||
|
const apiClient = createN8nApiClient(envConfig); // Use correct factory function
|
||||||
|
|
||||||
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
||||||
// Return all available dynamic resource templates
|
// Return all available dynamic resource templates
|
||||||
@@ -100,7 +105,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
|||||||
try {
|
try {
|
||||||
// Handle static resources
|
// Handle static resources
|
||||||
if (uri === getWorkflowsResourceUri()) {
|
if (uri === getWorkflowsResourceUri()) {
|
||||||
const content = await getWorkflowsResource(apiService);
|
const content = await getWorkflowsResource(apiClient); // Use apiClient instance
|
||||||
return {
|
return {
|
||||||
contents: [
|
contents: [
|
||||||
{
|
{
|
||||||
@@ -113,7 +118,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uri === getExecutionStatsResourceUri()) {
|
if (uri === getExecutionStatsResourceUri()) {
|
||||||
const content = await getExecutionStatsResource(apiService);
|
const content = await getExecutionStatsResource(apiClient); // Use apiClient instance
|
||||||
return {
|
return {
|
||||||
contents: [
|
contents: [
|
||||||
{
|
{
|
||||||
@@ -128,7 +133,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
|||||||
// Handle dynamic resources
|
// Handle dynamic resources
|
||||||
const workflowId = extractWorkflowIdFromUri(uri);
|
const workflowId = extractWorkflowIdFromUri(uri);
|
||||||
if (workflowId) {
|
if (workflowId) {
|
||||||
const content = await getWorkflowResource(apiService, workflowId);
|
const content = await getWorkflowResource(apiClient, workflowId); // Use apiClient instance
|
||||||
return {
|
return {
|
||||||
contents: [
|
contents: [
|
||||||
{
|
{
|
||||||
@@ -142,7 +147,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void {
|
|||||||
|
|
||||||
const executionId = extractExecutionIdFromUri(uri);
|
const executionId = extractExecutionIdFromUri(uri);
|
||||||
if (executionId) {
|
if (executionId) {
|
||||||
const content = await getExecutionResource(apiService, executionId);
|
const content = await getExecutionResource(apiClient, executionId); // Use apiClient instance
|
||||||
return {
|
return {
|
||||||
contents: [
|
contents: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,16 +4,23 @@
|
|||||||
* This module provides a base handler for execution-related tools.
|
* This module provides a base handler for execution-related tools.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ToolCallResult } from '../../types/index.js';
|
import { ToolCallResult, BaseToolHandler } from '../../types/index.js'; // Already has .js
|
||||||
import { N8nApiError } from '../../errors/index.js';
|
import { N8nApiError } from '../../errors/index.js'; // Already has .js
|
||||||
import { createApiService } from '../../api/n8n-client.js';
|
import { N8nApiService } from '../../api/n8n-client.js'; // Already has .js
|
||||||
import { getEnvConfig } from '../../config/environment.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for execution tool handlers
|
* Base class for execution tool handlers
|
||||||
*/
|
*/
|
||||||
export abstract class BaseExecutionToolHandler {
|
export abstract class BaseExecutionToolHandler implements BaseToolHandler { // Implement BaseToolHandler
|
||||||
protected apiService = createApiService(getEnvConfig());
|
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
|
* Validate and execute the tool
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import { ToolCallResult, ToolDefinition } from '../../types/index.js';
|
|||||||
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { ErrorCode } from '../../errors/error-codes.js';
|
import { ErrorCode } from '../../errors/error-codes.js';
|
||||||
|
|
||||||
|
// Define specific type for delete arguments
|
||||||
|
interface DeleteExecutionArgs {
|
||||||
|
executionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for the delete_execution tool
|
* Handler for the delete_execution tool
|
||||||
*/
|
*/
|
||||||
@@ -19,8 +24,8 @@ export class DeleteExecutionHandler extends BaseExecutionToolHandler {
|
|||||||
* @param args Tool arguments (executionId)
|
* @param args Tool arguments (executionId)
|
||||||
* @returns Result of the deletion operation
|
* @returns Result of the deletion operation
|
||||||
*/
|
*/
|
||||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
async execute(args: DeleteExecutionArgs): Promise<ToolCallResult> { // Use specific args type
|
||||||
return this.handleExecution(async () => {
|
return this.handleExecution(async (args) => { // Pass args to handler
|
||||||
// Validate required parameters
|
// Validate required parameters
|
||||||
if (!args.executionId) {
|
if (!args.executionId) {
|
||||||
throw new McpError(
|
throw new McpError(
|
||||||
@@ -39,7 +44,7 @@ export class DeleteExecutionHandler extends BaseExecutionToolHandler {
|
|||||||
{ id: executionId, deleted: true },
|
{ id: executionId, deleted: true },
|
||||||
`Successfully deleted execution with ID: ${executionId}`
|
`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 { ErrorCode } from '../../errors/error-codes.js';
|
||||||
import { formatExecutionDetails } from '../../utils/execution-formatter.js';
|
import { formatExecutionDetails } from '../../utils/execution-formatter.js';
|
||||||
|
|
||||||
|
// Define specific type for get arguments
|
||||||
|
interface GetExecutionArgs {
|
||||||
|
executionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for the get_execution tool
|
* Handler for the get_execution tool
|
||||||
*/
|
*/
|
||||||
@@ -20,8 +25,8 @@ export class GetExecutionHandler extends BaseExecutionToolHandler {
|
|||||||
* @param args Tool arguments (executionId)
|
* @param args Tool arguments (executionId)
|
||||||
* @returns Execution details
|
* @returns Execution details
|
||||||
*/
|
*/
|
||||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
async execute(args: GetExecutionArgs): Promise<ToolCallResult> { // Use specific args type
|
||||||
return this.handleExecution(async () => {
|
return this.handleExecution(async (args) => { // Pass args to handler
|
||||||
// Validate required parameters
|
// Validate required parameters
|
||||||
if (!args.executionId) {
|
if (!args.executionId) {
|
||||||
throw new McpError(
|
throw new McpError(
|
||||||
@@ -40,7 +45,7 @@ export class GetExecutionHandler extends BaseExecutionToolHandler {
|
|||||||
formattedExecution,
|
formattedExecution,
|
||||||
`Execution Details for ID: ${args.executionId}`
|
`Execution Details for ID: ${args.executionId}`
|
||||||
);
|
);
|
||||||
}, args);
|
}, args); // Pass args to handleExecution
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseExecutionToolHandler } from './base-handler.js';
|
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';
|
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
|
* Handler for the list_executions tool
|
||||||
*/
|
*/
|
||||||
@@ -15,14 +24,16 @@ export class ListExecutionsHandler extends BaseExecutionToolHandler {
|
|||||||
/**
|
/**
|
||||||
* Execute the tool
|
* Execute the tool
|
||||||
*
|
*
|
||||||
* @param args Tool arguments (workflowId, status, limit, lastId)
|
* @param args Tool arguments (workflowId, status, limit, lastId, includeSummary)
|
||||||
* @returns List of executions
|
* @returns List of executions
|
||||||
*/
|
*/
|
||||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
async execute(args: ListExecutionsArgs): Promise<ToolCallResult> { // Use specific args type
|
||||||
return this.handleExecution(async () => {
|
return this.handleExecution(async (args) => { // Pass args to handler
|
||||||
const executions = await this.apiService.getExecutions();
|
// 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;
|
let filteredExecutions = executions;
|
||||||
|
|
||||||
// Filter by workflow ID if provided
|
// Filter by workflow ID if provided
|
||||||
@@ -39,34 +50,43 @@ 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;
|
const limit = args.limit && args.limit > 0 ? args.limit : filteredExecutions.length;
|
||||||
filteredExecutions = filteredExecutions.slice(0, limit);
|
// Ensure limit is applied correctly after potential pagination logic
|
||||||
|
filteredExecutions = filteredExecutions.slice(0, limit);
|
||||||
|
|
||||||
// Format the executions for display
|
// Format the executions for display
|
||||||
const formattedExecutions = filteredExecutions.map((execution: Execution) =>
|
const formattedExecutions = filteredExecutions.map((execution: Execution) =>
|
||||||
formatExecutionSummary(execution)
|
formatExecutionSummary(execution)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate summary if requested
|
// Generate summary if requested (based on the initially fetched, unfiltered list)
|
||||||
let summary = undefined;
|
let summary = undefined;
|
||||||
if (args.includeSummary) {
|
if (args.includeSummary) {
|
||||||
summary = summarizeExecutions(executions);
|
// Summarize based on the *original* list before filtering/limiting for accurate stats
|
||||||
|
summary = summarizeExecutions(executions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare response data
|
// Prepare response data
|
||||||
const responseData = {
|
const responseData = {
|
||||||
executions: formattedExecutions,
|
// Return the filtered and limited list
|
||||||
|
executions: formattedExecutions,
|
||||||
summary: summary,
|
summary: summary,
|
||||||
total: formattedExecutions.length,
|
count: formattedExecutions.length, // Count of returned executions
|
||||||
filtered: args.workflowId || args.status ? true : false
|
// 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(
|
return this.formatSuccess(
|
||||||
responseData,
|
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 {
|
export function getListExecutionsToolDefinition(): ToolDefinition {
|
||||||
return {
|
return {
|
||||||
name: 'list_executions',
|
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: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -88,19 +108,23 @@ export function getListExecutionsToolDefinition(): ToolDefinition {
|
|||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: 'string',
|
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: {
|
limit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Maximum number of executions to return',
|
description: 'Maximum number of executions to return (default: all matching)',
|
||||||
},
|
|
||||||
lastId: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'ID of the last execution for pagination',
|
|
||||||
},
|
},
|
||||||
|
// 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: {
|
includeSummary: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Include summary statistics about executions',
|
description: 'Include summary statistics about all executions (before filtering/limiting)',
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* This module provides a tool for running n8n workflows via webhooks.
|
* 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 { 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 { BaseExecutionToolHandler } from './base-handler.js';
|
||||||
import { N8nApiError } from '../../errors/index.js';
|
import { N8nApiError } from '../../errors/index.js';
|
||||||
import { getEnvConfig } from '../../config/environment.js';
|
import { getEnvConfig } from '../../config/environment.js';
|
||||||
@@ -33,7 +33,8 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
|||||||
/**
|
/**
|
||||||
* Tool definition for execution via webhook
|
* 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
|
* Extract N8N base URL from N8N API URL by removing /api/v1
|
||||||
@@ -60,10 +61,11 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
|||||||
* @param args Tool arguments
|
* @param args Tool arguments
|
||||||
* @returns Tool call result
|
* @returns Tool call result
|
||||||
*/
|
*/
|
||||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
async execute(args: RunWebhookParams): Promise<ToolCallResult> { // Use specific args type
|
||||||
return this.handleExecution(async (args) => {
|
return this.handleExecution(async (args) => { // Pass args to handler
|
||||||
// Parse and validate arguments
|
// Parse and validate arguments using the Zod schema
|
||||||
const params = runWebhookSchema.parse(args);
|
// This ensures args conforms to RunWebhookParams
|
||||||
|
const params = runWebhookSchema.parse(args);
|
||||||
|
|
||||||
// Get environment config for auth credentials
|
// Get environment config for auth credentials
|
||||||
const config = getEnvConfig();
|
const config = getEnvConfig();
|
||||||
@@ -71,11 +73,13 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
|||||||
try {
|
try {
|
||||||
// Get the webhook URL with the proper prefix
|
// Get the webhook URL with the proper prefix
|
||||||
const baseUrl = this.getN8nBaseUrl();
|
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();
|
const webhookUrl = new URL(webhookPath, baseUrl).toString();
|
||||||
|
|
||||||
// Prepare request config with basic auth from environment
|
// Prepare request config with basic auth from environment
|
||||||
const requestConfig: any = {
|
const requestConfig: AxiosRequestConfig = { // Use AxiosRequestConfig type
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(params.headers || {})
|
...(params.headers || {})
|
||||||
@@ -97,7 +101,7 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
|||||||
return this.formatSuccess({
|
return this.formatSuccess({
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
data: response.data
|
data: response.data // Assuming response.data is JSON-serializable
|
||||||
}, 'Webhook executed successfully');
|
}, 'Webhook executed successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle error from the webhook request
|
// Handle error from the webhook request
|
||||||
@@ -106,20 +110,29 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
|||||||
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
errorMessage = `Webhook execution failed with status ${error.response.status}: ${error.response.statusText}`;
|
errorMessage = `Webhook execution failed with status ${error.response.status}: ${error.response.statusText}`;
|
||||||
if (error.response.data) {
|
// Attempt to stringify response data safely
|
||||||
return this.formatError(new N8nApiError(
|
let responseDataStr = '';
|
||||||
`${errorMessage}\n\n${JSON.stringify(error.response.data, null, 2)}`,
|
try {
|
||||||
error.response.status
|
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${responseDataStr}`,
|
||||||
|
statusCode
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.formatError(new N8nApiError(errorMessage, error.response?.status || 500));
|
// Cast error.response to any before accessing status
|
||||||
|
return this.formatError(new N8nApiError(errorMessage, (error.response as any)?.status || 500));
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error; // Re-throw non-axios errors for the handler to catch
|
// Re-throw non-axios errors for the base handler to catch
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}, args);
|
}, args); // Pass args to handleExecution
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +141,7 @@ export class RunWebhookHandler extends BaseExecutionToolHandler {
|
|||||||
*
|
*
|
||||||
* @returns Tool definition object
|
* @returns Tool definition object
|
||||||
*/
|
*/
|
||||||
export function getRunWebhookToolDefinition() {
|
export function getRunWebhookToolDefinition(): ToolDefinition { // Add return type
|
||||||
return {
|
return {
|
||||||
name: 'run_webhook',
|
name: 'run_webhook',
|
||||||
description: 'Execute a workflow via webhook with optional input data',
|
description: 'Execute a workflow via webhook with optional input data',
|
||||||
@@ -141,14 +154,17 @@ export function getRunWebhookToolDefinition() {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: 'object',
|
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: {
|
headers: {
|
||||||
type: 'object',
|
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']
|
required: ['workflowName']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ export class ActivateWorkflowHandler extends BaseWorkflowToolHandler {
|
|||||||
/**
|
/**
|
||||||
* Execute the tool
|
* Execute the tool
|
||||||
*
|
*
|
||||||
* @param args Tool arguments containing workflowId
|
* @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) => {
|
return this.handleExecution(async (args) => {
|
||||||
const { workflowId } = args;
|
const { workflowId } = args; // Destructuring remains the same
|
||||||
|
|
||||||
if (!workflowId) {
|
if (!workflowId) {
|
||||||
throw new N8nApiError('Missing required parameter: workflowId');
|
throw new N8nApiError('Missing required parameter: workflowId');
|
||||||
|
|||||||
@@ -4,16 +4,23 @@
|
|||||||
* This module provides a base handler for workflow-related tools.
|
* This module provides a base handler for workflow-related tools.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ToolCallResult } from '../../types/index.js';
|
import { ToolCallResult, BaseToolHandler } from '../../types/index.js'; // Already has .js
|
||||||
import { N8nApiError } from '../../errors/index.js';
|
import { N8nApiError } from '../../errors/index.js'; // Already has .js
|
||||||
import { createApiService } from '../../api/n8n-client.js';
|
import { N8nApiService } from '../../api/n8n-client.js'; // Already has .js
|
||||||
import { getEnvConfig } from '../../config/environment.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for workflow tool handlers
|
* Base class for workflow tool handlers
|
||||||
*/
|
*/
|
||||||
export abstract class BaseWorkflowToolHandler {
|
export abstract class BaseWorkflowToolHandler implements BaseToolHandler { // Implement BaseToolHandler
|
||||||
protected apiService = createApiService(getEnvConfig());
|
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
|
* Validate and execute the tool
|
||||||
|
|||||||
@@ -5,9 +5,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseWorkflowToolHandler } from './base-handler.js';
|
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';
|
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
|
* Handler for the create_workflow tool
|
||||||
*/
|
*/
|
||||||
@@ -18,43 +27,44 @@ export class CreateWorkflowHandler extends BaseWorkflowToolHandler {
|
|||||||
* @param args Tool arguments containing workflow details
|
* @param args Tool arguments containing workflow details
|
||||||
* @returns Created workflow information
|
* @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) => {
|
return this.handleExecution(async (args) => {
|
||||||
const { name, nodes, connections, active, tags } = args;
|
const { name, nodes, connections, active, tags } = args;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new N8nApiError('Missing required parameter: 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)) {
|
if (nodes && !Array.isArray(nodes)) {
|
||||||
throw new N8nApiError('Parameter "nodes" must be an array');
|
throw new N8nApiError('Parameter "nodes" must be an array');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate connections if provided
|
|
||||||
if (connections && typeof connections !== 'object') {
|
if (connections && typeof connections !== 'object') {
|
||||||
throw new N8nApiError('Parameter "connections" must be an 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
|
// Prepare workflow object using Partial<Workflow> for the API call
|
||||||
const workflowData: Record<string, any> = {
|
const workflowData: Partial<Workflow> = {
|
||||||
name,
|
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
|
// 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(
|
return this.formatSuccess(
|
||||||
{
|
{
|
||||||
id: workflow.id,
|
id: createdWorkflow.id,
|
||||||
name: workflow.name,
|
name: createdWorkflow.name,
|
||||||
active: workflow.active
|
active: createdWorkflow.active
|
||||||
},
|
},
|
||||||
`Workflow created successfully`
|
`Workflow created successfully`
|
||||||
);
|
);
|
||||||
@@ -80,18 +90,18 @@ export function getCreateWorkflowToolDefinition(): ToolDefinition {
|
|||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
description: 'Array of node objects that define the workflow',
|
description: 'Array of node objects (N8nNode structure) defining the workflow',
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object', // Ideally, reference a detailed N8nNode schema here
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'Connection mappings between nodes',
|
description: 'Connection mappings between nodes (N8nConnection structure)',
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether the workflow should be active upon creation',
|
description: 'Whether the workflow should be active upon creation (defaults to false)',
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ export class DeactivateWorkflowHandler extends BaseWorkflowToolHandler {
|
|||||||
/**
|
/**
|
||||||
* Execute the tool
|
* Execute the tool
|
||||||
*
|
*
|
||||||
* @param args Tool arguments containing workflowId
|
* @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) => {
|
return this.handleExecution(async (args) => {
|
||||||
const { workflowId } = args;
|
const { workflowId } = args; // Destructuring remains the same
|
||||||
|
|
||||||
if (!workflowId) {
|
if (!workflowId) {
|
||||||
throw new N8nApiError('Missing required parameter: workflowId');
|
throw new N8nApiError('Missing required parameter: workflowId');
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ export class DeleteWorkflowHandler extends BaseWorkflowToolHandler {
|
|||||||
/**
|
/**
|
||||||
* Execute the tool
|
* Execute the tool
|
||||||
*
|
*
|
||||||
* @param args Tool arguments containing workflowId
|
* @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) => {
|
return this.handleExecution(async (args) => {
|
||||||
const { workflowId } = args;
|
const { workflowId } = args; // Destructuring remains the same
|
||||||
|
|
||||||
if (!workflowId) {
|
if (!workflowId) {
|
||||||
throw new N8nApiError('Missing required parameter: workflowId');
|
throw new N8nApiError('Missing required parameter: workflowId');
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ export class GetWorkflowHandler extends BaseWorkflowToolHandler {
|
|||||||
/**
|
/**
|
||||||
* Execute the tool
|
* Execute the tool
|
||||||
*
|
*
|
||||||
* @param args Tool arguments containing workflowId
|
* @param args Tool arguments containing workflowId
|
||||||
* @returns Workflow details
|
* @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) => {
|
return this.handleExecution(async (args) => {
|
||||||
const { workflowId } = args;
|
const { workflowId } = args; // Destructuring remains the same
|
||||||
|
|
||||||
if (!workflowId) {
|
if (!workflowId) {
|
||||||
throw new N8nApiError('Missing required parameter: workflowId');
|
throw new N8nApiError('Missing required parameter: workflowId');
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ export class ListWorkflowsHandler extends BaseWorkflowToolHandler {
|
|||||||
/**
|
/**
|
||||||
* Execute the tool
|
* Execute the tool
|
||||||
*
|
*
|
||||||
* @param args Tool arguments
|
* @param args Tool arguments (expecting optional 'active' boolean)
|
||||||
* @returns List of workflows
|
* @returns List of workflows
|
||||||
*/
|
*/
|
||||||
async execute(args: Record<string, any>): Promise<ToolCallResult> {
|
async execute(args: { active?: boolean }): Promise<ToolCallResult> { // Use specific type for args
|
||||||
return this.handleExecution(async () => {
|
return this.handleExecution(async (args) => { // Pass args to the handler
|
||||||
const workflows = await this.apiService.getWorkflows();
|
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
|
// Format the workflows for display
|
||||||
const formattedWorkflows = workflows.map((workflow: Workflow) => ({
|
const formattedWorkflows = workflows.map((workflow: Workflow) => ({
|
||||||
@@ -31,9 +36,9 @@ export class ListWorkflowsHandler extends BaseWorkflowToolHandler {
|
|||||||
|
|
||||||
return this.formatSuccess(
|
return this.formatSuccess(
|
||||||
formattedWorkflows,
|
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 { 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';
|
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
|
* Handler for the update_workflow tool
|
||||||
*/
|
*/
|
||||||
@@ -18,51 +25,73 @@ export class UpdateWorkflowHandler extends BaseWorkflowToolHandler {
|
|||||||
* @param args Tool arguments containing workflow updates
|
* @param args Tool arguments containing workflow updates
|
||||||
* @returns Updated workflow information
|
* @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) => {
|
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) {
|
if (!workflowId) {
|
||||||
throw new N8nApiError('Missing required parameter: 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)) {
|
if (nodes && !Array.isArray(nodes)) {
|
||||||
throw new N8nApiError('Parameter "nodes" must be an array');
|
throw new N8nApiError('Parameter "nodes" must be an array');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate connections if provided
|
|
||||||
if (connections && typeof connections !== 'object') {
|
if (connections && typeof connections !== 'object') {
|
||||||
throw new N8nApiError('Parameter "connections" must be an 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
|
// Get the current workflow to compare changes (optional, but good for summary)
|
||||||
const currentWorkflow = await this.apiService.getWorkflow(workflowId);
|
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
|
// Prepare update object with only the provided changes
|
||||||
const workflowData: Record<string, any> = { ...currentWorkflow };
|
const workflowUpdateData: Partial<Workflow> = {};
|
||||||
|
if (name !== undefined) workflowUpdateData.name = name;
|
||||||
// Update fields if provided
|
if (nodes !== undefined) workflowUpdateData.nodes = nodes;
|
||||||
if (name !== undefined) workflowData.name = name;
|
if (connections !== undefined) workflowUpdateData.connections = connections;
|
||||||
if (nodes !== undefined) workflowData.nodes = nodes;
|
if (active !== undefined) workflowUpdateData.active = active;
|
||||||
if (connections !== undefined) workflowData.connections = connections;
|
if (tags !== undefined) workflowUpdateData.tags = tags;
|
||||||
if (active !== undefined) workflowData.active = active;
|
if (settings !== undefined) workflowUpdateData.settings = settings;
|
||||||
if (tags !== undefined) workflowData.tags = tags;
|
// Add other updatable fields from Workflow interface if needed
|
||||||
|
|
||||||
|
// 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
|
// 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 = [];
|
const changesArray = [];
|
||||||
if (name !== undefined && name !== currentWorkflow.name) changesArray.push(`name: "${currentWorkflow.name}" → "${name}"`);
|
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 (active !== undefined && active !== currentWorkflow.active) changesArray.push(`active: ${currentWorkflow.active} → ${active}`);
|
||||||
if (nodes !== undefined) changesArray.push('nodes updated');
|
if (nodes !== undefined) changesArray.push('nodes updated');
|
||||||
if (connections !== undefined) changesArray.push('connections updated');
|
if (connections !== undefined) changesArray.push('connections updated');
|
||||||
if (tags !== undefined) changesArray.push('tags updated');
|
if (tags !== undefined) changesArray.push('tags updated');
|
||||||
|
if (settings !== undefined) changesArray.push('settings updated');
|
||||||
|
|
||||||
const changesSummary = changesArray.length > 0
|
const changesSummary = changesArray.length > 0
|
||||||
? `Changes: ${changesArray.join(', ')}`
|
? `Changes: ${changesArray.join(', ')}`
|
||||||
: 'No changes were made';
|
: 'No effective changes were made (values might be the same as current)';
|
||||||
|
|
||||||
return this.formatSuccess(
|
return this.formatSuccess(
|
||||||
{
|
{
|
||||||
@@ -84,7 +113,7 @@ export class UpdateWorkflowHandler extends BaseWorkflowToolHandler {
|
|||||||
export function getUpdateWorkflowToolDefinition(): ToolDefinition {
|
export function getUpdateWorkflowToolDefinition(): ToolDefinition {
|
||||||
return {
|
return {
|
||||||
name: 'update_workflow',
|
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: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -98,14 +127,14 @@ export function getUpdateWorkflowToolDefinition(): ToolDefinition {
|
|||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
description: 'Updated array of node objects that define the workflow',
|
description: 'Updated array of node objects (N8nNode structure) defining the workflow',
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object', // Ideally, reference a detailed N8nNode schema here
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'Updated connection mappings between nodes',
|
description: 'Updated connection mappings between nodes (N8nConnection structure)',
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -118,8 +147,12 @@ export function getUpdateWorkflowToolDefinition(): ToolDefinition {
|
|||||||
type: 'string',
|
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;
|
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 {
|
export interface Workflow {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
nodes: any[];
|
nodes: N8nNode[]; // Use specific Node type
|
||||||
connections: any;
|
connections: N8nConnection; // Use specific Connection type
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: 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;
|
[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 {
|
export interface Execution {
|
||||||
id: string;
|
id: string;
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
finished: boolean;
|
finished: boolean;
|
||||||
mode: string;
|
mode: string; // e.g., 'manual', 'webhook', 'trigger'
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
stoppedAt: string;
|
stoppedAt: string | null; // Can be null if running
|
||||||
status: string;
|
status: 'waiting' | 'running' | 'success' | 'error' | 'unknown'; // More specific statuses
|
||||||
data: {
|
data?: { // Make data optional
|
||||||
resultData: {
|
resultData?: { // Make resultData optional
|
||||||
runData: any;
|
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.
|
* 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
|
* Format basic execution information for display
|
||||||
@@ -13,10 +13,11 @@ import { Execution } from '../types/index.js';
|
|||||||
* @param execution Execution object
|
* @param execution Execution object
|
||||||
* @returns Formatted execution summary
|
* @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
|
// Calculate duration
|
||||||
const startedAt = new Date(execution.startedAt);
|
const startedAt = new Date(execution.startedAt);
|
||||||
const stoppedAt = execution.stoppedAt ? new Date(execution.stoppedAt) : new Date();
|
// 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 durationMs = stoppedAt.getTime() - startedAt.getTime();
|
||||||
const durationSeconds = Math.round(durationMs / 1000);
|
const durationSeconds = Math.round(durationMs / 1000);
|
||||||
|
|
||||||
@@ -40,49 +41,56 @@ export function formatExecutionSummary(execution: Execution): Record<string, any
|
|||||||
* @param execution Execution object
|
* @param execution Execution object
|
||||||
* @returns Formatted execution details
|
* @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);
|
const summary = formatExecutionSummary(execution);
|
||||||
|
|
||||||
// Extract node results
|
// Extract node results
|
||||||
const nodeResults: Record<string, any> = {};
|
const nodeResults: Record<string, any> = {};
|
||||||
if (execution.data?.resultData?.runData) {
|
const runData: ExecutionRunData | undefined = execution.data?.resultData?.runData;
|
||||||
for (const [nodeName, nodeData] of Object.entries(execution.data.resultData.runData)) {
|
|
||||||
|
if (runData) {
|
||||||
|
for (const [nodeName, nodeDataArray] of Object.entries(runData)) {
|
||||||
try {
|
try {
|
||||||
// Get the last output
|
// Get the last output object from the node's execution history array
|
||||||
const lastOutput = Array.isArray(nodeData) && nodeData.length > 0
|
const lastOutput = Array.isArray(nodeDataArray) && nodeDataArray.length > 0
|
||||||
? nodeData[nodeData.length - 1]
|
? nodeDataArray[nodeDataArray.length - 1]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Check if the last output has the expected structure
|
||||||
if (lastOutput && lastOutput.data && Array.isArray(lastOutput.data.main)) {
|
if (lastOutput && lastOutput.data && Array.isArray(lastOutput.data.main)) {
|
||||||
// Extract the output data
|
// Extract the output data items
|
||||||
const outputData = lastOutput.data.main.length > 0
|
const outputItems = lastOutput.data.main.length > 0
|
||||||
? lastOutput.data.main[0]
|
? lastOutput.data.main[0] // Assuming the first element contains the items array
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
nodeResults[nodeName] = {
|
nodeResults[nodeName] = {
|
||||||
status: lastOutput.status,
|
status: lastOutput.status,
|
||||||
items: outputData.length,
|
items: Array.isArray(outputItems) ? outputItems.length : 0, // Ensure items is an array
|
||||||
data: outputData.slice(0, 3), // Limit to first 3 items to avoid overwhelming response
|
// 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) {
|
} catch (error) {
|
||||||
|
console.error(`Error parsing node output for ${nodeName}:`, error);
|
||||||
nodeResults[nodeName] = { error: 'Failed to parse node output' };
|
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
|
// Add node results and error information to the summary
|
||||||
return {
|
return {
|
||||||
...summary,
|
...summary,
|
||||||
mode: execution.mode,
|
mode: execution.mode,
|
||||||
nodeResults: nodeResults,
|
nodeResults: nodeResults,
|
||||||
// Include error information if present
|
error: errorDetails ? { // Use the defined ExecutionError type
|
||||||
error: execution.data?.resultData && 'error' in execution.data.resultData
|
message: errorDetails.message,
|
||||||
? {
|
stack: errorDetails.stack, // Include stack if available
|
||||||
message: (execution.data.resultData as any).error?.message,
|
} : undefined,
|
||||||
stack: (execution.data.resultData as any).error?.stack,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +100,7 @@ export function formatExecutionDetails(execution: Execution): Record<string, any
|
|||||||
* @param status Execution status string
|
* @param status Execution status string
|
||||||
* @returns Status indicator emoji
|
* @returns Status indicator emoji
|
||||||
*/
|
*/
|
||||||
export function getStatusIndicator(status: string): string {
|
export function getStatusIndicator(status: Execution['status']): string { // Use specific status type
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return '✅'; // Success
|
return '✅'; // Success
|
||||||
@@ -100,10 +108,11 @@ export function getStatusIndicator(status: string): string {
|
|||||||
return '❌'; // Error
|
return '❌'; // Error
|
||||||
case 'waiting':
|
case 'waiting':
|
||||||
return '⏳'; // Waiting
|
return '⏳'; // Waiting
|
||||||
case 'canceled':
|
// Add other potential statuses if known, e.g., 'canceled'
|
||||||
return '🛑'; // Canceled
|
// case 'canceled':
|
||||||
|
// return '🛑';
|
||||||
default:
|
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
|
* @param limit Maximum number of executions to include
|
||||||
* @returns Summary of execution results
|
* @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);
|
const limitedExecutions = executions.slice(0, limit);
|
||||||
|
|
||||||
// Group executions by status
|
// Group executions by status
|
||||||
@@ -132,7 +141,7 @@ export function summarizeExecutions(executions: Execution[], limit: number = 10)
|
|||||||
return {
|
return {
|
||||||
total: totalCount,
|
total: totalCount,
|
||||||
byStatus: Object.entries(byStatus).map(([status, count]) => ({
|
byStatus: Object.entries(byStatus).map(([status, count]) => ({
|
||||||
status: `${getStatusIndicator(status)} ${status}`,
|
status: `${getStatusIndicator(status as Execution['status'])} ${status}`, // Cast status
|
||||||
count,
|
count,
|
||||||
percentage: totalCount > 0 ? Math.round((count / totalCount) * 100) : 0
|
percentage: totalCount > 0 ? Math.round((count / totalCount) * 100) : 0
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* in a consistent, user-friendly manner for MCP resources.
|
* 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';
|
import { formatExecutionSummary, summarizeExecutions } from './execution-formatter.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +14,7 @@ import { formatExecutionSummary, summarizeExecutions } from './execution-formatt
|
|||||||
* @param workflow Workflow object
|
* @param workflow Workflow object
|
||||||
* @returns Formatted workflow summary
|
* @returns Formatted workflow summary
|
||||||
*/
|
*/
|
||||||
export function formatWorkflowSummary(workflow: Workflow): Record<string, any> {
|
export function formatWorkflowSummary(workflow: Workflow): Record<string, any> { // Keep return flexible
|
||||||
return {
|
return {
|
||||||
id: workflow.id,
|
id: workflow.id,
|
||||||
name: workflow.name,
|
name: workflow.name,
|
||||||
@@ -31,21 +31,23 @@ export function formatWorkflowSummary(workflow: Workflow): Record<string, any> {
|
|||||||
* @param workflow Workflow object
|
* @param workflow Workflow object
|
||||||
* @returns Formatted workflow details
|
* @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);
|
const summary = formatWorkflowSummary(workflow);
|
||||||
|
|
||||||
// Add additional details
|
// Add additional details
|
||||||
return {
|
return {
|
||||||
...summary,
|
...summary,
|
||||||
nodes: workflow.nodes.map(node => ({
|
nodes: workflow.nodes.map((node: N8nNode) => ({ // Use N8nNode type
|
||||||
id: node.id,
|
id: node.id,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
type: node.type,
|
type: node.type,
|
||||||
position: node.position,
|
position: node.position,
|
||||||
parameters: node.parameters,
|
parameters: node.parameters, // Keep parameters as is for now
|
||||||
|
disabled: node.disabled,
|
||||||
|
notes: node.notes,
|
||||||
})),
|
})),
|
||||||
connections: workflow.connections,
|
connections: workflow.connections, // Keep connections as is for now
|
||||||
staticData: workflow.staticData,
|
staticData: workflow.staticData, // Keep staticData as is
|
||||||
settings: workflow.settings,
|
settings: workflow.settings,
|
||||||
tags: workflow.tags,
|
tags: workflow.tags,
|
||||||
// Exclude potentially sensitive or unuseful information
|
// Exclude potentially sensitive or unuseful information
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* Axios mock utilities for n8n MCP Server tests
|
* Axios mock utilities for n8n MCP Server tests
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { jest } from '@jest/globals'; // Import jest
|
||||||
import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
|
|
||||||
export interface MockResponse {
|
export interface MockResponse {
|
||||||
@@ -27,83 +28,84 @@ export const createMockAxiosResponse = (options: Partial<MockResponse> = {}): Ax
|
|||||||
*/
|
*/
|
||||||
export const createMockAxiosInstance = () => {
|
export const createMockAxiosInstance = () => {
|
||||||
const mockRequests: Record<string, any[]> = {};
|
const mockRequests: Record<string, any[]> = {};
|
||||||
const mockResponses: Record<string, MockResponse[]> = {};
|
const mockResponses: Record<string, (MockResponse | Error)[]> = {}; // Allow Error type
|
||||||
|
|
||||||
const mockInstance = {
|
const mockInstance = {
|
||||||
get: jest.fn(),
|
get: jest.fn<any>(), // Add type hint for mock function
|
||||||
post: jest.fn(),
|
post: jest.fn<any>(), // Add type hint for mock function
|
||||||
put: jest.fn(),
|
put: jest.fn<any>(), // Add type hint for mock function
|
||||||
delete: jest.fn(),
|
delete: jest.fn<any>(), // Add type hint for mock function
|
||||||
interceptors: {
|
interceptors: {
|
||||||
request: {
|
request: {
|
||||||
use: jest.fn(),
|
use: jest.fn<any>(), // Add type hint for mock function
|
||||||
},
|
},
|
||||||
response: {
|
response: {
|
||||||
use: jest.fn(),
|
use: jest.fn<any>(), // Add type hint for mock function
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaults: {},
|
defaults: {},
|
||||||
|
|
||||||
// Helper method to add mock response
|
// Helper method to add mock response
|
||||||
addMockResponse(method: string, url: string, response: MockResponse | Error) {
|
addMockResponse(method: string, url: string, response: MockResponse | Error) {
|
||||||
if (!mockResponses[`${method}:${url}`]) {
|
const key = `${method}:${url}`;
|
||||||
mockResponses[`${method}:${url}`] = [];
|
if (!mockResponses[key]) {
|
||||||
}
|
mockResponses[key] = [];
|
||||||
|
|
||||||
if (response instanceof Error) {
|
|
||||||
mockResponses[`${method}:${url}`].push(response as any);
|
|
||||||
} else {
|
|
||||||
mockResponses[`${method}:${url}`].push(response);
|
|
||||||
}
|
}
|
||||||
|
mockResponses[key].push(response);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Helper method to get request history
|
// Helper method to get request history
|
||||||
getRequestHistory(method: string, url: string) {
|
getRequestHistory(method: string, url: string) {
|
||||||
return mockRequests[`${method}:${url}`] || [];
|
const key = `${method}:${url}`;
|
||||||
|
return mockRequests[key] || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
// Reset all mocks
|
// Reset all mocks
|
||||||
reset() {
|
reset() {
|
||||||
Object.keys(mockRequests).forEach(key => {
|
Object.keys(mockRequests).forEach(key => {
|
||||||
delete mockRequests[key];
|
delete mockRequests[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(mockResponses).forEach(key => {
|
Object.keys(mockResponses).forEach(key => {
|
||||||
delete mockResponses[key];
|
delete mockResponses[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
mockInstance.get.mockReset();
|
mockInstance.get.mockReset();
|
||||||
mockInstance.post.mockReset();
|
mockInstance.post.mockReset();
|
||||||
mockInstance.put.mockReset();
|
mockInstance.put.mockReset();
|
||||||
mockInstance.delete.mockReset();
|
mockInstance.delete.mockReset();
|
||||||
|
mockInstance.interceptors.request.use.mockReset();
|
||||||
|
mockInstance.interceptors.response.use.mockReset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup method implementations
|
// Setup method implementations
|
||||||
['get', 'post', 'put', 'delete'].forEach(method => {
|
['get', 'post', 'put', 'delete'].forEach((method) => { // Remove explicit type annotation
|
||||||
mockInstance[method].mockImplementation(async (url: string, data?: any) => {
|
(mockInstance as any)[method].mockImplementation(async (url: string, data?: any) => { // Keep cast for dynamic access
|
||||||
const requestKey = `${method}:${url}`;
|
const requestKey = `${method}:${url}`;
|
||||||
|
|
||||||
if (!mockRequests[requestKey]) {
|
if (!mockRequests[requestKey]) {
|
||||||
mockRequests[requestKey] = [];
|
mockRequests[requestKey] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
mockRequests[requestKey].push(data);
|
mockRequests[requestKey].push(data);
|
||||||
|
|
||||||
if (mockResponses[requestKey] && mockResponses[requestKey].length > 0) {
|
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) {
|
if (response instanceof Error) {
|
||||||
throw response;
|
throw response;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createMockAxiosResponse(response);
|
if (response) { // Check if response is defined
|
||||||
|
return createMockAxiosResponse(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`No mock response defined for ${method.toUpperCase()} ${url}`);
|
throw new Error(`No mock response defined for ${method.toUpperCase()} ${url}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return mockInstance;
|
return mockInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const createMockWorkflow = (overrides: Partial<Workflow> = {}): Workflow
|
|||||||
id: 'start',
|
id: 'start',
|
||||||
name: 'Start',
|
name: 'Start',
|
||||||
type: 'n8n-nodes-base.start',
|
type: 'n8n-nodes-base.start',
|
||||||
|
typeVersion: 1, // Added missing property
|
||||||
parameters: {},
|
parameters: {},
|
||||||
position: [100, 300],
|
position: [100, 300],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,10 +3,17 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["jest", "node"],
|
"types": ["jest", "node"],
|
||||||
"esModuleInterop": true,
|
"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": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts", // Keep existing test files
|
||||||
"**/*.tsx"
|
"../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
|
* N8nApiClient unit tests
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@jest/globals';
|
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; // Explicit import
|
||||||
import axios from 'axios';
|
import axios, { AxiosInstance } from 'axios'; // Import AxiosInstance type
|
||||||
import { N8nApiClient } from '../../../src/api/client.js';
|
import { N8nApiClient } from '../../../src/api/client.js'; // Add .js
|
||||||
import { EnvConfig } from '../../../src/config/environment.js';
|
import { EnvConfig } from '../../../src/config/environment.js'; // Add .js
|
||||||
import { N8nApiError } from '../../../src/errors/index.js';
|
import { N8nApiError } from '../../../src/errors/index.js'; // Add .js
|
||||||
import { createMockAxiosInstance, createMockAxiosResponse } from '../../mocks/axios-mock.js';
|
import { createMockAxiosInstance, createMockAxiosResponse } from '../../mocks/axios-mock.js'; // Add .js
|
||||||
import { mockApiResponses } from '../../mocks/n8n-fixtures.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', () => {
|
describe('N8nApiClient', () => {
|
||||||
// Mock configuration
|
// Mock configuration
|
||||||
const mockConfig: EnvConfig = {
|
const mockConfig: EnvConfig = {
|
||||||
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
||||||
n8nApiKey: 'test-api-key',
|
n8nApiKey: 'test-api-key',
|
||||||
|
n8nWebhookUsername: 'test-user', // Added missing property
|
||||||
|
n8nWebhookPassword: 'test-password', // Added missing property
|
||||||
debug: false,
|
debug: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Define a type for the mock axios instance based on axios-mock.ts
|
||||||
|
type MockAxiosInstance = ReturnType<typeof createMockAxiosInstance>;
|
||||||
|
|
||||||
// Mock axios instance
|
// Mock axios instance
|
||||||
let mockAxios;
|
let mockAxios: MockAxiosInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
// Create the mock instance
|
||||||
mockAxios = createMockAxiosInstance();
|
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(() => {
|
afterEach(() => {
|
||||||
@@ -42,7 +49,7 @@ describe('N8nApiClient', () => {
|
|||||||
new N8nApiClient(mockConfig);
|
new N8nApiClient(mockConfig);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(axios.create).toHaveBeenCalledWith({
|
expect(axios.create).toHaveBeenCalledWith({ // Check the spy
|
||||||
baseURL: mockConfig.n8nApiUrl,
|
baseURL: mockConfig.n8nApiUrl,
|
||||||
headers: {
|
headers: {
|
||||||
'X-N8N-API-KEY': mockConfig.n8nApiKey,
|
'X-N8N-API-KEY': mockConfig.n8nApiKey,
|
||||||
@@ -80,6 +87,7 @@ describe('N8nApiClient', () => {
|
|||||||
const client = new N8nApiClient(mockConfig);
|
const client = new N8nApiClient(mockConfig);
|
||||||
mockAxios.addMockResponse('get', '/workflows', {
|
mockAxios.addMockResponse('get', '/workflows', {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: { data: [] },
|
data: { data: [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,6 +100,7 @@ describe('N8nApiClient', () => {
|
|||||||
const client = new N8nApiClient(mockConfig);
|
const client = new N8nApiClient(mockConfig);
|
||||||
mockAxios.addMockResponse('get', '/workflows', {
|
mockAxios.addMockResponse('get', '/workflows', {
|
||||||
status: 500,
|
status: 500,
|
||||||
|
statusText: 'Internal Server Error', // Added statusText
|
||||||
data: { message: 'Server error' },
|
data: { message: 'Server error' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,6 +125,7 @@ describe('N8nApiClient', () => {
|
|||||||
const mockWorkflows = mockApiResponses.workflows.list;
|
const mockWorkflows = mockApiResponses.workflows.list;
|
||||||
mockAxios.addMockResponse('get', '/workflows', {
|
mockAxios.addMockResponse('get', '/workflows', {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockWorkflows,
|
data: mockWorkflows,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,6 +142,7 @@ describe('N8nApiClient', () => {
|
|||||||
const client = new N8nApiClient(mockConfig);
|
const client = new N8nApiClient(mockConfig);
|
||||||
mockAxios.addMockResponse('get', '/workflows', {
|
mockAxios.addMockResponse('get', '/workflows', {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,6 +171,7 @@ describe('N8nApiClient', () => {
|
|||||||
const mockWorkflow = mockApiResponses.workflows.single(workflowId);
|
const mockWorkflow = mockApiResponses.workflows.single(workflowId);
|
||||||
mockAxios.addMockResponse('get', `/workflows/${workflowId}`, {
|
mockAxios.addMockResponse('get', `/workflows/${workflowId}`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockWorkflow,
|
data: mockWorkflow,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,6 +205,7 @@ describe('N8nApiClient', () => {
|
|||||||
|
|
||||||
mockAxios.addMockResponse('post', `/workflows/${workflowId}/execute`, {
|
mockAxios.addMockResponse('post', `/workflows/${workflowId}/execute`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockResponse,
|
data: mockResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -214,6 +227,7 @@ describe('N8nApiClient', () => {
|
|||||||
|
|
||||||
mockAxios.addMockResponse('post', '/workflows', {
|
mockAxios.addMockResponse('post', '/workflows', {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockResponse,
|
data: mockResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -236,6 +250,7 @@ describe('N8nApiClient', () => {
|
|||||||
|
|
||||||
mockAxios.addMockResponse('put', `/workflows/${workflowId}`, {
|
mockAxios.addMockResponse('put', `/workflows/${workflowId}`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockResponse,
|
data: mockResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,6 +272,7 @@ describe('N8nApiClient', () => {
|
|||||||
|
|
||||||
mockAxios.addMockResponse('delete', `/workflows/${workflowId}`, {
|
mockAxios.addMockResponse('delete', `/workflows/${workflowId}`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockResponse,
|
data: mockResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -278,6 +294,7 @@ describe('N8nApiClient', () => {
|
|||||||
|
|
||||||
mockAxios.addMockResponse('post', `/workflows/${workflowId}/activate`, {
|
mockAxios.addMockResponse('post', `/workflows/${workflowId}/activate`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockResponse,
|
data: mockResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -299,6 +316,7 @@ describe('N8nApiClient', () => {
|
|||||||
|
|
||||||
mockAxios.addMockResponse('post', `/workflows/${workflowId}/deactivate`, {
|
mockAxios.addMockResponse('post', `/workflows/${workflowId}/deactivate`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockResponse,
|
data: mockResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,6 +336,7 @@ describe('N8nApiClient', () => {
|
|||||||
const mockExecutions = mockApiResponses.executions.list;
|
const mockExecutions = mockApiResponses.executions.list;
|
||||||
mockAxios.addMockResponse('get', '/executions', {
|
mockAxios.addMockResponse('get', '/executions', {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockExecutions,
|
data: mockExecutions,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -338,6 +357,7 @@ describe('N8nApiClient', () => {
|
|||||||
const mockExecution = mockApiResponses.executions.single(executionId);
|
const mockExecution = mockApiResponses.executions.single(executionId);
|
||||||
mockAxios.addMockResponse('get', `/executions/${executionId}`, {
|
mockAxios.addMockResponse('get', `/executions/${executionId}`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockExecution,
|
data: mockExecution,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -359,6 +379,7 @@ describe('N8nApiClient', () => {
|
|||||||
|
|
||||||
mockAxios.addMockResponse('delete', `/executions/${executionId}`, {
|
mockAxios.addMockResponse('delete', `/executions/${executionId}`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
statusText: 'OK', // Added statusText
|
||||||
data: mockResponse,
|
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 { 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 { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { ErrorCode } from '../../../src/errors/error-codes.js';
|
import { ErrorCode } from '../../../src/errors/error-codes.js'; // Add .js
|
||||||
import { mockEnv } from '../../test-setup.js';
|
import { mockEnv } from '../../test-setup.js'; // Add .js
|
||||||
|
|
||||||
describe('Environment Configuration', () => {
|
describe('Environment Configuration', () => {
|
||||||
const originalEnv = process.env;
|
const originalEnv = process.env;
|
||||||
@@ -36,6 +36,8 @@ describe('Environment Configuration', () => {
|
|||||||
// Setup
|
// Setup
|
||||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
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_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
|
// Execute
|
||||||
const config = getEnvConfig();
|
const config = getEnvConfig();
|
||||||
@@ -44,6 +46,8 @@ describe('Environment Configuration', () => {
|
|||||||
expect(config).toEqual({
|
expect(config).toEqual({
|
||||||
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
||||||
n8nApiKey: 'test-api-key',
|
n8nApiKey: 'test-api-key',
|
||||||
|
n8nWebhookUsername: 'test-user',
|
||||||
|
n8nWebhookPassword: 'test-pass',
|
||||||
debug: false,
|
debug: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -52,6 +56,8 @@ describe('Environment Configuration', () => {
|
|||||||
// Setup
|
// Setup
|
||||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
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_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';
|
process.env[ENV_VARS.DEBUG] = 'true';
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
@@ -65,6 +71,8 @@ describe('Environment Configuration', () => {
|
|||||||
// Setup
|
// Setup
|
||||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
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_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';
|
process.env[ENV_VARS.DEBUG] = 'TRUE';
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
@@ -78,6 +86,8 @@ describe('Environment Configuration', () => {
|
|||||||
// Setup
|
// Setup
|
||||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
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_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';
|
process.env[ENV_VARS.DEBUG] = 'yes';
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
@@ -117,6 +127,8 @@ describe('Environment Configuration', () => {
|
|||||||
// Setup
|
// Setup
|
||||||
process.env[ENV_VARS.N8N_API_URL] = 'invalid-url';
|
process.env[ENV_VARS.N8N_API_URL] = 'invalid-url';
|
||||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
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
|
// Execute & Assert
|
||||||
expect(() => getEnvConfig()).toThrow(
|
expect(() => getEnvConfig()).toThrow(
|
||||||
@@ -131,6 +143,8 @@ describe('Environment Configuration', () => {
|
|||||||
// Setup
|
// Setup
|
||||||
process.env[ENV_VARS.N8N_API_URL] = 'http://localhost:5678/api/v1';
|
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_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
|
// Execute
|
||||||
const config = getEnvConfig();
|
const config = getEnvConfig();
|
||||||
@@ -143,6 +157,8 @@ describe('Environment Configuration', () => {
|
|||||||
// Setup
|
// Setup
|
||||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1/';
|
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_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
|
// Execute
|
||||||
const config = getEnvConfig();
|
const config = getEnvConfig();
|
||||||
@@ -155,14 +171,18 @@ describe('Environment Configuration', () => {
|
|||||||
describe('with mockEnv helper', () => {
|
describe('with mockEnv helper', () => {
|
||||||
// Using the mockEnv helper from test-setup
|
// Using the mockEnv helper from test-setup
|
||||||
mockEnv({
|
mockEnv({
|
||||||
[ENV_VARS.N8N_API_URL]: 'https://n8n.example.com/api/v1',
|
[ENV_VARS.N8N_API_URL]: 'https://mock.n8n.com/api/v1',
|
||||||
[ENV_VARS.N8N_API_KEY]: 'test-api-key',
|
[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', () => {
|
it('should use the mocked environment variables', () => {
|
||||||
const config = getEnvConfig();
|
const config = getEnvConfig();
|
||||||
expect(config.n8nApiUrl).toBe('https://n8n.example.com/api/v1');
|
expect(config.n8nApiUrl).toBe('https://mock.n8n.com/api/v1');
|
||||||
expect(config.n8nApiKey).toBe('test-api-key');
|
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';
|
import { describe, it, expect } from '@jest/globals';
|
||||||
|
// Import the actual functions from the source file with .js extension
|
||||||
// Simple functions to test without complex imports
|
import {
|
||||||
function getWorkflowResourceTemplateUri() {
|
getWorkflowResourceTemplateUri,
|
||||||
return 'n8n://workflows/{id}';
|
extractWorkflowIdFromUri
|
||||||
}
|
} from '../../../../src/resources/dynamic/workflow.js';
|
||||||
|
|
||||||
function extractWorkflowIdFromUri(uri: string): string | null {
|
|
||||||
const regex = /^n8n:\/\/workflows\/([^/]+)$/;
|
|
||||||
const match = uri.match(regex);
|
|
||||||
return match ? match[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Workflow Resource URI Functions', () => {
|
describe('Workflow Resource URI Functions', () => {
|
||||||
describe('getWorkflowResourceTemplateUri', () => {
|
describe('getWorkflowResourceTemplateUri', () => {
|
||||||
it('should return the correct URI template', () => {
|
it('should return the correct URI template', () => {
|
||||||
|
// Test the actual imported function
|
||||||
expect(getWorkflowResourceTemplateUri()).toBe('n8n://workflows/{id}');
|
expect(getWorkflowResourceTemplateUri()).toBe('n8n://workflows/{id}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('extractWorkflowIdFromUri', () => {
|
describe('extractWorkflowIdFromUri', () => {
|
||||||
it('should extract workflow ID from valid URI', () => {
|
it('should extract workflow ID from valid URI', () => {
|
||||||
|
// Test the actual imported function
|
||||||
expect(extractWorkflowIdFromUri('n8n://workflows/123abc')).toBe('123abc');
|
expect(extractWorkflowIdFromUri('n8n://workflows/123abc')).toBe('123abc');
|
||||||
expect(extractWorkflowIdFromUri('n8n://workflows/workflow-name-with-dashes')).toBe('workflow-name-with-dashes');
|
expect(extractWorkflowIdFromUri('n8n://workflows/workflow-name-with-dashes')).toBe('workflow-name-with-dashes');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for invalid URI formats', () => {
|
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://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('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,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"rootDir": "src",
|
"rootDir": ".", // Set rootDir to encompass both src and tests
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2020",
|
"ES2020" // Removed "DOM" as it's likely unused in a Node.js server
|
||||||
"DOM"
|
|
||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
"node"
|
"node"
|
||||||
@@ -28,4 +27,5 @@
|
|||||||
"build",
|
"build",
|
||||||
"**/*.test.ts"
|
"**/*.test.ts"
|
||||||
]
|
]
|
||||||
|
// Remove project reference
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user