feat: Complete overhaul to enhanced documentation-only MCP server
- Removed all workflow execution capabilities per user requirements - Implemented enhanced documentation extraction with operations and API mappings - Fixed credential code extraction for all nodes - Fixed package info extraction (name and version) - Enhanced operations parser to handle n8n markdown format - Fixed documentation search to prioritize app nodes over trigger nodes - Comprehensive test coverage for Slack node extraction - All node information now includes: - Complete operations list (42 for Slack) - API method mappings with documentation URLs - Source code and credential definitions - Package metadata - Related resources and templates 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,592 +0,0 @@
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
// WebSocketServerTransport is not available in the SDK, we'll implement a custom solution
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { NodeDocumentationService } from '../services/node-documentation-service';
|
||||
import { nodeDocumentationTools } from './tools-v2';
|
||||
import { logger } from '../utils/logger';
|
||||
import { authenticateRequest } from '../utils/auth-middleware';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
interface HttpServerConfig {
|
||||
port: number;
|
||||
host: string;
|
||||
domain: string;
|
||||
authToken?: string;
|
||||
cors?: boolean;
|
||||
tlsCert?: string;
|
||||
tlsKey?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP/WebSocket MCP Server for remote access
|
||||
*/
|
||||
export class N8NDocumentationHttpServer {
|
||||
private app: express.Application;
|
||||
private server: any;
|
||||
private wss!: WebSocketServer;
|
||||
private nodeService: NodeDocumentationService;
|
||||
private config: HttpServerConfig;
|
||||
private activeSessions: Map<string, any> = new Map();
|
||||
|
||||
constructor(config: HttpServerConfig) {
|
||||
this.config = config;
|
||||
this.app = express();
|
||||
this.nodeService = new NodeDocumentationService();
|
||||
|
||||
this.setupMiddleware();
|
||||
this.setupRoutes();
|
||||
this.setupWebSocket();
|
||||
}
|
||||
|
||||
private setupMiddleware(): void {
|
||||
// JSON parsing
|
||||
this.app.use(express.json());
|
||||
|
||||
// CORS if enabled
|
||||
if (this.config.cors) {
|
||||
this.app.use((req, res, next): void => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Request logging
|
||||
this.app.use((req, res, next): void => {
|
||||
logger.info(`${req.method} ${req.path}`, {
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent')
|
||||
});
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// Health check endpoint
|
||||
this.app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'n8n-documentation-mcp',
|
||||
version: '2.0.0',
|
||||
uptime: process.uptime()
|
||||
});
|
||||
});
|
||||
|
||||
// MCP info endpoint
|
||||
this.app.get('/mcp', (req, res) => {
|
||||
res.json({
|
||||
name: 'n8n-node-documentation',
|
||||
version: '2.0.0',
|
||||
description: 'MCP server providing n8n node documentation and source code',
|
||||
transport: 'websocket',
|
||||
endpoint: `wss://${this.config.domain}/mcp/websocket`,
|
||||
authentication: 'bearer-token',
|
||||
tools: nodeDocumentationTools.map(t => ({
|
||||
name: t.name,
|
||||
description: t.description
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
// Database stats endpoint (public)
|
||||
this.app.get('/stats', async (req, res) => {
|
||||
try {
|
||||
const stats = this.nodeService.getStatistics();
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
logger.error('Failed to get statistics:', error);
|
||||
res.status(500).json({ error: 'Failed to retrieve statistics' });
|
||||
}
|
||||
});
|
||||
|
||||
// Rebuild endpoint (requires auth)
|
||||
this.app.post('/rebuild', authenticateRequest(this.config.authToken), async (req, res) => {
|
||||
try {
|
||||
logger.info('Database rebuild requested');
|
||||
const stats = await this.nodeService.rebuildDatabase();
|
||||
res.json({
|
||||
message: 'Database rebuild complete',
|
||||
stats
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Rebuild failed:', error);
|
||||
res.status(500).json({ error: 'Rebuild failed' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupWebSocket(): void {
|
||||
// Create HTTP server
|
||||
this.server = createServer(this.app);
|
||||
|
||||
// Create WebSocket server
|
||||
this.wss = new WebSocketServer({
|
||||
server: this.server,
|
||||
path: '/mcp/websocket'
|
||||
});
|
||||
|
||||
this.wss.on('connection', async (ws: WebSocket, req: any) => {
|
||||
const sessionId = crypto.randomUUID();
|
||||
logger.info(`WebSocket connection established: ${sessionId}`);
|
||||
|
||||
// Authenticate WebSocket connection
|
||||
const authHeader = req.headers.authorization;
|
||||
if (this.config.authToken && authHeader !== `Bearer ${this.config.authToken}`) {
|
||||
logger.warn(`Unauthorized WebSocket connection attempt: ${sessionId}`);
|
||||
ws.close(1008, 'Unauthorized');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create MCP server instance for this connection
|
||||
const mcpServer = new Server(
|
||||
{
|
||||
name: 'n8n-node-documentation',
|
||||
version: '2.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Setup MCP handlers
|
||||
this.setupMcpHandlers(mcpServer);
|
||||
|
||||
// WebSocket transport not available in SDK - implement JSON-RPC over WebSocket
|
||||
// For now, we'll handle messages directly
|
||||
ws.on('message', async (data: Buffer) => {
|
||||
try {
|
||||
const request = JSON.parse(data.toString());
|
||||
// Process request through MCP server handlers
|
||||
// This would need custom implementation
|
||||
logger.warn('WebSocket MCP not fully implemented yet');
|
||||
ws.send(JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: request.id,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: 'WebSocket transport not implemented'
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('WebSocket message error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.activeSessions.set(sessionId, { mcpServer, ws });
|
||||
logger.info(`MCP session established: ${sessionId}`);
|
||||
|
||||
// Handle disconnect
|
||||
ws.on('close', () => {
|
||||
logger.info(`WebSocket connection closed: ${sessionId}`);
|
||||
this.activeSessions.delete(sessionId);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Failed to establish MCP session: ${sessionId}`, error);
|
||||
ws.close(1011, 'Server error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupMcpHandlers(server: Server): void {
|
||||
// List available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: nodeDocumentationTools,
|
||||
}));
|
||||
|
||||
// List available resources
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: [
|
||||
{
|
||||
uri: 'nodes://list',
|
||||
name: 'Available n8n Nodes',
|
||||
description: 'List of all available n8n nodes',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'nodes://statistics',
|
||||
name: 'Database Statistics',
|
||||
description: 'Statistics about the node documentation database',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// Read resources
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const { uri } = request.params;
|
||||
|
||||
try {
|
||||
if (uri === 'nodes://list') {
|
||||
const nodes = await this.nodeService.listNodes();
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(nodes.map(n => ({
|
||||
nodeType: n.nodeType,
|
||||
name: n.name,
|
||||
displayName: n.displayName,
|
||||
category: n.category,
|
||||
description: n.description,
|
||||
hasDocumentation: !!n.documentation,
|
||||
hasExample: !!n.exampleWorkflow,
|
||||
})), null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (uri === 'nodes://statistics') {
|
||||
const stats = this.nodeService.getStatistics();
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(stats, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
||||
} catch (error) {
|
||||
logger.error('Resource read error:', error);
|
||||
throw error instanceof McpError ? error : new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to read resource: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
case 'list_nodes':
|
||||
return await this.handleListNodes(args);
|
||||
|
||||
case 'get_node_info':
|
||||
return await this.handleGetNodeInfo(args);
|
||||
|
||||
case 'search_nodes':
|
||||
return await this.handleSearchNodes(args);
|
||||
|
||||
case 'get_node_example':
|
||||
return await this.handleGetNodeExample(args);
|
||||
|
||||
case 'get_node_source_code':
|
||||
return await this.handleGetNodeSourceCode(args);
|
||||
|
||||
case 'get_node_documentation':
|
||||
return await this.handleGetNodeDocumentation(args);
|
||||
|
||||
case 'rebuild_database':
|
||||
return await this.handleRebuildDatabase(args);
|
||||
|
||||
case 'get_database_statistics':
|
||||
return await this.handleGetStatistics();
|
||||
|
||||
default:
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Tool execution error (${name}):`, error);
|
||||
throw error instanceof McpError ? error : new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Tool handlers (copied from server-v2.ts)
|
||||
private async handleListNodes(args: any): Promise<any> {
|
||||
const nodes = await this.nodeService.listNodes();
|
||||
|
||||
let filtered = nodes;
|
||||
|
||||
if (args.category) {
|
||||
filtered = filtered.filter(n => n.category === args.category);
|
||||
}
|
||||
|
||||
if (args.packageName) {
|
||||
filtered = filtered.filter(n => n.packageName === args.packageName);
|
||||
}
|
||||
|
||||
if (args.isTrigger !== undefined) {
|
||||
filtered = filtered.filter(n => n.isTrigger === args.isTrigger);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered.map(n => ({
|
||||
nodeType: n.nodeType,
|
||||
name: n.name,
|
||||
displayName: n.displayName,
|
||||
category: n.category,
|
||||
description: n.description,
|
||||
packageName: n.packageName,
|
||||
hasDocumentation: !!n.documentation,
|
||||
hasExample: !!n.exampleWorkflow,
|
||||
isTrigger: n.isTrigger,
|
||||
isWebhook: n.isWebhook,
|
||||
})), null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeInfo(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
nodeType: nodeInfo.nodeType,
|
||||
name: nodeInfo.name,
|
||||
displayName: nodeInfo.displayName,
|
||||
description: nodeInfo.description,
|
||||
category: nodeInfo.category,
|
||||
packageName: nodeInfo.packageName,
|
||||
sourceCode: nodeInfo.sourceCode,
|
||||
credentialCode: nodeInfo.credentialCode,
|
||||
documentation: nodeInfo.documentation,
|
||||
documentationUrl: nodeInfo.documentationUrl,
|
||||
exampleWorkflow: nodeInfo.exampleWorkflow,
|
||||
exampleParameters: nodeInfo.exampleParameters,
|
||||
propertiesSchema: nodeInfo.propertiesSchema,
|
||||
isTrigger: nodeInfo.isTrigger,
|
||||
isWebhook: nodeInfo.isWebhook,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleSearchNodes(args: any): Promise<any> {
|
||||
if (!args.query) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'query is required');
|
||||
}
|
||||
|
||||
const results = await this.nodeService.searchNodes({
|
||||
query: args.query,
|
||||
category: args.category,
|
||||
limit: args.limit || 20,
|
||||
});
|
||||
|
||||
let filtered = results;
|
||||
if (args.hasDocumentation) {
|
||||
filtered = filtered.filter(n => !!n.documentation);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered.map(n => ({
|
||||
nodeType: n.nodeType,
|
||||
name: n.name,
|
||||
displayName: n.displayName,
|
||||
category: n.category,
|
||||
description: n.description,
|
||||
hasDocumentation: !!n.documentation,
|
||||
hasExample: !!n.exampleWorkflow,
|
||||
})), null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeExample(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
if (!nodeInfo.exampleWorkflow) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No example available for node: ${args.nodeType}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(nodeInfo.exampleWorkflow, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeSourceCode(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
const response: any = {
|
||||
nodeType: nodeInfo.nodeType,
|
||||
sourceCode: nodeInfo.sourceCode,
|
||||
};
|
||||
|
||||
if (args.includeCredentials && nodeInfo.credentialCode) {
|
||||
response.credentialCode = nodeInfo.credentialCode;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeDocumentation(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
if (!nodeInfo.documentation) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No documentation available for node: ${args.nodeType}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const content = args.format === 'plain'
|
||||
? nodeInfo.documentation.replace(/[#*`]/g, '')
|
||||
: nodeInfo.documentation;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleRebuildDatabase(args: any): Promise<any> {
|
||||
logger.info('Database rebuild requested via MCP');
|
||||
|
||||
const stats = await this.nodeService.rebuildDatabase();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Database rebuild complete',
|
||||
stats,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetStatistics(): Promise<any> {
|
||||
const stats = this.nodeService.getStatistics();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(stats, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.server.listen(this.config.port, this.config.host, () => {
|
||||
logger.info(`n8n Documentation MCP HTTP server started`);
|
||||
logger.info(`HTTP endpoint: http://${this.config.host}:${this.config.port}`);
|
||||
logger.info(`WebSocket endpoint: ws://${this.config.host}:${this.config.port}/mcp/websocket`);
|
||||
logger.info(`Domain: ${this.config.domain}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
logger.info('Stopping n8n Documentation MCP HTTP server...');
|
||||
|
||||
// Close all WebSocket connections
|
||||
this.wss.clients.forEach((ws: WebSocket) => ws.close());
|
||||
|
||||
// Close HTTP server
|
||||
return new Promise((resolve) => {
|
||||
this.server.close(() => {
|
||||
this.nodeService.close();
|
||||
logger.info('Server stopped');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,608 +0,0 @@
|
||||
import express from 'express';
|
||||
import { createServer as createHttpServer } from 'http';
|
||||
import { createServer as createHttpsServer } from 'https';
|
||||
import {
|
||||
ErrorCode,
|
||||
McpError,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { NodeDocumentationService } from '../services/node-documentation-service';
|
||||
import { nodeDocumentationTools } from './tools-v2';
|
||||
import { logger } from '../utils/logger';
|
||||
import { authenticateRequest } from '../utils/auth-middleware';
|
||||
import * as fs from 'fs';
|
||||
|
||||
interface RemoteServerConfig {
|
||||
port: number;
|
||||
host: string;
|
||||
domain: string;
|
||||
authToken?: string;
|
||||
cors?: boolean;
|
||||
tlsCert?: string;
|
||||
tlsKey?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote MCP Server using Streamable HTTP transport
|
||||
* Based on MCP's modern approach for remote servers
|
||||
*/
|
||||
export class N8NDocumentationRemoteServer {
|
||||
private app: express.Application;
|
||||
private server: any;
|
||||
private nodeService: NodeDocumentationService;
|
||||
private config: RemoteServerConfig;
|
||||
|
||||
constructor(config: RemoteServerConfig) {
|
||||
this.config = config;
|
||||
this.app = express();
|
||||
this.nodeService = new NodeDocumentationService();
|
||||
|
||||
this.setupMiddleware();
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
private setupMiddleware(): void {
|
||||
// Parse JSON bodies with larger limit for MCP messages
|
||||
this.app.use(express.json({ limit: '10mb' }));
|
||||
|
||||
// CORS if enabled
|
||||
if (this.config.cors) {
|
||||
this.app.use((req, res, next): void => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Request-ID');
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Request logging
|
||||
this.app.use((req, res, next): void => {
|
||||
logger.info(`${req.method} ${req.path}`, {
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
requestId: req.get('X-Request-ID')
|
||||
});
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// Health check endpoint
|
||||
this.app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'n8n-documentation-mcp',
|
||||
version: '2.0.0',
|
||||
uptime: process.uptime(),
|
||||
domain: this.config.domain
|
||||
});
|
||||
});
|
||||
|
||||
// MCP info endpoint - provides server capabilities
|
||||
this.app.get('/', (req, res) => {
|
||||
res.json({
|
||||
name: 'n8n-node-documentation',
|
||||
version: '2.0.0',
|
||||
description: 'MCP server providing n8n node documentation and source code',
|
||||
transport: 'http',
|
||||
endpoint: `https://${this.config.domain}/mcp`,
|
||||
authentication: this.config.authToken ? 'bearer-token' : 'none',
|
||||
capabilities: {
|
||||
tools: nodeDocumentationTools.map(t => ({
|
||||
name: t.name,
|
||||
description: t.description
|
||||
})),
|
||||
resources: [
|
||||
{
|
||||
uri: 'nodes://list',
|
||||
name: 'Available n8n Nodes',
|
||||
description: 'List of all available n8n nodes',
|
||||
},
|
||||
{
|
||||
uri: 'nodes://statistics',
|
||||
name: 'Database Statistics',
|
||||
description: 'Statistics about the node documentation database',
|
||||
},
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Database stats endpoint (public)
|
||||
this.app.get('/stats', async (req, res) => {
|
||||
try {
|
||||
const stats = this.nodeService.getStatistics();
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
logger.error('Failed to get statistics:', error);
|
||||
res.status(500).json({ error: 'Failed to retrieve statistics' });
|
||||
}
|
||||
});
|
||||
|
||||
// Rebuild endpoint (requires auth)
|
||||
this.app.post('/rebuild', authenticateRequest(this.config.authToken), async (req, res) => {
|
||||
try {
|
||||
logger.info('Database rebuild requested');
|
||||
const stats = await this.nodeService.rebuildDatabase();
|
||||
res.json({
|
||||
message: 'Database rebuild complete',
|
||||
stats
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Rebuild failed:', error);
|
||||
res.status(500).json({ error: 'Rebuild failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Main MCP endpoint - handles all MCP protocol messages
|
||||
this.app.post('/mcp', authenticateRequest(this.config.authToken), async (req, res) => {
|
||||
const requestId = req.get('X-Request-ID') || 'unknown';
|
||||
|
||||
try {
|
||||
// Process the JSON-RPC request directly
|
||||
const response = await this.handleJsonRpcRequest(req.body);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
logger.error(`MCP request failed (${requestId}):`, error);
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
id: req.body?.id || null,
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal error',
|
||||
data: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleJsonRpcRequest(request: any): Promise<any> {
|
||||
const { jsonrpc, method, params, id } = request;
|
||||
|
||||
if (jsonrpc !== '2.0') {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id: id || null,
|
||||
error: {
|
||||
code: -32600,
|
||||
message: 'Invalid Request',
|
||||
data: 'JSON-RPC version must be "2.0"'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
switch (method) {
|
||||
case 'tools/list':
|
||||
result = await this.handleListTools();
|
||||
break;
|
||||
|
||||
case 'resources/list':
|
||||
result = await this.handleListResources();
|
||||
break;
|
||||
|
||||
case 'resources/read':
|
||||
result = await this.handleReadResource(params);
|
||||
break;
|
||||
|
||||
case 'tools/call':
|
||||
result = await this.handleToolCall(params);
|
||||
break;
|
||||
|
||||
default:
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id: id || null,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: 'Method not found',
|
||||
data: `Unknown method: ${method}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id: id || null,
|
||||
result
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error handling method ${method}:`, error);
|
||||
|
||||
const errorCode = error instanceof McpError ? error.code : -32603;
|
||||
const errorMessage = error instanceof Error ? error.message : 'Internal error';
|
||||
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id: id || null,
|
||||
error: {
|
||||
code: errorCode,
|
||||
message: errorMessage,
|
||||
data: error instanceof McpError ? error.data : undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async handleListTools(): Promise<any> {
|
||||
return {
|
||||
tools: nodeDocumentationTools,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleListResources(): Promise<any> {
|
||||
return {
|
||||
resources: [
|
||||
{
|
||||
uri: 'nodes://list',
|
||||
name: 'Available n8n Nodes',
|
||||
description: 'List of all available n8n nodes',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'nodes://statistics',
|
||||
name: 'Database Statistics',
|
||||
description: 'Statistics about the node documentation database',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleReadResource(params: any): Promise<any> {
|
||||
const { uri } = params;
|
||||
|
||||
if (uri === 'nodes://list') {
|
||||
const nodes = await this.nodeService.listNodes();
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(nodes.map(n => ({
|
||||
nodeType: n.nodeType,
|
||||
name: n.name,
|
||||
displayName: n.displayName,
|
||||
category: n.category,
|
||||
description: n.description,
|
||||
hasDocumentation: !!n.documentation,
|
||||
hasExample: !!n.exampleWorkflow,
|
||||
})), null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (uri === 'nodes://statistics') {
|
||||
const stats = this.nodeService.getStatistics();
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(stats, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
||||
}
|
||||
|
||||
private async handleToolCall(params: any): Promise<any> {
|
||||
const { name, arguments: args } = params;
|
||||
|
||||
switch (name) {
|
||||
case 'list_nodes':
|
||||
return await this.handleListNodes(args);
|
||||
|
||||
case 'get_node_info':
|
||||
return await this.handleGetNodeInfo(args);
|
||||
|
||||
case 'search_nodes':
|
||||
return await this.handleSearchNodes(args);
|
||||
|
||||
case 'get_node_example':
|
||||
return await this.handleGetNodeExample(args);
|
||||
|
||||
case 'get_node_source_code':
|
||||
return await this.handleGetNodeSourceCode(args);
|
||||
|
||||
case 'get_node_documentation':
|
||||
return await this.handleGetNodeDocumentation(args);
|
||||
|
||||
case 'rebuild_database':
|
||||
return await this.handleRebuildDatabase(args);
|
||||
|
||||
case 'get_database_statistics':
|
||||
return await this.handleGetStatistics();
|
||||
|
||||
default:
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Tool handlers
|
||||
private async handleListNodes(args: any): Promise<any> {
|
||||
const nodes = await this.nodeService.listNodes();
|
||||
|
||||
let filtered = nodes;
|
||||
|
||||
if (args.category) {
|
||||
filtered = filtered.filter(n => n.category === args.category);
|
||||
}
|
||||
|
||||
if (args.packageName) {
|
||||
filtered = filtered.filter(n => n.packageName === args.packageName);
|
||||
}
|
||||
|
||||
if (args.isTrigger !== undefined) {
|
||||
filtered = filtered.filter(n => n.isTrigger === args.isTrigger);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered.map(n => ({
|
||||
nodeType: n.nodeType,
|
||||
name: n.name,
|
||||
displayName: n.displayName,
|
||||
category: n.category,
|
||||
description: n.description,
|
||||
packageName: n.packageName,
|
||||
hasDocumentation: !!n.documentation,
|
||||
hasExample: !!n.exampleWorkflow,
|
||||
isTrigger: n.isTrigger,
|
||||
isWebhook: n.isWebhook,
|
||||
})), null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeInfo(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
nodeType: nodeInfo.nodeType,
|
||||
name: nodeInfo.name,
|
||||
displayName: nodeInfo.displayName,
|
||||
description: nodeInfo.description,
|
||||
category: nodeInfo.category,
|
||||
packageName: nodeInfo.packageName,
|
||||
sourceCode: nodeInfo.sourceCode,
|
||||
credentialCode: nodeInfo.credentialCode,
|
||||
documentation: nodeInfo.documentation,
|
||||
documentationUrl: nodeInfo.documentationUrl,
|
||||
exampleWorkflow: nodeInfo.exampleWorkflow,
|
||||
exampleParameters: nodeInfo.exampleParameters,
|
||||
propertiesSchema: nodeInfo.propertiesSchema,
|
||||
isTrigger: nodeInfo.isTrigger,
|
||||
isWebhook: nodeInfo.isWebhook,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleSearchNodes(args: any): Promise<any> {
|
||||
if (!args.query) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'query is required');
|
||||
}
|
||||
|
||||
const results = await this.nodeService.searchNodes({
|
||||
query: args.query,
|
||||
category: args.category,
|
||||
limit: args.limit || 20,
|
||||
});
|
||||
|
||||
let filtered = results;
|
||||
if (args.hasDocumentation) {
|
||||
filtered = filtered.filter(n => !!n.documentation);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered.map(n => ({
|
||||
nodeType: n.nodeType,
|
||||
name: n.name,
|
||||
displayName: n.displayName,
|
||||
category: n.category,
|
||||
description: n.description,
|
||||
hasDocumentation: !!n.documentation,
|
||||
hasExample: !!n.exampleWorkflow,
|
||||
})), null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeExample(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
if (!nodeInfo.exampleWorkflow) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No example available for node: ${args.nodeType}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(nodeInfo.exampleWorkflow, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeSourceCode(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
const response: any = {
|
||||
nodeType: nodeInfo.nodeType,
|
||||
sourceCode: nodeInfo.sourceCode,
|
||||
};
|
||||
|
||||
if (args.includeCredentials && nodeInfo.credentialCode) {
|
||||
response.credentialCode = nodeInfo.credentialCode;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeDocumentation(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
if (!nodeInfo.documentation) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No documentation available for node: ${args.nodeType}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const content = args.format === 'plain'
|
||||
? nodeInfo.documentation.replace(/[#*`]/g, '')
|
||||
: nodeInfo.documentation;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleRebuildDatabase(args: any): Promise<any> {
|
||||
logger.info('Database rebuild requested via MCP');
|
||||
|
||||
const stats = await this.nodeService.rebuildDatabase();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Database rebuild complete',
|
||||
stats,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetStatistics(): Promise<any> {
|
||||
const stats = this.nodeService.getStatistics();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(stats, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
// Create server (HTTP or HTTPS)
|
||||
if (this.config.tlsCert && this.config.tlsKey) {
|
||||
const tlsOptions = {
|
||||
cert: fs.readFileSync(this.config.tlsCert),
|
||||
key: fs.readFileSync(this.config.tlsKey),
|
||||
};
|
||||
this.server = createHttpsServer(tlsOptions, this.app);
|
||||
} else {
|
||||
this.server = createHttpServer(this.app);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.server.listen(this.config.port, this.config.host, () => {
|
||||
const protocol = this.config.tlsCert ? 'https' : 'http';
|
||||
logger.info(`n8n Documentation MCP Remote server started`);
|
||||
logger.info(`Endpoint: ${protocol}://${this.config.host}:${this.config.port}`);
|
||||
logger.info(`Domain: ${this.config.domain}`);
|
||||
logger.info(`MCP endpoint: ${protocol}://${this.config.domain}/mcp`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
logger.info('Stopping n8n Documentation MCP Remote server...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.server.close(() => {
|
||||
this.nodeService.close();
|
||||
logger.info('Server stopped');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { NodeDocumentationService } from '../services/node-documentation-service';
|
||||
import { nodeDocumentationTools } from './tools-v2';
|
||||
import { logger } from '../utils/logger';
|
||||
import { MCPServerConfig } from '../types';
|
||||
|
||||
/**
|
||||
* MCP Server focused on serving n8n node documentation and code
|
||||
*/
|
||||
export class N8NDocumentationMCPServer {
|
||||
private server: Server;
|
||||
private nodeService: NodeDocumentationService;
|
||||
|
||||
constructor(config: MCPServerConfig) {
|
||||
logger.info('Initializing n8n Documentation MCP server', { config });
|
||||
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'n8n-node-documentation',
|
||||
version: '2.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.nodeService = new NodeDocumentationService();
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
// List available tools
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: nodeDocumentationTools,
|
||||
}));
|
||||
|
||||
// List available resources
|
||||
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: [
|
||||
{
|
||||
uri: 'nodes://list',
|
||||
name: 'Available n8n Nodes',
|
||||
description: 'List of all available n8n nodes',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'nodes://statistics',
|
||||
name: 'Database Statistics',
|
||||
description: 'Statistics about the node documentation database',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// Read resources
|
||||
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const { uri } = request.params;
|
||||
|
||||
try {
|
||||
if (uri === 'nodes://list') {
|
||||
const nodes = await this.nodeService.listNodes();
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(nodes.map(n => ({
|
||||
nodeType: n.nodeType,
|
||||
name: n.name,
|
||||
displayName: n.displayName,
|
||||
category: n.category,
|
||||
description: n.description,
|
||||
hasDocumentation: !!n.documentation,
|
||||
hasExample: !!n.exampleWorkflow,
|
||||
})), null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (uri === 'nodes://statistics') {
|
||||
const stats = this.nodeService.getStatistics();
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(stats, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Handle specific node URIs like nodes://info/n8n-nodes-base.if
|
||||
const nodeMatch = uri.match(/^nodes:\/\/info\/(.+)$/);
|
||||
if (nodeMatch) {
|
||||
const nodeType = nodeMatch[1];
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${nodeType}`);
|
||||
}
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(nodeInfo, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
||||
} catch (error) {
|
||||
logger.error('Resource read error:', error);
|
||||
throw error instanceof McpError ? error : new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to read resource: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
case 'list_nodes':
|
||||
return await this.handleListNodes(args);
|
||||
|
||||
case 'get_node_info':
|
||||
return await this.handleGetNodeInfo(args);
|
||||
|
||||
case 'search_nodes':
|
||||
return await this.handleSearchNodes(args);
|
||||
|
||||
case 'get_node_example':
|
||||
return await this.handleGetNodeExample(args);
|
||||
|
||||
case 'get_node_source_code':
|
||||
return await this.handleGetNodeSourceCode(args);
|
||||
|
||||
case 'get_node_documentation':
|
||||
return await this.handleGetNodeDocumentation(args);
|
||||
|
||||
case 'rebuild_database':
|
||||
return await this.handleRebuildDatabase(args);
|
||||
|
||||
case 'get_database_statistics':
|
||||
return await this.handleGetStatistics();
|
||||
|
||||
default:
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Tool execution error (${name}):`, error);
|
||||
throw error instanceof McpError ? error : new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleListNodes(args: any): Promise<any> {
|
||||
const nodes = await this.nodeService.listNodes();
|
||||
|
||||
// Apply filters
|
||||
let filtered = nodes;
|
||||
|
||||
if (args.category) {
|
||||
filtered = filtered.filter(n => n.category === args.category);
|
||||
}
|
||||
|
||||
if (args.packageName) {
|
||||
filtered = filtered.filter(n => n.packageName === args.packageName);
|
||||
}
|
||||
|
||||
if (args.isTrigger !== undefined) {
|
||||
filtered = filtered.filter(n => n.isTrigger === args.isTrigger);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered.map(n => ({
|
||||
nodeType: n.nodeType,
|
||||
name: n.name,
|
||||
displayName: n.displayName,
|
||||
category: n.category,
|
||||
description: n.description,
|
||||
packageName: n.packageName,
|
||||
hasDocumentation: !!n.documentation,
|
||||
hasExample: !!n.exampleWorkflow,
|
||||
isTrigger: n.isTrigger,
|
||||
isWebhook: n.isWebhook,
|
||||
})), null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeInfo(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
nodeType: nodeInfo.nodeType,
|
||||
name: nodeInfo.name,
|
||||
displayName: nodeInfo.displayName,
|
||||
description: nodeInfo.description,
|
||||
category: nodeInfo.category,
|
||||
packageName: nodeInfo.packageName,
|
||||
sourceCode: nodeInfo.sourceCode,
|
||||
credentialCode: nodeInfo.credentialCode,
|
||||
documentation: nodeInfo.documentation,
|
||||
documentationUrl: nodeInfo.documentationUrl,
|
||||
exampleWorkflow: nodeInfo.exampleWorkflow,
|
||||
exampleParameters: nodeInfo.exampleParameters,
|
||||
propertiesSchema: nodeInfo.propertiesSchema,
|
||||
isTrigger: nodeInfo.isTrigger,
|
||||
isWebhook: nodeInfo.isWebhook,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleSearchNodes(args: any): Promise<any> {
|
||||
if (!args.query) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'query is required');
|
||||
}
|
||||
|
||||
const results = await this.nodeService.searchNodes({
|
||||
query: args.query,
|
||||
category: args.category,
|
||||
limit: args.limit || 20,
|
||||
});
|
||||
|
||||
// Filter by documentation if requested
|
||||
let filtered = results;
|
||||
if (args.hasDocumentation) {
|
||||
filtered = filtered.filter(n => !!n.documentation);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered.map(n => ({
|
||||
nodeType: n.nodeType,
|
||||
name: n.name,
|
||||
displayName: n.displayName,
|
||||
category: n.category,
|
||||
description: n.description,
|
||||
hasDocumentation: !!n.documentation,
|
||||
hasExample: !!n.exampleWorkflow,
|
||||
})), null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeExample(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
if (!nodeInfo.exampleWorkflow) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No example available for node: ${args.nodeType}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(nodeInfo.exampleWorkflow, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeSourceCode(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
const response: any = {
|
||||
nodeType: nodeInfo.nodeType,
|
||||
sourceCode: nodeInfo.sourceCode,
|
||||
};
|
||||
|
||||
if (args.includeCredentials && nodeInfo.credentialCode) {
|
||||
response.credentialCode = nodeInfo.credentialCode;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetNodeDocumentation(args: any): Promise<any> {
|
||||
if (!args.nodeType) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||
}
|
||||
|
||||
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||
}
|
||||
|
||||
if (!nodeInfo.documentation) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No documentation available for node: ${args.nodeType}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const content = args.format === 'plain'
|
||||
? nodeInfo.documentation.replace(/[#*`]/g, '')
|
||||
: nodeInfo.documentation;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleRebuildDatabase(args: any): Promise<any> {
|
||||
logger.info('Starting database rebuild...');
|
||||
|
||||
const stats = await this.nodeService.rebuildDatabase();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Database rebuild complete',
|
||||
stats,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGetStatistics(): Promise<any> {
|
||||
const stats = this.nodeService.getStatistics();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(stats, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
logger.info('Starting n8n Documentation MCP server...');
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
|
||||
logger.info('n8n Documentation MCP server started successfully');
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
logger.info('Stopping n8n Documentation MCP server...');
|
||||
await this.server.close();
|
||||
this.nodeService.close();
|
||||
logger.info('Server stopped');
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,18 @@ import { N8NApiClient } from '../utils/n8n-client';
|
||||
import { N8NMCPBridge } from '../utils/bridge';
|
||||
import { logger } from '../utils/logger';
|
||||
import { NodeSourceExtractor } from '../utils/node-source-extractor';
|
||||
import { SQLiteStorageService } from '../services/sqlite-storage-service';
|
||||
import { NodeDocumentationService } from '../services/node-documentation-service';
|
||||
|
||||
export class N8NMCPServer {
|
||||
private server: Server;
|
||||
private n8nClient: N8NApiClient;
|
||||
private nodeExtractor: NodeSourceExtractor;
|
||||
private nodeStorage: SQLiteStorageService;
|
||||
private nodeDocService: NodeDocumentationService;
|
||||
|
||||
constructor(config: MCPServerConfig, n8nConfig: N8NConfig) {
|
||||
this.n8nClient = new N8NApiClient(n8nConfig);
|
||||
this.nodeExtractor = new NodeSourceExtractor();
|
||||
this.nodeStorage = new SQLiteStorageService();
|
||||
this.nodeDocService = new NodeDocumentationService();
|
||||
logger.info('Initializing n8n MCP server', { config, n8nConfig });
|
||||
this.server = new Server(
|
||||
{
|
||||
@@ -164,12 +164,14 @@ export class N8NMCPServer {
|
||||
return this.getNodeSourceCode(args);
|
||||
case 'list_available_nodes':
|
||||
return this.listAvailableNodes(args);
|
||||
case 'extract_all_nodes':
|
||||
return this.extractAllNodes(args);
|
||||
case 'get_node_info':
|
||||
return this.getNodeInfo(args);
|
||||
case 'search_nodes':
|
||||
return this.searchNodes(args);
|
||||
case 'get_node_statistics':
|
||||
return this.getNodeStatistics(args);
|
||||
case 'rebuild_documentation_database':
|
||||
return this.rebuildDocumentationDatabase(args);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
@@ -323,84 +325,87 @@ export class N8NMCPServer {
|
||||
}
|
||||
}
|
||||
|
||||
private async extractAllNodes(args: any): Promise<any> {
|
||||
|
||||
private async getNodeInfo(args: any): Promise<any> {
|
||||
try {
|
||||
logger.info(`Extracting all nodes`, args);
|
||||
logger.info('Getting comprehensive node information', args);
|
||||
const nodeInfo = await this.nodeDocService.getNodeInfo(args.nodeType);
|
||||
|
||||
// Get list of all nodes
|
||||
const allNodes = await this.nodeExtractor.listAvailableNodes();
|
||||
let nodesToExtract = allNodes;
|
||||
|
||||
// Apply filters
|
||||
if (args.packageFilter) {
|
||||
nodesToExtract = nodesToExtract.filter(node =>
|
||||
node.packageName === args.packageFilter ||
|
||||
node.location?.includes(args.packageFilter)
|
||||
);
|
||||
if (!nodeInfo) {
|
||||
throw new Error(`Node ${args.nodeType} not found`);
|
||||
}
|
||||
|
||||
if (args.limit) {
|
||||
nodesToExtract = nodesToExtract.slice(0, args.limit);
|
||||
}
|
||||
|
||||
logger.info(`Extracting ${nodesToExtract.length} nodes...`);
|
||||
|
||||
const extractedNodes = [];
|
||||
const errors = [];
|
||||
|
||||
for (const node of nodesToExtract) {
|
||||
try {
|
||||
const nodeType = node.packageName ? `${node.packageName}.${node.name}` : node.name;
|
||||
const nodeInfo = await this.nodeExtractor.extractNodeSource(nodeType);
|
||||
await this.nodeStorage.storeNode(nodeInfo);
|
||||
extractedNodes.push(nodeType);
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
node: node.name,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const stats = await this.nodeStorage.getStatistics();
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
extracted: extractedNodes.length,
|
||||
failed: errors.length,
|
||||
totalStored: stats.totalNodes,
|
||||
errors: errors.slice(0, 10), // Limit error list
|
||||
statistics: stats
|
||||
nodeType: nodeInfo.nodeType,
|
||||
name: nodeInfo.name,
|
||||
displayName: nodeInfo.displayName,
|
||||
description: nodeInfo.description,
|
||||
category: nodeInfo.category,
|
||||
subcategory: nodeInfo.subcategory,
|
||||
icon: nodeInfo.icon,
|
||||
documentation: {
|
||||
markdown: nodeInfo.documentationMarkdown,
|
||||
url: nodeInfo.documentationUrl,
|
||||
title: nodeInfo.documentationTitle,
|
||||
},
|
||||
operations: nodeInfo.operations || [],
|
||||
apiMethods: nodeInfo.apiMethods || [],
|
||||
examples: nodeInfo.documentationExamples || [],
|
||||
templates: nodeInfo.templates || [],
|
||||
relatedResources: nodeInfo.relatedResources || [],
|
||||
requiredScopes: nodeInfo.requiredScopes || [],
|
||||
exampleWorkflow: nodeInfo.exampleWorkflow,
|
||||
exampleParameters: nodeInfo.exampleParameters,
|
||||
propertiesSchema: nodeInfo.propertiesSchema,
|
||||
metadata: {
|
||||
packageName: nodeInfo.packageName,
|
||||
version: nodeInfo.version,
|
||||
hasCredentials: nodeInfo.hasCredentials,
|
||||
isTrigger: nodeInfo.isTrigger,
|
||||
isWebhook: nodeInfo.isWebhook,
|
||||
aliases: nodeInfo.aliases,
|
||||
},
|
||||
sourceCode: {
|
||||
node: nodeInfo.sourceCode,
|
||||
credential: nodeInfo.credentialCode,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to extract all nodes`, error);
|
||||
throw new Error(`Failed to extract all nodes: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
logger.error(`Failed to get node info`, error);
|
||||
throw new Error(`Failed to get node info: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async searchNodes(args: any): Promise<any> {
|
||||
try {
|
||||
logger.info(`Searching nodes`, args);
|
||||
|
||||
const results = await this.nodeStorage.searchNodes({
|
||||
logger.info('Searching nodes with enhanced filtering', args);
|
||||
const results = await this.nodeDocService.searchNodes({
|
||||
query: args.query,
|
||||
category: args.category,
|
||||
packageName: args.packageName,
|
||||
hasCredentials: args.hasCredentials,
|
||||
limit: args.limit || 20
|
||||
isTrigger: args.isTrigger,
|
||||
limit: args.limit || 20,
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
nodes: results.map(node => ({
|
||||
nodeType: node.nodeType,
|
||||
name: node.name,
|
||||
packageName: node.packageName,
|
||||
displayName: node.displayName,
|
||||
description: node.description,
|
||||
codeLength: node.codeLength,
|
||||
hasCredentials: node.hasCredentials,
|
||||
location: node.sourceLocation
|
||||
category: node.category,
|
||||
packageName: node.packageName,
|
||||
hasDocumentation: !!node.documentationMarkdown,
|
||||
hasExamples: !!(node.documentationExamples && node.documentationExamples.length > 0),
|
||||
operationCount: node.operations?.length || 0,
|
||||
metadata: {
|
||||
hasCredentials: node.hasCredentials,
|
||||
isTrigger: node.isTrigger,
|
||||
isWebhook: node.isWebhook,
|
||||
},
|
||||
})),
|
||||
total: results.length
|
||||
total: results.length,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to search nodes`, error);
|
||||
@@ -411,12 +416,11 @@ export class N8NMCPServer {
|
||||
private async getNodeStatistics(args: any): Promise<any> {
|
||||
try {
|
||||
logger.info(`Getting node statistics`);
|
||||
const stats = await this.nodeStorage.getStatistics();
|
||||
const stats = this.nodeDocService.getStatistics();
|
||||
|
||||
return {
|
||||
...stats,
|
||||
formattedTotalSize: `${(stats.totalCodeSize / 1024 / 1024).toFixed(2)} MB`,
|
||||
formattedAverageSize: `${(stats.averageNodeSize / 1024).toFixed(2)} KB`
|
||||
formattedTotalSize: stats.totalCodeSize ? `${(stats.totalCodeSize / 1024 / 1024).toFixed(2)} MB` : '0 MB',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get node statistics`, error);
|
||||
@@ -424,6 +428,23 @@ export class N8NMCPServer {
|
||||
}
|
||||
}
|
||||
|
||||
private async rebuildDocumentationDatabase(args: any): Promise<any> {
|
||||
try {
|
||||
logger.info('Rebuilding documentation database', args);
|
||||
const stats = await this.nodeDocService.rebuildDatabase();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Documentation database rebuilt successfully',
|
||||
statistics: stats,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to rebuild documentation database`, error);
|
||||
throw new Error(`Failed to rebuild documentation database: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
logger.info('Starting n8n MCP server...');
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { ToolDefinition } from '../types';
|
||||
|
||||
/**
|
||||
* Simplified MCP tools focused on serving n8n node documentation and code
|
||||
*/
|
||||
export const nodeDocumentationTools: ToolDefinition[] = [
|
||||
{
|
||||
name: 'list_nodes',
|
||||
description: 'List all available n8n nodes with basic information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Filter by category (e.g., "Core Nodes", "Flow", "Data Transformation")',
|
||||
},
|
||||
packageName: {
|
||||
type: 'string',
|
||||
description: 'Filter by package name (e.g., "n8n-nodes-base")',
|
||||
},
|
||||
isTrigger: {
|
||||
type: 'boolean',
|
||||
description: 'Filter to show only trigger nodes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_node_info',
|
||||
description: 'Get complete information about a specific n8n node including source code, documentation, and examples',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'The node type or name (e.g., "n8n-nodes-base.if", "If", "webhook")',
|
||||
},
|
||||
},
|
||||
required: ['nodeType'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search_nodes',
|
||||
description: 'Search for n8n nodes by name, description, or documentation content',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query (searches in node names, descriptions, and documentation)',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Filter by category',
|
||||
},
|
||||
hasDocumentation: {
|
||||
type: 'boolean',
|
||||
description: 'Filter to show only nodes with documentation',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results to return',
|
||||
default: 20,
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_node_example',
|
||||
description: 'Get example workflow/usage for a specific n8n node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'The node type or name',
|
||||
},
|
||||
},
|
||||
required: ['nodeType'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_node_source_code',
|
||||
description: 'Get only the source code of a specific n8n node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'The node type or name',
|
||||
},
|
||||
includeCredentials: {
|
||||
type: 'boolean',
|
||||
description: 'Include credential type definitions if available',
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
required: ['nodeType'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_node_documentation',
|
||||
description: 'Get only the documentation for a specific n8n node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'The node type or name',
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: ['markdown', 'plain'],
|
||||
description: 'Documentation format',
|
||||
default: 'markdown',
|
||||
},
|
||||
},
|
||||
required: ['nodeType'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'rebuild_database',
|
||||
description: 'Rebuild the entire node database with latest information from n8n and documentation',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
includeDocumentation: {
|
||||
type: 'boolean',
|
||||
description: 'Include documentation from n8n-docs repository',
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_database_statistics',
|
||||
description: 'Get statistics about the node database',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -182,31 +182,40 @@ export const n8nTools: ToolDefinition[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'extract_all_nodes',
|
||||
description: 'Extract and store all available n8n nodes in the database',
|
||||
name: 'get_node_statistics',
|
||||
description: 'Get statistics about stored n8n nodes',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_node_info',
|
||||
description: 'Get comprehensive information about a specific n8n node including documentation, operations, API methods, and examples',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
packageFilter: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'Optional package name to filter extraction',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of nodes to extract',
|
||||
description: 'The node type identifier (e.g., n8n-nodes-base.slack)',
|
||||
},
|
||||
},
|
||||
required: ['nodeType'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search_nodes',
|
||||
description: 'Search for n8n nodes by name, package, or functionality',
|
||||
description: 'Search n8n nodes with full-text search and advanced filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
description: 'Search query for full-text search',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Filter by node category',
|
||||
},
|
||||
packageName: {
|
||||
type: 'string',
|
||||
@@ -214,7 +223,11 @@ export const n8nTools: ToolDefinition[] = [
|
||||
},
|
||||
hasCredentials: {
|
||||
type: 'boolean',
|
||||
description: 'Filter nodes that have credentials',
|
||||
description: 'Filter nodes that require credentials',
|
||||
},
|
||||
isTrigger: {
|
||||
type: 'boolean',
|
||||
description: 'Filter trigger nodes only',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
@@ -225,11 +238,16 @@ export const n8nTools: ToolDefinition[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_node_statistics',
|
||||
description: 'Get statistics about stored n8n nodes',
|
||||
name: 'rebuild_documentation_database',
|
||||
description: 'Rebuild the node documentation database with the latest information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
properties: {
|
||||
packageFilter: {
|
||||
type: 'string',
|
||||
description: 'Optional: Only rebuild nodes from specific package',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user