From ecd9133437ca6ef3d28b5242d44240111e7d963b Mon Sep 17 00:00:00 2001 From: leonardsellem Date: Mon, 31 Mar 2025 11:20:05 +0200 Subject: [PATCH] feat: Refactor codebase, improve types, attempt test fixes --- README.md | 29 ++-- babel.config.cjs | 30 +++- jest.config.cjs | 17 +- package.json | 6 +- src/api/client.ts | 71 ++++---- src/api/n8n-client.ts | 155 ++---------------- src/config/environment.ts | 2 +- src/config/server.ts | 113 +++++-------- src/errors/index.ts | 4 +- src/resources/index.ts | 31 ++-- src/tools/execution/base-handler.ts | 19 ++- src/tools/execution/delete.ts | 11 +- src/tools/execution/get.ts | 11 +- src/tools/execution/list.ts | 70 +++++--- src/tools/execution/run.ts | 60 ++++--- src/tools/workflow/activate.ts | 10 +- src/tools/workflow/base-handler.ts | 19 ++- src/tools/workflow/create.ts | 54 +++--- src/tools/workflow/deactivate.ts | 10 +- src/tools/workflow/delete.ts | 10 +- src/tools/workflow/get.ts | 10 +- src/tools/workflow/list.ts | 21 ++- src/tools/workflow/update.ts | 83 +++++++--- src/types/index.ts | 98 +++++++++-- src/utils/execution-formatter.ts | 63 ++++--- src/utils/resource-formatter.ts | 16 +- tests/mocks/axios-mock.ts | 68 ++++---- tests/mocks/n8n-fixtures.ts | 1 + tests/tsconfig.json | 13 +- .../{client.test.ts.bak => client.test.ts} | 49 ++++-- tests/unit/api/simple-client.test.ts | 54 ------ ...onment.test.ts.bak => environment.test.ts} | 34 +++- tests/unit/config/simple-environment.test.ts | 97 ----------- tests/unit/resources/dynamic/workflow.test.ts | 26 +-- tests/unit/tools/workflow/list.test.ts | 154 +++++++++++++++++ tests/unit/tools/workflow/list.test.ts.bak | 25 --- tests/unit/tools/workflow/simple-tool.test.ts | 90 ---------- tsconfig.json | 6 +- 38 files changed, 829 insertions(+), 811 deletions(-) rename tests/unit/api/{client.test.ts.bak => client.test.ts} (85%) delete mode 100644 tests/unit/api/simple-client.test.ts rename tests/unit/config/{environment.test.ts.bak => environment.test.ts} (71%) delete mode 100644 tests/unit/config/simple-environment.test.ts create mode 100644 tests/unit/tools/workflow/list.test.ts delete mode 100644 tests/unit/tools/workflow/list.test.ts.bak delete mode 100644 tests/unit/tools/workflow/simple-tool.test.ts diff --git a/README.md b/README.md index 117b4b1..1089e49 100644 --- a/README.md +++ b/README.md @@ -124,30 +124,29 @@ The webhook authentication is handled automatically using the `N8N_WEBHOOK_USERN ### Workflow Management -- `workflow_list`: List all workflows -- `workflow_get`: Get details of a specific workflow -- `workflow_create`: Create a new workflow -- `workflow_update`: Update an existing workflow -- `workflow_delete`: Delete a workflow -- `workflow_activate`: Activate a workflow -- `workflow_deactivate`: Deactivate a workflow +- `list_workflows`: List all workflows +- `get_workflow`: Get details of a specific workflow +- `create_workflow`: Create a new workflow +- `update_workflow`: Update an existing workflow +- `delete_workflow`: Delete a workflow +- `activate_workflow`: Activate a workflow +- `deactivate_workflow`: Deactivate a workflow ### Execution Management -- `execution_run`: Execute a workflow via the API - `run_webhook`: Execute a workflow via a webhook -- `execution_get`: Get details of a specific execution -- `execution_list`: List executions for a workflow -- `execution_stop`: Stop a running execution +- `get_execution`: Get details of a specific execution +- `list_executions`: List executions for a workflow +- `delete_execution`: Delete a specific execution ## Resources The server provides the following resources: -- `n8n://workflows/list`: List of all workflows -- `n8n://workflow/{id}`: Details of a specific workflow -- `n8n://executions/{workflowId}`: List of executions for a workflow -- `n8n://execution/{id}`: Details of a specific execution +- `n8n://workflows`: List of all workflows (static resource) +- `n8n://workflows/{id}`: Details of a specific workflow (dynamic resource template) +- `n8n://executions/{id}`: Details of a specific execution (dynamic resource template) +- `n8n://execution-stats`: Summary statistics of recent executions (static resource) ## Development diff --git a/babel.config.cjs b/babel.config.cjs index a919d2c..b04e958 100644 --- a/babel.config.cjs +++ b/babel.config.cjs @@ -1,9 +1,27 @@ -module.exports = { - presets: [ +module.exports = (api) => { + // Check if running in test environment (NODE_ENV is set to 'test' by run-tests.js) + const isTest = api.env('test'); + + const presets = [ ['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript', - ], - plugins: [ - ['@babel/plugin-transform-modules-commonjs'] - ] + ]; + + const plugins = []; // Keep only one declaration + + // Only add the CJS transform plugin if NOT in test environment + if (!isTest) { + plugins.push(['@babel/plugin-transform-modules-commonjs']); + } + + // For Jest (test environment), ensure node_modules are not completely ignored + // if needed, but rely on transformIgnorePatterns in jest.config.cjs primarily. + // This is more of a fallback if transformIgnorePatterns isn't sufficient. + const ignore = isTest ? [] : [/node_modules/]; + + return { + presets, + plugins, + ignore, // Add ignore configuration back + }; }; diff --git a/jest.config.cjs b/jest.config.cjs index c2d1a56..f03df6c 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,17 +1,18 @@ module.exports = { - // Use commonjs style export - preset: 'ts-jest', + preset: 'ts-jest/presets/default-esm', // Use ESM preset for ts-jest testEnvironment: 'node', transform: { - '^.+\\.tsx?$': 'babel-jest', + // Use ts-jest transformer with ESM support and point to tests tsconfig + '^.+\\.tsx?$': ['ts-jest', { useESM: true, tsconfig: 'tests/tsconfig.json' }], }, - // Allow src and test folders to resolve imports properly - moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1', + extensionsToTreatAsEsm: ['.ts'], // Treat .ts as ESM + moduleNameMapper: { + // Recommended mapper for ts-jest ESM support: map extensionless paths to .js + '^(\\.{1,2}/.*)$': '$1.js', }, - // Handle the modelcontextprotocol SDK + // Handle the modelcontextprotocol SDK and other potential ESM dependencies transformIgnorePatterns: [ - "node_modules/(?!(@modelcontextprotocol)/)" + "/node_modules/(?!(@modelcontextprotocol/sdk|axios|another-esm-dep)/)" // Adjust as needed ], collectCoverage: true, coverageDirectory: 'coverage', diff --git a/package.json b/package.json index c0fc8c0..5dcd644 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "start": "node build/index.js", "dev": "tsc -w", "lint": "eslint --ext .ts src/", - "test": "node --experimental-vm-modules run-tests.js", - "test:watch": "node --experimental-vm-modules run-tests.js --watch", - "test:coverage": "node --experimental-vm-modules run-tests.js --coverage", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs --watch", + "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs --coverage", "prepare": "npm run build" }, "bin": { diff --git a/src/api/client.ts b/src/api/client.ts index c37d8cd..8cc4a7b 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -4,9 +4,10 @@ * This module provides a client for interacting with the n8n API. */ -import axios, { AxiosInstance } from 'axios'; -import { EnvConfig } from '../config/environment.js'; -import { handleAxiosError, N8nApiError } from '../errors/index.js'; +import axios, { AxiosInstance, AxiosResponse } from 'axios'; // Import AxiosResponse +import { EnvConfig } from '../config/environment.js'; // Already has .js +import { handleAxiosError, N8nApiError } from '../errors/index.js'; // Already has .js +import { Workflow, Execution, ExecutionRunData } from '../types/index.js'; // Already has .js /** * n8n API Client class for making requests to the n8n API @@ -86,9 +87,9 @@ export class N8nApiClient { * * @returns Array of workflow objects */ - async getWorkflows(): Promise { + async getWorkflows(): Promise { // Use specific type try { - const response = await this.axiosInstance.get('/workflows'); + const response: AxiosResponse<{ data: Workflow[] }> = await this.axiosInstance.get('/workflows'); return response.data.data || []; } catch (error) { throw handleAxiosError(error, 'Failed to fetch workflows'); @@ -101,9 +102,9 @@ export class N8nApiClient { * @param id Workflow ID * @returns Workflow object */ - async getWorkflow(id: string): Promise { + async getWorkflow(id: string): Promise { // Use specific type try { - const response = await this.axiosInstance.get(`/workflows/${id}`); + const response: AxiosResponse = await this.axiosInstance.get(`/workflows/${id}`); return response.data; } catch (error) { throw handleAxiosError(error, `Failed to fetch workflow ${id}`); @@ -115,9 +116,9 @@ export class N8nApiClient { * * @returns Array of execution objects */ - async getExecutions(): Promise { + async getExecutions(): Promise { // Use specific type try { - const response = await this.axiosInstance.get('/executions'); + const response: AxiosResponse<{ data: Execution[] }> = await this.axiosInstance.get('/executions'); return response.data.data || []; } catch (error) { throw handleAxiosError(error, 'Failed to fetch executions'); @@ -130,9 +131,9 @@ export class N8nApiClient { * @param id Execution ID * @returns Execution object */ - async getExecution(id: string): Promise { + async getExecution(id: string): Promise { // Use specific type try { - const response = await this.axiosInstance.get(`/executions/${id}`); + const response: AxiosResponse = await this.axiosInstance.get(`/executions/${id}`); return response.data; } catch (error) { throw handleAxiosError(error, `Failed to fetch execution ${id}`); @@ -144,11 +145,12 @@ export class N8nApiClient { * * @param id Workflow ID * @param data Optional data to pass to the workflow - * @returns Execution result + * @returns Execution result (structure might vary) */ - async executeWorkflow(id: string, data?: Record): Promise { + async executeWorkflow(id: string, data?: Record): Promise { // Use specific type try { - const response = await this.axiosInstance.post(`/workflows/${id}/execute`, data || {}); + // Assuming the response data directly matches ExecutionRunData or similar + const response: AxiosResponse = await this.axiosInstance.post(`/workflows/${id}/execute`, data || {}); return response.data; } catch (error) { throw handleAxiosError(error, `Failed to execute workflow ${id}`); @@ -161,7 +163,7 @@ export class N8nApiClient { * @param workflow Workflow object to create * @returns Created workflow */ - async createWorkflow(workflow: Record): Promise { + async createWorkflow(workflow: Partial): Promise { // Use specific types try { // Make sure settings property is present if (!workflow.settings) { @@ -181,15 +183,16 @@ export class N8nApiClient { delete workflowToCreate.id; // Remove id property if it exists delete workflowToCreate.createdAt; // Remove createdAt property if it exists delete workflowToCreate.updatedAt; // Remove updatedAt property if it exists - delete workflowToCreate.tags; // Remove tags property as it's read-only + delete workflowToCreate.tags; // Remove tags property if it exists and is read-only - // Log request for debugging - console.error('[DEBUG] Creating workflow with data:', JSON.stringify(workflowToCreate, null, 2)); + // Removed debug log + // console.error('[DEBUG] Creating workflow with data:', JSON.stringify(workflowToCreate, null, 2)); - const response = await this.axiosInstance.post('/workflows', workflowToCreate); + const response: AxiosResponse = await this.axiosInstance.post('/workflows', workflowToCreate); return response.data; } catch (error) { - console.error('[ERROR] Create workflow error:', error); + // Removed error log, handleAxiosError should suffice + // console.error('[ERROR] Create workflow error:', error); throw handleAxiosError(error, 'Failed to create workflow'); } } @@ -201,9 +204,9 @@ export class N8nApiClient { * @param workflow Updated workflow object * @returns Updated workflow */ - async updateWorkflow(id: string, workflow: Record): Promise { + async updateWorkflow(id: string, workflow: Partial): Promise { // Use specific types try { - const response = await this.axiosInstance.put(`/workflows/${id}`, workflow); + const response: AxiosResponse = await this.axiosInstance.put(`/workflows/${id}`, workflow); return response.data; } catch (error) { throw handleAxiosError(error, `Failed to update workflow ${id}`); @@ -214,11 +217,12 @@ export class N8nApiClient { * Delete a workflow * * @param id Workflow ID - * @returns Deleted workflow + * @returns Success indicator */ - async deleteWorkflow(id: string): Promise { + async deleteWorkflow(id: string): Promise<{ success: boolean }> { // Use specific type try { - const response = await this.axiosInstance.delete(`/workflows/${id}`); + // Assuming API returns { success: true } or similar on successful delete + const response: AxiosResponse<{ success: boolean }> = await this.axiosInstance.delete(`/workflows/${id}`); return response.data; } catch (error) { throw handleAxiosError(error, `Failed to delete workflow ${id}`); @@ -231,9 +235,10 @@ export class N8nApiClient { * @param id Workflow ID * @returns Activated workflow */ - async activateWorkflow(id: string): Promise { + async activateWorkflow(id: string): Promise { // Use specific type try { - const response = await this.axiosInstance.post(`/workflows/${id}/activate`); + // Assuming API returns the updated workflow object + const response: AxiosResponse = await this.axiosInstance.post(`/workflows/${id}/activate`); return response.data; } catch (error) { throw handleAxiosError(error, `Failed to activate workflow ${id}`); @@ -246,9 +251,10 @@ export class N8nApiClient { * @param id Workflow ID * @returns Deactivated workflow */ - async deactivateWorkflow(id: string): Promise { + async deactivateWorkflow(id: string): Promise { // Use specific type try { - const response = await this.axiosInstance.post(`/workflows/${id}/deactivate`); + // Assuming API returns the updated workflow object + const response: AxiosResponse = await this.axiosInstance.post(`/workflows/${id}/deactivate`); return response.data; } catch (error) { throw handleAxiosError(error, `Failed to deactivate workflow ${id}`); @@ -259,11 +265,12 @@ export class N8nApiClient { * Delete an execution * * @param id Execution ID - * @returns Deleted execution or success message + * @returns Success indicator */ - async deleteExecution(id: string): Promise { + async deleteExecution(id: string): Promise<{ success: boolean }> { // Use specific type try { - const response = await this.axiosInstance.delete(`/executions/${id}`); + // Assuming API returns { success: true } or similar on successful delete + const response: AxiosResponse<{ success: boolean }> = await this.axiosInstance.delete(`/executions/${id}`); return response.data; } catch (error) { throw handleAxiosError(error, `Failed to delete execution ${id}`); diff --git a/src/api/n8n-client.ts b/src/api/n8n-client.ts index 850f95c..d7ae1d8 100644 --- a/src/api/n8n-client.ts +++ b/src/api/n8n-client.ts @@ -1,152 +1,27 @@ /** - * n8n API Client Interface + * n8n API Client Export * - * This module defines interfaces and types for the n8n API client. + * This module primarily exports the N8nApiClient class. + * The N8nApiService wrapper was removed as it provided little additional value. */ import { N8nApiClient } from './client.js'; import { EnvConfig } from '../config/environment.js'; -import { Workflow, Execution } from '../types/index.js'; +// Re-export the client class +export { N8nApiClient } from './client.js'; + +// Keep the factory function for consistency, but have it return N8nApiClient directly /** - * n8n API service - provides functions for interacting with n8n API - */ -export class N8nApiService { - private client: N8nApiClient; - - /** - * Create a new n8n API service - * - * @param config Environment configuration - */ - constructor(config: EnvConfig) { - this.client = new N8nApiClient(config); - } - - /** - * Check connectivity to the n8n API - */ - async checkConnectivity(): Promise { - return this.client.checkConnectivity(); - } - - /** - * Get all workflows from n8n - * - * @returns Array of workflow objects - */ - async getWorkflows(): Promise { - return this.client.getWorkflows(); - } - - /** - * Get a specific workflow by ID - * - * @param id Workflow ID - * @returns Workflow object - */ - async getWorkflow(id: string): Promise { - 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): Promise { - return this.client.executeWorkflow(id, data); - } - - /** - * Create a new workflow - * - * @param workflow Workflow object to create - * @returns Created workflow - */ - async createWorkflow(workflow: Record): Promise { - 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): Promise { - return this.client.updateWorkflow(id, workflow); - } - - /** - * Delete a workflow - * - * @param id Workflow ID - * @returns Deleted workflow or success message - */ - async deleteWorkflow(id: string): Promise { - return this.client.deleteWorkflow(id); - } - - /** - * Activate a workflow - * - * @param id Workflow ID - * @returns Activated workflow - */ - async activateWorkflow(id: string): Promise { - return this.client.activateWorkflow(id); - } - - /** - * Deactivate a workflow - * - * @param id Workflow ID - * @returns Deactivated workflow - */ - async deactivateWorkflow(id: string): Promise { - return this.client.deactivateWorkflow(id); - } - - /** - * Get all workflow executions - * - * @returns Array of execution objects - */ - async getExecutions(): Promise { - return this.client.getExecutions(); - } - - /** - * Get a specific execution by ID - * - * @param id Execution ID - * @returns Execution object - */ - async getExecution(id: string): Promise { - return this.client.getExecution(id); - } - - /** - * Delete an execution - * - * @param id Execution ID - * @returns Deleted execution or success message - */ - async deleteExecution(id: string): Promise { - return this.client.deleteExecution(id); - } -} - -/** - * Create a new n8n API service + * Create a new n8n API client instance * * @param config Environment configuration - * @returns n8n API service + * @returns n8n API client instance */ -export function createApiService(config: EnvConfig): N8nApiService { - return new N8nApiService(config); +export function createN8nApiClient(config: EnvConfig): N8nApiClient { + return new N8nApiClient(config); } + +// Export the type alias for convenience if needed elsewhere, +// though direct use of N8nApiClient is preferred. +export type N8nApiService = N8nApiClient; diff --git a/src/config/environment.ts b/src/config/environment.ts index 3ffda15..0cdb28c 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -7,7 +7,7 @@ import dotenv from 'dotenv'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; -import { ErrorCode } from '../errors/error-codes.js'; +import { ErrorCode } from '../errors/error-codes.js'; // Already has .js // Environment variable names export const ENV_VARS = { diff --git a/src/config/server.ts b/src/config/server.ts index 9ce687a..af4618b 100644 --- a/src/config/server.ts +++ b/src/config/server.ts @@ -13,14 +13,16 @@ import { ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; -import { getEnvConfig } from './environment.js'; -import { setupWorkflowTools } from '../tools/workflow/index.js'; -import { setupExecutionTools } from '../tools/execution/index.js'; +import { getEnvConfig, EnvConfig } from './environment.js'; // Import EnvConfig +import { setupWorkflowTools, ListWorkflowsHandler, GetWorkflowHandler, CreateWorkflowHandler, UpdateWorkflowHandler, DeleteWorkflowHandler, ActivateWorkflowHandler, DeactivateWorkflowHandler } from '../tools/workflow/index.js'; +import { setupExecutionTools, ListExecutionsHandler, GetExecutionHandler, DeleteExecutionHandler, RunWebhookHandler } from '../tools/execution/index.js'; import { setupResourceHandlers } from '../resources/index.js'; -import { createApiService } from '../api/n8n-client.js'; +// Update imports to use N8nApiClient and its factory +import { createN8nApiClient, N8nApiClient } from '../api/n8n-client.js'; +import { McpError, ErrorCode } from '../errors/index.js'; // Import types -import { ToolCallResult } from '../types/index.js'; +import { ToolCallResult, BaseToolHandler } from '../types/index.js'; /** * Configure and return an MCP server instance with all tools and resources @@ -31,13 +33,13 @@ export async function configureServer(): Promise { // Get validated environment configuration const envConfig = getEnvConfig(); - // Create n8n API service - const apiService = createApiService(envConfig); + // Create n8n API client instance + const apiClient = createN8nApiClient(envConfig); // Use new factory function name // Verify n8n API connectivity try { console.error('Verifying n8n API connectivity...'); - await apiService.checkConnectivity(); + await apiClient.checkConnectivity(); // Use apiClient instance console.error(`Successfully connected to n8n API at ${envConfig.n8nApiUrl}`); } catch (error) { console.error('ERROR: Failed to connect to n8n API:', error instanceof Error ? error.message : error); @@ -58,10 +60,11 @@ export async function configureServer(): Promise { } ); - // Set up all request handlers + // Set up all request handlers, passing the single apiClient instance where needed setupToolListRequestHandler(server); - setupToolCallRequestHandler(server); - setupResourceHandlers(server, envConfig); + setupToolCallRequestHandler(server, apiClient); // Pass apiClient + // Pass envConfig to resource handlers as originally intended + setupResourceHandlers(server, envConfig); return server; } @@ -87,72 +90,44 @@ function setupToolListRequestHandler(server: Server): void { * Set up the tool call request handler for the server * * @param server MCP server instance + * @param apiClient The shared N8nApiClient instance */ -function setupToolCallRequestHandler(server: Server): void { +// Update function signature to accept N8nApiClient +function setupToolCallRequestHandler(server: Server, apiClient: N8nApiClient): void { + + // Map tool names to their handler classes - Update constructor signature type + // The constructor now expects N8nApiClient (which is aliased as N8nApiService) + const toolHandlerMap: Record BaseToolHandler> = { + 'list_workflows': ListWorkflowsHandler, + 'get_workflow': GetWorkflowHandler, + 'create_workflow': CreateWorkflowHandler, + 'update_workflow': UpdateWorkflowHandler, + 'delete_workflow': DeleteWorkflowHandler, + 'activate_workflow': ActivateWorkflowHandler, + 'deactivate_workflow': DeactivateWorkflowHandler, + 'list_executions': ListExecutionsHandler, + 'get_execution': GetExecutionHandler, + 'delete_execution': DeleteExecutionHandler, + 'run_webhook': RunWebhookHandler, + // Add other tools here + }; + server.setRequestHandler(CallToolRequestSchema, async (request) => { const toolName = request.params.name; const args = request.params.arguments || {}; - let result: ToolCallResult; - try { - // Import handlers - const { - ListWorkflowsHandler, - GetWorkflowHandler, - CreateWorkflowHandler, - UpdateWorkflowHandler, - DeleteWorkflowHandler, - ActivateWorkflowHandler, - DeactivateWorkflowHandler - } = await import('../tools/workflow/index.js'); - - const { - 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}`); + const HandlerClass = toolHandlerMap[toolName]; + + if (!HandlerClass) { + throw new McpError(ErrorCode.NotImplemented, `Unknown tool: ${toolName}`); // Use NotImplemented } - // Converting to MCP SDK expected format + // Pass the apiClient instance to the constructor + const handler = new HandlerClass(apiClient); + const result: ToolCallResult = await handler.execute(args); + + // Return result in MCP SDK expected format return { content: result.content, isError: result.isError, diff --git a/src/errors/index.ts b/src/errors/index.ts index 5426bff..f2cafb4 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -5,8 +5,8 @@ * for the n8n MCP Server. */ -import { McpError as SdkMcpError } from '@modelcontextprotocol/sdk/types.js'; -import { ErrorCode } from './error-codes.js'; +import { McpError as SdkMcpError } from '@modelcontextprotocol/sdk/types.js'; // Add .js back +import { ErrorCode } from './error-codes.js'; // Add .js back // Re-export McpError from SDK export { McpError } from '@modelcontextprotocol/sdk/types.js'; diff --git a/src/resources/index.ts b/src/resources/index.ts index ebf4fb6..db65cd7 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -10,8 +10,8 @@ import { ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; -import { EnvConfig } from '../config/environment.js'; -import { createApiService } from '../api/n8n-client.js'; +import { EnvConfig } from '../config/environment.js'; // Re-add EnvConfig import +import { createN8nApiClient, N8nApiClient } from '../api/n8n-client.js'; // Use correct factory import import { McpError, ErrorCode } from '../errors/index.js'; // Import static resource handlers @@ -46,12 +46,13 @@ import { * @param server MCP server instance * @param envConfig Environment configuration */ -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 - setupStaticResources(server, envConfig); + setupStaticResources(server, envConfig); // Pass envConfig // Set up dynamic resources - setupDynamicResources(server, envConfig); + setupDynamicResources(server, envConfig); // Pass envConfig } /** @@ -60,8 +61,10 @@ export function setupResourceHandlers(server: Server, envConfig: EnvConfig): voi * @param server MCP server instance * @param envConfig Environment configuration */ -function setupStaticResources(server: Server, envConfig: EnvConfig): void { - const apiService = createApiService(envConfig); +// Revert function signature to accept 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 () => { // Return all available static resources @@ -80,8 +83,10 @@ function setupStaticResources(server: Server, envConfig: EnvConfig): void { * @param server MCP server instance * @param envConfig Environment configuration */ -function setupDynamicResources(server: Server, envConfig: EnvConfig): void { - const apiService = createApiService(envConfig); +// Revert function signature to accept 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 () => { // Return all available dynamic resource templates @@ -100,7 +105,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void { try { // Handle static resources if (uri === getWorkflowsResourceUri()) { - const content = await getWorkflowsResource(apiService); + const content = await getWorkflowsResource(apiClient); // Use apiClient instance return { contents: [ { @@ -113,7 +118,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void { } if (uri === getExecutionStatsResourceUri()) { - const content = await getExecutionStatsResource(apiService); + const content = await getExecutionStatsResource(apiClient); // Use apiClient instance return { contents: [ { @@ -128,7 +133,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void { // Handle dynamic resources const workflowId = extractWorkflowIdFromUri(uri); if (workflowId) { - const content = await getWorkflowResource(apiService, workflowId); + const content = await getWorkflowResource(apiClient, workflowId); // Use apiClient instance return { contents: [ { @@ -142,7 +147,7 @@ function setupDynamicResources(server: Server, envConfig: EnvConfig): void { const executionId = extractExecutionIdFromUri(uri); if (executionId) { - const content = await getExecutionResource(apiService, executionId); + const content = await getExecutionResource(apiClient, executionId); // Use apiClient instance return { contents: [ { diff --git a/src/tools/execution/base-handler.ts b/src/tools/execution/base-handler.ts index 166ff83..bb00684 100644 --- a/src/tools/execution/base-handler.ts +++ b/src/tools/execution/base-handler.ts @@ -4,16 +4,23 @@ * This module provides a base handler for execution-related tools. */ -import { ToolCallResult } from '../../types/index.js'; -import { N8nApiError } from '../../errors/index.js'; -import { createApiService } from '../../api/n8n-client.js'; -import { getEnvConfig } from '../../config/environment.js'; +import { ToolCallResult, BaseToolHandler } from '../../types/index.js'; // Already has .js +import { N8nApiError } from '../../errors/index.js'; // Already has .js +import { N8nApiService } from '../../api/n8n-client.js'; // Already has .js /** * Base class for execution tool handlers */ -export abstract class BaseExecutionToolHandler { - protected apiService = createApiService(getEnvConfig()); +export abstract class BaseExecutionToolHandler implements BaseToolHandler { // Implement BaseToolHandler + protected apiService: N8nApiService; // Declare apiService property + + /** + * Constructor to inject the API service + * @param apiService Instance of N8nApiService + */ + constructor(apiService: N8nApiService) { + this.apiService = apiService; + } /** * Validate and execute the tool diff --git a/src/tools/execution/delete.ts b/src/tools/execution/delete.ts index 8bd53fb..a5249a3 100644 --- a/src/tools/execution/delete.ts +++ b/src/tools/execution/delete.ts @@ -9,6 +9,11 @@ import { ToolCallResult, ToolDefinition } from '../../types/index.js'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; import { ErrorCode } from '../../errors/error-codes.js'; +// Define specific type for delete arguments +interface DeleteExecutionArgs { + executionId: string; +} + /** * Handler for the delete_execution tool */ @@ -19,8 +24,8 @@ export class DeleteExecutionHandler extends BaseExecutionToolHandler { * @param args Tool arguments (executionId) * @returns Result of the deletion operation */ - async execute(args: Record): Promise { - return this.handleExecution(async () => { + async execute(args: DeleteExecutionArgs): Promise { // Use specific args type + return this.handleExecution(async (args) => { // Pass args to handler // Validate required parameters if (!args.executionId) { throw new McpError( @@ -39,7 +44,7 @@ export class DeleteExecutionHandler extends BaseExecutionToolHandler { { id: executionId, deleted: true }, `Successfully deleted execution with ID: ${executionId}` ); - }, args); + }, args); // Pass args to handleExecution } } diff --git a/src/tools/execution/get.ts b/src/tools/execution/get.ts index 8888e4c..1436aba 100644 --- a/src/tools/execution/get.ts +++ b/src/tools/execution/get.ts @@ -10,6 +10,11 @@ import { McpError } from '@modelcontextprotocol/sdk/types.js'; import { ErrorCode } from '../../errors/error-codes.js'; import { formatExecutionDetails } from '../../utils/execution-formatter.js'; +// Define specific type for get arguments +interface GetExecutionArgs { + executionId: string; +} + /** * Handler for the get_execution tool */ @@ -20,8 +25,8 @@ export class GetExecutionHandler extends BaseExecutionToolHandler { * @param args Tool arguments (executionId) * @returns Execution details */ - async execute(args: Record): Promise { - return this.handleExecution(async () => { + async execute(args: GetExecutionArgs): Promise { // Use specific args type + return this.handleExecution(async (args) => { // Pass args to handler // Validate required parameters if (!args.executionId) { throw new McpError( @@ -40,7 +45,7 @@ export class GetExecutionHandler extends BaseExecutionToolHandler { formattedExecution, `Execution Details for ID: ${args.executionId}` ); - }, args); + }, args); // Pass args to handleExecution } } diff --git a/src/tools/execution/list.ts b/src/tools/execution/list.ts index 7176500..bc3d9c5 100644 --- a/src/tools/execution/list.ts +++ b/src/tools/execution/list.ts @@ -5,9 +5,18 @@ */ import { BaseExecutionToolHandler } from './base-handler.js'; -import { ToolCallResult, ToolDefinition, Execution } from '../../types/index.js'; +import { ToolCallResult, ToolDefinition, Execution } from '../../types/index.js'; // Import Execution type import { formatExecutionSummary, summarizeExecutions } from '../../utils/execution-formatter.js'; +// Define specific type for list arguments based on ToolDefinition +interface ListExecutionsArgs { + workflowId?: string; + status?: 'success' | 'error' | 'waiting' | 'canceled'; // Use specific statuses if known + limit?: number; + lastId?: string; // Note: n8n API might not support cursor pagination with lastId easily + includeSummary?: boolean; +} + /** * Handler for the list_executions tool */ @@ -15,14 +24,16 @@ export class ListExecutionsHandler extends BaseExecutionToolHandler { /** * Execute the tool * - * @param args Tool arguments (workflowId, status, limit, lastId) + * @param args Tool arguments (workflowId, status, limit, lastId, includeSummary) * @returns List of executions */ - async execute(args: Record): Promise { - return this.handleExecution(async () => { - const executions = await this.apiService.getExecutions(); + async execute(args: ListExecutionsArgs): Promise { // Use specific args type + return this.handleExecution(async (args) => { // Pass args to handler + // Fetch all executions first (n8n API might require filtering via query params) + // TODO: Update apiService.getExecutions to accept filter parameters if possible + const executions: Execution[] = await this.apiService.getExecutions(); - // Apply filters if provided + // Apply filters locally for now let filteredExecutions = executions; // Filter by workflow ID if provided @@ -39,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; - filteredExecutions = filteredExecutions.slice(0, limit); + // Ensure limit is applied correctly after potential pagination logic + filteredExecutions = filteredExecutions.slice(0, limit); // Format the executions for display const formattedExecutions = filteredExecutions.map((execution: Execution) => formatExecutionSummary(execution) ); - // Generate summary if requested + // Generate summary if requested (based on the initially fetched, unfiltered list) let summary = undefined; if (args.includeSummary) { - summary = summarizeExecutions(executions); + // Summarize based on the *original* list before filtering/limiting for accurate stats + summary = summarizeExecutions(executions); } // Prepare response data const responseData = { - executions: formattedExecutions, + // Return the filtered and limited list + executions: formattedExecutions, summary: summary, - total: formattedExecutions.length, - filtered: args.workflowId || args.status ? true : false + count: formattedExecutions.length, // Count of returned executions + // Indicate if filters were applied + filtersApplied: args.workflowId || args.status ? true : false, + // Note: totalAvailable might be misleading if pagination isn't fully implemented + totalAvailable: executions.length, }; return this.formatSuccess( responseData, - `Found ${formattedExecutions.length} execution(s)` + `Found ${formattedExecutions.length} execution(s)` + (responseData.filtersApplied ? ' matching filters.' : '.') ); - }, args); + }, args); // Pass args to handleExecution } } @@ -78,7 +98,7 @@ export class ListExecutionsHandler extends BaseExecutionToolHandler { export function getListExecutionsToolDefinition(): ToolDefinition { return { name: 'list_executions', - description: 'Retrieve a list of workflow executions from n8n', + description: 'Retrieve a list of workflow executions from n8n, with optional filtering.', inputSchema: { type: 'object', properties: { @@ -88,19 +108,23 @@ export function getListExecutionsToolDefinition(): ToolDefinition { }, status: { type: 'string', - description: 'Optional status to filter by (success, error, waiting, or canceled)', + description: 'Optional status to filter by (e.g., success, error, waiting)', + // Consider using enum if statuses are fixed: + // enum: ['success', 'error', 'waiting', 'canceled'] }, limit: { type: 'number', - description: 'Maximum number of executions to return', - }, - lastId: { - type: 'string', - description: 'ID of the last execution for pagination', + description: 'Maximum number of executions to return (default: all matching)', }, + // lastId is hard to implement without API support for cursor pagination + // lastId: { + // type: 'string', + // description: 'ID of the last execution for pagination (if supported)', + // }, includeSummary: { type: 'boolean', - description: 'Include summary statistics about executions', + description: 'Include summary statistics about all executions (before filtering/limiting)', + default: false, }, }, required: [], diff --git a/src/tools/execution/run.ts b/src/tools/execution/run.ts index 4e8dc2e..9273b4d 100644 --- a/src/tools/execution/run.ts +++ b/src/tools/execution/run.ts @@ -4,9 +4,9 @@ * This module provides a tool for running n8n workflows via webhooks. */ -import axios from 'axios'; +import axios, { AxiosRequestConfig } from 'axios'; // Import AxiosRequestConfig import { z } from 'zod'; -import { ToolCallResult } from '../../types/index.js'; +import { ToolCallResult, ToolDefinition } from '../../types/index.js'; // Import ToolDefinition import { BaseExecutionToolHandler } from './base-handler.js'; import { N8nApiError } from '../../errors/index.js'; import { getEnvConfig } from '../../config/environment.js'; @@ -33,7 +33,8 @@ export class RunWebhookHandler extends BaseExecutionToolHandler { /** * Tool definition for execution via webhook */ - public static readonly inputSchema = runWebhookSchema; + // Note: Static properties on classes aren't directly usable for instance methods in TS + // The schema is used within the execute method instead. /** * Extract N8N base URL from N8N API URL by removing /api/v1 @@ -60,10 +61,11 @@ export class RunWebhookHandler extends BaseExecutionToolHandler { * @param args Tool arguments * @returns Tool call result */ - async execute(args: Record): Promise { - return this.handleExecution(async (args) => { - // Parse and validate arguments - const params = runWebhookSchema.parse(args); + async execute(args: RunWebhookParams): Promise { // Use specific args type + return this.handleExecution(async (args) => { // Pass args to handler + // Parse and validate arguments using the Zod schema + // This ensures args conforms to RunWebhookParams + const params = runWebhookSchema.parse(args); // Get environment config for auth credentials const config = getEnvConfig(); @@ -71,11 +73,13 @@ export class RunWebhookHandler extends BaseExecutionToolHandler { try { // Get the webhook URL with the proper prefix const baseUrl = this.getN8nBaseUrl(); - const webhookPath = `webhook/${params.workflowName}`; + // Ensure workflowName doesn't contain slashes that could break the URL path + const safeWorkflowName = params.workflowName.replace(/\//g, ''); + const webhookPath = `webhook/${safeWorkflowName}`; const webhookUrl = new URL(webhookPath, baseUrl).toString(); // Prepare request config with basic auth from environment - const requestConfig: any = { + const requestConfig: AxiosRequestConfig = { // Use AxiosRequestConfig type headers: { 'Content-Type': 'application/json', ...(params.headers || {}) @@ -97,7 +101,7 @@ export class RunWebhookHandler extends BaseExecutionToolHandler { return this.formatSuccess({ status: response.status, statusText: response.statusText, - data: response.data + data: response.data // Assuming response.data is JSON-serializable }, 'Webhook executed successfully'); } catch (error) { // Handle error from the webhook request @@ -106,20 +110,29 @@ export class RunWebhookHandler extends BaseExecutionToolHandler { if (error.response) { errorMessage = `Webhook execution failed with status ${error.response.status}: ${error.response.statusText}`; - if (error.response.data) { - return this.formatError(new N8nApiError( - `${errorMessage}\n\n${JSON.stringify(error.response.data, null, 2)}`, - error.response.status - )); + // Attempt to stringify response data safely + let responseDataStr = ''; + try { + responseDataStr = JSON.stringify(error.response.data, null, 2); + } catch (stringifyError) { + responseDataStr = '[Could not stringify response data]'; } + // Add explicit check for error.response before accessing status + const statusCode = error.response?.status || 500; + return this.formatError(new N8nApiError( + `${errorMessage}\n\n${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 */ -export function getRunWebhookToolDefinition() { +export function getRunWebhookToolDefinition(): ToolDefinition { // Add return type return { name: 'run_webhook', description: 'Execute a workflow via webhook with optional input data', @@ -141,14 +154,17 @@ export function getRunWebhookToolDefinition() { }, data: { type: 'object', - description: 'Input data to pass to the webhook' + description: 'Input data (JSON object) to pass to the webhook', + // Indicate that properties can be anything for an object + additionalProperties: true, }, headers: { type: 'object', - description: 'Additional headers to send with the request' + description: 'Additional headers (key-value pairs) to send with the request', + additionalProperties: { type: 'string' }, } }, required: ['workflowName'] } }; -} \ No newline at end of file +} diff --git a/src/tools/workflow/activate.ts b/src/tools/workflow/activate.ts index eb1b8c3..fbbc32d 100644 --- a/src/tools/workflow/activate.ts +++ b/src/tools/workflow/activate.ts @@ -15,12 +15,12 @@ export class ActivateWorkflowHandler extends BaseWorkflowToolHandler { /** * Execute the tool * - * @param args Tool arguments containing workflowId - * @returns Activation confirmation - */ - async execute(args: Record): Promise { + * @param args Tool arguments containing workflowId + * @returns Activated workflow details + */ + async execute(args: { workflowId: string }): Promise { // Use specific type for args return this.handleExecution(async (args) => { - const { workflowId } = args; + const { workflowId } = args; // Destructuring remains the same if (!workflowId) { throw new N8nApiError('Missing required parameter: workflowId'); diff --git a/src/tools/workflow/base-handler.ts b/src/tools/workflow/base-handler.ts index d0e777d..dc4e16b 100644 --- a/src/tools/workflow/base-handler.ts +++ b/src/tools/workflow/base-handler.ts @@ -4,16 +4,23 @@ * This module provides a base handler for workflow-related tools. */ -import { ToolCallResult } from '../../types/index.js'; -import { N8nApiError } from '../../errors/index.js'; -import { createApiService } from '../../api/n8n-client.js'; -import { getEnvConfig } from '../../config/environment.js'; +import { ToolCallResult, BaseToolHandler } from '../../types/index.js'; // Already has .js +import { N8nApiError } from '../../errors/index.js'; // Already has .js +import { N8nApiService } from '../../api/n8n-client.js'; // Already has .js /** * Base class for workflow tool handlers */ -export abstract class BaseWorkflowToolHandler { - protected apiService = createApiService(getEnvConfig()); +export abstract class BaseWorkflowToolHandler implements BaseToolHandler { // Implement BaseToolHandler + protected apiService: N8nApiService; // Declare apiService property + + /** + * Constructor to inject the API service + * @param apiService Instance of N8nApiService + */ + constructor(apiService: N8nApiService) { + this.apiService = apiService; + } /** * Validate and execute the tool diff --git a/src/tools/workflow/create.ts b/src/tools/workflow/create.ts index a872ee2..4127662 100644 --- a/src/tools/workflow/create.ts +++ b/src/tools/workflow/create.ts @@ -5,9 +5,18 @@ */ import { BaseWorkflowToolHandler } from './base-handler.js'; -import { ToolCallResult, ToolDefinition } from '../../types/index.js'; +import { ToolCallResult, ToolDefinition, Workflow, N8nNode, N8nConnection } from '../../types/index.js'; // Import specific types import { N8nApiError } from '../../errors/index.js'; +// Define specific type for create arguments based on ToolDefinition +interface CreateWorkflowArgs { + name: string; + nodes?: N8nNode[]; + connections?: N8nConnection; + active?: boolean; + tags?: string[]; +} + /** * Handler for the create_workflow tool */ @@ -18,43 +27,44 @@ export class CreateWorkflowHandler extends BaseWorkflowToolHandler { * @param args Tool arguments containing workflow details * @returns Created workflow information */ - async execute(args: Record): Promise { + async execute(args: CreateWorkflowArgs): Promise { // Use specific args type return this.handleExecution(async (args) => { const { name, nodes, connections, active, tags } = args; 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)) { throw new N8nApiError('Parameter "nodes" must be an array'); } - - // Validate connections if provided if (connections && typeof connections !== 'object') { throw new N8nApiError('Parameter "connections" must be an object'); } + if (tags && !Array.isArray(tags)) { + throw new N8nApiError('Parameter "tags" must be an array of strings'); + } - // Prepare workflow object - const workflowData: Record = { + // Prepare workflow object using Partial for the API call + const workflowData: Partial = { name, - active: active === true, // Default to false if not specified + active: active === true, // Default to false if not specified or undefined + nodes: nodes || [], // Default to empty array if not provided + connections: connections || {}, // Default to empty object if not provided + tags: tags || [], // Default to empty array if not provided }; - // Add optional fields if provided - if (nodes) workflowData.nodes = nodes; - if (connections) workflowData.connections = connections; - if (tags) workflowData.tags = tags; - // Create the workflow - const workflow = await this.apiService.createWorkflow(workflowData); + const createdWorkflow = await this.apiService.createWorkflow(workflowData); + // Return summary of the created workflow return this.formatSuccess( { - id: workflow.id, - name: workflow.name, - active: workflow.active + id: createdWorkflow.id, + name: createdWorkflow.name, + active: createdWorkflow.active }, `Workflow created successfully` ); @@ -80,18 +90,18 @@ export function getCreateWorkflowToolDefinition(): ToolDefinition { }, nodes: { type: 'array', - description: 'Array of node objects that define the workflow', + description: 'Array of node objects (N8nNode structure) defining the workflow', items: { - type: 'object', + type: 'object', // Ideally, reference a detailed N8nNode schema here }, }, connections: { type: 'object', - description: 'Connection mappings between nodes', + description: 'Connection mappings between nodes (N8nConnection structure)', }, active: { type: 'boolean', - description: 'Whether the workflow should be active upon creation', + description: 'Whether the workflow should be active upon creation (defaults to false)', }, tags: { type: 'array', diff --git a/src/tools/workflow/deactivate.ts b/src/tools/workflow/deactivate.ts index d8009ae..e656678 100644 --- a/src/tools/workflow/deactivate.ts +++ b/src/tools/workflow/deactivate.ts @@ -15,12 +15,12 @@ export class DeactivateWorkflowHandler extends BaseWorkflowToolHandler { /** * Execute the tool * - * @param args Tool arguments containing workflowId - * @returns Deactivation confirmation - */ - async execute(args: Record): Promise { + * @param args Tool arguments containing workflowId + * @returns Deactivated workflow details + */ + async execute(args: { workflowId: string }): Promise { // Use specific type for args return this.handleExecution(async (args) => { - const { workflowId } = args; + const { workflowId } = args; // Destructuring remains the same if (!workflowId) { throw new N8nApiError('Missing required parameter: workflowId'); diff --git a/src/tools/workflow/delete.ts b/src/tools/workflow/delete.ts index 4864967..340359e 100644 --- a/src/tools/workflow/delete.ts +++ b/src/tools/workflow/delete.ts @@ -15,12 +15,12 @@ export class DeleteWorkflowHandler extends BaseWorkflowToolHandler { /** * Execute the tool * - * @param args Tool arguments containing workflowId - * @returns Deletion confirmation - */ - async execute(args: Record): Promise { + * @param args Tool arguments containing workflowId + * @returns Success message + */ + async execute(args: { workflowId: string }): Promise { // Use specific type for args return this.handleExecution(async (args) => { - const { workflowId } = args; + const { workflowId } = args; // Destructuring remains the same if (!workflowId) { throw new N8nApiError('Missing required parameter: workflowId'); diff --git a/src/tools/workflow/get.ts b/src/tools/workflow/get.ts index dda85a7..48ccc5b 100644 --- a/src/tools/workflow/get.ts +++ b/src/tools/workflow/get.ts @@ -15,12 +15,12 @@ export class GetWorkflowHandler extends BaseWorkflowToolHandler { /** * Execute the tool * - * @param args Tool arguments containing workflowId - * @returns Workflow details - */ - async execute(args: Record): Promise { + * @param args Tool arguments containing workflowId + * @returns Workflow details + */ + async execute(args: { workflowId: string }): Promise { // Use specific type for args return this.handleExecution(async (args) => { - const { workflowId } = args; + const { workflowId } = args; // Destructuring remains the same if (!workflowId) { throw new N8nApiError('Missing required parameter: workflowId'); diff --git a/src/tools/workflow/list.ts b/src/tools/workflow/list.ts index 942442c..20cb88c 100644 --- a/src/tools/workflow/list.ts +++ b/src/tools/workflow/list.ts @@ -14,12 +14,17 @@ export class ListWorkflowsHandler extends BaseWorkflowToolHandler { /** * Execute the tool * - * @param args Tool arguments - * @returns List of workflows - */ - async execute(args: Record): Promise { - return this.handleExecution(async () => { - const workflows = await this.apiService.getWorkflows(); + * @param args Tool arguments (expecting optional 'active' boolean) + * @returns List of workflows + */ + async execute(args: { active?: boolean }): Promise { // Use specific type for args + return this.handleExecution(async (args) => { // Pass args to the handler + let workflows: Workflow[] = await this.apiService.getWorkflows(); // Add type annotation + + // Apply filtering if the 'active' argument is provided + if (args && typeof args.active === 'boolean') { + workflows = workflows.filter((workflow: Workflow) => workflow.active === args.active); + } // Format the workflows for display const formattedWorkflows = workflows.map((workflow: Workflow) => ({ @@ -31,9 +36,9 @@ export class ListWorkflowsHandler extends BaseWorkflowToolHandler { return this.formatSuccess( formattedWorkflows, - `Found ${formattedWorkflows.length} workflow(s)` + `Found ${formattedWorkflows.length} workflow(s)` + (typeof args?.active === 'boolean' ? ` (filtered by active=${args.active})` : '') ); - }, args); + }, args); // Pass args to handleExecution } } diff --git a/src/tools/workflow/update.ts b/src/tools/workflow/update.ts index 0f986dd..0aa543e 100644 --- a/src/tools/workflow/update.ts +++ b/src/tools/workflow/update.ts @@ -5,9 +5,16 @@ */ import { BaseWorkflowToolHandler } from './base-handler.js'; -import { ToolCallResult, ToolDefinition } from '../../types/index.js'; +import { ToolCallResult, ToolDefinition, Workflow, N8nNode, N8nConnection } from '../../types/index.js'; // Import specific types import { N8nApiError } from '../../errors/index.js'; +// Define specific type for update arguments +// Intersect with Partial to allow any workflow property update +// Requires workflowId to identify the workflow +interface UpdateWorkflowArgs extends Partial { + workflowId: string; +} + /** * Handler for the update_workflow tool */ @@ -18,51 +25,73 @@ export class UpdateWorkflowHandler extends BaseWorkflowToolHandler { * @param args Tool arguments containing workflow updates * @returns Updated workflow information */ - async execute(args: Record): Promise { + async execute(args: UpdateWorkflowArgs): Promise { // Use specific args type return this.handleExecution(async (args) => { - const { workflowId, name, nodes, connections, active, tags } = args; + const { workflowId, name, nodes, connections, active, tags, settings } = args; // Destructure known properties if (!workflowId) { throw new N8nApiError('Missing required parameter: workflowId'); } - // Validate nodes if provided + // Basic validation (more robust validation could use Zod or similar) if (nodes && !Array.isArray(nodes)) { throw new N8nApiError('Parameter "nodes" must be an array'); } - - // Validate connections if provided if (connections && typeof connections !== 'object') { throw new N8nApiError('Parameter "connections" must be an object'); } + if (tags && !Array.isArray(tags)) { + throw new N8nApiError('Parameter "tags" must be an array of strings'); + } + if (settings && typeof settings !== 'object') { + throw new N8nApiError('Parameter "settings" must be an object'); + } - // Get the current workflow to update - const currentWorkflow = await this.apiService.getWorkflow(workflowId); + // Get the current workflow to compare changes (optional, but good for summary) + let currentWorkflow: Workflow | null = null; + try { + currentWorkflow = await this.apiService.getWorkflow(workflowId); + } catch (error) { + // Handle case where workflow to update doesn't exist + if (error instanceof N8nApiError && error.message.includes('not found')) { // Adjust error check as needed + throw new N8nApiError(`Workflow with ID "${workflowId}" not found.`); + } + throw error; // Re-throw other errors + } - // Prepare update object with changes - const workflowData: Record = { ...currentWorkflow }; - - // Update fields if provided - if (name !== undefined) workflowData.name = name; - if (nodes !== undefined) workflowData.nodes = nodes; - if (connections !== undefined) workflowData.connections = connections; - if (active !== undefined) workflowData.active = active; - if (tags !== undefined) workflowData.tags = tags; + // Prepare update object with only the provided changes + const workflowUpdateData: Partial = {}; + if (name !== undefined) workflowUpdateData.name = name; + if (nodes !== undefined) workflowUpdateData.nodes = nodes; + if (connections !== undefined) workflowUpdateData.connections = connections; + if (active !== undefined) workflowUpdateData.active = active; + if (tags !== undefined) workflowUpdateData.tags = tags; + if (settings !== undefined) workflowUpdateData.settings = settings; + // Add other updatable fields from Workflow interface if needed + // Check if there are any actual changes to send + if (Object.keys(workflowUpdateData).length === 0) { + return this.formatSuccess( + { id: workflowId, name: currentWorkflow.name, active: currentWorkflow.active }, + `No update parameters provided for workflow ${workflowId}. No changes made.` + ); + } + // Update the workflow - const updatedWorkflow = await this.apiService.updateWorkflow(workflowId, workflowData); + const updatedWorkflow = await this.apiService.updateWorkflow(workflowId, workflowUpdateData); - // Build a summary of changes + // Build a summary of changes (optional) const changesArray = []; if (name !== undefined && name !== currentWorkflow.name) changesArray.push(`name: "${currentWorkflow.name}" → "${name}"`); if (active !== undefined && active !== currentWorkflow.active) changesArray.push(`active: ${currentWorkflow.active} → ${active}`); if (nodes !== undefined) changesArray.push('nodes updated'); if (connections !== undefined) changesArray.push('connections updated'); if (tags !== undefined) changesArray.push('tags updated'); + if (settings !== undefined) changesArray.push('settings updated'); const changesSummary = changesArray.length > 0 ? `Changes: ${changesArray.join(', ')}` - : 'No changes were made'; + : 'No effective changes were made (values might be the same as current)'; return this.formatSuccess( { @@ -84,7 +113,7 @@ export class UpdateWorkflowHandler extends BaseWorkflowToolHandler { export function getUpdateWorkflowToolDefinition(): ToolDefinition { return { name: 'update_workflow', - description: 'Update an existing workflow in n8n', + description: 'Update an existing workflow in n8n. Provide only the fields you want to change.', inputSchema: { type: 'object', properties: { @@ -98,14 +127,14 @@ export function getUpdateWorkflowToolDefinition(): ToolDefinition { }, nodes: { type: 'array', - description: 'Updated array of node objects that define the workflow', + description: 'Updated array of node objects (N8nNode structure) defining the workflow', items: { - type: 'object', + type: 'object', // Ideally, reference a detailed N8nNode schema here }, }, connections: { type: 'object', - description: 'Updated connection mappings between nodes', + description: 'Updated connection mappings between nodes (N8nConnection structure)', }, active: { type: 'boolean', @@ -118,8 +147,12 @@ export function getUpdateWorkflowToolDefinition(): ToolDefinition { type: 'string', }, }, + settings: { + type: 'object', + description: 'Updated workflow settings (WorkflowSettings structure)', + }, }, - required: ['workflowId'], + required: ['workflowId'], // Only ID is strictly required to identify the workflow }, }; } diff --git a/src/types/index.ts b/src/types/index.ts index db67224..b6bb92f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -25,31 +25,107 @@ export interface ToolCallResult { isError?: boolean; } -// Type for n8n workflow object +// Base interface for tool handlers +export interface BaseToolHandler { + execute(args: Record): Promise; +} + +// --- 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; + notes?: string; + disabled?: boolean; + [key: string]: any; // Allow other properties +} + +// Interface for n8n Connection Data +export interface ConnectionData { + // Define specific properties if known + [key: string]: any; +} + +// Interface for n8n Connection +export interface N8nConnection { + [outputNodeId: string]: { + [outputType: string]: Array<{ + node: string; // Input node ID + type: string; // Input type + data?: ConnectionData; + }>; + }; +} + +// Interface for n8n Workflow Settings +export interface WorkflowSettings { + saveExecutionProgress?: boolean; + saveManualExecutions?: boolean; + saveDataErrorExecution?: string; // e.g., "all", "none" + saveDataSuccessExecution?: string; // e.g., "all", "none" + executionTimeout?: number; + timezone?: string; + errorWorkflow?: string; + [key: string]: any; // Allow other settings +} + +// Enhanced Type for n8n workflow object export interface Workflow { id: string; name: string; active: boolean; - nodes: any[]; - connections: any; + nodes: N8nNode[]; // Use specific Node type + connections: N8nConnection; // Use specific Connection type createdAt: string; updatedAt: string; + settings?: WorkflowSettings; // Use specific Settings type + staticData?: Record | null; + tags?: string[]; // Assuming tags are strings + pinData?: Record; + [key: string]: any; // Keep for flexibility if needed +} + +// Interface for n8n Execution Error Details +export interface ExecutionError { + message?: string; + stack?: string; [key: string]: any; } -// Type for n8n execution object +// Interface for n8n Execution Run Data +export interface ExecutionRunData { + // Define specific properties if known structure exists + [key: string]: any; +} + +// Enhanced Type for n8n execution object export interface Execution { id: string; workflowId: string; finished: boolean; - mode: string; + mode: string; // e.g., 'manual', 'webhook', 'trigger' startedAt: string; - stoppedAt: string; - status: string; - data: { - resultData: { - runData: any; + stoppedAt: string | null; // Can be null if running + status: 'waiting' | 'running' | 'success' | 'error' | 'unknown'; // More specific statuses + data?: { // Make data optional + resultData?: { // Make resultData optional + runData?: ExecutionRunData; // Use specific RunData type + error?: ExecutionError; // Add error details }; }; - [key: string]: any; + workflowData?: Partial; // Workflow data might be partial + waitTill?: string | null; + [key: string]: any; // Keep for flexibility } diff --git a/src/utils/execution-formatter.ts b/src/utils/execution-formatter.ts index 16a0e3d..519f1aa 100644 --- a/src/utils/execution-formatter.ts +++ b/src/utils/execution-formatter.ts @@ -5,7 +5,7 @@ * in a consistent, user-friendly manner. */ -import { Execution } from '../types/index.js'; +import { Execution, ExecutionRunData, ExecutionError } from '../types/index.js'; // Import specific types /** * Format basic execution information for display @@ -13,10 +13,11 @@ import { Execution } from '../types/index.js'; * @param execution Execution object * @returns Formatted execution summary */ -export function formatExecutionSummary(execution: Execution): Record { +export function formatExecutionSummary(execution: Execution): Record { // Keep return flexible for now // Calculate duration 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 durationSeconds = Math.round(durationMs / 1000); @@ -40,49 +41,56 @@ export function formatExecutionSummary(execution: Execution): Record { +export function formatExecutionDetails(execution: Execution): Record { // Keep return flexible const summary = formatExecutionSummary(execution); // Extract node results const nodeResults: Record = {}; - if (execution.data?.resultData?.runData) { - for (const [nodeName, nodeData] of Object.entries(execution.data.resultData.runData)) { + const runData: ExecutionRunData | undefined = execution.data?.resultData?.runData; + + if (runData) { + for (const [nodeName, nodeDataArray] of Object.entries(runData)) { try { - // Get the last output - const lastOutput = Array.isArray(nodeData) && nodeData.length > 0 - ? nodeData[nodeData.length - 1] + // Get the last output object from the node's execution history array + const lastOutput = Array.isArray(nodeDataArray) && nodeDataArray.length > 0 + ? nodeDataArray[nodeDataArray.length - 1] : null; + // Check if the last output has the expected structure if (lastOutput && lastOutput.data && Array.isArray(lastOutput.data.main)) { - // Extract the output data - const outputData = lastOutput.data.main.length > 0 - ? lastOutput.data.main[0] + // Extract the output data items + const outputItems = lastOutput.data.main.length > 0 + ? lastOutput.data.main[0] // Assuming the first element contains the items array : []; nodeResults[nodeName] = { status: lastOutput.status, - items: outputData.length, - data: outputData.slice(0, 3), // Limit to first 3 items to avoid overwhelming response + items: Array.isArray(outputItems) ? outputItems.length : 0, // Ensure items is an array + // Limit data preview to avoid overwhelming response + dataPreview: Array.isArray(outputItems) ? outputItems.slice(0, 3) : [], }; + } else { + nodeResults[nodeName] = { status: lastOutput?.status || 'unknown', items: 0, dataPreview: [] }; } } catch (error) { + console.error(`Error parsing node output for ${nodeName}:`, error); nodeResults[nodeName] = { error: 'Failed to parse node output' }; } } } + // Extract error details if present + const errorDetails: ExecutionError | undefined = execution.data?.resultData?.error; + // Add node results and error information to the summary return { ...summary, mode: execution.mode, nodeResults: nodeResults, - // Include error information if present - error: execution.data?.resultData && 'error' in execution.data.resultData - ? { - message: (execution.data.resultData as any).error?.message, - stack: (execution.data.resultData as any).error?.stack, - } - : undefined, + error: errorDetails ? { // Use the defined ExecutionError type + message: errorDetails.message, + stack: errorDetails.stack, // Include stack if available + } : undefined, }; } @@ -92,7 +100,7 @@ export function formatExecutionDetails(execution: Execution): Record { +export function summarizeExecutions(executions: Execution[], limit: number = 10): Record { // Keep return flexible const limitedExecutions = executions.slice(0, limit); // Group executions by status @@ -132,7 +141,7 @@ export function summarizeExecutions(executions: Execution[], limit: number = 10) return { total: totalCount, byStatus: Object.entries(byStatus).map(([status, count]) => ({ - status: `${getStatusIndicator(status)} ${status}`, + status: `${getStatusIndicator(status as Execution['status'])} ${status}`, // Cast status count, percentage: totalCount > 0 ? Math.round((count / totalCount) * 100) : 0 })), diff --git a/src/utils/resource-formatter.ts b/src/utils/resource-formatter.ts index 5a9792a..6630454 100644 --- a/src/utils/resource-formatter.ts +++ b/src/utils/resource-formatter.ts @@ -5,7 +5,7 @@ * in a consistent, user-friendly manner for MCP resources. */ -import { Workflow, Execution } from '../types/index.js'; +import { Workflow, Execution, N8nNode } from '../types/index.js'; // Import N8nNode import { formatExecutionSummary, summarizeExecutions } from './execution-formatter.js'; /** @@ -14,7 +14,7 @@ import { formatExecutionSummary, summarizeExecutions } from './execution-formatt * @param workflow Workflow object * @returns Formatted workflow summary */ -export function formatWorkflowSummary(workflow: Workflow): Record { +export function formatWorkflowSummary(workflow: Workflow): Record { // Keep return flexible return { id: workflow.id, name: workflow.name, @@ -31,21 +31,23 @@ export function formatWorkflowSummary(workflow: Workflow): Record { * @param workflow Workflow object * @returns Formatted workflow details */ -export function formatWorkflowDetails(workflow: Workflow): Record { +export function formatWorkflowDetails(workflow: Workflow): Record { // Keep return flexible const summary = formatWorkflowSummary(workflow); // Add additional details return { ...summary, - nodes: workflow.nodes.map(node => ({ + nodes: workflow.nodes.map((node: N8nNode) => ({ // Use N8nNode type id: node.id, name: node.name, type: node.type, position: node.position, - parameters: node.parameters, + parameters: node.parameters, // Keep parameters as is for now + disabled: node.disabled, + notes: node.notes, })), - connections: workflow.connections, - staticData: workflow.staticData, + connections: workflow.connections, // Keep connections as is for now + staticData: workflow.staticData, // Keep staticData as is settings: workflow.settings, tags: workflow.tags, // Exclude potentially sensitive or unuseful information diff --git a/tests/mocks/axios-mock.ts b/tests/mocks/axios-mock.ts index b4aedca..36847a4 100644 --- a/tests/mocks/axios-mock.ts +++ b/tests/mocks/axios-mock.ts @@ -2,6 +2,7 @@ * Axios mock utilities for n8n MCP Server tests */ +import { jest } from '@jest/globals'; // Import jest import { AxiosRequestConfig, AxiosResponse } from 'axios'; export interface MockResponse { @@ -27,83 +28,84 @@ export const createMockAxiosResponse = (options: Partial = {}): Ax */ export const createMockAxiosInstance = () => { const mockRequests: Record = {}; - const mockResponses: Record = {}; - + const mockResponses: Record = {}; // Allow Error type + const mockInstance = { - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), - delete: jest.fn(), + get: jest.fn(), // Add type hint for mock function + post: jest.fn(), // Add type hint for mock function + put: jest.fn(), // Add type hint for mock function + delete: jest.fn(), // Add type hint for mock function interceptors: { request: { - use: jest.fn(), + use: jest.fn(), // Add type hint for mock function }, response: { - use: jest.fn(), + use: jest.fn(), // Add type hint for mock function }, }, defaults: {}, - + // Helper method to add mock response addMockResponse(method: string, url: string, response: MockResponse | Error) { - if (!mockResponses[`${method}:${url}`]) { - mockResponses[`${method}:${url}`] = []; - } - - if (response instanceof Error) { - mockResponses[`${method}:${url}`].push(response as any); - } else { - mockResponses[`${method}:${url}`].push(response); + const key = `${method}:${url}`; + if (!mockResponses[key]) { + mockResponses[key] = []; } + mockResponses[key].push(response); }, - + // Helper method to get request history getRequestHistory(method: string, url: string) { - return mockRequests[`${method}:${url}`] || []; + const key = `${method}:${url}`; + return mockRequests[key] || []; }, - + // Reset all mocks reset() { Object.keys(mockRequests).forEach(key => { delete mockRequests[key]; }); - + Object.keys(mockResponses).forEach(key => { delete mockResponses[key]; }); - + mockInstance.get.mockReset(); mockInstance.post.mockReset(); mockInstance.put.mockReset(); mockInstance.delete.mockReset(); + mockInstance.interceptors.request.use.mockReset(); + mockInstance.interceptors.response.use.mockReset(); } }; - + // Setup method implementations - ['get', 'post', 'put', 'delete'].forEach(method => { - mockInstance[method].mockImplementation(async (url: string, data?: any) => { + ['get', 'post', 'put', 'delete'].forEach((method) => { // Remove explicit type annotation + (mockInstance as any)[method].mockImplementation(async (url: string, data?: any) => { // Keep cast for dynamic access const requestKey = `${method}:${url}`; - + if (!mockRequests[requestKey]) { mockRequests[requestKey] = []; } - + mockRequests[requestKey].push(data); - + if (mockResponses[requestKey] && mockResponses[requestKey].length > 0) { - const response = mockResponses[requestKey].shift(); - + const response = mockResponses[requestKey].shift(); // shift() can return undefined + if (response instanceof Error) { throw response; } - return createMockAxiosResponse(response); + if (response) { // Check if response is defined + return createMockAxiosResponse(response); + } } - + throw new Error(`No mock response defined for ${method.toUpperCase()} ${url}`); }); }); - + return mockInstance; }; diff --git a/tests/mocks/n8n-fixtures.ts b/tests/mocks/n8n-fixtures.ts index 1d10769..34db0c6 100644 --- a/tests/mocks/n8n-fixtures.ts +++ b/tests/mocks/n8n-fixtures.ts @@ -21,6 +21,7 @@ export const createMockWorkflow = (overrides: Partial = {}): Workflow id: 'start', name: 'Start', type: 'n8n-nodes-base.start', + typeVersion: 1, // Added missing property parameters: {}, position: [100, 300], }, diff --git a/tests/tsconfig.json b/tests/tsconfig.json index e476267..949044c 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -3,10 +3,17 @@ "compilerOptions": { "types": ["jest", "node"], "esModuleInterop": true, - "rootDir": ".." + // Remove rootDir override as base config now handles it + // "rootDir": "..", + "outDir": "../build/tests", // Optional: specify separate output for tests if needed + "composite": true // Keep composite for potential future use }, "include": [ - "**/*.ts", - "**/*.tsx" + "**/*.ts", // Keep existing test files + "../src/**/*.ts" // Add back source files referenced by tests + ], + "exclude": [ // Add exclude for build output if needed + "../node_modules", + "../build" ] } diff --git a/tests/unit/api/client.test.ts.bak b/tests/unit/api/client.test.ts similarity index 85% rename from tests/unit/api/client.test.ts.bak rename to tests/unit/api/client.test.ts index 0d1d965..a6315fc 100644 --- a/tests/unit/api/client.test.ts.bak +++ b/tests/unit/api/client.test.ts @@ -2,33 +2,40 @@ * N8nApiClient unit tests */ -import '@jest/globals'; -import axios from 'axios'; -import { N8nApiClient } from '../../../src/api/client.js'; -import { EnvConfig } from '../../../src/config/environment.js'; -import { N8nApiError } from '../../../src/errors/index.js'; -import { createMockAxiosInstance, createMockAxiosResponse } from '../../mocks/axios-mock.js'; -import { mockApiResponses } from '../../mocks/n8n-fixtures.js'; +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; // Explicit import +import axios, { AxiosInstance } from 'axios'; // Import AxiosInstance type +import { N8nApiClient } from '../../../src/api/client.js'; // Add .js +import { EnvConfig } from '../../../src/config/environment.js'; // Add .js +import { N8nApiError } from '../../../src/errors/index.js'; // Add .js +import { createMockAxiosInstance, createMockAxiosResponse } from '../../mocks/axios-mock.js'; // Add .js +import { mockApiResponses } from '../../mocks/n8n-fixtures.js'; // Add .js + +// We will spy on axios.create instead of mocking the whole module +// jest.mock('axios'); +// const mockedAxios = axios as jest.Mocked; -// Mock axios -jest.mock('axios', () => ({ - create: jest.fn(), -})); describe('N8nApiClient', () => { // Mock configuration const mockConfig: EnvConfig = { n8nApiUrl: 'https://n8n.example.com/api/v1', n8nApiKey: 'test-api-key', + n8nWebhookUsername: 'test-user', // Added missing property + n8nWebhookPassword: 'test-password', // Added missing property debug: false, }; + // Define a type for the mock axios instance based on axios-mock.ts + type MockAxiosInstance = ReturnType; + // Mock axios instance - let mockAxios; + let mockAxios: MockAxiosInstance; beforeEach(() => { + // Create the mock instance mockAxios = createMockAxiosInstance(); - (axios.create as jest.Mock).mockReturnValue(mockAxios); + // Spy on axios.create and mock its return value + jest.spyOn(axios, 'create').mockReturnValue(mockAxios as any); }); afterEach(() => { @@ -42,7 +49,7 @@ describe('N8nApiClient', () => { new N8nApiClient(mockConfig); // Assert - expect(axios.create).toHaveBeenCalledWith({ + expect(axios.create).toHaveBeenCalledWith({ // Check the spy baseURL: mockConfig.n8nApiUrl, headers: { 'X-N8N-API-KEY': mockConfig.n8nApiKey, @@ -80,6 +87,7 @@ describe('N8nApiClient', () => { const client = new N8nApiClient(mockConfig); mockAxios.addMockResponse('get', '/workflows', { status: 200, + statusText: 'OK', // Added statusText data: { data: [] }, }); @@ -92,6 +100,7 @@ describe('N8nApiClient', () => { const client = new N8nApiClient(mockConfig); mockAxios.addMockResponse('get', '/workflows', { status: 500, + statusText: 'Internal Server Error', // Added statusText data: { message: 'Server error' }, }); @@ -116,6 +125,7 @@ describe('N8nApiClient', () => { const mockWorkflows = mockApiResponses.workflows.list; mockAxios.addMockResponse('get', '/workflows', { status: 200, + statusText: 'OK', // Added statusText data: mockWorkflows, }); @@ -132,6 +142,7 @@ describe('N8nApiClient', () => { const client = new N8nApiClient(mockConfig); mockAxios.addMockResponse('get', '/workflows', { status: 200, + statusText: 'OK', // Added statusText data: {}, }); @@ -160,6 +171,7 @@ describe('N8nApiClient', () => { const mockWorkflow = mockApiResponses.workflows.single(workflowId); mockAxios.addMockResponse('get', `/workflows/${workflowId}`, { status: 200, + statusText: 'OK', // Added statusText data: mockWorkflow, }); @@ -193,6 +205,7 @@ describe('N8nApiClient', () => { mockAxios.addMockResponse('post', `/workflows/${workflowId}/execute`, { status: 200, + statusText: 'OK', // Added statusText data: mockResponse, }); @@ -214,6 +227,7 @@ describe('N8nApiClient', () => { mockAxios.addMockResponse('post', '/workflows', { status: 200, + statusText: 'OK', // Added statusText data: mockResponse, }); @@ -236,6 +250,7 @@ describe('N8nApiClient', () => { mockAxios.addMockResponse('put', `/workflows/${workflowId}`, { status: 200, + statusText: 'OK', // Added statusText data: mockResponse, }); @@ -257,6 +272,7 @@ describe('N8nApiClient', () => { mockAxios.addMockResponse('delete', `/workflows/${workflowId}`, { status: 200, + statusText: 'OK', // Added statusText data: mockResponse, }); @@ -278,6 +294,7 @@ describe('N8nApiClient', () => { mockAxios.addMockResponse('post', `/workflows/${workflowId}/activate`, { status: 200, + statusText: 'OK', // Added statusText data: mockResponse, }); @@ -299,6 +316,7 @@ describe('N8nApiClient', () => { mockAxios.addMockResponse('post', `/workflows/${workflowId}/deactivate`, { status: 200, + statusText: 'OK', // Added statusText data: mockResponse, }); @@ -318,6 +336,7 @@ describe('N8nApiClient', () => { const mockExecutions = mockApiResponses.executions.list; mockAxios.addMockResponse('get', '/executions', { status: 200, + statusText: 'OK', // Added statusText data: mockExecutions, }); @@ -338,6 +357,7 @@ describe('N8nApiClient', () => { const mockExecution = mockApiResponses.executions.single(executionId); mockAxios.addMockResponse('get', `/executions/${executionId}`, { status: 200, + statusText: 'OK', // Added statusText data: mockExecution, }); @@ -359,6 +379,7 @@ describe('N8nApiClient', () => { mockAxios.addMockResponse('delete', `/executions/${executionId}`, { status: 200, + statusText: 'OK', // Added statusText data: mockResponse, }); diff --git a/tests/unit/api/simple-client.test.ts b/tests/unit/api/simple-client.test.ts deleted file mode 100644 index 5d731a3..0000000 --- a/tests/unit/api/simple-client.test.ts +++ /dev/null @@ -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 { - 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`); - }); -}); diff --git a/tests/unit/config/environment.test.ts.bak b/tests/unit/config/environment.test.ts similarity index 71% rename from tests/unit/config/environment.test.ts.bak rename to tests/unit/config/environment.test.ts index 2f24aaa..a1adfe5 100644 --- a/tests/unit/config/environment.test.ts.bak +++ b/tests/unit/config/environment.test.ts @@ -3,10 +3,10 @@ */ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; -import { getEnvConfig, loadEnvironmentVariables, ENV_VARS } from '../../../src/config/environment.js'; +import { getEnvConfig, loadEnvironmentVariables, ENV_VARS } from '../../../src/config/environment.js'; // Add .js import { McpError } from '@modelcontextprotocol/sdk/types.js'; -import { ErrorCode } from '../../../src/errors/error-codes.js'; -import { mockEnv } from '../../test-setup.js'; +import { ErrorCode } from '../../../src/errors/error-codes.js'; // Add .js +import { mockEnv } from '../../test-setup.js'; // Add .js describe('Environment Configuration', () => { const originalEnv = process.env; @@ -36,6 +36,8 @@ describe('Environment Configuration', () => { // Setup process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1'; process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; + process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user + process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass // Execute const config = getEnvConfig(); @@ -44,6 +46,8 @@ describe('Environment Configuration', () => { expect(config).toEqual({ n8nApiUrl: 'https://n8n.example.com/api/v1', n8nApiKey: 'test-api-key', + n8nWebhookUsername: 'test-user', + n8nWebhookPassword: 'test-pass', debug: false, }); }); @@ -52,6 +56,8 @@ describe('Environment Configuration', () => { // Setup process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1'; process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; + process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user + process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass process.env[ENV_VARS.DEBUG] = 'true'; // Execute @@ -65,6 +71,8 @@ describe('Environment Configuration', () => { // Setup process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1'; process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; + process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user + process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass process.env[ENV_VARS.DEBUG] = 'TRUE'; // Execute @@ -78,6 +86,8 @@ describe('Environment Configuration', () => { // Setup process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1'; process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; + process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user + process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass process.env[ENV_VARS.DEBUG] = 'yes'; // Execute @@ -117,6 +127,8 @@ describe('Environment Configuration', () => { // Setup process.env[ENV_VARS.N8N_API_URL] = 'invalid-url'; process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; + process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user + process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass // Execute & Assert expect(() => getEnvConfig()).toThrow( @@ -131,6 +143,8 @@ describe('Environment Configuration', () => { // Setup process.env[ENV_VARS.N8N_API_URL] = 'http://localhost:5678/api/v1'; process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; + process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user + process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass // Execute const config = getEnvConfig(); @@ -143,6 +157,8 @@ describe('Environment Configuration', () => { // Setup process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1/'; process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; + process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user + process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass // Execute const config = getEnvConfig(); @@ -155,14 +171,18 @@ describe('Environment Configuration', () => { describe('with mockEnv helper', () => { // Using the mockEnv helper from test-setup mockEnv({ - [ENV_VARS.N8N_API_URL]: 'https://n8n.example.com/api/v1', - [ENV_VARS.N8N_API_KEY]: 'test-api-key', + [ENV_VARS.N8N_API_URL]: 'https://mock.n8n.com/api/v1', + [ENV_VARS.N8N_API_KEY]: 'mock-api-key', + [ENV_VARS.N8N_WEBHOOK_USERNAME]: 'mock-user', // Add webhook user + [ENV_VARS.N8N_WEBHOOK_PASSWORD]: 'mock-pass', // Add webhook pass }); it('should use the mocked environment variables', () => { const config = getEnvConfig(); - expect(config.n8nApiUrl).toBe('https://n8n.example.com/api/v1'); - expect(config.n8nApiKey).toBe('test-api-key'); + expect(config.n8nApiUrl).toBe('https://mock.n8n.com/api/v1'); + expect(config.n8nApiKey).toBe('mock-api-key'); + expect(config.n8nWebhookUsername).toBe('mock-user'); + expect(config.n8nWebhookPassword).toBe('mock-pass'); }); }); }); diff --git a/tests/unit/config/simple-environment.test.ts b/tests/unit/config/simple-environment.test.ts deleted file mode 100644 index 3d09af6..0000000 --- a/tests/unit/config/simple-environment.test.ts +++ /dev/null @@ -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): { - 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' - ); - }); - }); -}); diff --git a/tests/unit/resources/dynamic/workflow.test.ts b/tests/unit/resources/dynamic/workflow.test.ts index fe6c957..c4b956c 100644 --- a/tests/unit/resources/dynamic/workflow.test.ts +++ b/tests/unit/resources/dynamic/workflow.test.ts @@ -1,38 +1,38 @@ /** - * Simple test for URI Template functionality + * Tests for dynamic workflow resource URI functions */ import { describe, it, expect } from '@jest/globals'; - -// Simple functions to test without complex imports -function getWorkflowResourceTemplateUri() { - return 'n8n://workflows/{id}'; -} - -function extractWorkflowIdFromUri(uri: string): string | null { - const regex = /^n8n:\/\/workflows\/([^/]+)$/; - const match = uri.match(regex); - return match ? match[1] : null; -} +// Import the actual functions from the source file with .js extension +import { + getWorkflowResourceTemplateUri, + extractWorkflowIdFromUri +} from '../../../../src/resources/dynamic/workflow.js'; describe('Workflow Resource URI Functions', () => { describe('getWorkflowResourceTemplateUri', () => { it('should return the correct URI template', () => { + // Test the actual imported function expect(getWorkflowResourceTemplateUri()).toBe('n8n://workflows/{id}'); }); }); describe('extractWorkflowIdFromUri', () => { it('should extract workflow ID from valid URI', () => { + // Test the actual imported function expect(extractWorkflowIdFromUri('n8n://workflows/123abc')).toBe('123abc'); expect(extractWorkflowIdFromUri('n8n://workflows/workflow-name-with-dashes')).toBe('workflow-name-with-dashes'); }); it('should return null for invalid URI formats', () => { + // Test the actual imported function expect(extractWorkflowIdFromUri('n8n://workflows/')).toBeNull(); expect(extractWorkflowIdFromUri('n8n://workflows')).toBeNull(); - expect(extractWorkflowIdFromUri('n8n://workflow/123')).toBeNull(); + expect(extractWorkflowIdFromUri('n8n://workflow/123')).toBeNull(); // Should fail based on regex expect(extractWorkflowIdFromUri('invalid://workflows/123')).toBeNull(); + expect(extractWorkflowIdFromUri('n8n://workflows/123/extra')).toBeNull(); // Should fail based on regex }); }); + + // TODO: Add tests for getWorkflowResource function (requires mocking apiService) }); diff --git a/tests/unit/tools/workflow/list.test.ts b/tests/unit/tools/workflow/list.test.ts new file mode 100644 index 0000000..2de7abb --- /dev/null +++ b/tests/unit/tools/workflow/list.test.ts @@ -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'); + }); + }); +}); diff --git a/tests/unit/tools/workflow/list.test.ts.bak b/tests/unit/tools/workflow/list.test.ts.bak deleted file mode 100644 index 3f8c740..0000000 --- a/tests/unit/tools/workflow/list.test.ts.bak +++ /dev/null @@ -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([]); - }); -}); diff --git a/tests/unit/tools/workflow/simple-tool.test.ts b/tests/unit/tools/workflow/simple-tool.test.ts deleted file mode 100644 index 1c24e5d..0000000 --- a/tests/unit/tools/workflow/simple-tool.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index eb26f3c..cfdd4f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,10 +11,9 @@ "resolveJsonModule": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "rootDir": "src", + "rootDir": ".", // Set rootDir to encompass both src and tests "lib": [ - "ES2020", - "DOM" + "ES2020" // Removed "DOM" as it's likely unused in a Node.js server ], "types": [ "node" @@ -28,4 +27,5 @@ "build", "**/*.test.ts" ] + // Remove project reference }