UNPKG

groove-mcp

Version:

Model Context Protocol server for Groove HQ

689 lines 26.3 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { GrooveClient } from './groove-client.js'; import { ConversationTools } from './tools/conversations.js'; import { MessageTools } from './tools/messages.js'; import { ContactTools } from './tools/contacts.js'; import { AgentTools } from './tools/agents.js'; import { KnowledgeBaseResources } from './resources/kb-articles.js'; const apiToken = process.env.GROOVE_API_TOKEN; const apiUrl = process.env.GROOVE_API_URL || 'https://api.groovehq.com/v2/graphql'; if (!apiToken) { console.error('Error: GROOVE_API_TOKEN environment variable is required'); process.exit(1); } const grooveClient = new GrooveClient(apiToken, apiUrl); const conversationTools = new ConversationTools(grooveClient); const messageTools = new MessageTools(grooveClient); const contactTools = new ContactTools(grooveClient); const agentTools = new AgentTools(grooveClient); const kbResources = new KnowledgeBaseResources(grooveClient); const server = new Server({ name: 'groove-mcp-server', version: '0.1.0', }, { capabilities: { tools: {}, resources: {}, }, }); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'listConversations', description: 'List conversations with optional filters', inputSchema: { type: 'object', properties: { status: { type: 'string', enum: ['unread', 'opened', 'closed', 'snoozed'], description: 'Filter by conversation status', }, assigneeId: { type: 'string', description: 'Filter by assigned agent ID', }, contactId: { type: 'string', description: 'Filter by contact ID', }, tagIds: { type: 'array', items: { type: 'string' }, description: 'Filter by tag IDs', }, limit: { type: 'number', description: 'Maximum number of conversations to return', default: 20, }, }, }, }, { name: 'getConversation', description: 'Get detailed information about a specific conversation', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The conversation ID', }, }, required: ['id'], }, }, { name: 'createConversation', description: 'Create a new conversation', inputSchema: { type: 'object', properties: { contactId: { type: 'string', description: 'ID of the contact to start conversation with', }, subject: { type: 'string', description: 'Subject of the conversation', }, body: { type: 'string', description: 'Initial message body', }, assigneeId: { type: 'string', description: 'ID of agent to assign the conversation to', }, tagIds: { type: 'array', items: { type: 'string' }, description: 'Tag IDs to apply to the conversation', }, }, required: ['contactId', 'subject', 'body'], }, }, { name: 'updateConversation', description: 'Update a conversation (status, assignee, tags)', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The conversation ID', }, status: { type: 'string', enum: ['opened', 'closed', 'snoozed'], description: 'New conversation status', }, assigneeId: { type: 'string', description: 'ID of agent to assign the conversation to (or null to unassign)', }, tagIds: { type: 'array', items: { type: 'string' }, description: 'Tag IDs to apply to the conversation', }, snoozedUntil: { type: 'string', description: 'ISO 8601 datetime to snooze until', }, }, required: ['id'], }, }, { name: 'closeConversation', description: 'Close a conversation', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The conversation ID to close', }, }, required: ['id'], }, }, { name: 'listMessages', description: 'List messages in a conversation', inputSchema: { type: 'object', properties: { conversationId: { type: 'string', description: 'The conversation ID to list messages for', }, limit: { type: 'number', description: 'Maximum number of messages to return', default: 50, }, after: { type: 'string', description: 'Cursor for pagination', }, }, required: ['conversationId'], }, }, { name: 'sendMessage', description: 'Send a message in a conversation', inputSchema: { type: 'object', properties: { conversationId: { type: 'string', description: 'The conversation ID to send message to', }, body: { type: 'string', description: 'The message body content', }, attachmentIds: { type: 'array', items: { type: 'string' }, description: 'IDs of attachments to include', }, }, required: ['conversationId', 'body'], }, }, { name: 'createNote', description: 'Create an internal note in a conversation', inputSchema: { type: 'object', properties: { conversationId: { type: 'string', description: 'The conversation ID to add note to', }, body: { type: 'string', description: 'The note content', }, }, required: ['conversationId', 'body'], }, }, { name: 'listContacts', description: 'List contacts with optional search', inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Search string to filter contacts', }, limit: { type: 'number', description: 'Maximum number of contacts to return', default: 20, }, after: { type: 'string', description: 'Cursor for pagination', }, }, }, }, { name: 'getContact', description: 'Get detailed information about a specific contact', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The contact ID', }, }, required: ['id'], }, }, { name: 'createContact', description: 'Create a new contact', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'Contact email address', }, firstName: { type: 'string', description: 'Contact first name', }, lastName: { type: 'string', description: 'Contact last name', }, company: { type: 'string', description: 'Contact company', }, title: { type: 'string', description: 'Contact job title', }, phone: { type: 'string', description: 'Contact phone number', }, }, required: ['email'], }, }, { name: 'updateContact', description: 'Update contact information', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The contact ID', }, email: { type: 'string', description: 'Contact email address', }, firstName: { type: 'string', description: 'Contact first name', }, lastName: { type: 'string', description: 'Contact last name', }, company: { type: 'string', description: 'Contact company', }, title: { type: 'string', description: 'Contact job title', }, phone: { type: 'string', description: 'Contact phone number', }, }, required: ['id'], }, }, { name: 'listAgents', description: 'List all agents in the organization', inputSchema: { type: 'object', properties: {}, }, }, { name: 'getAgent', description: 'Get detailed information about a specific agent', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The agent ID', }, }, required: ['id'], }, }, { name: 'getAvailableAgents', description: 'List all available agents', inputSchema: { type: 'object', properties: {}, }, }, { name: 'searchKbArticles', description: 'Search knowledge base articles', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query', }, limit: { type: 'number', description: 'Maximum number of articles to return', default: 20, }, }, required: ['query'], }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'listConversations': { const conversations = await conversationTools.listConversations(args || {}); return { content: [ { type: 'text', text: JSON.stringify(conversations, null, 2), }, ], }; } case 'getConversation': { if (!args || typeof args.id !== 'string') { throw new Error('Conversation ID is required'); } const conversation = await conversationTools.getConversation(args.id); if (!conversation) { throw new Error(`Conversation not found: ${args.id}`); } return { content: [ { type: 'text', text: JSON.stringify(conversation, null, 2), }, ], }; } case 'createConversation': { if (!args || !args.contactId || !args.subject || !args.body) { throw new Error('contactId, subject, and body are required'); } const conversation = await conversationTools.createConversation(args); return { content: [ { type: 'text', text: JSON.stringify(conversation, null, 2), }, ], }; } case 'updateConversation': { if (!args || typeof args.id !== 'string') { throw new Error('Conversation ID is required'); } const conversation = await conversationTools.updateConversation(args); return { content: [ { type: 'text', text: JSON.stringify(conversation, null, 2), }, ], }; } case 'closeConversation': { if (!args || typeof args.id !== 'string') { throw new Error('Conversation ID is required'); } const conversation = await conversationTools.closeConversation(args.id); return { content: [ { type: 'text', text: JSON.stringify(conversation, null, 2), }, ], }; } case 'listMessages': { if (!args || typeof args.conversationId !== 'string') { throw new Error('Conversation ID is required'); } const messages = await messageTools.listMessages(args); return { content: [ { type: 'text', text: JSON.stringify(messages, null, 2), }, ], }; } case 'sendMessage': { if (!args || !args.conversationId || !args.body) { throw new Error('conversationId and body are required'); } const message = await messageTools.sendMessage(args); return { content: [ { type: 'text', text: JSON.stringify(message, null, 2), }, ], }; } case 'createNote': { if (!args || !args.conversationId || !args.body) { throw new Error('conversationId and body are required'); } const note = await messageTools.createNote(args); return { content: [ { type: 'text', text: JSON.stringify(note, null, 2), }, ], }; } case 'listContacts': { const contacts = await contactTools.listContacts(args || {}); return { content: [ { type: 'text', text: JSON.stringify(contacts, null, 2), }, ], }; } case 'getContact': { if (!args || typeof args.id !== 'string') { throw new Error('Contact ID is required'); } const contact = await contactTools.getContact(args.id); if (!contact) { throw new Error(`Contact not found: ${args.id}`); } return { content: [ { type: 'text', text: JSON.stringify(contact, null, 2), }, ], }; } case 'createContact': { if (!args || !args.email) { throw new Error('email is required'); } const contact = await contactTools.createContact(args); return { content: [ { type: 'text', text: JSON.stringify(contact, null, 2), }, ], }; } case 'updateContact': { if (!args || typeof args.id !== 'string') { throw new Error('Contact ID is required'); } const contact = await contactTools.updateContact(args); return { content: [ { type: 'text', text: JSON.stringify(contact, null, 2), }, ], }; } case 'listAgents': { const agents = await agentTools.listAgents(); return { content: [ { type: 'text', text: JSON.stringify(agents, null, 2), }, ], }; } case 'getAgent': { if (!args || typeof args.id !== 'string') { throw new Error('Agent ID is required'); } const agent = await agentTools.getAgent(args.id); if (!agent) { throw new Error(`Agent not found: ${args.id}`); } return { content: [ { type: 'text', text: JSON.stringify(agent, null, 2), }, ], }; } case 'getAvailableAgents': { const agents = await agentTools.getAvailableAgents(); return { content: [ { type: 'text', text: JSON.stringify(agents, null, 2), }, ], }; } case 'searchKbArticles': { if (!args || !args.query) { throw new Error('Search query is required'); } const articles = await kbResources.searchArticles(args); return { content: [ { type: 'text', text: JSON.stringify(articles, null, 2), }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [ { type: 'text', text: JSON.stringify({ error: errorMessage }, null, 2), }, ], }; } }); server.setRequestHandler(ListResourcesRequestSchema, async () => { try { // Get published KB articles const articles = await kbResources.listArticles({ published: true, limit: 50 }); const resources = articles.map(article => kbResources.formatArticleResource(article)); return { resources, }; } catch (error) { console.error('Error listing KB resources:', error); return { resources: [], }; } }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; // Parse the URI to extract the type and ID const match = uri.match(/^groove:\/\/kb\/(article|category)\/(.+)$/); if (!match) { throw new Error(`Invalid resource URI: ${uri}`); } const [, type, id] = match; if (type === 'article') { const article = await kbResources.getArticle(id); if (!article) { throw new Error(`Knowledge base article not found: ${id}`); } return { contents: [ { uri, mimeType: 'text/markdown', text: `# ${article.title}\n\n${article.body}`, metadata: { category: article.category?.name, views: article.views, helpful: article.helpfulVotes, unhelpful: article.unhelpfulVotes, published: article.published, featured: article.featured, createdAt: article.createdAt, updatedAt: article.updatedAt, }, }, ], }; } throw new Error(`Unsupported resource type: ${type}`); }); async function main() { console.error('Validating Groove API connection...'); const isValid = await grooveClient.validateConnection(); if (!isValid) { console.error('Failed to connect to Groove API. Please check your API token.'); process.exit(1); } const transport = new StdioServerTransport(); await server.connect(transport); console.error('Groove MCP server started successfully'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); }); //# sourceMappingURL=index.js.map