Initial commit of n8n MCP Server

A Model Context Protocol (MCP) server that integrates with n8n, providing tools for workflow and execution management via the n8n API.
This commit is contained in:
leonardsellem
2025-03-12 17:12:35 +01:00
commit 2cd565cfa6
79 changed files with 19654 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
# Architecture
This document describes the architectural design of the n8n MCP Server.
## Overview
The n8n MCP Server follows a layered architecture pattern that separates concerns and promotes maintainability. The main architectural layers are:
1. **Transport Layer**: Handles communication with AI assistants via the Model Context Protocol
2. **API Client Layer**: Interacts with the n8n API
3. **Tools Layer**: Implements executable operations as MCP tools
4. **Resources Layer**: Provides data access through URI-based resources
5. **Configuration Layer**: Manages environment variables and server settings
6. **Error Handling Layer**: Provides consistent error management and reporting
## System Components
![Architecture Diagram](../images/architecture.png.placeholder)
### Entry Point
The server entry point is defined in `src/index.ts`. This file:
1. Initializes the configuration from environment variables
2. Creates and configures the MCP server instance
3. Registers tool and resource handlers
4. Connects to the transport layer (typically stdio)
### Configuration
The configuration layer (`src/config/`) handles:
- Loading environment variables
- Validating required configuration
- Providing typed access to configuration values
The main configuration component is the `Environment` class, which validates and manages environment variables like `N8N_API_URL` and `N8N_API_KEY`.
### API Client
The API client layer (`src/api/`) provides a clean interface for interacting with the n8n API. It includes:
- `N8nClient`: The main client that encapsulates communication with n8n
- API-specific functionality divided by resource type (workflows, executions)
- Authentication handling using the n8n API key
The client uses Axios for HTTP requests and includes error handling specific to the n8n API responses.
### MCP Tools
The tools layer (`src/tools/`) implements the executable operations exposed to AI assistants. Each tool follows a common pattern:
1. A tool definition that specifies name, description, and input schema
2. A handler function that processes input parameters and executes the operation
3. Error handling for validation and execution errors
Tools are categorized by resource type:
- Workflow tools: Create, list, update, delete, activate, and deactivate workflows
- Execution tools: Run, list, and manage workflow executions
Each tool is designed to be independently testable and maintains a clean separation of concerns.
### MCP Resources
The resources layer (`src/resources/`) provides data access through URI-based templates. Resources are divided into two categories:
1. **Static Resources** (`src/resources/static/`): Fixed resources like workflow listings
2. **Dynamic Resources** (`src/resources/dynamic/`): Parameterized resources like specific workflow details
Each resource implements:
- URI pattern matching
- Content retrieval
- Error handling
- Response formatting
### Error Handling
The error handling layer (`src/errors/`) provides consistent error management across the server. It includes:
- Custom error types that map to MCP error codes
- Error translation functions to convert n8n API errors to MCP errors
- Common error patterns and handling strategies
## Data Flow
A typical data flow through the system:
1. AI assistant sends a request via stdin to the MCP server
2. Server routes the request to the appropriate handler based on the request type
3. Handler validates input and delegates to the appropriate tool or resource
4. Tool/resource uses the n8n API client to interact with n8n
5. Response is processed, formatted, and returned via stdout
6. AI assistant receives and processes the response
## Key Design Principles
### 1. Separation of Concerns
Each component has a single responsibility, making the codebase easier to understand, test, and extend.
### 2. Type Safety
TypeScript interfaces and types are used extensively to ensure type safety and provide better developer experience.
### 3. Error Handling
Comprehensive error handling ensures that errors are caught at the appropriate level and translated into meaningful messages for AI assistants.
### 4. Testability
The architecture supports unit testing by keeping components loosely coupled and maintaining clear boundaries between layers.
### 5. Extensibility
New tools and resources can be added without modifying existing code, following the open-closed principle.
## Implementation Patterns
### Factory Pattern
Used for creating client instances and tool handlers based on configuration.
### Adapter Pattern
The n8n API client adapts the n8n API to the internal representation used by the server.
### Strategy Pattern
Different resource handlers implement a common interface but provide different strategies for retrieving and formatting data.
### Decorator Pattern
Used to add cross-cutting concerns like logging and error handling to base functionality.
## Core Files and Their Purposes
| File | Purpose |
|------|---------|
| `src/index.ts` | Main entry point, initializes and configures the server |
| `src/config/environment.ts` | Manages environment variables and configuration |
| `src/api/n8n-client.ts` | Main client for interacting with the n8n API |
| `src/tools/workflow/handler.ts` | Handles workflow-related tool requests |
| `src/tools/execution/handler.ts` | Handles execution-related tool requests |
| `src/resources/index.ts` | Registers and manages resource handlers |
| `src/resources/dynamic/workflow.ts` | Provides access to specific workflow resources |
| `src/resources/static/workflows.ts` | Provides access to workflow listings |
| `src/errors/index.ts` | Defines and manages error types and handling |
## Extension Points
To extend the server with new capabilities:
1. **Adding a new tool**: Create a new handler in the appropriate category under `src/tools/` and register it in the main server setup
2. **Adding a new resource**: Create a new resource handler in `src/resources/` and register it in the resource manager
3. **Supporting new n8n API features**: Extend the API client in `src/api/` to support new API endpoints or features
For detailed instructions on extending the server, see [Extending the Server](./extending.md).

View File

@@ -0,0 +1,653 @@
# Extending the Server
This guide explains how to extend the n8n MCP Server with new functionality.
## Overview
The n8n MCP Server is designed to be extensible, allowing developers to add new tools and resources without modifying existing code. This extensibility makes it easy to support new n8n features or customize the server for specific use cases.
## Adding a New Tool
Tools in the MCP server represent executable operations that AI assistants can use. To add a new tool, follow these steps:
### 1. Define the Tool Interface
Create a new TypeScript interface that defines the input parameters for your tool:
```typescript
// src/types/tools/my-tool.ts
export interface MyToolParams {
param1: string;
param2?: number; // Optional parameter
}
```
### 2. Create the Tool Handler
Create a new file for your tool in the appropriate category under `src/tools/`:
```typescript
// src/tools/category/my-tool.ts
import { ToolCallResponse, ToolDefinition } from '@modelcontextprotocol/sdk/types.js';
import { N8nClient } from '../../api/n8n-client.js';
import { MyToolParams } from '../../types/tools/my-tool.js';
// Define the tool
export function getMyToolDefinition(): ToolDefinition {
return {
name: 'my_tool',
description: 'Description of what my tool does',
inputSchema: {
type: 'object',
properties: {
param1: {
type: 'string',
description: 'Description of param1'
},
param2: {
type: 'number',
description: 'Description of param2'
}
},
required: ['param1']
}
};
}
// Implement the tool handler
export async function handleMyTool(
client: N8nClient,
params: MyToolParams
): Promise<ToolCallResponse> {
try {
// Implement the tool logic here
// Use the N8nClient to interact with n8n
// Return the response
return {
content: [
{
type: 'text',
text: 'Result of the operation'
}
]
};
} catch (error) {
// Handle errors
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`
}
],
isError: true
};
}
}
```
### 3. Register the Tool in the Handler
Update the main handler file for your tool category (e.g., `src/tools/category/handler.ts`):
```typescript
// src/tools/category/handler.ts
import { getMyToolDefinition, handleMyTool } from './my-tool.js';
// Add your tool to the tools object
export const categoryTools = {
// ... existing tools
my_tool: {
definition: getMyToolDefinition,
handler: handleMyTool
}
};
```
### 4. Add Handler to Main Server
Update the main tool handler registration in `src/index.ts`:
```typescript
// src/index.ts
import { categoryTools } from './tools/category/handler.js';
// In the server initialization
const server = new Server(
{
name: 'n8n-mcp-server',
version: '0.1.0'
},
{
capabilities: {
tools: {
// ... existing categories
category: true
}
}
}
);
// Register tool handlers
Object.entries(categoryTools).forEach(([name, { definition, handler }]) => {
server.setToolHandler(definition(), async (request) => {
return await handler(client, request.params.arguments as any);
});
});
```
### 5. Add Unit Tests
Create unit tests for your new tool:
```typescript
// tests/unit/tools/category/my-tool.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import { getMyToolDefinition, handleMyTool } from '../../../../src/tools/category/my-tool.js';
describe('My Tool', () => {
describe('getMyToolDefinition', () => {
it('should return the correct tool definition', () => {
const definition = getMyToolDefinition();
expect(definition.name).toBe('my_tool');
expect(definition.description).toBeTruthy();
expect(definition.inputSchema).toBeDefined();
expect(definition.inputSchema.properties).toHaveProperty('param1');
expect(definition.inputSchema.required).toEqual(['param1']);
});
});
describe('handleMyTool', () => {
it('should handle valid parameters', async () => {
const mockClient = {
// Mock the necessary client methods
};
const result = await handleMyTool(mockClient as any, {
param1: 'test value'
});
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toBeTruthy();
});
it('should handle errors properly', async () => {
const mockClient = {
// Mock client that throws an error
someMethod: jest.fn().mockRejectedValue(new Error('Test error'))
};
const result = await handleMyTool(mockClient as any, {
param1: 'test value'
});
expect(result.isError).toBeTruthy();
expect(result.content[0].text).toContain('Error');
});
});
});
```
## Adding a New Resource
Resources in the MCP server provide data access through URI-based templates. To add a new resource, follow these steps:
### 1. Create a Static Resource (No Parameters)
For a resource that doesn't require parameters:
```typescript
// src/resources/static/my-resource.ts
import { McpError, ReadResourceResponse } from '@modelcontextprotocol/sdk/types.js';
import { ErrorCode } from '../../errors/error-codes.js';
import { N8nClient } from '../../api/n8n-client.js';
export const MY_RESOURCE_URI = 'n8n://my-resource';
export async function handleMyResourceRequest(
client: N8nClient
): Promise<ReadResourceResponse> {
try {
// Implement the resource logic
// Use the N8nClient to interact with n8n
// Return the response
return {
contents: [
{
uri: MY_RESOURCE_URI,
mimeType: 'application/json',
text: JSON.stringify(
{
// Resource data
property1: 'value1',
property2: 'value2'
},
null,
2
)
}
]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to retrieve resource: ${error.message}`
);
}
}
```
### 2. Create a Dynamic Resource (With Parameters)
For a resource that requires parameters:
```typescript
// src/resources/dynamic/my-resource.ts
import { McpError, ReadResourceResponse } from '@modelcontextprotocol/sdk/types.js';
import { ErrorCode } from '../../errors/error-codes.js';
import { N8nClient } from '../../api/n8n-client.js';
export const MY_RESOURCE_URI_TEMPLATE = 'n8n://my-resource/{id}';
export function matchMyResourceUri(uri: string): { id: string } | null {
const match = uri.match(/^n8n:\/\/my-resource\/([^/]+)$/);
if (!match) return null;
return {
id: decodeURIComponent(match[1])
};
}
export async function handleMyResourceRequest(
client: N8nClient,
uri: string
): Promise<ReadResourceResponse> {
const params = matchMyResourceUri(uri);
if (!params) {
throw new McpError(
ErrorCode.InvalidRequest,
`Invalid URI format: ${uri}`
);
}
try {
// Implement the resource logic using params.id
// Use the N8nClient to interact with n8n
// Return the response
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(
{
// Resource data with the specific ID
id: params.id,
property1: 'value1',
property2: 'value2'
},
null,
2
)
}
]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to retrieve resource: ${error.message}`
);
}
}
```
### 3. Register Resources in the Handler Files
Update the resource handler registration:
#### For Static Resources
```typescript
// src/resources/static/index.ts
import { MY_RESOURCE_URI, handleMyResourceRequest } from './my-resource.js';
export const staticResources = {
// ... existing static resources
[MY_RESOURCE_URI]: handleMyResourceRequest
};
```
#### For Dynamic Resources
```typescript
// src/resources/dynamic/index.ts
import { MY_RESOURCE_URI_TEMPLATE, matchMyResourceUri, handleMyResourceRequest } from './my-resource.js';
export const dynamicResourceMatchers = [
// ... existing dynamic resource matchers
{
uriTemplate: MY_RESOURCE_URI_TEMPLATE,
match: matchMyResourceUri,
handler: handleMyResourceRequest
}
];
```
### 4. Add Resource Listings
Update the resource listing functions:
```typescript
// src/resources/index.ts
// Update the resource templates listing
export function getResourceTemplates() {
return [
// ... existing templates
{
uriTemplate: MY_RESOURCE_URI_TEMPLATE,
name: 'My Resource',
description: 'Description of my resource'
}
];
}
// Update the static resources listing
export function getStaticResources() {
return [
// ... existing resources
{
uri: MY_RESOURCE_URI,
name: 'My Resource List',
description: 'List of all my resources'
}
];
}
```
### 5. Add Unit Tests
Create tests for your new resource:
```typescript
// tests/unit/resources/static/my-resource.test.ts
// or
// tests/unit/resources/dynamic/my-resource.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import {
MY_RESOURCE_URI,
handleMyResourceRequest
} from '../../../../src/resources/static/my-resource.js';
describe('My Resource', () => {
it('should return resource data', async () => {
const mockClient = {
// Mock the necessary client methods
};
const response = await handleMyResourceRequest(mockClient as any);
expect(response.contents).toHaveLength(1);
expect(response.contents[0].uri).toBe(MY_RESOURCE_URI);
expect(response.contents[0].mimeType).toBe('application/json');
const data = JSON.parse(response.contents[0].text);
expect(data).toHaveProperty('property1');
expect(data).toHaveProperty('property2');
});
it('should handle errors properly', async () => {
const mockClient = {
// Mock client that throws an error
someMethod: jest.fn().mockRejectedValue(new Error('Test error'))
};
await expect(handleMyResourceRequest(mockClient as any))
.rejects
.toThrow('Failed to retrieve resource');
});
});
```
## Extending the API Client
If you need to add support for new n8n API features, extend the N8nClient class:
### 1. Add New Methods to the Client
```typescript
// src/api/n8n-client.ts
export class N8nClient {
// ... existing methods
// Add new methods
async myNewApiMethod(param1: string): Promise<any> {
try {
const response = await this.httpClient.get(`/endpoint/${param1}`);
return response.data;
} catch (error) {
this.handleApiError(error);
}
}
}
```
### 2. Add Type Definitions
```typescript
// src/types/api.ts
// Add types for API responses and requests
export interface MyApiResponse {
id: string;
name: string;
// Other properties
}
export interface MyApiRequest {
param1: string;
param2?: number;
}
```
### 3. Add Tests for the New API Methods
```typescript
// tests/unit/api/n8n-client.test.ts
describe('N8nClient', () => {
// ... existing tests
describe('myNewApiMethod', () => {
it('should call the correct API endpoint', async () => {
// Set up mock Axios
axiosMock.onGet('/endpoint/test').reply(200, {
id: '123',
name: 'Test'
});
const client = new N8nClient({
apiUrl: 'http://localhost:5678/api/v1',
apiKey: 'test-api-key'
});
const result = await client.myNewApiMethod('test');
expect(result).toEqual({
id: '123',
name: 'Test'
});
});
it('should handle errors correctly', async () => {
// Set up mock Axios
axiosMock.onGet('/endpoint/test').reply(404, {
message: 'Not found'
});
const client = new N8nClient({
apiUrl: 'http://localhost:5678/api/v1',
apiKey: 'test-api-key'
});
await expect(client.myNewApiMethod('test'))
.rejects
.toThrow('Resource not found');
});
});
});
```
## Best Practices for Extensions
1. **Follow the Existing Patterns**: Try to follow the patterns already established in the codebase.
2. **Type Safety**: Use TypeScript types and interfaces to ensure type safety.
3. **Error Handling**: Implement comprehensive error handling in all extensions.
4. **Testing**: Write thorough tests for all new functionality.
5. **Documentation**: Document your extensions, including JSDoc comments for all public methods.
6. **Backward Compatibility**: Ensure that your extensions don't break existing functionality.
## Example: Adding Support for n8n Tags
Here's a complete example of adding support for n8n tags:
### API Client Extension
```typescript
// src/api/n8n-client.ts
export class N8nClient {
// ... existing methods
// Add tag methods
async getTags(): Promise<Tag[]> {
try {
const response = await this.httpClient.get('/tags');
return response.data;
} catch (error) {
this.handleApiError(error);
}
}
async createTag(data: CreateTagRequest): Promise<Tag> {
try {
const response = await this.httpClient.post('/tags', data);
return response.data;
} catch (error) {
this.handleApiError(error);
}
}
async deleteTag(id: string): Promise<void> {
try {
await this.httpClient.delete(`/tags/${id}`);
} catch (error) {
this.handleApiError(error);
}
}
}
```
### Type Definitions
```typescript
// src/types/api.ts
export interface Tag {
id: string;
name: string;
createdAt: string;
updatedAt: string;
}
export interface CreateTagRequest {
name: string;
}
```
### Tool Implementations
```typescript
// src/tools/tag/list.ts
export function getTagListToolDefinition(): ToolDefinition {
return {
name: 'tag_list',
description: 'List all tags in n8n',
inputSchema: {
type: 'object',
properties: {},
required: []
}
};
}
export async function handleTagList(
client: N8nClient,
params: any
): Promise<ToolCallResponse> {
try {
const tags = await client.getTags();
return {
content: [
{
type: 'text',
text: JSON.stringify(tags, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error listing tags: ${error.message}`
}
],
isError: true
};
}
}
```
### Resource Implementation
```typescript
// src/resources/static/tags.ts
export const TAGS_URI = 'n8n://tags';
export async function handleTagsRequest(
client: N8nClient
): Promise<ReadResourceResponse> {
try {
const tags = await client.getTags();
return {
contents: [
{
uri: TAGS_URI,
mimeType: 'application/json',
text: JSON.stringify(
{
tags,
count: tags.length
},
null,
2
)
}
]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to retrieve tags: ${error.message}`
);
}
}
```
### Integration
Register the new tools and resources in the appropriate handler files, and update the main server initialization to include them.
By following these patterns, you can extend the n8n MCP Server to support any n8n feature or add custom functionality tailored to your specific needs.

85
docs/development/index.md Normal file
View File

@@ -0,0 +1,85 @@
# Development Guide
This section provides information for developers who want to understand, maintain, or extend the n8n MCP Server.
## Overview
The n8n MCP Server is built with TypeScript and implements the Model Context Protocol (MCP) to provide AI assistants with access to n8n workflows and executions. This development guide covers the architecture, extension points, and testing procedures.
## Topics
- [Architecture](./architecture.md): Overview of the codebase organization and design patterns
- [Extending the Server](./extending.md): Guide to adding new tools and resources
- [Testing](./testing.md): Information on testing procedures and writing tests
## Development Setup
To set up a development environment:
1. Clone the repository:
```bash
git clone https://github.com/yourusername/n8n-mcp-server.git
cd n8n-mcp-server
```
2. Install dependencies:
```bash
npm install
```
3. Create a `.env` file for local development:
```bash
cp .env.example .env
# Edit the .env file with your n8n API credentials
```
4. Start the development server:
```bash
npm run dev
```
This will compile the TypeScript code in watch mode, allowing you to make changes and see them take effect immediately.
## Project Structure
The project follows a modular structure:
```
n8n-mcp-server/
├── src/ # Source code
│ ├── api/ # API client for n8n
│ ├── config/ # Configuration and environment settings
│ ├── errors/ # Error handling
│ ├── resources/ # MCP resources implementation
│ │ ├── static/ # Static resources
│ │ └── dynamic/ # Dynamic (parameterized) resources
│ ├── tools/ # MCP tools implementation
│ │ ├── workflow/ # Workflow management tools
│ │ └── execution/ # Execution management tools
│ ├── types/ # TypeScript type definitions
│ └── utils/ # Utility functions
├── tests/ # Test files
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ └── e2e/ # End-to-end tests
└── build/ # Compiled output
```
## Build and Distribution
To build the project for distribution:
```bash
npm run build
```
This will compile the TypeScript code to JavaScript in the `build` directory and make the executable script file.
## Development Workflow
1. Create a feature branch for your changes
2. Make your changes and ensure tests pass
3. Update documentation as needed
4. Submit a pull request
For more detailed instructions on specific development tasks, see the linked guides.

486
docs/development/testing.md Normal file
View File

@@ -0,0 +1,486 @@
# Testing
This document describes the testing approach for the n8n MCP Server and provides guidelines for writing effective tests.
## Overview
The n8n MCP Server uses Jest as its testing framework and follows a multi-level testing approach:
1. **Unit Tests**: Test individual components in isolation
2. **Integration Tests**: Test interactions between components
3. **End-to-End Tests**: Test the entire system as a whole
Tests are organized in the `tests/` directory, with a structure that mirrors the `src/` directory.
## Running Tests
### Running All Tests
To run all tests:
```bash
npm test
```
This command runs all tests and outputs a summary of the results.
### Running Tests with Coverage
To run tests with coverage reporting:
```bash
npm run test:coverage
```
This generates coverage reports in the `coverage/` directory, including HTML reports that you can view in a browser.
### Running Tests in Watch Mode
During development, you can run tests in watch mode, which will automatically rerun tests when files change:
```bash
npm run test:watch
```
### Running Specific Tests
To run tests in a specific file or directory:
```bash
npx jest path/to/test-file.test.ts
```
Or to run tests matching a specific pattern:
```bash
npx jest -t "test pattern"
```
## Test Structure
Tests are organized into the following directories:
- `tests/unit/`: Unit tests for individual components
- `tests/integration/`: Integration tests that test interactions between components
- `tests/e2e/`: End-to-end tests that test the entire system
- `tests/mocks/`: Shared test fixtures and mocks
### Unit Tests
Unit tests are organized in a structure that mirrors the `src/` directory. For example:
- `src/api/n8n-client.ts` has a corresponding test at `tests/unit/api/n8n-client.test.ts`
- `src/tools/workflow/list.ts` has a corresponding test at `tests/unit/tools/workflow/list.test.ts`
### Integration Tests
Integration tests focus on testing interactions between components, such as:
- Testing that tools correctly use the API client
- Testing that resources correctly format data from the API
### End-to-End Tests
End-to-end tests test the entire system, from the transport layer to the API client and back.
## Writing Effective Tests
### Unit Test Example
Here's an example of a unit test for a workflow tool:
```typescript
// tests/unit/tools/workflow/list.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import { getListWorkflowsToolDefinition, handleListWorkflows } from '../../../../src/tools/workflow/list.js';
import { N8nClient } from '../../../../src/api/n8n-client.js';
// Mock 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'
},
{
id: '5678def',
name: 'Test Workflow 2',
active: false,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-12T10:15:00.000Z'
}
];
describe('Workflow List Tool', () => {
describe('getListWorkflowsToolDefinition', () => {
it('should return the correct tool definition', () => {
const definition = getListWorkflowsToolDefinition();
expect(definition.name).toBe('workflow_list');
expect(definition.description).toBeTruthy();
expect(definition.inputSchema).toBeDefined();
expect(definition.inputSchema.properties).toHaveProperty('active');
expect(definition.inputSchema.required).toEqual([]);
});
});
describe('handleListWorkflows', () => {
it('should return all workflows when no filter is provided', async () => {
// Mock the API client
const mockClient = {
getWorkflows: jest.fn().mockResolvedValue(mockWorkflows)
};
const result = await handleListWorkflows(mockClient as unknown as N8nClient, {});
expect(mockClient.getWorkflows).toHaveBeenCalledWith(undefined);
expect(result.isError).toBeFalsy();
// Parse the JSON text to check the content
const content = JSON.parse(result.content[0].text);
expect(content).toHaveLength(2);
expect(content[0].id).toBe('1234abc');
expect(content[1].id).toBe('5678def');
});
it('should filter workflows by active status', async () => {
// Mock the API client
const mockClient = {
getWorkflows: jest.fn().mockResolvedValue(mockWorkflows)
};
const result = await handleListWorkflows(mockClient as unknown as N8nClient, { active: true });
expect(mockClient.getWorkflows).toHaveBeenCalledWith(true);
expect(result.isError).toBeFalsy();
// Parse the JSON text to check the content
const content = JSON.parse(result.content[0].text);
expect(content).toHaveLength(2);
});
it('should handle API errors', async () => {
// Mock the API client to throw an error
const mockClient = {
getWorkflows: jest.fn().mockRejectedValue(new Error('API error'))
};
const result = await handleListWorkflows(mockClient as unknown as N8nClient, {});
expect(result.isError).toBeTruthy();
expect(result.content[0].text).toContain('API error');
});
});
});
```
### Integration Test Example
Here's an example of an integration test that tests the interaction between a resource handler and the API client:
```typescript
// tests/integration/resources/static/workflows.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import { handleWorkflowsRequest, WORKFLOWS_URI } from '../../../../src/resources/static/workflows.js';
import { N8nClient } from '../../../../src/api/n8n-client.js';
// Mock 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'
},
{
id: '5678def',
name: 'Test Workflow 2',
active: false,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-12T10:15:00.000Z'
}
];
describe('Workflows Resource Handler', () => {
it('should return a properly formatted response', async () => {
// Mock the API client
const mockClient = {
getWorkflows: jest.fn().mockResolvedValue(mockWorkflows)
};
const response = await handleWorkflowsRequest(mockClient as unknown as N8nClient);
expect(mockClient.getWorkflows).toHaveBeenCalled();
expect(response.contents).toHaveLength(1);
expect(response.contents[0].uri).toBe(WORKFLOWS_URI);
expect(response.contents[0].mimeType).toBe('application/json');
// Parse the JSON text to check the content
const content = JSON.parse(response.contents[0].text);
expect(content).toHaveProperty('workflows');
expect(content.workflows).toHaveLength(2);
expect(content.count).toBe(2);
expect(content.workflows[0].id).toBe('1234abc');
});
it('should handle API errors', async () => {
// Mock the API client to throw an error
const mockClient = {
getWorkflows: jest.fn().mockRejectedValue(new Error('API error'))
};
await expect(handleWorkflowsRequest(mockClient as unknown as N8nClient))
.rejects
.toThrow('Failed to retrieve workflows');
});
});
```
### End-to-End Test Example
Here's an example of an end-to-end test that tests the entire system:
```typescript
// tests/e2e/workflow-operations.test.ts
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { MemoryServerTransport } from '@modelcontextprotocol/sdk/server/memory.js';
import { createServer } from '../../src/index.js';
describe('End-to-End Workflow Operations', () => {
let server: Server;
let transport: MemoryServerTransport;
beforeAll(async () => {
// Mock the environment
process.env.N8N_API_URL = 'http://localhost:5678/api/v1';
process.env.N8N_API_KEY = 'test-api-key';
// Create the server with a memory transport
transport = new MemoryServerTransport();
server = await createServer(transport);
});
afterAll(async () => {
await server.close();
});
it('should list workflows', async () => {
// Send a request to list workflows
const response = await transport.sendRequest({
jsonrpc: '2.0',
id: '1',
method: 'callTool',
params: {
name: 'workflow_list',
arguments: {}
}
});
expect(response.result).toBeDefined();
expect(response.result.content).toHaveLength(1);
expect(response.result.content[0].type).toBe('text');
// Parse the JSON text to check the content
const content = JSON.parse(response.result.content[0].text);
expect(Array.isArray(content)).toBe(true);
});
it('should retrieve a workflow by ID', async () => {
// Send a request to get a workflow
const response = await transport.sendRequest({
jsonrpc: '2.0',
id: '2',
method: 'callTool',
params: {
name: 'workflow_get',
arguments: {
id: '1234abc'
}
}
});
expect(response.result).toBeDefined();
expect(response.result.content).toHaveLength(1);
expect(response.result.content[0].type).toBe('text');
// Parse the JSON text to check the content
const content = JSON.parse(response.result.content[0].text);
expect(content).toHaveProperty('id');
expect(content.id).toBe('1234abc');
});
});
```
## Test Fixtures and Mocks
To avoid duplication and improve test maintainability, common test fixtures and mocks are stored in the `tests/mocks/` directory.
### Axios Mock
The Axios HTTP client is mocked using `axios-mock-adapter` to simulate HTTP responses without making actual API calls:
```typescript
// tests/mocks/axios-mock.ts
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
// Create a new instance of the mock adapter
export const axiosMock = new MockAdapter(axios);
// Helper function to reset the mock adapter before each test
export function resetAxiosMock() {
axiosMock.reset();
}
```
### n8n API Fixtures
Common fixtures for n8n API responses are stored in a shared file:
```typescript
// tests/mocks/n8n-fixtures.ts
export 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: 'node1',
name: 'Start',
type: 'n8n-nodes-base.start',
position: [100, 200],
parameters: {}
}
],
connections: {}
},
{
id: '5678def',
name: 'Test Workflow 2',
active: false,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-12T10:15:00.000Z',
nodes: [],
connections: {}
}
];
export const mockExecutions = [
{
id: 'exec123',
workflowId: '1234abc',
workflowName: 'Test Workflow 1',
status: 'success',
startedAt: '2025-03-10T15:00:00.000Z',
finishedAt: '2025-03-10T15:01:00.000Z',
mode: 'manual'
},
{
id: 'exec456',
workflowId: '1234abc',
workflowName: 'Test Workflow 1',
status: 'error',
startedAt: '2025-03-09T12:00:00.000Z',
finishedAt: '2025-03-09T12:00:10.000Z',
mode: 'manual'
}
];
```
## Test Environment
The test environment is configured in `jest.config.js` and `babel.config.js`. Key configurations include:
- TypeScript support via Babel
- ES module support
- Coverage reporting
The `tests/test-setup.ts` file contains global setup code that runs before tests:
```typescript
// tests/test-setup.ts
import { jest } from '@jest/globals';
import { resetAxiosMock } from './mocks/axios-mock';
// Reset mocks before each test
beforeEach(() => {
jest.clearAllMocks();
resetAxiosMock();
});
```
## Best Practices
### General Testing Guidelines
1. **Write tests first**: Follow a test-driven development (TDD) approach when possible.
2. **Test behavior, not implementation**: Focus on what a component does, not how it's implemented.
3. **Keep tests simple**: Each test should test one behavior or aspect of functionality.
4. **Use descriptive test names**: Test names should describe the expected behavior.
5. **Follow the AAA pattern**: Arrange, Act, Assert (setup, execute, verify).
### Mocking Best Practices
1. **Mock dependencies, not the unit under test**: Only mock external dependencies, not the code you're testing.
2. **Use the minimum viable mock**: Only mock the methods and behavior needed for the test.
3. **Ensure mock behavior is realistic**: Mocks should behave similarly to the real implementation.
4. **Verify interactions with mocks**: Use `expect(mock).toHaveBeenCalled()` to verify interactions.
### Error Testing Best Practices
1. **Test error cases**: Don't just test the happy path; test error handling too.
2. **Simulate errors with mocks**: Use mocks to simulate error scenarios.
3. **Verify error messages**: Ensure error messages are helpful and descriptive.
### Performance Testing Considerations
1. **Monitor test performance**: Slow tests can slow down development.
2. **Use test timeout values wisely**: Set appropriate timeout values for async tests.
3. **Minimize redundant setup**: Use `beforeEach` and `beforeAll` to avoid redundant setup.
## Continuous Integration
Tests are run automatically in CI environments on pull requests and commits to the main branch. The CI configuration ensures tests pass before code can be merged.
### CI Test Requirements
- All tests must pass
- Test coverage must not decrease
- Linting checks must pass
## Debugging Tests
### Console Output
You can use `console.log()` statements in your tests to debug issues:
```typescript
it('should do something', () => {
const result = doSomething();
console.log('Result:', result);
expect(result).toBe(expectedValue);
});
```
When running tests with Jest, console output will be displayed for failing tests by default.
### Using the Debugger
You can also use the Node.js debugger with Jest:
```bash
node --inspect-brk node_modules/.bin/jest --runInBand path/to/test
```
Then connect to the debugger with Chrome DevTools or VS Code.
## Conclusion
Thorough testing is essential for maintaining a reliable and robust n8n MCP Server. By following these guidelines and examples, you can write effective tests that help ensure your code works as expected and catches issues early.