UNPKG

espocrm-graphql-server

Version:

A modern GraphQL server for EspoCRM with TypeScript, MCP integration, and AI-optimized dynamic schema generation

401 lines (359 loc) 10.5 kB
/** * EspoCRM MCP Server for Cloudflare Workers * * This is a remote MCP server that can be deployed to Cloudflare Workers * to provide internet-accessible MCP tools for AI assistants. */ import { McpAgent } from '@cloudflare/mcp-agent'; export interface Env { MCP_GRAPHQL_ENDPOINT: string; MCP_API_KEY: string; MCP_CACHE: KVNamespace; } class EspoCRMMcpAgent extends McpAgent<Env> { async executeGraphQL(query: string, variables?: any): Promise<any> { const response = await fetch(this.env.MCP_GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': this.env.MCP_API_KEY, }, body: JSON.stringify({ query, variables }), }); if (!response.ok) { throw new Error(`GraphQL request failed: ${response.statusText}`); } const result = await response.json(); if (result.errors) { throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`); } return result.data; } async listTools() { return [ { name: 'query_contacts', description: 'Search and retrieve contact information from EspoCRM', inputSchema: { type: 'object', properties: { filter: { type: 'object', properties: { firstName: { type: 'string' }, lastName: { type: 'string' }, email: { type: 'string' }, }, }, limit: { type: 'number', default: 10 }, }, }, }, { name: 'query_accounts', description: 'Search and retrieve account/company information', inputSchema: { type: 'object', properties: { filter: { type: 'object', properties: { name: { type: 'string' }, }, }, limit: { type: 'number', default: 10 }, }, }, }, { name: 'query_leads', description: 'Search and retrieve sales leads', inputSchema: { type: 'object', properties: { filter: { type: 'object', properties: { status: { type: 'string' }, }, }, limit: { type: 'number', default: 10 }, }, }, }, { name: 'create_contact', description: 'Create a new contact in the CRM', inputSchema: { type: 'object', required: ['firstName', 'lastName'], properties: { firstName: { type: 'string' }, lastName: { type: 'string' }, email: { type: 'string' }, phone: { type: 'string' }, }, }, }, { name: 'execute_graphql', description: 'Execute a custom GraphQL query', inputSchema: { type: 'object', required: ['query'], properties: { query: { type: 'string' }, variables: { type: 'object' }, }, }, }, ]; } async executeTool(name: string, args: any) { // Cache key for results const cacheKey = `tool:${name}:${JSON.stringify(args)}`; // Check cache first const cached = await this.env.MCP_CACHE.get(cacheKey); if (cached) { return JSON.parse(cached); } let result; switch (name) { case 'query_contacts': { const { filter = {}, limit = 10 } = args || {}; const query = ` query GetContacts($first: Int!, $filter: ContactFilter) { contacts(first: $first, filter: $filter) { edges { node { id firstName lastName email createdAt modifiedAt } } totalCount } } `; result = await this.executeGraphQL(query, { first: limit, filter }); break; } case 'query_accounts': { const { limit = 10 } = args || {}; const query = ` query GetAccounts($first: Int!) { accounts(first: $first) { edges { node { id name createdAt } } totalCount } } `; result = await this.executeGraphQL(query, { first: limit }); break; } case 'query_leads': { const { limit = 10 } = args || {}; const query = ` query GetLeads($first: Int!) { leads(first: $first) { edges { node { id firstName lastName status createdAt } } totalCount } } `; result = await this.executeGraphQL(query, { first: limit }); break; } case 'create_contact': { const { firstName, lastName, email, phone } = args || {}; if (!firstName || !lastName) { throw new Error('firstName and lastName are required'); } const mutation = ` mutation CreateContact($input: JSON!) { createContact(input: $input) { id firstName lastName } } `; result = await this.executeGraphQL(mutation, { input: { firstName, lastName, email, phone }, }); break; } case 'execute_graphql': { const { query, variables } = args || {}; if (!query) { throw new Error('query is required'); } result = await this.executeGraphQL(query, variables); break; } default: throw new Error(`Unknown tool: ${name}`); } // Cache the result for 5 minutes await this.env.MCP_CACHE.put(cacheKey, JSON.stringify(result), { expirationTtl: 300, }); return result; } async listResources() { return [ { uri: 'espocrm://schema', name: 'GraphQL Schema', description: 'The current GraphQL schema with all available entities', mimeType: 'application/json', }, { uri: 'espocrm://entities', name: 'Available Entities', description: 'List of all discovered EspoCRM entities', mimeType: 'application/json', }, { uri: 'espocrm://health', name: 'System Health', description: 'Current health status of the GraphQL server', mimeType: 'application/json', }, ]; } async readResource(uri: string) { const baseUrl = this.env.MCP_GRAPHQL_ENDPOINT.replace('/graphql', ''); switch (uri) { case 'espocrm://schema': { const response = await fetch(`${baseUrl}/schema-info`, { headers: this.env.MCP_API_KEY ? { 'X-API-Key': this.env.MCP_API_KEY } : {}, }); return await response.json(); } case 'espocrm://entities': { const response = await fetch(`${baseUrl}/schema-info`, { headers: this.env.MCP_API_KEY ? { 'X-API-Key': this.env.MCP_API_KEY } : {}, }); const data = await response.json(); return data.discoveredEntities; } case 'espocrm://health': { const response = await fetch(`${baseUrl}/health`); return await response.json(); } default: throw new Error(`Unknown resource: ${uri}`); } } async listPrompts() { return [ { name: 'crm_search', description: 'Search across CRM entities for information', arguments: [ { name: 'search_term', description: 'What to search for', required: true, }, ], }, { name: 'contact_analysis', description: 'Analyze contacts in the CRM', arguments: [ { name: 'filter', description: 'Optional filter criteria', required: false, }, ], }, ]; } async getPrompt(name: string, args: Record<string, any>) { switch (name) { case 'crm_search': { const searchTerm = args['search_term']; if (!searchTerm) { throw new Error('search_term is required'); } return { description: `Search CRM for: ${searchTerm}`, messages: [ { role: 'user', content: { type: 'text', text: `Search the CRM system for information related to "${searchTerm}". Check contacts, accounts, leads, and opportunities.`, }, }, ], }; } case 'contact_analysis': { const filter = args['filter'] || 'all'; return { description: `Analyze contacts with filter: ${filter}`, messages: [ { role: 'user', content: { type: 'text', text: `Analyze the contacts in the CRM system. ${filter !== 'all' ? `Focus on: ${filter}` : 'Include all contacts.'}`, }, }, ], }; } default: throw new Error(`Unknown prompt: ${name}`); } } } export default { async fetch(request: Request, env: Env): Promise<Response> { const agent = new EspoCRMMcpAgent(env); // Support both SSE and Streamable HTTP transports const url = new URL(request.url); if (url.pathname === '/sse') { // Legacy SSE transport return agent.serveSSE(request); } else if (url.pathname === '/mcp') { // New Streamable HTTP transport return agent.serve(request); } else if (url.pathname === '/') { // Health check / info endpoint return new Response(JSON.stringify({ name: 'EspoCRM MCP Server', version: '1.0.0', transports: ['sse', 'streamable-http'], endpoints: { sse: '/sse', 'streamable-http': '/mcp', }, }), { headers: { 'Content-Type': 'application/json' }, }); } return new Response('Not Found', { status: 404 }); }, };