import { MCPTool } from 'mcp-framework';
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';
import { z } from 'zod';
import { generateTypeScriptTypes } from './utils/typeGenerator.js';
interface NextJSIntegrationOptions {
workflowId: string;
workflowName: string;
apiBasePath: string;
outputDir: string;
generateOpenAPI: boolean;
generateTypes: boolean;
includeWebhooks: boolean;
}
interface Endpoint {
nodeId: string;
nodeName: string;
method: string;
path: string;
inputSchema: any;
outputSchema: any;
}
interface WorkflowNode {
id: string;
name: string;
type: string;
parameters?: {
httpMethod?: string;
method?: string;
path?: string;
[key: string]: any;
};
[key: string]: any;
}
interface Workflow {
id: string;
name: string;
nodes: WorkflowNode[];
[key: string]: any;
}
// Interface simplifiée pour OpenAPI
interface OpenAPIDocument {
openapi: string;
info: {
title: string;
version: string;
description?: string;
};
paths: Record<string, any>;
components?: {
schemas?: Record<string, any>;
securitySchemes?: Record<string, any>;
};
security?: Array<Record<string, any>>;
}
// Interface simplifiée pour les opérations OpenAPI
interface PathItemObject {
[method: string]: any;
}
// Interface simplifiée pour les opérations OpenAPI
interface OperationObject {
summary?: string;
description?: string;
tags?: string[];
requestBody?: any;
responses?: Record<string, any>;
}
/**
* Tool pour générer des intégrations NextJS pour les workflows n8n
*/
class NextJSIntegrationTool extends MCPTool<NextJSIntegrationOptions> {
private n8nApiUrl: string;
private n8nApiKey: string;
/**
* Nom de l'outil
*/
name = 'nextjs_integration';
/**
* Description de l'outil
*/
description = 'Génère des intégrations NextJS pour les workflows n8n';
/**
* Schéma de validation des paramètres
*/
schema = {
workflowId: {
type: z.string(),
description: 'ID du workflow n8n à intégrer'
},
workflowName: {
type: z.string(),
description: 'Nom du workflow n8n à intégrer'
},
apiBasePath: {
type: z.string(),
description: 'Chemin de base de l\'API pour les endpoints générés'
},
outputDir: {
type: z.string(),
description: 'Répertoire de sortie pour les fichiers générés'
},
generateOpenAPI: {
type: z.boolean(),
description: 'Générer la documentation OpenAPI'
},
generateTypes: {
type: z.boolean(),
description: 'Générer les types TypeScript'
},
includeWebhooks: {
type: z.boolean(),
description: 'Inclure les webhooks dans la génération'
}
};
constructor() {
super();
// Récupérer les variables d'environnement
this.n8nApiUrl = process.env.N8N_API_URL || 'http://localhost:5678/api/v1';
this.n8nApiKey = process.env.N8N_API_KEY || '';
}
/**
* Exécute l'outil
*/
async execute(options: NextJSIntegrationOptions): Promise<any> {
try {
console.log(`Génération de l'intégration NextJS pour le workflow ${options.workflowName} (${options.workflowId})`);
// Créer le répertoire de sortie s'il n'existe pas
if (!fs.existsSync(options.outputDir)) {
fs.mkdirSync(options.outputDir, { recursive: true });
}
// Récupérer les détails du workflow
const workflow = await this.getWorkflow(options.workflowId);
if (!workflow) {
throw new Error(`Workflow ${options.workflowId} non trouvé`);
}
// Générer les endpoints REST
const endpoints = this.generateEndpoints(workflow, options);
// Générer la documentation OpenAPI si demandé
if (options.generateOpenAPI) {
const openApiSpec = this.generateOpenAPISpec(workflow, endpoints, options);
this.saveOpenAPISpec(openApiSpec, options.outputDir);
}
// Générer les types TypeScript si demandé
if (options.generateTypes) {
const types = this.generateTypeScriptInterfaces(workflow, endpoints);
this.saveTypeScriptTypes(types, options.outputDir);
}
// Générer les fichiers d'intégration NextJS
this.generateNextJSIntegration(workflow, endpoints, options);
return {
success: true,
message: `Intégration NextJS générée avec succès dans ${options.outputDir}`,
endpoints: endpoints.map(e => e.path)
};
} catch (error: unknown) {
console.error('Erreur lors de la génération de l\'intégration NextJS:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `Erreur: ${errorMessage}`
};
}
}
/**
* Récupère les détails d'un workflow depuis l'API n8n
*/
private async getWorkflow(workflowId: string): Promise<Workflow> {
try {
const response = await axios.get(`${this.n8nApiUrl}/workflows/${workflowId}`, {
headers: {
'X-N8N-API-KEY': this.n8nApiKey
}
});
return response.data;
} catch (error: unknown) {
console.error(`Erreur lors de la récupération du workflow ${workflowId}:`, error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Impossible de récupérer le workflow: ${errorMessage}`);
}
}
/**
* Génère les endpoints REST pour un workflow
*/
private generateEndpoints(workflow: Workflow, options: NextJSIntegrationOptions): Endpoint[] {
const endpoints: Endpoint[] = [];
// Analyser le workflow pour identifier les points d'entrée
const webhookNodes = workflow.nodes.filter(node =>
node.type === 'n8n-nodes-base.webhook' ||
node.type === 'n8n-nodes-base.httpRequest' ||
(options.includeWebhooks && node.type.includes('Trigger'))
);
// Générer un endpoint pour chaque nœud webhook
webhookNodes.forEach(node => {
const method = this.getNodeMethod(node);
const path = this.getNodePath(node, options.apiBasePath);
// Extraire les paramètres d'entrée et de sortie
const inputSchema = this.extractInputSchema(node);
const outputSchema = this.extractOutputSchema(node);
endpoints.push({
nodeId: node.id,
nodeName: node.name,
method,
path,
inputSchema,
outputSchema
});
});
return endpoints;
}
/**
* Détermine la méthode HTTP pour un nœud
*/
private getNodeMethod(node: WorkflowNode): string {
if (node.type === 'n8n-nodes-base.webhook') {
return (node.parameters?.httpMethod || 'GET').toUpperCase();
}
if (node.type === 'n8n-nodes-base.httpRequest') {
return (node.parameters?.method || 'GET').toUpperCase();
}
// Par défaut, utiliser POST pour les autres types de nœuds
return 'POST';
}
/**
* Détermine le chemin de l'endpoint pour un nœud
*/
private getNodePath(node: WorkflowNode, basePath: string): string {
if (node.type === 'n8n-nodes-base.webhook' && node.parameters?.path) {
return `${basePath}/${node.parameters.path.replace(/^\//, '')}`;
}
// Pour les autres types de nœuds, utiliser le nom du nœud
return `${basePath}/${this.slugify(node.name)}`;
}
/**
* Extrait le schéma d'entrée d'un nœud
*/
private extractInputSchema(node: WorkflowNode): any {
// Logique pour extraire le schéma d'entrée
return {
type: 'object',
properties: {}
};
}
/**
* Extrait le schéma de sortie d'un nœud
*/
private extractOutputSchema(node: WorkflowNode): any {
// Logique pour extraire le schéma de sortie
return {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' }
}
};
}
/**
* Génère la spécification OpenAPI pour un workflow
*/
private generateOpenAPISpec(workflow: Workflow, endpoints: Endpoint[], options: NextJSIntegrationOptions): OpenAPIDocument {
// Créer la structure de base de la spécification OpenAPI
const openApiSpec: OpenAPIDocument = {
openapi: '3.0.0',
info: {
title: `API pour ${workflow.name}`,
version: '1.0.0',
description: `API générée automatiquement pour le workflow n8n "${workflow.name}"`
},
paths: {},
components: {
schemas: {},
securitySchemes: {
ApiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'X-API-KEY'
}
}
},
security: [
{ ApiKeyAuth: [] }
]
};
// Ajouter les endpoints à la spécification
endpoints.forEach(endpoint => {
const pathItem: PathItemObject = {};
// Créer l'objet d'opération pour la méthode HTTP
const operation: OperationObject = {
summary: `Endpoint pour ${endpoint.nodeName}`,
description: `Exécute le nœud "${endpoint.nodeName}" du workflow "${workflow.name}"`,
tags: [workflow.name],
requestBody: {
content: {
'application/json': {
schema: endpoint.inputSchema
}
}
},
responses: {
'200': {
description: 'Succès',
content: {
'application/json': {
schema: endpoint.outputSchema
}
}
},
'400': {
description: 'Requête invalide'
},
'500': {
description: 'Erreur serveur'
}
}
};
// Ajouter l'opération à l'objet de chemin
pathItem[endpoint.method.toLowerCase()] = operation;
// Ajouter le chemin à la spécification
openApiSpec.paths[endpoint.path] = pathItem;
});
return openApiSpec;
}
/**
* Sauvegarde la spécification OpenAPI dans un fichier
*/
private saveOpenAPISpec(openApiSpec: OpenAPIDocument, outputDir: string): void {
const filePath = path.join(outputDir, 'openapi.json');
fs.writeFileSync(filePath, JSON.stringify(openApiSpec, null, 2));
console.log(`Spécification OpenAPI sauvegardée dans ${filePath}`);
}
/**
* Génère les interfaces TypeScript pour un workflow
*/
private generateTypeScriptInterfaces(workflow: Workflow, endpoints: Endpoint[]): string {
let types = '// Types générés automatiquement pour le workflow ' + workflow.name + '\n\n';
// Générer les types pour chaque endpoint
endpoints.forEach(endpoint => {
const inputTypeName = this.pascalCase(endpoint.nodeName) + 'Input';
const outputTypeName = this.pascalCase(endpoint.nodeName) + 'Output';
// Générer le type d'entrée
types += `export interface ${inputTypeName} {\n`;
types += generateTypeScriptTypes(endpoint.inputSchema);
types += '}\n\n';
// Générer le type de sortie
types += `export interface ${outputTypeName} {\n`;
types += generateTypeScriptTypes(endpoint.outputSchema);
types += '}\n\n';
});
return types;
}
/**
* Sauvegarde les types TypeScript dans un fichier
*/
private saveTypeScriptTypes(types: string, outputDir: string): void {
const filePath = path.join(outputDir, 'types.ts');
fs.writeFileSync(filePath, types);
console.log(`Types TypeScript sauvegardés dans ${filePath}`);
}
/**
* Génère les fichiers d'intégration NextJS
*/
private generateNextJSIntegration(workflow: Workflow, endpoints: Endpoint[], options: NextJSIntegrationOptions): void {
// Créer le répertoire api s'il n'existe pas
const apiDir = path.join(options.outputDir, 'api');
if (!fs.existsSync(apiDir)) {
fs.mkdirSync(apiDir, { recursive: true });
}
// Générer les fichiers de route pour chaque endpoint
endpoints.forEach(endpoint => {
this.generateApiRoute(endpoint, workflow, options);
});
// Générer le client API
this.generateApiClient(workflow, endpoints, options);
}
/**
* Génère un fichier de route API NextJS pour un endpoint
*/
private generateApiRoute(endpoint: Endpoint, workflow: Workflow, options: NextJSIntegrationOptions): void {
// Extraire le chemin relatif de l'endpoint
const relativePath = endpoint.path.replace(options.apiBasePath, '').replace(/^\//, '');
// Créer le répertoire pour la route
const routeDir = path.join(options.outputDir, 'api', relativePath);
if (!fs.existsSync(routeDir)) {
fs.mkdirSync(routeDir, { recursive: true });
}
// Générer le contenu du fichier de route
const routeContent = `
import { NextApiRequest, NextApiResponse } from 'next';
import axios from 'axios';
// Endpoint pour ${endpoint.nodeName}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Vérifier la méthode HTTP
if (req.method !== '${endpoint.method}') {
return res.status(405).json({ error: 'Méthode non autorisée' });
}
try {
// Récupérer les données de la requête
const data = req.method === 'GET' ? req.query : req.body;
// Appeler le workflow n8n
const response = await axios.post(
process.env.N8N_API_URL + '/workflows/${workflow.id}/execute',
{
data,
workflowData: {
id: '${workflow.id}',
name: '${workflow.name}'
}
},
{
headers: {
'X-N8N-API-KEY': process.env.N8N_API_KEY || ''
}
}
);
// Renvoyer la réponse
return res.status(200).json(response.data);
} catch (error) {
console.error('Erreur lors de l\\'exécution du workflow:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
return res.status(500).json({ error: errorMessage });
}
}
`;
// Écrire le fichier de route
const routeFilePath = path.join(routeDir, 'index.ts');
fs.writeFileSync(routeFilePath, routeContent);
console.log(`Route API générée dans ${routeFilePath}`);
}
/**
* Génère un client API pour le workflow
*/
private generateApiClient(workflow: Workflow, endpoints: Endpoint[], options: NextJSIntegrationOptions): void {
// Générer le contenu du client API
let clientContent = `
// Client API généré automatiquement pour le workflow ${workflow.name}
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
export class ${this.pascalCase(workflow.name)}Client {
private client: AxiosInstance;
constructor(baseURL: string, apiKey?: string) {
this.client = axios.create({
baseURL,
headers: apiKey ? { 'X-API-KEY': apiKey } : {}
});
}
`;
// Ajouter les méthodes pour chaque endpoint
endpoints.forEach(endpoint => {
const methodName = this.camelCase(endpoint.nodeName);
const inputTypeName = this.pascalCase(endpoint.nodeName) + 'Input';
const outputTypeName = this.pascalCase(endpoint.nodeName) + 'Output';
clientContent += `
/**
* ${endpoint.nodeName}
* @param data Données d'entrée pour l'endpoint
* @param options Options de requête Axios
* @returns Résultat de l'exécution du workflow
*/
async ${methodName}(data: ${inputTypeName}, options?: AxiosRequestConfig): Promise<${outputTypeName}> {
const response = await this.client.${endpoint.method.toLowerCase()}('${endpoint.path}', ${endpoint.method === 'GET' ? '{ params: data, ...options }' : 'data, options'});
return response.data;
}
`;
});
clientContent += '}\n';
// Écrire le fichier client
const clientFilePath = path.join(options.outputDir, 'api-client.ts');
fs.writeFileSync(clientFilePath, clientContent);
console.log(`Client API généré dans ${clientFilePath}`);
}
/**
* Convertit une chaîne en format slug (minuscules, tirets)
*/
private slugify(text: string): string {
return text
.toString()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '');
}
/**
* Convertit une chaîne en format camelCase
*/
private camelCase(text: string): string {
return text
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) =>
index === 0 ? word.toLowerCase() : word.toUpperCase()
)
.replace(/\s+/g, '');
}
/**
* Convertit une chaîne en format PascalCase
*/
private pascalCase(text: string): string {
return text
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase())
.replace(/\s+/g, '');
}
}
export default NextJSIntegrationTool;