Skip to main content
Glama
conversation-tools.ts36.2 kB
/** * MCP Conversation Tools for GoHighLevel Integration * Exposes messaging and conversation capabilities to ChatGPT */ import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { GHLApiClient } from '../clients/ghl-api-client.js'; import { MCPSendSMSParams, MCPSendEmailParams, MCPSearchConversationsParams, MCPGetConversationParams, MCPCreateConversationParams, MCPUpdateConversationParams, MCPDeleteConversationParams, MCPGetEmailMessageParams, MCPGetMessageParams, MCPUploadMessageAttachmentsParams, MCPUpdateMessageStatusParams, MCPAddInboundMessageParams, MCPAddOutboundCallParams, MCPGetMessageRecordingParams, MCPGetMessageTranscriptionParams, MCPDownloadTranscriptionParams, MCPCancelScheduledMessageParams, MCPCancelScheduledEmailParams, MCPLiveChatTypingParams, GHLConversation, GHLMessage, GHLEmailMessage, GHLSendMessageResponse, GHLSearchConversationsResponse, GHLGetMessagesResponse, GHLProcessMessageResponse, GHLCancelScheduledResponse, GHLMessageRecordingResponse, GHLMessageTranscriptionResponse, GHLLiveChatTypingResponse, GHLUploadFilesResponse } from '../types/ghl-types.js'; /** * Conversation Tools Class * Implements MCP tools for messaging and conversation management */ export class ConversationTools { constructor(private ghlClient: GHLApiClient) {} /** * Get all conversation tool definitions for MCP server */ getToolDefinitions(): Tool[] { return [ { name: 'send_sms', description: 'Send an SMS message to a contact in GoHighLevel', inputSchema: { type: 'object', properties: { contactId: { type: 'string', description: 'The unique ID of the contact to send SMS to' }, message: { type: 'string', description: 'The SMS message content to send', maxLength: 1600 }, fromNumber: { type: 'string', description: 'Optional: Phone number to send from (must be configured in GHL)' } }, required: ['contactId', 'message'] } }, { name: 'send_email', description: 'Send an email message to a contact in GoHighLevel', inputSchema: { type: 'object', properties: { contactId: { type: 'string', description: 'The unique ID of the contact to send email to' }, subject: { type: 'string', description: 'Email subject line' }, message: { type: 'string', description: 'Plain text email content' }, html: { type: 'string', description: 'HTML email content (optional, takes precedence over message)' }, emailFrom: { type: 'string', description: 'Optional: Email address to send from (must be configured in GHL)', format: 'email' }, attachments: { type: 'array', items: { type: 'string' }, description: 'Optional: Array of attachment URLs' }, emailCc: { type: 'array', items: { type: 'string' }, description: 'Optional: Array of CC email addresses' }, emailBcc: { type: 'array', items: { type: 'string' }, description: 'Optional: Array of BCC email addresses' } }, required: ['contactId', 'subject'] } }, { name: 'search_conversations', description: 'Search conversations in GoHighLevel with various filters', inputSchema: { type: 'object', properties: { contactId: { type: 'string', description: 'Filter conversations for a specific contact' }, query: { type: 'string', description: 'Search query to filter conversations' }, status: { type: 'string', enum: ['all', 'read', 'unread', 'starred', 'recents'], description: 'Filter conversations by read status', default: 'all' }, limit: { type: 'number', description: 'Maximum number of conversations to return (default: 20, max: 100)', minimum: 1, maximum: 100, default: 20 }, assignedTo: { type: 'string', description: 'Filter by user ID assigned to conversations' } } } }, { name: 'get_conversation', description: 'Get detailed conversation information including message history', inputSchema: { type: 'object', properties: { conversationId: { type: 'string', description: 'The unique ID of the conversation to retrieve' }, limit: { type: 'number', description: 'Maximum number of messages to return (default: 20)', minimum: 1, maximum: 100, default: 20 }, messageTypes: { type: 'array', items: { type: 'string', enum: [ 'TYPE_SMS', 'TYPE_EMAIL', 'TYPE_CALL', 'TYPE_FACEBOOK', 'TYPE_INSTAGRAM', 'TYPE_WHATSAPP', 'TYPE_LIVE_CHAT' ] }, description: 'Filter messages by type (optional)' } }, required: ['conversationId'] } }, { name: 'create_conversation', description: 'Create a new conversation with a contact', inputSchema: { type: 'object', properties: { contactId: { type: 'string', description: 'The unique ID of the contact to create conversation with' } }, required: ['contactId'] } }, { name: 'update_conversation', description: 'Update conversation properties (star, mark read, etc.)', inputSchema: { type: 'object', properties: { conversationId: { type: 'string', description: 'The unique ID of the conversation to update' }, starred: { type: 'boolean', description: 'Star or unstar the conversation' }, unreadCount: { type: 'number', description: 'Set the unread message count (0 to mark as read)', minimum: 0 } }, required: ['conversationId'] } }, { name: 'get_recent_messages', description: 'Get recent messages across all conversations for monitoring', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of conversations to check (default: 10)', minimum: 1, maximum: 50, default: 10 }, status: { type: 'string', enum: ['all', 'unread'], description: 'Filter by conversation status', default: 'unread' } } } }, { name: 'delete_conversation', description: 'Delete a conversation permanently', inputSchema: { type: 'object', properties: { conversationId: { type: 'string', description: 'The unique ID of the conversation to delete' } }, required: ['conversationId'] } }, // MESSAGE MANAGEMENT TOOLS { name: 'get_email_message', description: 'Get detailed email message information by email message ID', inputSchema: { type: 'object', properties: { emailMessageId: { type: 'string', description: 'The unique ID of the email message to retrieve' } }, required: ['emailMessageId'] } }, { name: 'get_message', description: 'Get detailed message information by message ID', inputSchema: { type: 'object', properties: { messageId: { type: 'string', description: 'The unique ID of the message to retrieve' } }, required: ['messageId'] } }, { name: 'upload_message_attachments', description: 'Upload file attachments for use in messages', inputSchema: { type: 'object', properties: { conversationId: { type: 'string', description: 'The conversation ID to upload attachments for' }, attachmentUrls: { type: 'array', items: { type: 'string' }, description: 'Array of file URLs to upload as attachments' } }, required: ['conversationId', 'attachmentUrls'] } }, { name: 'update_message_status', description: 'Update the delivery status of a message', inputSchema: { type: 'object', properties: { messageId: { type: 'string', description: 'The unique ID of the message to update' }, status: { type: 'string', enum: ['delivered', 'failed', 'pending', 'read'], description: 'New status for the message' }, error: { type: 'object', description: 'Error details if status is failed', properties: { code: { type: 'string' }, type: { type: 'string' }, message: { type: 'string' } } }, emailMessageId: { type: 'string', description: 'Email message ID if updating email status' }, recipients: { type: 'array', items: { type: 'string' }, description: 'Email delivery status for additional recipients' } }, required: ['messageId', 'status'] } }, // MANUAL MESSAGE CREATION TOOLS { name: 'add_inbound_message', description: 'Manually add an inbound message to a conversation', inputSchema: { type: 'object', properties: { type: { type: 'string', enum: ['SMS', 'Email', 'WhatsApp', 'GMB', 'IG', 'FB', 'Custom', 'WebChat', 'Live_Chat', 'Call'], description: 'Type of inbound message to add' }, conversationId: { type: 'string', description: 'The conversation to add the message to' }, conversationProviderId: { type: 'string', description: 'Conversation provider ID for the message' }, message: { type: 'string', description: 'Message content (for text-based messages)' }, attachments: { type: 'array', items: { type: 'string' }, description: 'Array of attachment URLs' }, html: { type: 'string', description: 'HTML content for email messages' }, subject: { type: 'string', description: 'Subject line for email messages' }, emailFrom: { type: 'string', description: 'From email address' }, emailTo: { type: 'string', description: 'To email address' }, emailCc: { type: 'array', items: { type: 'string' }, description: 'CC email addresses' }, emailBcc: { type: 'array', items: { type: 'string' }, description: 'BCC email addresses' }, emailMessageId: { type: 'string', description: 'Email message ID for threading' }, altId: { type: 'string', description: 'External provider message ID' }, date: { type: 'string', description: 'Date of the message (ISO format)' }, call: { type: 'object', description: 'Call details for call-type messages', properties: { to: { type: 'string', description: 'Called number' }, from: { type: 'string', description: 'Caller number' }, status: { type: 'string', enum: ['pending', 'completed', 'answered', 'busy', 'no-answer', 'failed', 'canceled', 'voicemail'], description: 'Call status' } } } }, required: ['type', 'conversationId', 'conversationProviderId'] } }, { name: 'add_outbound_call', description: 'Manually add an outbound call record to a conversation', inputSchema: { type: 'object', properties: { conversationId: { type: 'string', description: 'The conversation to add the call to' }, conversationProviderId: { type: 'string', description: 'Conversation provider ID for the call' }, to: { type: 'string', description: 'Called phone number' }, from: { type: 'string', description: 'Caller phone number' }, status: { type: 'string', enum: ['pending', 'completed', 'answered', 'busy', 'no-answer', 'failed', 'canceled', 'voicemail'], description: 'Call completion status' }, attachments: { type: 'array', items: { type: 'string' }, description: 'Array of attachment URLs' }, altId: { type: 'string', description: 'External provider call ID' }, date: { type: 'string', description: 'Date of the call (ISO format)' } }, required: ['conversationId', 'conversationProviderId', 'to', 'from', 'status'] } }, // CALL RECORDING & TRANSCRIPTION TOOLS { name: 'get_message_recording', description: 'Get call recording audio for a message', inputSchema: { type: 'object', properties: { messageId: { type: 'string', description: 'The unique ID of the call message to get recording for' } }, required: ['messageId'] } }, { name: 'get_message_transcription', description: 'Get call transcription text for a message', inputSchema: { type: 'object', properties: { messageId: { type: 'string', description: 'The unique ID of the call message to get transcription for' } }, required: ['messageId'] } }, { name: 'download_transcription', description: 'Download call transcription as a text file', inputSchema: { type: 'object', properties: { messageId: { type: 'string', description: 'The unique ID of the call message to download transcription for' } }, required: ['messageId'] } }, // SCHEDULING MANAGEMENT TOOLS { name: 'cancel_scheduled_message', description: 'Cancel a scheduled message before it is sent', inputSchema: { type: 'object', properties: { messageId: { type: 'string', description: 'The unique ID of the scheduled message to cancel' } }, required: ['messageId'] } }, { name: 'cancel_scheduled_email', description: 'Cancel a scheduled email before it is sent', inputSchema: { type: 'object', properties: { emailMessageId: { type: 'string', description: 'The unique ID of the scheduled email to cancel' } }, required: ['emailMessageId'] } }, // LIVE CHAT TOOLS { name: 'live_chat_typing', description: 'Send typing indicator for live chat conversations', inputSchema: { type: 'object', properties: { visitorId: { type: 'string', description: 'Unique visitor ID for the live chat session' }, conversationId: { type: 'string', description: 'The conversation ID for the live chat' }, isTyping: { type: 'boolean', description: 'Whether the agent is currently typing' } }, required: ['visitorId', 'conversationId', 'isTyping'] } } ]; } /** * Execute conversation tool based on tool name and arguments */ async executeTool(name: string, args: any): Promise<any> { switch (name) { case 'send_sms': return this.sendSMS(args as MCPSendSMSParams); case 'send_email': return this.sendEmail(args as MCPSendEmailParams); case 'search_conversations': return this.searchConversations(args as MCPSearchConversationsParams); case 'get_conversation': return this.getConversation(args as MCPGetConversationParams); case 'create_conversation': return this.createConversation(args as MCPCreateConversationParams); case 'update_conversation': return this.updateConversation(args as MCPUpdateConversationParams); case 'get_recent_messages': return this.getRecentMessages(args); case 'delete_conversation': return this.deleteConversation(args as MCPDeleteConversationParams); case 'get_email_message': return this.getEmailMessage(args as MCPGetEmailMessageParams); case 'get_message': return this.getMessage(args as MCPGetMessageParams); case 'upload_message_attachments': return this.uploadMessageAttachments(args as MCPUploadMessageAttachmentsParams); case 'update_message_status': return this.updateMessageStatus(args as MCPUpdateMessageStatusParams); case 'add_inbound_message': return this.addInboundMessage(args as MCPAddInboundMessageParams); case 'add_outbound_call': return this.addOutboundCall(args as MCPAddOutboundCallParams); case 'get_message_recording': return this.getMessageRecording(args as MCPGetMessageRecordingParams); case 'get_message_transcription': return this.getMessageTranscription(args as MCPGetMessageTranscriptionParams); case 'download_transcription': return this.downloadTranscription(args as MCPDownloadTranscriptionParams); case 'cancel_scheduled_message': return this.cancelScheduledMessage(args as MCPCancelScheduledMessageParams); case 'cancel_scheduled_email': return this.cancelScheduledEmail(args as MCPCancelScheduledEmailParams); case 'live_chat_typing': return this.liveChatTyping(args as MCPLiveChatTypingParams); default: throw new Error(`Unknown tool: ${name}`); } } /** * SEND SMS */ private async sendSMS(params: MCPSendSMSParams): Promise<{ success: boolean; messageId: string; conversationId: string; message: string }> { try { const response = await this.ghlClient.sendSMS( params.contactId, params.message, params.fromNumber ); const result = response.data as GHLSendMessageResponse; return { success: true, messageId: result.messageId, conversationId: result.conversationId, message: `SMS sent successfully to contact ${params.contactId}` }; } catch (error) { throw new Error(`Failed to send SMS: ${error}`); } } /** * SEND EMAIL */ private async sendEmail(params: MCPSendEmailParams): Promise<{ success: boolean; messageId: string; conversationId: string; emailMessageId?: string; message: string }> { try { const response = await this.ghlClient.sendEmail( params.contactId, params.subject, params.message, params.html, { emailFrom: params.emailFrom, emailCc: params.emailCc, emailBcc: params.emailBcc, attachments: params.attachments } ); const result = response.data as GHLSendMessageResponse; return { success: true, messageId: result.messageId, conversationId: result.conversationId, emailMessageId: result.emailMessageId, message: `Email sent successfully to contact ${params.contactId}` }; } catch (error) { throw new Error(`Failed to send email: ${error}`); } } /** * SEARCH CONVERSATIONS */ private async searchConversations(params: MCPSearchConversationsParams): Promise<{ success: boolean; conversations: GHLConversation[]; total: number; message: string }> { try { const searchParams = { locationId: this.ghlClient.getConfig().locationId, contactId: params.contactId, query: params.query, status: params.status || 'all', limit: params.limit || 20, assignedTo: params.assignedTo }; const response = await this.ghlClient.searchConversations(searchParams); const data = response.data as GHLSearchConversationsResponse; return { success: true, conversations: data.conversations, total: data.total, message: `Found ${data.conversations.length} conversations (${data.total} total)` }; } catch (error) { throw new Error(`Failed to search conversations: ${error}`); } } /** * GET CONVERSATION */ private async getConversation(params: MCPGetConversationParams): Promise<{ success: boolean; conversation: GHLConversation; messages: GHLMessage[]; hasMoreMessages: boolean; message: string }> { try { // Get conversation details const conversationResponse = await this.ghlClient.getConversation(params.conversationId); const conversation = conversationResponse.data as GHLConversation; // Get messages const messagesResponse = await this.ghlClient.getConversationMessages( params.conversationId, { limit: params.limit || 20, type: params.messageTypes?.join(',') } ); const messagesData = messagesResponse.data as GHLGetMessagesResponse; return { success: true, conversation, messages: messagesData.messages, hasMoreMessages: messagesData.nextPage, message: `Retrieved conversation with ${messagesData.messages.length} messages` }; } catch (error) { throw new Error(`Failed to get conversation: ${error}`); } } /** * CREATE CONVERSATION */ private async createConversation(params: MCPCreateConversationParams): Promise<{ success: boolean; conversationId: string; message: string }> { try { const response = await this.ghlClient.createConversation({ locationId: this.ghlClient.getConfig().locationId, contactId: params.contactId }); const result = response.data; return { success: true, conversationId: result!.id, message: `Conversation created successfully with contact ${params.contactId}` }; } catch (error) { throw new Error(`Failed to create conversation: ${error}`); } } /** * UPDATE CONVERSATION */ private async updateConversation(params: MCPUpdateConversationParams): Promise<{ success: boolean; conversation: GHLConversation; message: string }> { try { const updateData = { locationId: this.ghlClient.getConfig().locationId, starred: params.starred, unreadCount: params.unreadCount }; const response = await this.ghlClient.updateConversation(params.conversationId, updateData); return { success: true, conversation: response.data!, message: `Conversation updated successfully` }; } catch (error) { throw new Error(`Failed to update conversation: ${error}`); } } /** * GET RECENT MESSAGES */ private async getRecentMessages(params: { limit?: number; status?: string }): Promise<{ success: boolean; conversations: any[]; message: string }> { try { const status: 'all' | 'read' | 'unread' | 'starred' | 'recents' = (params.status === 'all' || params.status === 'unread') ? params.status : 'unread'; const searchParams = { locationId: this.ghlClient.getConfig().locationId, limit: params.limit || 10, status, sortBy: 'last_message_date' as const, sort: 'desc' as const }; const response = await this.ghlClient.searchConversations(searchParams); const data = response.data as GHLSearchConversationsResponse; // Enhance with recent message details const enhancedConversations = data.conversations.map(conv => ({ conversationId: conv.id, contactName: conv.fullName || conv.contactName, contactEmail: conv.email, contactPhone: conv.phone, lastMessageBody: conv.lastMessageBody, lastMessageType: conv.lastMessageType, unreadCount: conv.unreadCount, starred: conv.starred })); return { success: true, conversations: enhancedConversations, message: `Retrieved ${enhancedConversations.length} recent conversations` }; } catch (error) { throw new Error(`Failed to get recent messages: ${error}`); } } private async deleteConversation(params: MCPDeleteConversationParams): Promise<{ success: boolean; message: string }> { try { const response = await this.ghlClient.deleteConversation(params.conversationId); return { success: true, message: `Conversation deleted successfully` }; } catch (error) { throw new Error(`Failed to delete conversation: ${error}`); } } private async getEmailMessage(params: MCPGetEmailMessageParams): Promise<{ success: boolean; emailMessage: GHLEmailMessage; message: string }> { try { const response = await this.ghlClient.getEmailMessage(params.emailMessageId); const emailMessage = response.data as GHLEmailMessage; return { success: true, emailMessage, message: `Retrieved email message with ID ${params.emailMessageId}` }; } catch (error) { throw new Error(`Failed to get email message: ${error}`); } } private async getMessage(params: MCPGetMessageParams): Promise<{ success: boolean; messageData: GHLMessage; message: string }> { try { const response = await this.ghlClient.getMessage(params.messageId); const messageData = response.data as GHLMessage; return { success: true, messageData, message: `Retrieved message with ID ${params.messageId}` }; } catch (error) { throw new Error(`Failed to get message: ${error}`); } } private async uploadMessageAttachments(params: MCPUploadMessageAttachmentsParams): Promise<{ success: boolean; uploadedFiles: any; message: string }> { try { const uploadData = { conversationId: params.conversationId, locationId: this.ghlClient.getConfig().locationId, attachmentUrls: params.attachmentUrls }; const response = await this.ghlClient.uploadMessageAttachments(uploadData); const result = response.data as GHLUploadFilesResponse; return { success: true, uploadedFiles: result.uploadedFiles, message: `Attachments uploaded successfully to conversation ${params.conversationId}` }; } catch (error) { throw new Error(`Failed to upload message attachments: ${error}`); } } private async updateMessageStatus(params: MCPUpdateMessageStatusParams): Promise<{ success: boolean; message: string }> { try { const statusData = { status: params.status as 'delivered' | 'failed' | 'pending' | 'read', error: params.error, emailMessageId: params.emailMessageId, recipients: params.recipients }; const response = await this.ghlClient.updateMessageStatus(params.messageId, statusData); return { success: true, message: `Message status updated to ${params.status} successfully` }; } catch (error) { throw new Error(`Failed to update message status: ${error}`); } } private async addInboundMessage(params: MCPAddInboundMessageParams): Promise<{ success: boolean; messageId: string; conversationId: string; message: string }> { try { const messageData = { type: params.type as 'SMS' | 'Email' | 'WhatsApp' | 'GMB' | 'IG' | 'FB' | 'Custom' | 'WebChat' | 'Live_Chat' | 'Call', conversationId: params.conversationId, conversationProviderId: params.conversationProviderId, message: params.message, attachments: params.attachments, html: params.html, subject: params.subject, emailFrom: params.emailFrom, emailTo: params.emailTo, emailCc: params.emailCc, emailBcc: params.emailBcc, emailMessageId: params.emailMessageId, altId: params.altId, date: params.date, call: params.call }; const response = await this.ghlClient.addInboundMessage(messageData); const result = response.data as GHLProcessMessageResponse; return { success: true, messageId: result.messageId, conversationId: result.conversationId, message: `Inbound message added successfully to conversation ${params.conversationId}` }; } catch (error) { throw new Error(`Failed to add inbound message: ${error}`); } } private async addOutboundCall(params: MCPAddOutboundCallParams): Promise<{ success: boolean; messageId: string; conversationId: string; message: string }> { try { const callData = { type: 'Call' as const, conversationId: params.conversationId, conversationProviderId: params.conversationProviderId, attachments: params.attachments, altId: params.altId, date: params.date, call: { to: params.to, from: params.from, status: params.status as 'pending' | 'completed' | 'answered' | 'busy' | 'no-answer' | 'failed' | 'canceled' | 'voicemail' } }; const response = await this.ghlClient.addOutboundCall(callData); const result = response.data as GHLProcessMessageResponse; return { success: true, messageId: result.messageId, conversationId: result.conversationId, message: `Outbound call added successfully to conversation ${params.conversationId}` }; } catch (error) { throw new Error(`Failed to add outbound call: ${error}`); } } private async getMessageRecording(params: MCPGetMessageRecordingParams): Promise<{ success: boolean; recording: any; contentType: string; message: string }> { try { const response = await this.ghlClient.getMessageRecording(params.messageId); const recording = response.data as GHLMessageRecordingResponse; return { success: true, recording: recording.audioData, contentType: recording.contentType, message: `Retrieved call recording for message ${params.messageId}` }; } catch (error) { throw new Error(`Failed to get message recording: ${error}`); } } private async getMessageTranscription(params: MCPGetMessageTranscriptionParams): Promise<{ success: boolean; transcriptions: any[]; message: string }> { try { const response = await this.ghlClient.getMessageTranscription(params.messageId); const transcriptionData = response.data as GHLMessageTranscriptionResponse; return { success: true, transcriptions: transcriptionData.transcriptions, message: `Retrieved call transcription for message ${params.messageId}` }; } catch (error) { throw new Error(`Failed to get message transcription: ${error}`); } } private async downloadTranscription(params: MCPDownloadTranscriptionParams): Promise<{ success: boolean; transcription: string; message: string }> { try { const response = await this.ghlClient.downloadMessageTranscription(params.messageId); const transcription = response.data as string; return { success: true, transcription, message: `Downloaded call transcription for message ${params.messageId}` }; } catch (error) { throw new Error(`Failed to download transcription: ${error}`); } } private async cancelScheduledMessage(params: MCPCancelScheduledMessageParams): Promise<{ success: boolean; status: number; message: string }> { try { const response = await this.ghlClient.cancelScheduledMessage(params.messageId); const result = response.data as GHLCancelScheduledResponse; return { success: true, status: result.status, message: result.message || `Scheduled message cancelled successfully` }; } catch (error) { throw new Error(`Failed to cancel scheduled message: ${error}`); } } private async cancelScheduledEmail(params: MCPCancelScheduledEmailParams): Promise<{ success: boolean; status: number; message: string }> { try { const response = await this.ghlClient.cancelScheduledEmail(params.emailMessageId); const result = response.data as GHLCancelScheduledResponse; return { success: true, status: result.status, message: result.message || `Scheduled email cancelled successfully` }; } catch (error) { throw new Error(`Failed to cancel scheduled email: ${error}`); } } private async liveChatTyping(params: MCPLiveChatTypingParams): Promise<{ success: boolean; message: string }> { try { const typingData = { locationId: this.ghlClient.getConfig().locationId, isTyping: params.isTyping, visitorId: params.visitorId, conversationId: params.conversationId }; const response = await this.ghlClient.liveChatTyping(typingData); const result = response.data as GHLLiveChatTypingResponse; return { success: result.success, message: `Live chat typing indicator ${params.isTyping ? 'enabled' : 'disabled'} successfully` }; } catch (error) { throw new Error(`Failed to send live chat typing indicator: ${error}`); } } }

Latest Blog Posts

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/cpretzinger/GoHighLevel-MCP'

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