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.

1,147 lines 33.8 kB
import { GraphQLClient } from 'graphql-request'; export class TwentyClient { client; baseUrl; constructor(config) { this.baseUrl = config.baseUrl || 'https://api.twenty.com'; this.client = new GraphQLClient(`${this.baseUrl}/graphql`, { headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', }, }); } async createPerson(person) { const mutation = ` mutation CreatePerson($data: PersonCreateInput!) { createPerson(data: $data) { id name { firstName lastName } emails { primaryEmail } phones { primaryPhoneNumber primaryPhoneCountryCode } companyId jobTitle linkedinLink { primaryLinkUrl primaryLinkLabel } city avatarUrl } } `; const result = await this.client.request(mutation, { data: person }); return result.createPerson; } async getPerson(id) { const query = ` query GetPerson($filter: PersonFilterInput!) { people(filter: $filter) { edges { node { id name { firstName lastName } emails { primaryEmail } phones { primaryPhoneNumber primaryPhoneCountryCode } companyId jobTitle linkedinLink { primaryLinkUrl primaryLinkLabel } city avatarUrl } } } } `; const result = await this.client.request(query, { filter: { id: { eq: id } } }); return result.people.edges[0]?.node; } async updatePerson(id, updates) { const mutation = ` mutation UpdatePerson($id: ID!, $data: PersonUpdateInput!) { updatePerson(id: $id, data: $data) { id name { firstName lastName } emails { primaryEmail } phones { primaryPhoneNumber primaryPhoneCountryCode } companyId jobTitle linkedinLink { primaryLinkUrl primaryLinkLabel } city avatarUrl } } `; const result = await this.client.request(mutation, { id, data: updates }); return result.updatePerson; } async searchPeople(query, options = {}) { const searchQuery = ` query SearchPeople($filter: PersonFilterInput, $first: Int, $after: String) { people(filter: $filter, first: $first, after: $after) { edges { node { id name { firstName lastName } emails { primaryEmail } phones { primaryPhoneNumber primaryPhoneCountryCode } companyId jobTitle linkedinLink { primaryLinkUrl primaryLinkLabel } city avatarUrl } } } } `; const filter = { or: [ { name: { firstName: { ilike: `%${query}%` } } }, { name: { lastName: { ilike: `%${query}%` } } }, { emails: { primaryEmail: { ilike: `%${query}%` } } } ] }; const result = await this.client.request(searchQuery, { filter, first: options.limit || 20, }); return result.people.edges.map(edge => edge.node); } async createCompany(company) { const mutation = ` mutation CreateCompany($data: CompanyCreateInput!) { createCompany(data: $data) { id name domainName { primaryLinkUrl primaryLinkLabel } address { addressStreet1 addressCity addressState addressCountry addressPostcode } employees linkedinLink { primaryLinkUrl primaryLinkLabel } xLink { primaryLinkUrl primaryLinkLabel } annualRecurringRevenue { amountMicros currencyCode } idealCustomerProfile } } `; const result = await this.client.request(mutation, { data: company }); return result.createCompany; } async getCompany(id) { const query = ` query GetCompany($filter: CompanyFilterInput!) { companies(filter: $filter) { edges { node { id name domainName { primaryLinkUrl primaryLinkLabel } address { addressStreet1 addressCity addressState addressCountry addressPostcode } employees linkedinLink { primaryLinkUrl primaryLinkLabel } xLink { primaryLinkUrl primaryLinkLabel } annualRecurringRevenue { amountMicros currencyCode } idealCustomerProfile } } } } `; const result = await this.client.request(query, { filter: { id: { eq: id } } }); return result.companies.edges[0]?.node; } async updateCompany(id, updates) { const mutation = ` mutation UpdateCompany($id: ID!, $data: CompanyUpdateInput!) { updateCompany(id: $id, data: $data) { id name domainName { primaryLinkUrl primaryLinkLabel } address { addressStreet1 addressCity addressState addressCountry addressPostcode } employees linkedinLink { primaryLinkUrl primaryLinkLabel } xLink { primaryLinkUrl primaryLinkLabel } annualRecurringRevenue { amountMicros currencyCode } idealCustomerProfile } } `; const result = await this.client.request(mutation, { id, data: updates }); return result.updateCompany; } async searchCompanies(query, options = {}) { const searchQuery = ` query SearchCompanies($filter: CompanyFilterInput, $first: Int, $after: String) { companies(filter: $filter, first: $first, after: $after) { edges { node { id name domainName { primaryLinkUrl primaryLinkLabel } address { addressStreet1 addressCity addressState addressCountry addressPostcode } employees linkedinLink { primaryLinkUrl primaryLinkLabel } xLink { primaryLinkUrl primaryLinkLabel } annualRecurringRevenue { amountMicros currencyCode } idealCustomerProfile } } } } `; const filter = { or: [ { name: { ilike: `%${query}%` } }, { domainName: { primaryLinkUrl: { ilike: `%${query}%` } } } ] }; const result = await this.client.request(searchQuery, { filter, first: options.limit || 20, }); return result.companies.edges.map(edge => edge.node); } async createTask(task) { const mutation = ` mutation CreateTask($data: TaskCreateInput!) { createTask(data: $data) { id title body dueAt status assigneeId } } `; const result = await this.client.request(mutation, { data: task }); return result.createTask; } async getTasks(options = {}) { const query = ` { tasks { edges { node { id title body status } } } } `; const result = await this.client.request(query); return result.tasks.edges.map(edge => edge.node); } async createNote(note) { const mutation = ` mutation CreateNote($data: NoteCreateInput!) { createNote(data: $data) { id title body authorId } } `; const result = await this.client.request(mutation, { data: note }); return result.createNote; } async createOpportunity(opportunity) { const mutation = ` mutation CreateOpportunity($data: OpportunityCreateInput!) { createOpportunity(data: $data) { id name amount { amountMicros currencyCode } stage closeDate companyId pointOfContactId createdAt updatedAt } } `; const result = await this.client.request(mutation, { data: opportunity }); return result.createOpportunity; } async getOpportunity(id) { const query = ` query GetOpportunity($filter: OpportunityFilterInput!) { opportunities(filter: $filter) { edges { node { id name amount { amountMicros currencyCode } stage closeDate companyId pointOfContactId createdAt updatedAt } } } } `; const result = await this.client.request(query, { filter: { id: { eq: id } } }); return result.opportunities.edges[0]?.node; } async updateOpportunity(input) { const { id, ...data } = input; const mutation = ` mutation UpdateOpportunity($id: ID!, $data: OpportunityUpdateInput!) { updateOpportunity(id: $id, data: $data) { id name amount { amountMicros currencyCode } stage closeDate companyId pointOfContactId createdAt updatedAt } } `; const result = await this.client.request(mutation, { id, data }); return result.updateOpportunity; } async searchOpportunities(input) { const query = ` query SearchOpportunities($filter: OpportunityFilterInput, $first: Int, $skip: Int) { opportunities(filter: $filter, first: $first, skip: $skip) { edges { node { id name amount { amountMicros currencyCode } stage closeDate companyId pointOfContactId createdAt updatedAt } } } } `; const filters = {}; if (input.query) { filters.name = { ilike: `%${input.query}%` }; } if (input.stage) { filters.stage = { eq: input.stage }; } if (input.companyId) { filters.companyId = { eq: input.companyId }; } if (input.startDate || input.endDate) { filters.closeDate = {}; if (input.startDate) filters.closeDate.gte = input.startDate; if (input.endDate) filters.closeDate.lte = input.endDate; } if (input.minAmount || input.maxAmount) { filters.amount = { amountMicros: {} }; if (input.minAmount) filters.amount.amountMicros.gte = input.minAmount * 1000000; if (input.maxAmount) filters.amount.amountMicros.lte = input.maxAmount * 1000000; } const result = await this.client.request(query, { filter: Object.keys(filters).length > 0 ? filters : undefined, first: input.limit || 20, skip: input.offset || 0, }); return result.opportunities.edges.map(edge => edge.node); } async listOpportunitiesByStage() { const query = ` query ListAllOpportunities { opportunities(first: 100) { edges { node { id name amount { amountMicros currencyCode } stage closeDate companyId pointOfContactId createdAt updatedAt } } } } `; const result = await this.client.request(query); const opportunities = result.opportunities.edges.map(edge => edge.node); // Group by stage const groupedByStage = {}; opportunities.forEach(opp => { const stage = opp.stage || 'No Stage'; if (!groupedByStage[stage]) { groupedByStage[stage] = []; } groupedByStage[stage].push(opp); }); return groupedByStage; } async getActivities(filter) { // Get both tasks and notes as activities const tasksQuery = ` query GetTasks($first: Int) { tasks(first: $first, orderBy: { createdAt: DescNullsLast }) { edges { node { id title body status dueAt assigneeId assignee { id name { firstName lastName } } createdAt updatedAt } } } } `; const notesQuery = ` query GetNotes($first: Int) { notes(first: $first, orderBy: { createdAt: DescNullsLast }) { edges { node { id title body createdAt updatedAt } } } } `; const limit = filter?.limit || 20; const [tasksResult, notesResult] = await Promise.all([ this.client.request(tasksQuery, { first: limit }), this.client.request(notesQuery, { first: limit }) ]); const typedTasksResult = tasksResult; const typedNotesResult = notesResult; // Transform and combine activities const activities = []; typedTasksResult.tasks.edges.forEach(edge => { const task = edge.node; activities.push({ id: task.id, type: 'task', title: task.title, body: task.body, createdAt: task.createdAt, updatedAt: task.updatedAt, authorId: task.assigneeId, author: task.assignee }); }); typedNotesResult.notes.edges.forEach(edge => { const note = edge.node; activities.push({ id: note.id, type: 'note', title: note.title, body: note.body, createdAt: note.createdAt, updatedAt: note.updatedAt, authorId: undefined, author: undefined }); }); // Sort by creation date (newest first) activities.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); // Apply filters let filteredActivities = activities; if (filter?.type && filter.type.length > 0) { filteredActivities = filteredActivities.filter(activity => filter.type.includes(activity.type)); } if (filter?.dateFrom) { filteredActivities = filteredActivities.filter(activity => new Date(activity.createdAt) >= new Date(filter.dateFrom)); } if (filter?.dateTo) { filteredActivities = filteredActivities.filter(activity => new Date(activity.createdAt) <= new Date(filter.dateTo)); } if (filter?.authorId) { filteredActivities = filteredActivities.filter(activity => activity.authorId === filter.authorId); } return { activities: filteredActivities, totalCount: filteredActivities.length, hasMore: filteredActivities.length === limit }; } async filterActivities(filter) { const timeline = await this.getActivities(filter); return timeline.activities; } async createComment(input) { const mutation = ` mutation CreateComment($data: CommentCreateInput!) { createComment(data: $data) { id body authorId author { id name { firstName lastName } } createdAt updatedAt } } `; const commentData = { body: input.body, ...(input.authorId && { authorId: input.authorId }), ...(input.activityTargetId && { activityTargetId: input.activityTargetId }) }; const result = await this.client.request(mutation, { data: commentData }); return result.createComment; } async getEntityActivities(input) { // For now, we'll get general activities and filter client-side // In a real implementation, you'd want to use the entity relationships in the GraphQL query const activities = await this.getActivities({ limit: input.limit, offset: input.offset }); // Note: This is a simplified implementation. In practice, you'd want to query // activities that are specifically related to the entity through proper GraphQL relationships return activities; } async listAllObjects(options = {}) { const query = ` query GetObjectMetadata { objects { edges { node { id nameSingular namePlural labelSingular labelPlural description icon isCustom isActive isSystem createdAt updatedAt } } } } `; const result = await this.client.request(query); const allObjects = result.objects.edges.map(edge => edge.node); // Filter based on options let filteredObjects = allObjects; if (options.activeOnly !== false) { filteredObjects = filteredObjects.filter(obj => obj.isActive); } if (options.includeCustom === false) { filteredObjects = filteredObjects.filter(obj => !obj.isCustom); } if (options.includeSystem === false) { filteredObjects = filteredObjects.filter(obj => !obj.isSystem); } // Group objects by type const standard = filteredObjects.filter(obj => !obj.isCustom && !obj.isSystem); const custom = filteredObjects.filter(obj => obj.isCustom); const system = filteredObjects.filter(obj => obj.isSystem); return { standard, custom, system, totalCount: filteredObjects.length, standardCount: standard.length, customCount: custom.length, systemCount: system.length }; } async getObjectSchema(objectNameOrId) { const objectQuery = ` query GetObjectSchema($filter: ObjectFilterInput!) { objects(filter: $filter) { edges { node { id nameSingular namePlural labelSingular labelPlural description icon isCustom isActive isSystem createdAt updatedAt fields { edges { node { id name label description type isCustom isActive isNullable isSystem defaultValue createdAt updatedAt } } } } } } } `; // Try to find object by name first, then by ID let filter; if (objectNameOrId.match(/^[0-9a-fA-F-]{36}$/)) { // Looks like a UUID filter = { id: { eq: objectNameOrId } }; } else { // Assume it's a name filter = { or: [ { nameSingular: { eq: objectNameOrId } }, { namePlural: { eq: objectNameOrId } } ] }; } const result = await this.client.request(objectQuery, { filter }); if (result.objects.edges.length === 0) { throw new Error(`Object not found: ${objectNameOrId}`); } const objectNode = result.objects.edges[0].node; const fields = objectNode.fields.edges.map(edge => edge.node); return { object: { id: objectNode.id, nameSingular: objectNode.nameSingular, namePlural: objectNode.namePlural, labelSingular: objectNode.labelSingular, labelPlural: objectNode.labelPlural, description: objectNode.description, icon: objectNode.icon, isCustom: objectNode.isCustom, isActive: objectNode.isActive, isSystem: objectNode.isSystem, createdAt: objectNode.createdAt, updatedAt: objectNode.updatedAt, fields }, fields, relationships: [] // TODO: Implement relationship discovery }; } async getFieldMetadata(options = {}) { let query; let variables = {}; if (options.objectId || options.objectName) { // Get fields for a specific object query = ` query GetFieldsForObject($filter: ObjectFilterInput!) { objects(filter: $filter) { edges { node { fields { edges { node { id name label description type isCustom isActive isNullable isSystem defaultValue createdAt updatedAt } } } } } } } `; if (options.objectId) { variables.filter = { id: { eq: options.objectId } }; } else { variables.filter = { or: [ { nameSingular: { eq: options.objectName } }, { namePlural: { eq: options.objectName } } ] }; } } else { // Get all fields across all objects query = ` query GetAllFields { fields { edges { node { id name label description type isCustom isActive isNullable isSystem defaultValue createdAt updatedAt } } } } `; } const result = await this.client.request(query, variables); let fields; if (options.objectId || options.objectName) { if (result.objects.edges.length === 0) { throw new Error(`Object not found: ${options.objectId || options.objectName}`); } fields = result.objects.edges[0].node.fields.edges.map((edge) => edge.node); } else { fields = result.fields.edges.map((edge) => edge.node); } // Apply filters let filteredFields = fields; if (options.activeOnly !== false) { filteredFields = filteredFields.filter(field => field.isActive); } if (options.includeCustom === false) { filteredFields = filteredFields.filter(field => !field.isCustom); } if (options.includeSystem === false) { filteredFields = filteredFields.filter(field => !field.isSystem); } if (options.fieldType) { filteredFields = filteredFields.filter(field => field.type === options.fieldType); } return filteredFields; } // Relationship Management Methods async getCompanyContacts(companyId) { const query = ` query GetCompanyContacts($companyId: String!) { company(filter: { id: { eq: $companyId } }) { id name } people(filter: { companyId: { eq: $companyId } }, first: 100) { edges { node { id name { firstName lastName } emails { primaryEmail } phones { primaryPhoneNumber } jobTitle createdAt } } } } `; const result = await this.client.request(query, { companyId }); const contacts = result.people.edges.map((edge) => ({ id: edge.node.id, name: edge.node.name, email: edge.node.emails?.primaryEmail, phone: edge.node.phones?.primaryPhoneNumber, jobTitle: edge.node.jobTitle, createdAt: edge.node.createdAt })); return { companyId, companyName: result.company?.name || 'Unknown Company', contacts, totalContacts: contacts.length }; } async getPersonOpportunities(personId) { const query = ` query GetPersonOpportunities($personId: String!) { person(filter: { id: { eq: $personId } }) { id name { firstName lastName } } opportunities(filter: { pointOfContactId: { eq: $personId } }, first: 100) { edges { node { id name amount { amountMicros currencyCode } stage closeDate companyId company { id name } createdAt } } } } `; const result = await this.client.request(query, { personId }); const opportunities = result.opportunities.edges.map((edge) => edge.node); const person = result.person; return { personId, personName: person ? `${person.name.firstName} ${person.name.lastName}` : 'Unknown Person', opportunities, totalOpportunities: opportunities.length }; } async linkOpportunityToCompany(input) { const mutation = ` mutation LinkOpportunity($id: String!, $data: OpportunityUpdateInput!) { updateOpportunity(id: $id, data: $data) { id name companyId pointOfContactId company { id name } pointOfContact { id name { firstName lastName } } } } `; const updateData = {}; if (input.companyId) updateData.companyId = input.companyId; if (input.pointOfContactId) updateData.pointOfContactId = input.pointOfContactId; const result = await this.client.request(mutation, { id: input.opportunityId, data: updateData }); return result.updateOpportunity; } async transferContactToCompany(input) { const mutation = ` mutation TransferContact($id: String!, $data: PersonUpdateInput!) { updatePerson(id: $id, data: $data) { id name { firstName lastName } companyId company { id name } } } `; const result = await this.client.request(mutation, { id: input.contactId, data: { companyId: input.toCompanyId } }); return result.updatePerson; } async getRelationshipSummary(entityId, entityType) { let counts = { companies: 0, contacts: 0, opportunities: 0, tasks: 0, activities: 0 }; try { switch (entityType.toLowerCase()) { case 'company': // Get contacts for this company const companyContacts = await this.getCompanyContacts(entityId); counts.contacts = companyContacts.totalContacts; // Get opportunities for this company const companyOpps = await this.searchOpportunities({ companyId: entityId, limit: 1000 }); counts.opportunities = companyOpps.length; break; case 'person': // Get opportunities for this person const personOpps = await this.getPersonOpportunities(entityId); counts.opportunities = personOpps.totalOpportunities; // Get tasks assigned to this person (would need a specific query for filtering) // For now, we'll skip task counting as it requires a filtered query counts.tasks = 0; break; } } catch (error) { console.warn('Error calculating relationship summary:', error); } return { entityId, entityType, relationships: counts }; } async findOrphanedRecords() { const orphaned = { companies: [], contacts: [], opportunities: [], tasks: [] }; try { // Find companies with no contacts const companiesQuery = ` query GetCompaniesWithContacts { companies(first: 1000) { edges { node { id name people { totalCount } opportunities { totalCount } } } } } `; const companiesResult = await this.client.request(companiesQuery); orphaned.companies = companiesResult.companies.edges .map((edge) => edge.node) .filter((company) => company.people.totalCount === 0) .map((company) => ({ id: company.id, name: company.name, contactCount: 0, opportunityCount: company.opportunities.totalCount })); // Find contacts without companies const contactsQuery = ` query GetContactsWithoutCompanies { people(filter: { companyId: { is: "NULL" } }, first: 1000) { edges { node { id name { firstName lastName } companyId opportunities { totalCount } } } } } `; const contactsResult = await this.client.request(contactsQuery); orphaned.contacts = contactsResult.people.edges.map((edge) => ({ id: edge.node.id, name: `${edge.node.name.firstName} ${edge.node.name.lastName}`, hasCompany: !!edge.node.companyId, opportunityCount: edge.node.opportunities.totalCount })); } catch (error) { console.warn('Error finding orphaned records:', error); } return orphaned; } } //# sourceMappingURL=twenty-client.js.map