UNPKG

twenty-mcp-server

Version:

Easy-to-install Model Context Protocol server for Twenty CRM. Try instantly with 'npx twenty-mcp-server setup' or install globally for permanent use.

228 lines 9.79 kB
import { z } from 'zod'; export function registerOpportunityTools(server, client) { server.tool('create_opportunity', 'Create a new opportunity/deal in Twenty CRM', { name: z.string().describe('Opportunity name'), amount: z.object({ value: z.number().describe('Amount in currency units (e.g., 1000.50 for $1,000.50)'), currency: z.string().default('USD').describe('Currency code (e.g., USD, EUR)') }).optional().describe('Deal amount'), stage: z.string().optional().describe('Sales stage (e.g., NEW, SCREENING, MEETING, PROPOSAL, CUSTOMER)'), closeDate: z.string().optional().describe('Expected close date (ISO 8601 format)'), companyId: z.string().optional().describe('ID of associated company'), pointOfContactId: z.string().optional().describe('ID of main contact person') }, async ({ name, amount, stage, closeDate, companyId, pointOfContactId }) => { try { const opportunityData = { name }; if (amount) { opportunityData.amount = { amountMicros: Math.round(amount.value * 1000000), currencyCode: amount.currency }; } if (stage) opportunityData.stage = stage; if (closeDate) opportunityData.closeDate = closeDate; if (companyId) opportunityData.companyId = companyId; if (pointOfContactId) opportunityData.pointOfContactId = pointOfContactId; const opportunity = await client.createOpportunity(opportunityData); return { content: [{ type: 'text', text: `Created opportunity: ${opportunity.name} (ID: ${opportunity.id})` }], data: opportunity }; } catch (error) { return { content: [{ type: 'text', text: `Failed to create opportunity: ${error.message}` }], isError: true }; } }); server.tool('get_opportunity', 'Retrieve an opportunity by ID from Twenty CRM', { id: z.string().describe('Opportunity ID to retrieve') }, async ({ id }) => { try { const opportunity = await client.getOpportunity(id); if (!opportunity) { return { content: [{ type: 'text', text: `Opportunity with ID ${id} not found` }], isError: true }; } const amountStr = opportunity.amount ? `${opportunity.amount.currencyCode} ${(opportunity.amount.amountMicros / 1000000).toFixed(2)}` : 'Not specified'; return { content: [{ type: 'text', text: `Opportunity: ${opportunity.name} Amount: ${amountStr} Stage: ${opportunity.stage || 'Not specified'} Close Date: ${opportunity.closeDate || 'Not specified'} Company ID: ${opportunity.companyId || 'None'} Contact ID: ${opportunity.pointOfContactId || 'None'}` }], data: opportunity }; } catch (error) { return { content: [{ type: 'text', text: `Failed to retrieve opportunity: ${error.message}` }], isError: true }; } }); server.tool('update_opportunity', 'Update an existing opportunity in Twenty CRM', { id: z.string().describe('Opportunity ID to update'), name: z.string().optional().describe('Opportunity name'), amount: z.object({ value: z.number().describe('Amount in currency units'), currency: z.string().describe('Currency code') }).optional().describe('Deal amount'), stage: z.string().optional().describe('Sales stage'), closeDate: z.string().optional().describe('Expected close date'), companyId: z.string().optional().describe('ID of associated company'), pointOfContactId: z.string().optional().describe('ID of main contact person') }, async (input) => { try { const updateData = {}; if (input.name) updateData.name = input.name; if (input.amount) { updateData.amount = { amountMicros: Math.round(input.amount.value * 1000000), currencyCode: input.amount.currency }; } if (input.stage) updateData.stage = input.stage; if (input.closeDate) updateData.closeDate = input.closeDate; if (input.companyId) updateData.companyId = input.companyId; if (input.pointOfContactId) updateData.pointOfContactId = input.pointOfContactId; const opportunity = await client.updateOpportunity({ id: input.id, ...updateData }); return { content: [{ type: 'text', text: `Updated opportunity: ${opportunity.name} (ID: ${opportunity.id})` }], data: opportunity }; } catch (error) { return { content: [{ type: 'text', text: `Failed to update opportunity: ${error.message}` }], isError: true }; } }); server.tool('search_opportunities', 'Search for opportunities in Twenty CRM with various filters', { query: z.string().optional().describe('Search query for opportunity name'), stage: z.string().optional().describe('Filter by specific stage'), minAmount: z.number().optional().describe('Minimum deal amount'), maxAmount: z.number().optional().describe('Maximum deal amount'), startDate: z.string().optional().describe('Start date for close date range (ISO 8601)'), endDate: z.string().optional().describe('End date for close date range (ISO 8601)'), companyId: z.string().optional().describe('Filter by company ID'), limit: z.number().default(20).describe('Maximum number of results'), offset: z.number().default(0).describe('Number of results to skip') }, async (input) => { try { const opportunities = await client.searchOpportunities(input); if (opportunities.length === 0) { return { content: [{ type: 'text', text: 'No opportunities found matching the search criteria' }], data: [] }; } const opportunityList = opportunities.map(opp => { const amount = opp.amount ? `${opp.amount.currencyCode} ${(opp.amount.amountMicros / 1000000).toFixed(2)}` : 'N/A'; return `- ${opp.name} (${opp.stage || 'No stage'}) - ${amount}`; }).join('\n'); return { content: [{ type: 'text', text: `Found ${opportunities.length} opportunities:\n${opportunityList}` }], data: opportunities }; } catch (error) { return { content: [{ type: 'text', text: `Failed to search opportunities: ${error.message}` }], isError: true }; } }); server.tool('list_opportunities_by_stage', 'List all opportunities grouped by their sales stage (pipeline view)', {}, async () => { try { const opportunitiesByStage = await client.listOpportunitiesByStage(); let output = 'Opportunities by Stage:\n\n'; let totalCount = 0; let totalValue = 0; for (const [stage, opportunities] of Object.entries(opportunitiesByStage)) { const stageValue = opportunities.reduce((sum, opp) => { return sum + (opp.amount ? opp.amount.amountMicros / 1000000 : 0); }, 0); output += `${stage} (${opportunities.length} opportunities, $${stageValue.toFixed(2)}):\n`; opportunities.forEach(opp => { const amount = opp.amount ? `$${(opp.amount.amountMicros / 1000000).toFixed(2)}` : 'No amount'; output += ` - ${opp.name}: ${amount}\n`; }); output += '\n'; totalCount += opportunities.length; totalValue += stageValue; } output += `\nTotal: ${totalCount} opportunities worth $${totalValue.toFixed(2)}`; return { content: [{ type: 'text', text: output }], data: opportunitiesByStage }; } catch (error) { return { content: [{ type: 'text', text: `Failed to list opportunities by stage: ${error.message}` }], isError: true }; } }); } //# sourceMappingURL=opportunities.js.map