Skip to main content
Glama

n8n-MCP

by 88-888
n8n-api-client.tsβ€’15.9 kB
import axios, { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'; import { logger } from '../utils/logger'; import { Workflow, WorkflowListParams, WorkflowListResponse, Execution, ExecutionListParams, ExecutionListResponse, Credential, CredentialListParams, CredentialListResponse, Tag, TagListParams, TagListResponse, HealthCheckResponse, Variable, WebhookRequest, WorkflowExport, WorkflowImport, SourceControlStatus, SourceControlPullResult, SourceControlPushResult, } from '../types/n8n-api'; import { handleN8nApiError, logN8nError } from '../utils/n8n-errors'; import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation'; export interface N8nApiClientConfig { baseUrl: string; apiKey: string; timeout?: number; maxRetries?: number; } export class N8nApiClient { private client: AxiosInstance; private maxRetries: number; constructor(config: N8nApiClientConfig) { const { baseUrl, apiKey, timeout = 30000, maxRetries = 3 } = config; this.maxRetries = maxRetries; // Ensure baseUrl ends with /api/v1 const apiUrl = baseUrl.endsWith('/api/v1') ? baseUrl : `${baseUrl.replace(/\/$/, '')}/api/v1`; this.client = axios.create({ baseURL: apiUrl, timeout, headers: { 'X-N8N-API-KEY': apiKey, 'Content-Type': 'application/json', }, }); // Request interceptor for logging this.client.interceptors.request.use( (config: InternalAxiosRequestConfig) => { logger.debug(`n8n API Request: ${config.method?.toUpperCase()} ${config.url}`, { params: config.params, data: config.data, }); return config; }, (error: unknown) => { logger.error('n8n API Request Error:', error); return Promise.reject(error); } ); // Response interceptor for logging this.client.interceptors.response.use( (response: any) => { logger.debug(`n8n API Response: ${response.status} ${response.config.url}`); return response; }, (error: unknown) => { const n8nError = handleN8nApiError(error); logN8nError(n8nError, 'n8n API Response'); return Promise.reject(n8nError); } ); } // Health check to verify API connectivity async healthCheck(): Promise<HealthCheckResponse> { try { // Try the standard healthz endpoint (available on all n8n instances) const baseUrl = this.client.defaults.baseURL || ''; const healthzUrl = baseUrl.replace(/\/api\/v\d+\/?$/, '') + '/healthz'; const response = await axios.get(healthzUrl, { timeout: 5000, validateStatus: (status) => status < 500 }); if (response.status === 200 && response.data?.status === 'ok') { return { status: 'ok', features: {} // Features detection would require additional endpoints }; } // If healthz doesn't work, fall back to API check throw new Error('healthz endpoint not available'); } catch (error) { // If healthz endpoint doesn't exist, try listing workflows with limit 1 // This is a fallback for older n8n versions try { await this.client.get('/workflows', { params: { limit: 1 } }); return { status: 'ok', features: {} }; } catch (fallbackError) { throw handleN8nApiError(fallbackError); } } } // Workflow Management async createWorkflow(workflow: Partial<Workflow>): Promise<Workflow> { try { const cleanedWorkflow = cleanWorkflowForCreate(workflow); const response = await this.client.post('/workflows', cleanedWorkflow); return response.data; } catch (error) { throw handleN8nApiError(error); } } async getWorkflow(id: string): Promise<Workflow> { try { const response = await this.client.get(`/workflows/${id}`); return response.data; } catch (error) { throw handleN8nApiError(error); } } async updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow> { try { // First, try PUT method (newer n8n versions) const cleanedWorkflow = cleanWorkflowForUpdate(workflow as Workflow); try { const response = await this.client.put(`/workflows/${id}`, cleanedWorkflow); return response.data; } catch (putError: any) { // If PUT fails with 405 (Method Not Allowed), try PATCH if (putError.response?.status === 405) { logger.debug('PUT method not supported, falling back to PATCH'); const response = await this.client.patch(`/workflows/${id}`, cleanedWorkflow); return response.data; } throw putError; } } catch (error) { throw handleN8nApiError(error); } } async deleteWorkflow(id: string): Promise<Workflow> { try { const response = await this.client.delete(`/workflows/${id}`); return response.data; } catch (error) { throw handleN8nApiError(error); } } /** * Lists workflows from n8n instance. * * @param params - Query parameters for filtering and pagination * @returns Paginated list of workflows * * @remarks * This method handles two response formats for backwards compatibility: * - Modern (n8n v0.200.0+): {data: Workflow[], nextCursor?: string} * - Legacy (older versions): Workflow[] (wrapped automatically) * * @see https://github.com/czlonkowski/n8n-mcp/issues/349 */ async listWorkflows(params: WorkflowListParams = {}): Promise<WorkflowListResponse> { try { const response = await this.client.get('/workflows', { params }); return this.validateListResponse<Workflow>(response.data, 'workflows'); } catch (error) { throw handleN8nApiError(error); } } // Execution Management async getExecution(id: string, includeData = false): Promise<Execution> { try { const response = await this.client.get(`/executions/${id}`, { params: { includeData }, }); return response.data; } catch (error) { throw handleN8nApiError(error); } } /** * Lists executions from n8n instance. * * @param params - Query parameters for filtering and pagination * @returns Paginated list of executions * * @remarks * This method handles two response formats for backwards compatibility: * - Modern (n8n v0.200.0+): {data: Execution[], nextCursor?: string} * - Legacy (older versions): Execution[] (wrapped automatically) * * @see https://github.com/czlonkowski/n8n-mcp/issues/349 */ async listExecutions(params: ExecutionListParams = {}): Promise<ExecutionListResponse> { try { const response = await this.client.get('/executions', { params }); return this.validateListResponse<Execution>(response.data, 'executions'); } catch (error) { throw handleN8nApiError(error); } } async deleteExecution(id: string): Promise<void> { try { await this.client.delete(`/executions/${id}`); } catch (error) { throw handleN8nApiError(error); } } // Webhook Execution async triggerWebhook(request: WebhookRequest): Promise<any> { try { const { webhookUrl, httpMethod, data, headers, waitForResponse = true } = request; // SECURITY: Validate URL for SSRF protection (includes DNS resolution) // See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-03) const { SSRFProtection } = await import('../utils/ssrf-protection'); const validation = await SSRFProtection.validateWebhookUrl(webhookUrl); if (!validation.valid) { throw new Error(`SSRF protection: ${validation.reason}`); } // Extract path from webhook URL const url = new URL(webhookUrl); const webhookPath = url.pathname; // Make request directly to webhook endpoint const config: AxiosRequestConfig = { method: httpMethod, url: webhookPath, headers: { ...headers, // Don't override API key header for webhook endpoints 'X-N8N-API-KEY': undefined, }, data: httpMethod !== 'GET' ? data : undefined, params: httpMethod === 'GET' ? data : undefined, // Webhooks might take longer timeout: waitForResponse ? 120000 : 30000, }; // Create a new axios instance for webhook requests to avoid API interceptors const webhookClient = axios.create({ baseURL: new URL('/', webhookUrl).toString(), validateStatus: (status) => status < 500, // Don't throw on 4xx }); const response = await webhookClient.request(config); return { status: response.status, statusText: response.statusText, data: response.data, headers: response.headers, }; } catch (error) { throw handleN8nApiError(error); } } // Credential Management /** * Lists credentials from n8n instance. * * @param params - Query parameters for filtering and pagination * @returns Paginated list of credentials * * @remarks * This method handles two response formats for backwards compatibility: * - Modern (n8n v0.200.0+): {data: Credential[], nextCursor?: string} * - Legacy (older versions): Credential[] (wrapped automatically) * * @see https://github.com/czlonkowski/n8n-mcp/issues/349 */ async listCredentials(params: CredentialListParams = {}): Promise<CredentialListResponse> { try { const response = await this.client.get('/credentials', { params }); return this.validateListResponse<Credential>(response.data, 'credentials'); } catch (error) { throw handleN8nApiError(error); } } async getCredential(id: string): Promise<Credential> { try { const response = await this.client.get(`/credentials/${id}`); return response.data; } catch (error) { throw handleN8nApiError(error); } } async createCredential(credential: Partial<Credential>): Promise<Credential> { try { const response = await this.client.post('/credentials', credential); return response.data; } catch (error) { throw handleN8nApiError(error); } } async updateCredential(id: string, credential: Partial<Credential>): Promise<Credential> { try { const response = await this.client.patch(`/credentials/${id}`, credential); return response.data; } catch (error) { throw handleN8nApiError(error); } } async deleteCredential(id: string): Promise<void> { try { await this.client.delete(`/credentials/${id}`); } catch (error) { throw handleN8nApiError(error); } } // Tag Management /** * Lists tags from n8n instance. * * @param params - Query parameters for filtering and pagination * @returns Paginated list of tags * * @remarks * This method handles two response formats for backwards compatibility: * - Modern (n8n v0.200.0+): {data: Tag[], nextCursor?: string} * - Legacy (older versions): Tag[] (wrapped automatically) * * @see https://github.com/czlonkowski/n8n-mcp/issues/349 */ async listTags(params: TagListParams = {}): Promise<TagListResponse> { try { const response = await this.client.get('/tags', { params }); return this.validateListResponse<Tag>(response.data, 'tags'); } catch (error) { throw handleN8nApiError(error); } } async createTag(tag: Partial<Tag>): Promise<Tag> { try { const response = await this.client.post('/tags', tag); return response.data; } catch (error) { throw handleN8nApiError(error); } } async updateTag(id: string, tag: Partial<Tag>): Promise<Tag> { try { const response = await this.client.patch(`/tags/${id}`, tag); return response.data; } catch (error) { throw handleN8nApiError(error); } } async deleteTag(id: string): Promise<void> { try { await this.client.delete(`/tags/${id}`); } catch (error) { throw handleN8nApiError(error); } } // Source Control Management (Enterprise feature) async getSourceControlStatus(): Promise<SourceControlStatus> { try { const response = await this.client.get('/source-control/status'); return response.data; } catch (error) { throw handleN8nApiError(error); } } async pullSourceControl(force = false): Promise<SourceControlPullResult> { try { const response = await this.client.post('/source-control/pull', { force }); return response.data; } catch (error) { throw handleN8nApiError(error); } } async pushSourceControl( message: string, fileNames?: string[] ): Promise<SourceControlPushResult> { try { const response = await this.client.post('/source-control/push', { message, fileNames, }); return response.data; } catch (error) { throw handleN8nApiError(error); } } // Variable Management (via Source Control API) async getVariables(): Promise<Variable[]> { try { const response = await this.client.get('/variables'); return response.data.data || []; } catch (error) { // Variables might not be available in all n8n versions logger.warn('Variables API not available, returning empty array'); return []; } } async createVariable(variable: Partial<Variable>): Promise<Variable> { try { const response = await this.client.post('/variables', variable); return response.data; } catch (error) { throw handleN8nApiError(error); } } async updateVariable(id: string, variable: Partial<Variable>): Promise<Variable> { try { const response = await this.client.patch(`/variables/${id}`, variable); return response.data; } catch (error) { throw handleN8nApiError(error); } } async deleteVariable(id: string): Promise<void> { try { await this.client.delete(`/variables/${id}`); } catch (error) { throw handleN8nApiError(error); } } /** * Validates and normalizes n8n API list responses. * Handles both modern format {data: [], nextCursor?: string} and legacy array format. * * @param responseData - Raw response data from n8n API * @param resourceType - Resource type for error messages (e.g., 'workflows', 'executions') * @returns Normalized response in modern format * @throws Error if response structure is invalid */ private validateListResponse<T>( responseData: any, resourceType: string ): { data: T[]; nextCursor?: string | null } { // Validate response structure if (!responseData || typeof responseData !== 'object') { throw new Error(`Invalid response from n8n API for ${resourceType}: response is not an object`); } // Handle legacy case where API returns array directly (older n8n versions) if (Array.isArray(responseData)) { logger.warn( `n8n API returned array directly instead of {data, nextCursor} object for ${resourceType}. ` + 'Wrapping in expected format for backwards compatibility.' ); return { data: responseData, nextCursor: null }; } // Validate expected format {data: [], nextCursor?: string} if (!Array.isArray(responseData.data)) { const keys = Object.keys(responseData).slice(0, 5); const keysPreview = keys.length < Object.keys(responseData).length ? `${keys.join(', ')}...` : keys.join(', '); throw new Error( `Invalid response from n8n API for ${resourceType}: expected {data: [], nextCursor?: string}, ` + `got object with keys: [${keysPreview}]` ); } return responseData; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server