Skip to main content
Glama

n8n-MCP

by 88-888
node-repository.ts29.9 kB
import { DatabaseAdapter } from './database-adapter'; import { ParsedNode } from '../parsers/node-parser'; import { SQLiteStorageService } from '../services/sqlite-storage-service'; import { NodeTypeNormalizer } from '../utils/node-type-normalizer'; export class NodeRepository { private db: DatabaseAdapter; constructor(dbOrService: DatabaseAdapter | SQLiteStorageService) { if (dbOrService instanceof SQLiteStorageService) { this.db = dbOrService.db; return; } this.db = dbOrService; } /** * Save node with proper JSON serialization */ saveNode(node: ParsedNode): void { const stmt = this.db.prepare(` INSERT OR REPLACE INTO nodes ( node_type, package_name, display_name, description, category, development_style, is_ai_tool, is_trigger, is_webhook, is_versioned, version, documentation, properties_schema, operations, credentials_required, outputs, output_names ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( node.nodeType, node.packageName, node.displayName, node.description, node.category, node.style, node.isAITool ? 1 : 0, node.isTrigger ? 1 : 0, node.isWebhook ? 1 : 0, node.isVersioned ? 1 : 0, node.version, node.documentation || null, JSON.stringify(node.properties, null, 2), JSON.stringify(node.operations, null, 2), JSON.stringify(node.credentials, null, 2), node.outputs ? JSON.stringify(node.outputs, null, 2) : null, node.outputNames ? JSON.stringify(node.outputNames, null, 2) : null ); } /** * Get node with proper JSON deserialization * Automatically normalizes node type to full form for consistent lookups */ getNode(nodeType: string): any { // Normalize to full form first for consistent lookups const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); const row = this.db.prepare(` SELECT * FROM nodes WHERE node_type = ? `).get(normalizedType) as any; // Fallback: try original type if normalization didn't help (e.g., community nodes) if (!row && normalizedType !== nodeType) { const originalRow = this.db.prepare(` SELECT * FROM nodes WHERE node_type = ? `).get(nodeType) as any; if (originalRow) { return this.parseNodeRow(originalRow); } } if (!row) return null; return this.parseNodeRow(row); } /** * Get AI tools with proper filtering */ getAITools(): any[] { const rows = this.db.prepare(` SELECT node_type, display_name, description, package_name FROM nodes WHERE is_ai_tool = 1 ORDER BY display_name `).all() as any[]; return rows.map(row => ({ nodeType: row.node_type, displayName: row.display_name, description: row.description, package: row.package_name })); } private safeJsonParse(json: string, defaultValue: any): any { try { return JSON.parse(json); } catch { return defaultValue; } } // Additional methods for benchmarks upsertNode(node: ParsedNode): void { this.saveNode(node); } getNodeByType(nodeType: string): any { return this.getNode(nodeType); } getNodesByCategory(category: string): any[] { const rows = this.db.prepare(` SELECT * FROM nodes WHERE category = ? ORDER BY display_name `).all(category) as any[]; return rows.map(row => this.parseNodeRow(row)); } /** * Legacy LIKE-based search method for direct repository usage. * * NOTE: MCP tools do NOT use this method. They use MCPServer.searchNodes() * which automatically detects and uses FTS5 full-text search when available. * See src/mcp/server.ts:1135-1148 for FTS5 implementation. * * This method remains for: * - Direct repository access in scripts/benchmarks * - Fallback when FTS5 table doesn't exist * - Legacy compatibility */ searchNodes(query: string, mode: 'OR' | 'AND' | 'FUZZY' = 'OR', limit: number = 20): any[] { let sql = ''; const params: any[] = []; if (mode === 'FUZZY') { // Simple fuzzy search sql = ` SELECT * FROM nodes WHERE node_type LIKE ? OR display_name LIKE ? OR description LIKE ? ORDER BY display_name LIMIT ? `; const fuzzyQuery = `%${query}%`; params.push(fuzzyQuery, fuzzyQuery, fuzzyQuery, limit); } else { // OR/AND mode const words = query.split(/\s+/).filter(w => w.length > 0); const conditions = words.map(() => '(node_type LIKE ? OR display_name LIKE ? OR description LIKE ?)' ); const operator = mode === 'AND' ? ' AND ' : ' OR '; sql = ` SELECT * FROM nodes WHERE ${conditions.join(operator)} ORDER BY display_name LIMIT ? `; for (const word of words) { const searchTerm = `%${word}%`; params.push(searchTerm, searchTerm, searchTerm); } params.push(limit); } const rows = this.db.prepare(sql).all(...params) as any[]; return rows.map(row => this.parseNodeRow(row)); } getAllNodes(limit?: number): any[] { let sql = 'SELECT * FROM nodes ORDER BY display_name'; if (limit) { sql += ` LIMIT ${limit}`; } const rows = this.db.prepare(sql).all() as any[]; return rows.map(row => this.parseNodeRow(row)); } getNodeCount(): number { const result = this.db.prepare('SELECT COUNT(*) as count FROM nodes').get() as any; return result.count; } getAIToolNodes(): any[] { return this.getAITools(); } getNodesByPackage(packageName: string): any[] { const rows = this.db.prepare(` SELECT * FROM nodes WHERE package_name = ? ORDER BY display_name `).all(packageName) as any[]; return rows.map(row => this.parseNodeRow(row)); } searchNodeProperties(nodeType: string, query: string, maxResults: number = 20): any[] { const node = this.getNode(nodeType); if (!node || !node.properties) return []; const results: any[] = []; const searchLower = query.toLowerCase(); function searchProperties(properties: any[], path: string[] = []) { for (const prop of properties) { if (results.length >= maxResults) break; const currentPath = [...path, prop.name || prop.displayName]; const pathString = currentPath.join('.'); if (prop.name?.toLowerCase().includes(searchLower) || prop.displayName?.toLowerCase().includes(searchLower) || prop.description?.toLowerCase().includes(searchLower)) { results.push({ path: pathString, property: prop, description: prop.description }); } // Search nested properties if (prop.options) { searchProperties(prop.options, currentPath); } } } searchProperties(node.properties); return results; } private parseNodeRow(row: any): any { return { nodeType: row.node_type, displayName: row.display_name, description: row.description, category: row.category, developmentStyle: row.development_style, package: row.package_name, isAITool: Number(row.is_ai_tool) === 1, isTrigger: Number(row.is_trigger) === 1, isWebhook: Number(row.is_webhook) === 1, isVersioned: Number(row.is_versioned) === 1, version: row.version, properties: this.safeJsonParse(row.properties_schema, []), operations: this.safeJsonParse(row.operations, []), credentials: this.safeJsonParse(row.credentials_required, []), hasDocumentation: !!row.documentation, outputs: row.outputs ? this.safeJsonParse(row.outputs, null) : null, outputNames: row.output_names ? this.safeJsonParse(row.output_names, null) : null }; } /** * Get operations for a specific node, optionally filtered by resource */ getNodeOperations(nodeType: string, resource?: string): any[] { const node = this.getNode(nodeType); if (!node) return []; const operations: any[] = []; // Parse operations field if (node.operations) { if (Array.isArray(node.operations)) { operations.push(...node.operations); } else if (typeof node.operations === 'object') { // Operations might be grouped by resource if (resource && node.operations[resource]) { return node.operations[resource]; } else { // Return all operations Object.values(node.operations).forEach(ops => { if (Array.isArray(ops)) { operations.push(...ops); } }); } } } // Also check properties for operation fields if (node.properties && Array.isArray(node.properties)) { for (const prop of node.properties) { if (prop.name === 'operation' && prop.options) { // If resource is specified, filter by displayOptions if (resource && prop.displayOptions?.show?.resource) { const allowedResources = Array.isArray(prop.displayOptions.show.resource) ? prop.displayOptions.show.resource : [prop.displayOptions.show.resource]; if (!allowedResources.includes(resource)) { continue; } } // Add operations from this property operations.push(...prop.options); } } } return operations; } /** * Get all resources defined for a node */ getNodeResources(nodeType: string): any[] { const node = this.getNode(nodeType); if (!node || !node.properties) return []; const resources: any[] = []; // Look for resource property for (const prop of node.properties) { if (prop.name === 'resource' && prop.options) { resources.push(...prop.options); } } return resources; } /** * Get operations that are valid for a specific resource */ getOperationsForResource(nodeType: string, resource: string): any[] { const node = this.getNode(nodeType); if (!node || !node.properties) return []; const operations: any[] = []; // Find operation properties that are visible for this resource for (const prop of node.properties) { if (prop.name === 'operation' && prop.displayOptions?.show?.resource) { const allowedResources = Array.isArray(prop.displayOptions.show.resource) ? prop.displayOptions.show.resource : [prop.displayOptions.show.resource]; if (allowedResources.includes(resource) && prop.options) { operations.push(...prop.options); } } } return operations; } /** * Get all operations across all nodes (for analysis) */ getAllOperations(): Map<string, any[]> { const allOperations = new Map<string, any[]>(); const nodes = this.getAllNodes(); for (const node of nodes) { const operations = this.getNodeOperations(node.nodeType); if (operations.length > 0) { allOperations.set(node.nodeType, operations); } } return allOperations; } /** * Get all resources across all nodes (for analysis) */ getAllResources(): Map<string, any[]> { const allResources = new Map<string, any[]>(); const nodes = this.getAllNodes(); for (const node of nodes) { const resources = this.getNodeResources(node.nodeType); if (resources.length > 0) { allResources.set(node.nodeType, resources); } } return allResources; } /** * Get default values for node properties */ getNodePropertyDefaults(nodeType: string): Record<string, any> { try { const node = this.getNode(nodeType); if (!node || !node.properties) return {}; const defaults: Record<string, any> = {}; for (const prop of node.properties) { if (prop.name && prop.default !== undefined) { defaults[prop.name] = prop.default; } } return defaults; } catch (error) { // Log error and return empty defaults rather than throwing console.error(`Error getting property defaults for ${nodeType}:`, error); return {}; } } /** * Get the default operation for a specific resource */ getDefaultOperationForResource(nodeType: string, resource?: string): string | undefined { try { const node = this.getNode(nodeType); if (!node || !node.properties) return undefined; // Find operation property that's visible for this resource for (const prop of node.properties) { if (prop.name === 'operation') { // If there's a resource dependency, check if it matches if (resource && prop.displayOptions?.show?.resource) { // Validate displayOptions structure const resourceDep = prop.displayOptions.show.resource; if (!Array.isArray(resourceDep) && typeof resourceDep !== 'string') { continue; // Skip malformed displayOptions } const allowedResources = Array.isArray(resourceDep) ? resourceDep : [resourceDep]; if (!allowedResources.includes(resource)) { continue; // This operation property doesn't apply to our resource } } // Return the default value if it exists if (prop.default !== undefined) { return prop.default; } // If no default but has options, return the first option's value if (prop.options && Array.isArray(prop.options) && prop.options.length > 0) { const firstOption = prop.options[0]; return typeof firstOption === 'string' ? firstOption : firstOption.value; } } } } catch (error) { // Log error and return undefined rather than throwing // This ensures validation continues even with malformed node data console.error(`Error getting default operation for ${nodeType}:`, error); return undefined; } return undefined; } /** * VERSION MANAGEMENT METHODS * Methods for working with node_versions and version_property_changes tables */ /** * Save a specific node version to the database */ saveNodeVersion(versionData: { nodeType: string; version: string; packageName: string; displayName: string; description?: string; category?: string; isCurrentMax?: boolean; propertiesSchema?: any; operations?: any; credentialsRequired?: any; outputs?: any; minimumN8nVersion?: string; breakingChanges?: any[]; deprecatedProperties?: string[]; addedProperties?: string[]; releasedAt?: Date; }): void { const stmt = this.db.prepare(` INSERT OR REPLACE INTO node_versions ( node_type, version, package_name, display_name, description, category, is_current_max, properties_schema, operations, credentials_required, outputs, minimum_n8n_version, breaking_changes, deprecated_properties, added_properties, released_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( versionData.nodeType, versionData.version, versionData.packageName, versionData.displayName, versionData.description || null, versionData.category || null, versionData.isCurrentMax ? 1 : 0, versionData.propertiesSchema ? JSON.stringify(versionData.propertiesSchema) : null, versionData.operations ? JSON.stringify(versionData.operations) : null, versionData.credentialsRequired ? JSON.stringify(versionData.credentialsRequired) : null, versionData.outputs ? JSON.stringify(versionData.outputs) : null, versionData.minimumN8nVersion || null, versionData.breakingChanges ? JSON.stringify(versionData.breakingChanges) : null, versionData.deprecatedProperties ? JSON.stringify(versionData.deprecatedProperties) : null, versionData.addedProperties ? JSON.stringify(versionData.addedProperties) : null, versionData.releasedAt || null ); } /** * Get all available versions for a specific node type */ getNodeVersions(nodeType: string): any[] { const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); const rows = this.db.prepare(` SELECT * FROM node_versions WHERE node_type = ? ORDER BY version DESC `).all(normalizedType) as any[]; return rows.map(row => this.parseNodeVersionRow(row)); } /** * Get the latest (current max) version for a node type */ getLatestNodeVersion(nodeType: string): any | null { const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); const row = this.db.prepare(` SELECT * FROM node_versions WHERE node_type = ? AND is_current_max = 1 LIMIT 1 `).get(normalizedType) as any; if (!row) return null; return this.parseNodeVersionRow(row); } /** * Get a specific version of a node */ getNodeVersion(nodeType: string, version: string): any | null { const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); const row = this.db.prepare(` SELECT * FROM node_versions WHERE node_type = ? AND version = ? `).get(normalizedType, version) as any; if (!row) return null; return this.parseNodeVersionRow(row); } /** * Save a property change between versions */ savePropertyChange(changeData: { nodeType: string; fromVersion: string; toVersion: string; propertyName: string; changeType: 'added' | 'removed' | 'renamed' | 'type_changed' | 'requirement_changed' | 'default_changed'; isBreaking?: boolean; oldValue?: string; newValue?: string; migrationHint?: string; autoMigratable?: boolean; migrationStrategy?: any; severity?: 'LOW' | 'MEDIUM' | 'HIGH'; }): void { const stmt = this.db.prepare(` INSERT INTO version_property_changes ( node_type, from_version, to_version, property_name, change_type, is_breaking, old_value, new_value, migration_hint, auto_migratable, migration_strategy, severity ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( changeData.nodeType, changeData.fromVersion, changeData.toVersion, changeData.propertyName, changeData.changeType, changeData.isBreaking ? 1 : 0, changeData.oldValue || null, changeData.newValue || null, changeData.migrationHint || null, changeData.autoMigratable ? 1 : 0, changeData.migrationStrategy ? JSON.stringify(changeData.migrationStrategy) : null, changeData.severity || 'MEDIUM' ); } /** * Get property changes between two versions */ getPropertyChanges(nodeType: string, fromVersion: string, toVersion: string): any[] { const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); const rows = this.db.prepare(` SELECT * FROM version_property_changes WHERE node_type = ? AND from_version = ? AND to_version = ? ORDER BY severity DESC, property_name `).all(normalizedType, fromVersion, toVersion) as any[]; return rows.map(row => this.parsePropertyChangeRow(row)); } /** * Get all breaking changes for upgrading from one version to another * Can handle multi-step upgrades (e.g., 1.0 -> 2.0 via 1.5) */ getBreakingChanges(nodeType: string, fromVersion: string, toVersion?: string): any[] { const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); let sql = ` SELECT * FROM version_property_changes WHERE node_type = ? AND is_breaking = 1 `; const params: any[] = [normalizedType]; if (toVersion) { // Get changes between specific versions sql += ` AND from_version >= ? AND to_version <= ?`; params.push(fromVersion, toVersion); } else { // Get all breaking changes from this version onwards sql += ` AND from_version >= ?`; params.push(fromVersion); } sql += ` ORDER BY from_version, to_version, severity DESC`; const rows = this.db.prepare(sql).all(...params) as any[]; return rows.map(row => this.parsePropertyChangeRow(row)); } /** * Get auto-migratable changes for a version upgrade */ getAutoMigratableChanges(nodeType: string, fromVersion: string, toVersion: string): any[] { const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); const rows = this.db.prepare(` SELECT * FROM version_property_changes WHERE node_type = ? AND from_version = ? AND to_version = ? AND auto_migratable = 1 ORDER BY severity DESC `).all(normalizedType, fromVersion, toVersion) as any[]; return rows.map(row => this.parsePropertyChangeRow(row)); } /** * Check if a version upgrade path exists between two versions */ hasVersionUpgradePath(nodeType: string, fromVersion: string, toVersion: string): boolean { const versions = this.getNodeVersions(nodeType); if (versions.length === 0) return false; // Check if both versions exist const fromExists = versions.some(v => v.version === fromVersion); const toExists = versions.some(v => v.version === toVersion); return fromExists && toExists; } /** * Get count of nodes with multiple versions */ getVersionedNodesCount(): number { const result = this.db.prepare(` SELECT COUNT(DISTINCT node_type) as count FROM node_versions `).get() as any; return result.count; } /** * Parse node version row from database */ private parseNodeVersionRow(row: any): any { return { id: row.id, nodeType: row.node_type, version: row.version, packageName: row.package_name, displayName: row.display_name, description: row.description, category: row.category, isCurrentMax: Number(row.is_current_max) === 1, propertiesSchema: row.properties_schema ? this.safeJsonParse(row.properties_schema, []) : null, operations: row.operations ? this.safeJsonParse(row.operations, []) : null, credentialsRequired: row.credentials_required ? this.safeJsonParse(row.credentials_required, []) : null, outputs: row.outputs ? this.safeJsonParse(row.outputs, null) : null, minimumN8nVersion: row.minimum_n8n_version, breakingChanges: row.breaking_changes ? this.safeJsonParse(row.breaking_changes, []) : [], deprecatedProperties: row.deprecated_properties ? this.safeJsonParse(row.deprecated_properties, []) : [], addedProperties: row.added_properties ? this.safeJsonParse(row.added_properties, []) : [], releasedAt: row.released_at, createdAt: row.created_at }; } /** * Parse property change row from database */ private parsePropertyChangeRow(row: any): any { return { id: row.id, nodeType: row.node_type, fromVersion: row.from_version, toVersion: row.to_version, propertyName: row.property_name, changeType: row.change_type, isBreaking: Number(row.is_breaking) === 1, oldValue: row.old_value, newValue: row.new_value, migrationHint: row.migration_hint, autoMigratable: Number(row.auto_migratable) === 1, migrationStrategy: row.migration_strategy ? this.safeJsonParse(row.migration_strategy, null) : null, severity: row.severity, createdAt: row.created_at }; } // ======================================== // Workflow Versioning Methods // ======================================== /** * Create a new workflow version (backup before modification) */ createWorkflowVersion(data: { workflowId: string; versionNumber: number; workflowName: string; workflowSnapshot: any; trigger: 'partial_update' | 'full_update' | 'autofix'; operations?: any[]; fixTypes?: string[]; metadata?: any; }): number { const stmt = this.db.prepare(` INSERT INTO workflow_versions ( workflow_id, version_number, workflow_name, workflow_snapshot, trigger, operations, fix_types, metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run( data.workflowId, data.versionNumber, data.workflowName, JSON.stringify(data.workflowSnapshot), data.trigger, data.operations ? JSON.stringify(data.operations) : null, data.fixTypes ? JSON.stringify(data.fixTypes) : null, data.metadata ? JSON.stringify(data.metadata) : null ); return result.lastInsertRowid as number; } /** * Get workflow versions ordered by version number (newest first) */ getWorkflowVersions(workflowId: string, limit?: number): any[] { let sql = ` SELECT * FROM workflow_versions WHERE workflow_id = ? ORDER BY version_number DESC `; if (limit) { sql += ` LIMIT ?`; const rows = this.db.prepare(sql).all(workflowId, limit) as any[]; return rows.map(row => this.parseWorkflowVersionRow(row)); } const rows = this.db.prepare(sql).all(workflowId) as any[]; return rows.map(row => this.parseWorkflowVersionRow(row)); } /** * Get a specific workflow version by ID */ getWorkflowVersion(versionId: number): any | null { const row = this.db.prepare(` SELECT * FROM workflow_versions WHERE id = ? `).get(versionId) as any; if (!row) return null; return this.parseWorkflowVersionRow(row); } /** * Get the latest workflow version for a workflow */ getLatestWorkflowVersion(workflowId: string): any | null { const row = this.db.prepare(` SELECT * FROM workflow_versions WHERE workflow_id = ? ORDER BY version_number DESC LIMIT 1 `).get(workflowId) as any; if (!row) return null; return this.parseWorkflowVersionRow(row); } /** * Delete a specific workflow version */ deleteWorkflowVersion(versionId: number): void { this.db.prepare(` DELETE FROM workflow_versions WHERE id = ? `).run(versionId); } /** * Delete all versions for a specific workflow */ deleteWorkflowVersionsByWorkflowId(workflowId: string): number { const result = this.db.prepare(` DELETE FROM workflow_versions WHERE workflow_id = ? `).run(workflowId); return result.changes; } /** * Prune old workflow versions, keeping only the most recent N versions * Returns number of versions deleted */ pruneWorkflowVersions(workflowId: string, keepCount: number): number { // Get all versions ordered by version_number DESC const versions = this.db.prepare(` SELECT id FROM workflow_versions WHERE workflow_id = ? ORDER BY version_number DESC `).all(workflowId) as any[]; // If we have fewer versions than keepCount, no pruning needed if (versions.length <= keepCount) { return 0; } // Get IDs of versions to delete (all except the most recent keepCount) const idsToDelete = versions.slice(keepCount).map(v => v.id); if (idsToDelete.length === 0) { return 0; } // Delete old versions const placeholders = idsToDelete.map(() => '?').join(','); const result = this.db.prepare(` DELETE FROM workflow_versions WHERE id IN (${placeholders}) `).run(...idsToDelete); return result.changes; } /** * Truncate the entire workflow_versions table * Returns number of rows deleted */ truncateWorkflowVersions(): number { const result = this.db.prepare(` DELETE FROM workflow_versions `).run(); return result.changes; } /** * Get count of versions for a specific workflow */ getWorkflowVersionCount(workflowId: string): number { const result = this.db.prepare(` SELECT COUNT(*) as count FROM workflow_versions WHERE workflow_id = ? `).get(workflowId) as any; return result.count; } /** * Get storage statistics for workflow versions */ getVersionStorageStats(): any { // Total versions const totalResult = this.db.prepare(` SELECT COUNT(*) as count FROM workflow_versions `).get() as any; // Total size (approximate - sum of JSON lengths) const sizeResult = this.db.prepare(` SELECT SUM(LENGTH(workflow_snapshot)) as total_size FROM workflow_versions `).get() as any; // Per-workflow breakdown const byWorkflow = this.db.prepare(` SELECT workflow_id, workflow_name, COUNT(*) as version_count, SUM(LENGTH(workflow_snapshot)) as total_size, MAX(created_at) as last_backup FROM workflow_versions GROUP BY workflow_id ORDER BY version_count DESC `).all() as any[]; return { totalVersions: totalResult.count, totalSize: sizeResult.total_size || 0, byWorkflow: byWorkflow.map(row => ({ workflowId: row.workflow_id, workflowName: row.workflow_name, versionCount: row.version_count, totalSize: row.total_size, lastBackup: row.last_backup })) }; } /** * Parse workflow version row from database */ private parseWorkflowVersionRow(row: any): any { return { id: row.id, workflowId: row.workflow_id, versionNumber: row.version_number, workflowName: row.workflow_name, workflowSnapshot: this.safeJsonParse(row.workflow_snapshot, null), trigger: row.trigger, operations: row.operations ? this.safeJsonParse(row.operations, null) : null, fixTypes: row.fix_types ? this.safeJsonParse(row.fix_types, null) : null, metadata: row.metadata ? this.safeJsonParse(row.metadata, null) : null, createdAt: row.created_at }; } }

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