UNPKG

@gravitykit/gravitymcp

Version:

Full-featured MCP server for Gravity Forms

660 lines (619 loc) 20.5 kB
#!/usr/bin/env node /** * Gravity MCP Server * Model Context Protocol server for Gravity Forms * Tools for forms, entries, and add-ons */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import dotenv from 'dotenv'; import GravityFormsClient from './gravity-forms-client.js'; import { createFieldOperations, fieldOperationHandlers, fieldOperationTools } from './field-operations/index.js'; import fieldRegistry from './field-definitions/field-registry.js'; import FieldAwareValidator from './config/field-validation.js'; import logger from './utils/logger.js'; import { sanitize } from './utils/sanitize.js'; // Load environment variables dotenv.config(); // Initialize the MCP server const server = new Server( { name: 'gravitymcp', version: '1.0.0' }, { capabilities: { tools: {} } } ); // Global client instance let gravityFormsClient = null; let fieldOperations = null; let fieldValidator = null; /** * Initialize Gravity Forms client */ async function initializeClient() { try { gravityFormsClient = new GravityFormsClient(process.env); const validation = await gravityFormsClient.initialize(); if (!validation.available) { throw new Error(`Failed to initialize Gravity Forms client: ${validation.error}`); } // Initialize field operations infrastructure fieldValidator = new FieldAwareValidator(); fieldOperations = createFieldOperations( gravityFormsClient, fieldRegistry, fieldValidator ); logger.info('✅ Gravity MCP initialized successfully'); logger.info('✅ Field operations infrastructure initialized'); return true; } catch (error) { logger.error(`❌ Failed to initialize: ${error.message}`); throw error; } } /** * Create standard error response */ function createErrorResponse(message, details = null) { return { content: [ { type: "text", text: `Error: ${message}${details ? `\nDetails: ${JSON.stringify(details)}` : ''}` } ], isError: true }; } /** * Wrap async handler with error handling */ function wrapHandler(handler) { return async (params) => { if (!gravityFormsClient) { return createErrorResponse('Gravity Forms client not initialized'); } try { const result = await handler(params); // MCP expects content to be an array of content blocks // Each block should have a type (usually "text") and the actual content return { content: [ { type: "text", text: JSON.stringify(result, null, 2) } ] }; } catch (error) { // Sanitize error details to prevent logging sensitive data const safeDetails = error.details ? sanitize(error.details) : undefined; console.error(`Tool error: ${error.message}`); return createErrorResponse(error.message, safeDetails); } }; } // ================================= // FORMS MANAGEMENT TOOLS (6) // ================================= server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Forms Management (6 tools) { name: 'gf_list_forms', description: 'List all forms (returns all forms as object keyed by ID)', inputSchema: { type: 'object', properties: { include: { type: 'array', items: { type: 'number' }, description: 'Array of form IDs to include (returns full form details)' } } } }, { name: 'gf_get_form', description: 'Get a specific form by ID with complete schema', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Form ID' } }, required: ['id'] } }, { name: 'gf_create_form', description: 'Create a new form with fields and settings', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Form title' }, description: { type: 'string', description: 'Form description' }, fields: { type: 'array', description: 'Array of field objects', items: { type: 'object' } }, button: { type: 'object', description: 'Submit button settings' }, confirmations: { type: 'object', description: 'Confirmation settings' }, notifications: { type: 'object', description: 'Notification settings' }, is_active: { type: 'boolean', description: 'Whether form is active' } }, required: ['title'] } }, { name: 'gf_update_form', description: 'Update an existing form', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Form ID' }, title: { type: 'string', description: 'Form title' }, description: { type: 'string', description: 'Form description' }, fields: { type: 'array', description: 'Array of field objects', items: { type: 'object' } }, button: { type: 'object', description: 'Submit button settings' }, confirmations: { type: 'object', description: 'Confirmation settings' }, notifications: { type: 'object', description: 'Notification settings' }, is_active: { type: 'boolean', description: 'Whether form is active' } }, required: ['id'] } }, { name: 'gf_delete_form', description: 'Delete or trash a form (requires ALLOW_DELETE=true)', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Form ID' }, force: { type: 'boolean', description: 'Permanently delete if true, trash if false' } }, required: ['id'] } }, { name: 'gf_validate_form', description: 'Validate form submission data', inputSchema: { type: 'object', properties: { form_id: { type: 'number', description: 'Form ID' } }, additionalProperties: true, required: ['form_id'] } }, // Entries Management (6 tools) { name: 'gf_list_entries', description: 'Search and list entries with advanced filtering', inputSchema: { type: 'object', properties: { form_ids: { type: 'array', items: { type: 'number' }, description: 'Array of form IDs to filter entries' }, include: { type: 'array', items: { type: 'number' }, description: 'Array of entry IDs to include' }, exclude: { type: 'array', items: { type: 'number' }, description: 'Array of entry IDs to exclude' }, status: { type: 'string', enum: ['active', 'spam', 'trash'], description: 'Filter by entry status' }, search: { type: 'object', properties: { field_filters: { type: 'array', items: { type: 'object', properties: { key: { type: 'string' }, value: { type: 'string' }, operator: { type: 'string', enum: ['=', 'IS', 'CONTAINS', 'IS NOT', 'ISNOT', '<>', 'LIKE', 'NOT IN', 'NOTIN', 'IN', '>', '<', '>=', '<='] } } } }, mode: { type: 'string', enum: ['any', 'all'], description: 'Search mode' } } }, sorting: { type: 'object', properties: { key: { type: 'string' }, direction: { type: 'string', enum: ['asc', 'desc', 'ASC', 'DESC'] } } }, paging: { type: 'object', properties: { page_size: { type: 'number' }, current_page: { type: 'number' } } } } } }, { name: 'gf_get_entry', description: 'Get a specific entry by ID', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Entry ID' } }, required: ['id'] } }, { name: 'gf_create_entry', description: 'Create a new entry', inputSchema: { type: 'object', properties: { form_id: { type: 'number', description: 'Form ID' }, created_by: { type: 'number', description: 'User ID who created the entry' }, status: { type: 'string', enum: ['active', 'spam', 'trash'], description: 'Entry status' }, date_created: { type: 'string', description: 'Date created in ISO format' } }, additionalProperties: true, required: ['form_id'] } }, { name: 'gf_update_entry', description: 'Update an existing entry', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Entry ID' }, status: { type: 'string', enum: ['active', 'spam', 'trash'], description: 'Entry status' } }, additionalProperties: true, required: ['id'] } }, { name: 'gf_delete_entry', description: 'Delete or trash an entry (requires ALLOW_DELETE=true)', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Entry ID' }, force: { type: 'boolean', description: 'Permanently delete if true, trash if false' } }, required: ['id'] } }, // Form Submissions (2 tools) { name: 'gf_submit_form_data', description: 'Submit form data with full processing pipeline', inputSchema: { type: 'object', properties: { form_id: { type: 'number', description: 'Form ID' }, field_values: { type: 'object', description: 'Additional field values' } }, additionalProperties: true, required: ['form_id'] } }, { name: 'gf_validate_submission', description: 'Validate form submission without processing', inputSchema: { type: 'object', properties: { form_id: { type: 'number', description: 'Form ID' } }, additionalProperties: true, required: ['form_id'] } }, // Notifications (1 tool) { name: 'gf_send_notifications', description: 'Send notifications for an entry', inputSchema: { type: 'object', properties: { entry_id: { type: 'number', description: 'Entry ID' }, notification_ids: { type: 'array', items: { type: 'string' }, description: 'Array of notification IDs to send' } }, required: ['entry_id'] } }, // Add-on Feeds (7 tools) { name: 'gf_list_feeds', description: 'List all add-on feeds', inputSchema: { type: 'object', properties: { addon: { type: 'string', description: 'Filter by addon slug' }, form_id: { type: 'number', description: 'Filter by form ID' } } } }, { name: 'gf_get_feed', description: 'Get a specific feed by ID', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Feed ID' } }, required: ['id'] } }, { name: 'gf_list_form_feeds', description: 'Get all feeds for a specific form', inputSchema: { type: 'object', properties: { form_id: { type: 'number', description: 'Form ID' } }, required: ['form_id'] } }, { name: 'gf_create_feed', description: 'Create a new add-on feed', inputSchema: { type: 'object', properties: { addon_slug: { type: 'string', description: 'Add-on slug' }, form_id: { type: 'number', description: 'Form ID' }, is_active: { type: 'boolean', description: 'Whether feed is active' }, meta: { type: 'object', description: 'Feed configuration metadata' } }, required: ['addon_slug', 'form_id', 'meta'] } }, { name: 'gf_update_feed', description: 'Update an existing feed', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Feed ID' }, is_active: { type: 'boolean', description: 'Whether feed is active' }, meta: { type: 'object', description: 'Feed configuration metadata' } }, required: ['id'] } }, { name: 'gf_patch_feed', description: 'Partially update a feed', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Feed ID' }, is_active: { type: 'boolean', description: 'Whether feed is active' }, meta: { type: 'object', description: 'Feed configuration metadata' } }, required: ['id'] } }, { name: 'gf_delete_feed', description: 'Delete an add-on feed', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Feed ID' } }, required: ['id'] } }, // Field Filters (1 tool) { name: 'gf_get_field_filters', description: 'Get field filters for a form', inputSchema: { type: 'object', properties: { form_id: { type: 'number', description: 'Form ID' } }, required: ['form_id'] } }, // Results (1 tool) { name: 'gf_get_results', description: 'Get Quiz, Poll, or Survey results', inputSchema: { type: 'object', properties: { form_id: { type: 'number', description: 'Form ID' } }, required: ['form_id'] } }, // Field Operations (4 tools) - Intelligent field management ...fieldOperationTools ] }; }); // ================================= // TOOL HANDLERS // ================================= // Forms Management Handlers server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: params } = request.params; // Ensure client is initialized if (!gravityFormsClient) { await initializeClient(); } // Route to appropriate handler // The client already validates internally, just pass params directly switch (name) { // Forms Management case 'gf_list_forms': return wrapHandler(() => gravityFormsClient.listForms(params))(); case 'gf_get_form': return wrapHandler(() => gravityFormsClient.getForm(params))(); case 'gf_create_form': return wrapHandler(() => gravityFormsClient.createForm(params))(); case 'gf_update_form': return wrapHandler(() => gravityFormsClient.updateForm(params))(); case 'gf_delete_form': return wrapHandler(() => gravityFormsClient.deleteForm(params))(); case 'gf_validate_form': return wrapHandler(() => gravityFormsClient.validateForm(params))(); // Entries Management case 'gf_list_entries': return wrapHandler(() => gravityFormsClient.listEntries(params))(); case 'gf_get_entry': return wrapHandler(() => gravityFormsClient.getEntry(params))(); case 'gf_create_entry': return wrapHandler(() => gravityFormsClient.createEntry(params))(); case 'gf_update_entry': return wrapHandler(() => gravityFormsClient.updateEntry(params))(); case 'gf_delete_entry': return wrapHandler(() => gravityFormsClient.deleteEntry(params))(); // Form Submissions case 'gf_submit_form_data': return wrapHandler(() => gravityFormsClient.submitFormData(params))(); case 'gf_validate_submission': return wrapHandler(() => gravityFormsClient.validateSubmission(params))(); // Notifications case 'gf_send_notifications': return wrapHandler(() => gravityFormsClient.sendNotifications(params))(); // Add-on Feeds case 'gf_list_feeds': return wrapHandler(() => gravityFormsClient.listFeeds(params))(); case 'gf_get_feed': return wrapHandler(() => gravityFormsClient.getFeed(params))(); case 'gf_list_form_feeds': return wrapHandler(() => gravityFormsClient.listFormFeeds(params))(); case 'gf_create_feed': return wrapHandler(() => gravityFormsClient.createFeed(params))(); case 'gf_update_feed': return wrapHandler(() => gravityFormsClient.updateFeed(params))(); case 'gf_patch_feed': return wrapHandler(() => gravityFormsClient.patchFeed(params))(); case 'gf_delete_feed': return wrapHandler(() => gravityFormsClient.deleteFeed(params))(); // Utilities case 'gf_get_field_filters': return wrapHandler(() => gravityFormsClient.getFieldFilters(params))(); case 'gf_get_results': return wrapHandler(() => gravityFormsClient.getResults(params))(); // Field Operations - Intelligent field management case 'gf_add_field': return wrapHandler(async () => { if (!fieldOperations) { throw new Error('Field operations not initialized'); } return await fieldOperationHandlers.gf_add_field(params, fieldOperations); })(); case 'gf_update_field': return wrapHandler(async () => { if (!fieldOperations) { throw new Error('Field operations not initialized'); } return await fieldOperationHandlers.gf_update_field(params, fieldOperations); })(); case 'gf_delete_field': return wrapHandler(async () => { if (!fieldOperations) { throw new Error('Field operations not initialized'); } return await fieldOperationHandlers.gf_delete_field(params, fieldOperations); })(); case 'gf_list_field_types': return wrapHandler(async () => { if (!fieldOperations) { throw new Error('Field operations not initialized'); } return await fieldOperationHandlers.gf_list_field_types(params, fieldOperations); })(); default: return createErrorResponse(`Unknown tool: ${name}`); } }); // ================================= // SERVER INITIALIZATION // ================================= async function main() { try { // Initialize client on startup await initializeClient(); // Create stdio transport const transport = new StdioServerTransport(); // Connect server to transport await server.connect(transport); logger.info('🚀 Gravity MCP running on stdio'); } catch (error) { logger.error(`Failed to start server: ${error}`); process.exit(1); } } // Handle graceful shutdown process.on('SIGINT', async () => { logger.info('👋 Shutting down Gravity MCP...'); process.exit(0); }); process.on('SIGTERM', async () => { logger.info('👋 Shutting down Gravity MCP...'); process.exit(0); }); // Start the server main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });