Skip to main content
Glama

HubSpot MCP Server

index.ts52.8 kB
#!/usr/bin/env node import dotenv from "dotenv"; dotenv.config(); import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequest, CallToolRequestSchema, ListToolsRequestSchema, Tool } from "@modelcontextprotocol/sdk/types.js"; import { Client } from "@hubspot/api-client"; import { FilterOperatorEnum, PublicObjectSearchRequest } from "@hubspot/api-client/lib/codegen/crm/companies/index.js"; // Type definitions for tool arguments interface SearchContactsArgs { query: string; count?: number; propertyList?: string[]; } interface GetSalesAnalyticsArgs { period: "daily" | "weekly" | "monthly" | "quarterly" | "yearly"; startDate: string; // ISO format date endDate?: string; // ISO format date pipeline?: string; // Pipeline ID dealStage?: string; // Deal stage ID dealOwner?: string; // Owner ID } interface GetDealHistoryArgs { dealId: string; } interface GetDealNotesArgs { dealId: string; limit?: number; after?: string; } interface GetEngagementsByDealArgs { dealId: string; types?: Array<"CALL" | "EMAIL" | "MEETING" | "TASK" | "NOTE">; limit?: number; after?: string; } interface GetSalesPerformanceArgs { ownerIds?: string[]; period: "daily" | "weekly" | "monthly" | "quarterly" | "yearly"; startDate: string; // ISO format date endDate?: string; // ISO format date pipeline?: string; // Pipeline ID } interface GetPipelineAnalyticsArgs { pipelineId: string; period: "daily" | "weekly" | "monthly" | "quarterly" | "yearly"; startDate: string; // ISO format date endDate?: string; // ISO format date } interface GetForecastAnalyticsArgs { period: "monthly" | "quarterly" | "yearly"; numberOfPeriods?: number; pipeline?: string; // Pipeline ID } interface GetContactArgs { contactId: string; properties?: string[]; } interface CreateContactArgs { properties: { email: string; firstname?: string; lastname?: string; [key: string]: any; }; } interface UpdateContactArgs { contactId: string; properties: { [key: string]: any; }; } interface ListDealsArgs { limit?: number; after?: string; properties?: string[]; } interface GetDealArgs { dealId: string; properties?: string[]; } interface CreateDealArgs { properties: { dealname: string; amount?: number; dealstage?: string; pipeline?: string; [key: string]: any; }; } interface UpdateDealArgs { dealId: string; properties: { [key: string]: any; }; } interface ListCompaniesArgs { limit?: number; after?: string; properties?: string[]; } interface GetCompanyArgs { companyId: string; properties?: string[]; } // Tool definitions const searchContactsTool: Tool = { name: "hubspot_search_contacts", description: "Search for contacts in HubSpot using a query string", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query string (email, name, etc.)", }, count: { type: "number", description: "Maximum number of contacts to return (default 10)", }, propertyList: { type: "array", items: { type: "string", }, description: "List of properties to include in the results", }, }, required: ["query"], }, }; const getContactTool: Tool = { name: "hubspot_get_contact", description: "Get a specific contact by ID", inputSchema: { type: "object", properties: { contactId: { type: "string", description: "The ID of the contact to retrieve", }, properties: { type: "array", items: { type: "string", }, description: "List of properties to include in the results", }, }, required: ["contactId"], }, }; const createContactTool: Tool = { name: "hubspot_create_contact", description: "Create a new contact in HubSpot", inputSchema: { type: "object", properties: { properties: { type: "object", properties: { email: { type: "string", description: "Contact's email address", }, firstname: { type: "string", description: "Contact's first name", }, lastname: { type: "string", description: "Contact's last name", }, }, required: ["email"], additionalProperties: true, description: "Contact properties", }, }, required: ["properties"], }, }; const updateContactTool: Tool = { name: "hubspot_update_contact", description: "Update an existing contact in HubSpot", inputSchema: { type: "object", properties: { contactId: { type: "string", description: "The ID of the contact to update", }, properties: { type: "object", additionalProperties: true, description: "Contact properties to update", }, }, required: ["contactId", "properties"], }, }; const listDealsTool: Tool = { name: "hubspot_list_deals", description: "List deals in HubSpot with pagination", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of deals to return (default 10, max 100)", default: 10, }, after: { type: "string", description: "Pagination cursor for next page of results", }, properties: { type: "array", items: { type: "string", }, description: "List of properties to include in the results", }, }, }, }; const getDealTool: Tool = { name: "hubspot_get_deal", description: "Get a specific deal by ID", inputSchema: { type: "object", properties: { dealId: { type: "string", description: "The ID of the deal to retrieve", }, properties: { type: "array", items: { type: "string", }, description: "List of properties to include in the results", }, }, required: ["dealId"], }, }; const createDealTool: Tool = { name: "hubspot_create_deal", description: "Create a new deal in HubSpot", inputSchema: { type: "object", properties: { properties: { type: "object", properties: { dealname: { type: "string", description: "Name of the deal", }, amount: { type: "number", description: "Deal amount", }, dealstage: { type: "string", description: "Stage of the deal (ID)", }, pipeline: { type: "string", description: "Pipeline ID", }, }, required: ["dealname"], additionalProperties: true, description: "Deal properties", }, }, required: ["properties"], }, }; const updateDealTool: Tool = { name: "hubspot_update_deal", description: "Update an existing deal in HubSpot", inputSchema: { type: "object", properties: { dealId: { type: "string", description: "The ID of the deal to update", }, properties: { type: "object", additionalProperties: true, description: "Deal properties to update", }, }, required: ["dealId", "properties"], }, }; const listCompaniesTool: Tool = { name: "hubspot_list_companies", description: "List companies in HubSpot with pagination", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of companies to return (default 10, max 100)", default: 10, }, after: { type: "string", description: "Pagination cursor for next page of results", }, properties: { type: "array", items: { type: "string", }, description: "List of properties to include in the results", }, }, }, }; const getCompanyTool: Tool = { name: "hubspot_get_company", description: "Get a specific company by ID", inputSchema: { type: "object", properties: { companyId: { type: "string", description: "The ID of the company to retrieve", }, properties: { type: "array", items: { type: "string", }, description: "List of properties to include in the results", }, }, required: ["companyId"], }, }; const getSalesAnalyticsTool: Tool = { name: "hubspot_get_sales_analytics", description: "Get aggregated sales analytics data for specific time periods", inputSchema: { type: "object", properties: { period: { type: "string", enum: ["daily", "weekly", "monthly", "quarterly", "yearly"], description: "Time period to group analytics data by", }, startDate: { type: "string", description: "Start date for analysis in ISO format (YYYY-MM-DD)", }, endDate: { type: "string", description: "End date for analysis in ISO format (YYYY-MM-DD)", }, pipeline: { type: "string", description: "Pipeline ID to filter by", }, dealStage: { type: "string", description: "Deal stage ID to filter by", }, dealOwner: { type: "string", description: "Deal owner ID to filter by", }, }, required: ["period", "startDate"], }, }; const getDealHistoryTool: Tool = { name: "hubspot_get_deal_history", description: "Get the complete history of changes to a deal", inputSchema: { type: "object", properties: { dealId: { type: "string", description: "The ID of the deal to get history for", }, }, required: ["dealId"], }, }; const getDealNotesTool: Tool = { name: "hubspot_get_deal_notes", description: "Get notes associated with a specific deal", inputSchema: { type: "object", properties: { dealId: { type: "string", description: "The ID of the deal to get notes for", }, limit: { type: "number", description: "Maximum number of notes to return (default 20)", default: 20, }, after: { type: "string", description: "Pagination cursor for next page of results", }, }, required: ["dealId"], }, }; const getEngagementsByDealTool: Tool = { name: "hubspot_get_engagements_by_deal", description: "Get all engagement activities (calls, emails, meetings, etc.) associated with a deal", inputSchema: { type: "object", properties: { dealId: { type: "string", description: "The ID of the deal to get engagements for", }, types: { type: "array", items: { type: "string", enum: ["CALL", "EMAIL", "MEETING", "TASK", "NOTE"], }, description: "Types of engagements to include", }, limit: { type: "number", description: "Maximum number of engagements to return (default 20)", default: 20, }, after: { type: "string", description: "Pagination cursor for next page of results", }, }, required: ["dealId"], }, }; const getSalesPerformanceTool: Tool = { name: "hubspot_get_sales_performance", description: "Get performance metrics for sales reps over a specific time period", inputSchema: { type: "object", properties: { ownerIds: { type: "array", items: { type: "string", }, description: "List of owner IDs to include in the analysis", }, period: { type: "string", enum: ["daily", "weekly", "monthly", "quarterly", "yearly"], description: "Time period to group performance data by", }, startDate: { type: "string", description: "Start date for analysis in ISO format (YYYY-MM-DD)", }, endDate: { type: "string", description: "End date for analysis in ISO format (YYYY-MM-DD)", }, pipeline: { type: "string", description: "Pipeline ID to filter by", }, }, required: ["period", "startDate"], }, }; const getPipelineAnalyticsTool: Tool = { name: "hubspot_get_pipeline_analytics", description: "Get analytics for a specific sales pipeline including conversion rates between stages", inputSchema: { type: "object", properties: { pipelineId: { type: "string", description: "The ID of the pipeline to analyze", }, period: { type: "string", enum: ["daily", "weekly", "monthly", "quarterly", "yearly"], description: "Time period to group analytics data by", }, startDate: { type: "string", description: "Start date for analysis in ISO format (YYYY-MM-DD)", }, endDate: { type: "string", description: "End date for analysis in ISO format (YYYY-MM-DD)", }, }, required: ["pipelineId", "period", "startDate"], }, }; const getForecastAnalyticsTool: Tool = { name: "hubspot_get_forecast_analytics", description: "Get forecasted sales data based on current pipeline and historical performance", inputSchema: { type: "object", properties: { period: { type: "string", enum: ["monthly", "quarterly", "yearly"], description: "Time period to group forecast data by", }, numberOfPeriods: { type: "number", description: "Number of future periods to forecast (default 3)", default: 3, }, pipeline: { type: "string", description: "Pipeline ID to filter by", }, }, required: ["period"], }, }; class HubSpotClient { private client: Client; constructor(apiKey: string) { this.client = new Client({ accessToken: apiKey }); } async searchContacts(query: string, count: number = 10, propertyList?: string[]): Promise<any> { const publicObjectSearchRequest: PublicObjectSearchRequest = { filterGroups: [ { filters: [ { propertyName: "email", operator: FilterOperatorEnum.ContainsToken, value: query, }, ], }, ], sorts: ["lastmodifieddate"], properties: propertyList || ["email", "firstname", "lastname", "phone"], limit: count, }; return await this.client.crm.contacts.searchApi.doSearch(publicObjectSearchRequest); } async getContact(contactId: string, properties?: string[]): Promise<any> { return await this.client.crm.contacts.basicApi.getById(contactId, properties || ["email", "firstname", "lastname", "phone"]); } async createContact(properties: { [key: string]: any }): Promise<any> { const contactInput = { properties, }; return await this.client.crm.contacts.basicApi.create(contactInput); } async updateContact(contactId: string, properties: { [key: string]: any }): Promise<any> { const contactInput = { properties, }; return await this.client.crm.contacts.basicApi.update(contactId, contactInput); } async listDeals(limit: number = 10, after?: string, properties?: string[]): Promise<any> { return await this.client.crm.deals.basicApi.getPage( limit, after, properties || ["dealname", "amount", "dealstage", "closedate"], undefined, undefined, undefined ); } async getDeal(dealId: string, properties?: string[]): Promise<any> { return await this.client.crm.deals.basicApi.getById(dealId, properties || ["dealname", "amount", "dealstage", "closedate"]); } async createDeal(properties: { [key: string]: any }): Promise<any> { const dealInput = { properties, }; return await this.client.crm.deals.basicApi.create(dealInput); } async updateDeal(dealId: string, properties: { [key: string]: any }): Promise<any> { const dealInput = { properties, }; return await this.client.crm.deals.basicApi.update(dealId, dealInput); } async listCompanies(limit: number = 10, after?: string, properties?: string[]): Promise<any> { return await this.client.crm.companies.basicApi.getPage( limit, after, properties || ["name", "domain", "phone", "address"], undefined, undefined, undefined ); } async getCompany(companyId: string, properties?: string[]): Promise<any> { return await this.client.crm.companies.basicApi.getById(companyId, properties || ["name", "domain", "phone", "address"]); } async getSalesAnalytics( period: "daily" | "weekly" | "monthly" | "quarterly" | "yearly", startDate: string, endDate?: string, pipeline?: string, dealStage?: string, dealOwner?: string ): Promise<any> { // We'll need to gather deal data and then do our own aggregation // First, get all deals within the timeframe const allDeals = await this.getAllDeals(startDate, endDate, pipeline, dealStage, dealOwner); // Group and aggregate deals by the requested period return this.aggregateDealsByPeriod(allDeals, period, startDate, endDate); } private async getAllDeals( startDate: string, endDate?: string, pipeline?: string, dealStage?: string, dealOwner?: string ): Promise<any[]> { // Need to use search API to filter deals effectively const filterGroups = []; // Date range filter const dateFilters = []; dateFilters.push({ propertyName: "createdate", operator: FilterOperatorEnum.Gte, value: new Date(startDate).getTime().toString(), }); if (endDate) { dateFilters.push({ propertyName: "createdate", operator: FilterOperatorEnum.Lte, value: new Date(endDate).getTime().toString(), }); } filterGroups.push({ filters: dateFilters }); // Additional filters if (pipeline || dealStage || dealOwner) { const additionalFilters = []; if (pipeline) { additionalFilters.push({ propertyName: "pipeline", operator: FilterOperatorEnum.Eq, value: pipeline, }); } if (dealStage) { additionalFilters.push({ propertyName: "dealstage", operator: FilterOperatorEnum.Eq, value: dealStage, }); } if (dealOwner) { additionalFilters.push({ propertyName: "hubspot_owner_id", operator: FilterOperatorEnum.Eq, value: dealOwner, }); } filterGroups.push({ filters: additionalFilters }); } const searchRequest: PublicObjectSearchRequest = { filterGroups, sorts: ["createdate"], properties: ["dealname", "amount", "createdate", "closedate", "dealstage", "pipeline", "hubspot_owner_id"], limit: 100, // Maximum per request }; // Need to handle pagination for potentially large datasets let allDeals: any[] = []; let hasMore = true; let after = undefined; while (hasMore) { const response = await this.client.crm.deals.searchApi.doSearch({ ...searchRequest, after, }); allDeals = [...allDeals, ...response.results]; if (response.paging && response.paging.next) { after = response.paging.next.after; } else { hasMore = false; } } return allDeals; } private aggregateDealsByPeriod( deals: any[], period: "daily" | "weekly" | "monthly" | "quarterly" | "yearly", startDate: string, endDate?: string ): any { const endDateObj = endDate ? new Date(endDate) : new Date(); const startDateObj = new Date(startDate); // Group deals by period const dealsByPeriod: { [key: string]: any[] } = {}; deals.forEach((deal) => { const createDate = new Date(deal.properties.createdate); let periodKey; switch (period) { case "daily": periodKey = createDate.toISOString().split("T")[0]; break; case "weekly": // Get the start of the week (Sunday) const day = createDate.getUTCDay(); const diff = createDate.getUTCDate() - day; const weekStart = new Date(createDate); weekStart.setUTCDate(diff); periodKey = weekStart.toISOString().split("T")[0]; break; case "monthly": periodKey = `${createDate.getUTCFullYear()}-${(createDate.getUTCMonth() + 1).toString().padStart(2, "0")}`; break; case "quarterly": const quarter = Math.floor(createDate.getUTCMonth() / 3) + 1; periodKey = `${createDate.getUTCFullYear()}-Q${quarter}`; break; case "yearly": periodKey = createDate.getUTCFullYear().toString(); break; } if (!dealsByPeriod[periodKey]) { dealsByPeriod[periodKey] = []; } dealsByPeriod[periodKey].push(deal); }); // Aggregate data for each period const analytics = Object.keys(dealsByPeriod).map((periodKey) => { const periodDeals = dealsByPeriod[periodKey]; const totalDeals = periodDeals.length; let totalAmount = 0; let wonDeals = 0; let lostDeals = 0; let openDeals = 0; periodDeals.forEach((deal) => { // Sum up the deal amounts (if available) if (deal.properties.amount) { totalAmount += parseFloat(deal.properties.amount); } // Count deals by status (assuming standard deal stages) const stage = deal.properties.dealstage; if (stage && stage.includes("closedwon")) { wonDeals++; } else if (stage && stage.includes("closedlost")) { lostDeals++; } else { openDeals++; } }); return { period: periodKey, totalDeals, totalAmount, wonDeals, lostDeals, openDeals, winRate: totalDeals > 0 ? wonDeals / (wonDeals + lostDeals) : 0, averageDealSize: totalDeals > 0 ? totalAmount / totalDeals : 0, }; }); // Sort by period analytics.sort((a, b) => a.period.localeCompare(b.period)); return { startDate: startDateObj.toISOString().split("T")[0], endDate: endDateObj.toISOString().split("T")[0], period, analytics, }; } async getDealHistory(dealId: string): Promise<any> { try { return await this.client.crm.deals.basicApi.getById(dealId, ["hs_lastmodifieddate"]); } catch (error) { console.error("Error getting deal history:", error); throw error; } } async getDealNotes(dealId: string, limit: number = 20, after?: string): Promise<any> { try { // First, we need to check if the deal exists await this.client.crm.deals.basicApi.getById(dealId, ["dealname"]); // Use a more direct approach: get notes and filter by association const params: any = { limit, }; if (after) { params.after = after; } // Use search endpoint instead of associations const searchRequest: PublicObjectSearchRequest = { filterGroups: [ { filters: [ { propertyName: "hs_attachment_ids", operator: FilterOperatorEnum.ContainsToken, value: dealId, }, ], }, ], sorts: ["hs_createdate"], properties: ["hs_note_body", "hs_createdate", "hs_lastmodifieddate"], limit, }; const response = await this.client.crm.objects.notes.searchApi.doSearch(searchRequest); return { results: response.results || [], paging: response.paging, }; } catch (error) { console.error("Error getting deal notes:", error); throw error; } } async getEngagementsByDeal( dealId: string, types?: Array<"CALL" | "EMAIL" | "MEETING" | "TASK" | "NOTE">, limit: number = 20, after?: string ): Promise<any> { try { // First, we need to check if the deal exists await this.client.crm.deals.basicApi.getById(dealId, ["dealname"]); // Map engagement types to HubSpot object types const typeMap: { [key: string]: string } = { CALL: "calls", EMAIL: "emails", MEETING: "meetings", TASK: "tasks", NOTE: "notes", }; // If no types specified, get all types const engagementTypes = types && types.length > 0 ? types.map((type) => typeMap[type]) : Object.values(typeMap); // For each type, use the search API instead of associations const enrichedEngagements: any[] = []; for (const engagementType of engagementTypes) { // Search for engagements associated with this deal // This is a simplified approach - in a real implementation, you would use the proper association property const searchRequest: PublicObjectSearchRequest = { filterGroups: [ { filters: [ { propertyName: "hs_attachment_ids", operator: FilterOperatorEnum.ContainsToken, value: dealId, }, ], }, ], sorts: ["hs_createdate"], limit, properties: [ "hs_createdate", "hs_lastmodifieddate", // Add relevant properties based on engagement type ...(engagementType === "calls" ? ["hs_call_body", "hs_call_direction", "hs_call_disposition", "hs_call_duration"] : []), ...(engagementType === "emails" ? ["hs_email_subject", "hs_email_text", "hs_email_direction", "hs_email_status"] : []), ...(engagementType === "meetings" ? ["hs_meeting_title", "hs_meeting_body", "hs_meeting_start_time", "hs_meeting_end_time"] : []), ...(engagementType === "tasks" ? ["hs_task_body", "hs_task_priority", "hs_task_status", "hs_task_subject"] : []), ...(engagementType === "notes" ? ["hs_note_body"] : []), ], }; try { // The API path will differ based on engagement type let response: any; switch (engagementType) { case "calls": response = await this.client.crm.objects.calls.searchApi.doSearch(searchRequest); break; case "emails": response = await this.client.crm.objects.emails.searchApi.doSearch(searchRequest); break; case "meetings": response = await this.client.crm.objects.meetings.searchApi.doSearch(searchRequest); break; case "tasks": response = await this.client.crm.objects.tasks.searchApi.doSearch(searchRequest); break; case "notes": response = await this.client.crm.objects.notes.searchApi.doSearch(searchRequest); break; } if (response && response.results) { // Add the type to each engagement response.results.forEach((result: any) => { result.engagementType = engagementType; enrichedEngagements.push(result); }); } } catch (err) { console.error(`Error getting ${engagementType} for deal ${dealId}:`, err); } } // Sort by created date (newest first) enrichedEngagements.sort((a: any, b: any) => { const dateA = new Date(a.properties.hs_createdate || 0).getTime(); const dateB = new Date(b.properties.hs_createdate || 0).getTime(); return dateB - dateA; }); // Limit to the requested number const limitedEngagements = enrichedEngagements.slice(0, limit); return { results: limitedEngagements, totalCount: enrichedEngagements.length, }; } catch (error) { console.error("Error getting engagements for deal:", error); throw error; } } async getSalesPerformance( period: "daily" | "weekly" | "monthly" | "quarterly" | "yearly", startDate: string, endDate?: string, ownerIds?: string[], pipeline?: string ): Promise<any> { try { // Get all deals for the specified period const allDeals = await this.getAllDeals(startDate, endDate, pipeline); // Filter by owner IDs if provided const filteredDeals = ownerIds && ownerIds.length > 0 ? allDeals.filter((deal) => ownerIds.includes(deal.properties.hubspot_owner_id)) : allDeals; // Group deals by owner const dealsByOwner: { [key: string]: any[] } = {}; filteredDeals.forEach((deal) => { const ownerId = deal.properties.hubspot_owner_id; if (!ownerId) return; if (!dealsByOwner[ownerId]) { dealsByOwner[ownerId] = []; } dealsByOwner[ownerId].push(deal); }); // Get owner details for each owner with deals const ownerDetails: { [key: string]: any } = {}; for (const ownerId of Object.keys(dealsByOwner)) { try { // Cast the ID to number as required by the API const ownerResponse = await this.client.crm.owners.ownersApi.getById(parseInt(ownerId, 10)); ownerDetails[ownerId] = { id: ownerId, firstName: ownerResponse.firstName, lastName: ownerResponse.lastName, email: ownerResponse.email, }; } catch (error) { console.error(`Error getting owner details for ${ownerId}:`, error); ownerDetails[ownerId] = { id: ownerId }; } } // Generate performance metrics for each owner const ownerPerformance = Object.keys(dealsByOwner).map((ownerId) => { const ownerDeals = dealsByOwner[ownerId]; const performance = this.calculatePerformanceMetrics(ownerDeals, period); return { owner: ownerDetails[ownerId], ...performance, }; }); // Sort by most revenue ownerPerformance.sort((a, b) => b.totalRevenue - a.totalRevenue); return { startDate: new Date(startDate).toISOString().split("T")[0], endDate: endDate ? new Date(endDate).toISOString().split("T")[0] : new Date().toISOString().split("T")[0], period, performance: ownerPerformance, }; } catch (error) { console.error("Error getting sales performance:", error); throw error; } } private calculatePerformanceMetrics(deals: any[], period: string): any { // Group deals by status const closedWonDeals = deals.filter((deal) => { const stage = deal.properties.dealstage; return stage && stage.includes("closedwon"); }); const closedLostDeals = deals.filter((deal) => { const stage = deal.properties.dealstage; return stage && stage.includes("closedlost"); }); const openDeals = deals.filter((deal) => { const stage = deal.properties.dealstage; return !(stage && (stage.includes("closedwon") || stage.includes("closedlost"))); }); // Calculate metrics const totalDeals = deals.length; const totalWon = closedWonDeals.length; const totalLost = closedLostDeals.length; const totalOpen = openDeals.length; let totalRevenue = 0; let averageDealSize = 0; closedWonDeals.forEach((deal) => { if (deal.properties.amount) { totalRevenue += parseFloat(deal.properties.amount); } }); if (totalWon > 0) { averageDealSize = totalRevenue / totalWon; } const winRate = totalWon + totalLost > 0 ? totalWon / (totalWon + totalLost) : 0; return { totalDeals, totalWon, totalLost, totalOpen, totalRevenue, averageDealSize, winRate, }; } async getPipelineAnalytics( pipelineId: string, period: "daily" | "weekly" | "monthly" | "quarterly" | "yearly", startDate: string, endDate?: string ): Promise<any> { try { // Get the pipeline stages first const pipeline = await this.client.crm.pipelines.pipelinesApi.getById("deals", pipelineId); if (!pipeline || !pipeline.stages || pipeline.stages.length === 0) { throw new Error(`Pipeline ${pipelineId} not found or has no stages`); } // Get all deals in this pipeline const allDeals = await this.getAllDeals(startDate, endDate, pipelineId); // Create a mapping of stage IDs to stage data const stageMap: { [key: string]: any } = {}; pipeline.stages.forEach((stage) => { stageMap[stage.id] = { id: stage.id, label: stage.label, displayOrder: stage.displayOrder, totalDeals: 0, totalValue: 0, deals: [], conversionRate: 0, // Will calculate later }; }); // Group deals by stage allDeals.forEach((deal) => { const stageId = deal.properties.dealstage; if (stageMap[stageId]) { stageMap[stageId].totalDeals++; stageMap[stageId].deals.push(deal); if (deal.properties.amount) { stageMap[stageId].totalValue += parseFloat(deal.properties.amount); } } }); // Calculate conversion rates between stages const stages = Object.values(stageMap).sort((a: any, b: any) => a.displayOrder - b.displayOrder); for (let i = 0; i < stages.length - 1; i++) { const currentStage = stages[i] as any; const nextStage = stages[i + 1] as any; if (currentStage.totalDeals > 0) { nextStage.conversionRate = nextStage.totalDeals / currentStage.totalDeals; } } // Calculate time spent in each stage (average) stages.forEach((stage: any) => { let totalDays = 0; let dealsWithDates = 0; stage.deals.forEach((deal: any) => { // This is a simplification since HubSpot doesn't directly expose time in stage // In a real implementation, you'd track stage changes in the deal history const createDate = new Date(deal.properties.createdate); const closeDate = deal.properties.closedate ? new Date(deal.properties.closedate) : new Date(); if (createDate && closeDate) { const daysDiff = Math.ceil((closeDate.getTime() - createDate.getTime()) / (1000 * 60 * 60 * 24)); totalDays += daysDiff; dealsWithDates++; } }); stage.averageDaysInStage = dealsWithDates > 0 ? totalDays / dealsWithDates : 0; // Clean up the deals array to reduce response size delete stage.deals; }); return { pipelineId, pipelineName: pipeline.label, startDate: new Date(startDate).toISOString().split("T")[0], endDate: endDate ? new Date(endDate).toISOString().split("T")[0] : new Date().toISOString().split("T")[0], period, stages, }; } catch (error) { console.error("Error getting pipeline analytics:", error); throw error; } } async getForecastAnalytics(period: "monthly" | "quarterly" | "yearly", numberOfPeriods: number = 3, pipeline?: string): Promise<any> { try { // For forecasting, we need historical data to predict future performance // Using a simple approach based on recent performance and current pipeline // Define the end date as today const today = new Date(); let startDate; // For the historical period, look back 3x the forecast period switch (period) { case "monthly": startDate = new Date(today); startDate.setMonth(today.getMonth() - numberOfPeriods * 3); break; case "quarterly": startDate = new Date(today); startDate.setMonth(today.getMonth() - numberOfPeriods * 3 * 3); break; case "yearly": startDate = new Date(today); startDate.setFullYear(today.getFullYear() - numberOfPeriods * 3); break; } // Get historical deals to analyze const allDeals = await this.getAllDeals(startDate.toISOString(), today.toISOString(), pipeline); // Group historical deals by period const historicalData = this.aggregateDealsByPeriod(allDeals, period, startDate.toISOString(), today.toISOString()); // Get current pipeline (open deals) const openDeals = await this.getOpenDeals(pipeline); // Generate forecast const forecast = this.generateForecast(historicalData.analytics, openDeals, period, numberOfPeriods); return { period, numberOfPeriods, historicalData: historicalData.analytics, forecast, }; } catch (error) { console.error("Error generating forecast analytics:", error); throw error; } } private async getOpenDeals(pipeline?: string): Promise<any[]> { const filterGroups = []; // Filter for open deals (not closed won/lost) const stageFilters = []; stageFilters.push({ propertyName: "dealstage", operator: FilterOperatorEnum.NotHasProperty, value: "closed", }); filterGroups.push({ filters: stageFilters }); // Pipeline filter if specified if (pipeline) { filterGroups.push({ filters: [ { propertyName: "pipeline", operator: FilterOperatorEnum.Eq, value: pipeline, }, ], }); } const searchRequest: PublicObjectSearchRequest = { filterGroups, sorts: ["amount"], properties: ["dealname", "amount", "dealstage", "pipeline", "closedate", "hs_probability"], limit: 100, }; // Get all open deals with pagination let allDeals: any[] = []; let hasMore = true; let after = undefined; while (hasMore) { const response = await this.client.crm.deals.searchApi.doSearch({ ...searchRequest, after, }); allDeals = [...allDeals, ...response.results]; if (response.paging && response.paging.next) { after = response.paging.next.after; } else { hasMore = false; } } return allDeals; } private generateForecast( historicalData: any[], openDeals: any[], period: "monthly" | "quarterly" | "yearly", numberOfPeriods: number ): any[] { // Calculate average metrics from historical data let totalRevenue = 0; let totalDeals = 0; let totalWinRate = 0; historicalData.forEach((data) => { totalRevenue += data.totalAmount; totalDeals += data.totalDeals; if (data.winRate) totalWinRate += data.winRate; }); const avgRevenuePerPeriod = historicalData.length > 0 ? totalRevenue / historicalData.length : 0; const avgDealsPerPeriod = historicalData.length > 0 ? totalDeals / historicalData.length : 0; const avgWinRate = historicalData.length > 0 ? totalWinRate / historicalData.length : 0; // Analyze open deals to add to the forecast // Group them by expected close period const dealsByClosePeriod: { [key: string]: any[] } = {}; const today = new Date(); openDeals.forEach((deal) => { if (!deal.properties.closedate) return; const closeDate = new Date(deal.properties.closedate); let periodKey; switch (period) { case "monthly": periodKey = `${closeDate.getUTCFullYear()}-${(closeDate.getUTCMonth() + 1).toString().padStart(2, "0")}`; break; case "quarterly": const quarter = Math.floor(closeDate.getUTCMonth() / 3) + 1; periodKey = `${closeDate.getUTCFullYear()}-Q${quarter}`; break; case "yearly": periodKey = closeDate.getUTCFullYear().toString(); break; } if (!dealsByClosePeriod[periodKey]) { dealsByClosePeriod[periodKey] = []; } dealsByClosePeriod[periodKey].push(deal); }); // Generate forecast periods const forecast = []; let currentDate = new Date(); for (let i = 0; i < numberOfPeriods; i++) { let periodKey; let periodLabel; let nextDate; switch (period) { case "monthly": periodKey = `${currentDate.getUTCFullYear()}-${(currentDate.getUTCMonth() + 1).toString().padStart(2, "0")}`; periodLabel = `${currentDate.toLocaleString("default", { month: "long" })} ${currentDate.getUTCFullYear()}`; nextDate = new Date(currentDate); nextDate.setMonth(currentDate.getMonth() + 1); break; case "quarterly": const quarter = Math.floor(currentDate.getUTCMonth() / 3) + 1; periodKey = `${currentDate.getUTCFullYear()}-Q${quarter}`; periodLabel = `Q${quarter} ${currentDate.getUTCFullYear()}`; nextDate = new Date(currentDate); nextDate.setMonth(currentDate.getMonth() + 3); break; case "yearly": periodKey = currentDate.getUTCFullYear().toString(); periodLabel = periodKey; nextDate = new Date(currentDate); nextDate.setFullYear(currentDate.getFullYear() + 1); break; } // Calculate expected revenue from open deals for this period const periodDeals = dealsByClosePeriod[periodKey] || []; let expectedRevenueFromOpenDeals = 0; periodDeals.forEach((deal) => { if (deal.properties.amount) { const amount = parseFloat(deal.properties.amount); const probability = deal.properties.hs_probability ? parseFloat(deal.properties.hs_probability) / 100 : avgWinRate; expectedRevenueFromOpenDeals += amount * probability; } }); // Combine historical average with open pipeline const forecastedRevenue = avgRevenuePerPeriod * 0.7 + expectedRevenueFromOpenDeals * 0.3; const forecastedDeals = avgDealsPerPeriod * 0.7 + periodDeals.length * 0.3; forecast.push({ period: periodKey, label: periodLabel, forecastedRevenue, forecastedDeals: Math.round(forecastedDeals), openDealsCount: periodDeals.length, openDealsValue: periodDeals.reduce((sum, deal) => { return sum + (deal.properties.amount ? parseFloat(deal.properties.amount) : 0); }, 0), historicalAvgRevenue: avgRevenuePerPeriod, confidence: i === 0 ? "High" : i === 1 ? "Medium" : "Low", }); currentDate = nextDate; } return forecast; } } async function main() { const hubspotApiKey = process.env.HUBSPOT_API_KEY; if (!hubspotApiKey) { console.error("Please set HUBSPOT_API_KEY environment variable"); process.exit(1); } console.error("Starting HubSpot MCP Server..."); const server = new Server( { name: "HubSpot MCP Server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); const hubspotClient = new HubSpotClient(hubspotApiKey); server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => { console.error("Received CallToolRequest:", request); try { if (!request.params.arguments) { throw new Error("No arguments provided"); } switch (request.params.name) { case "hubspot_search_contacts": { const args = request.params.arguments as unknown as SearchContactsArgs; if (!args.query) { throw new Error("Missing required argument: query"); } const response = await hubspotClient.searchContacts(args.query, args.count, args.propertyList); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_contact": { const args = request.params.arguments as unknown as GetContactArgs; if (!args.contactId) { throw new Error("Missing required argument: contactId"); } const response = await hubspotClient.getContact(args.contactId, args.properties); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_create_contact": { const args = request.params.arguments as unknown as CreateContactArgs; if (!args.properties || !args.properties.email) { throw new Error("Missing required arguments: properties including email"); } const response = await hubspotClient.createContact(args.properties); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_update_contact": { const args = request.params.arguments as unknown as UpdateContactArgs; if (!args.contactId || !args.properties) { throw new Error("Missing required arguments: contactId and properties"); } const response = await hubspotClient.updateContact(args.contactId, args.properties); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_list_deals": { const args = request.params.arguments as unknown as ListDealsArgs; const response = await hubspotClient.listDeals(args.limit, args.after, args.properties); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_deal": { const args = request.params.arguments as unknown as GetDealArgs; if (!args.dealId) { throw new Error("Missing required argument: dealId"); } const response = await hubspotClient.getDeal(args.dealId, args.properties); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_create_deal": { const args = request.params.arguments as unknown as CreateDealArgs; if (!args.properties || !args.properties.dealname) { throw new Error("Missing required arguments: properties including dealname"); } const response = await hubspotClient.createDeal(args.properties); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_update_deal": { const args = request.params.arguments as unknown as UpdateDealArgs; if (!args.dealId || !args.properties) { throw new Error("Missing required arguments: dealId and properties"); } const response = await hubspotClient.updateDeal(args.dealId, args.properties); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_list_companies": { const args = request.params.arguments as unknown as ListCompaniesArgs; const response = await hubspotClient.listCompanies(args.limit, args.after, args.properties); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_company": { const args = request.params.arguments as unknown as GetCompanyArgs; if (!args.companyId) { throw new Error("Missing required argument: companyId"); } const response = await hubspotClient.getCompany(args.companyId, args.properties); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_sales_analytics": { const args = request.params.arguments as unknown as GetSalesAnalyticsArgs; if (!args.period || !args.startDate) { throw new Error("Missing required arguments: period and startDate"); } const response = await hubspotClient.getSalesAnalytics( args.period, args.startDate, args.endDate, args.pipeline, args.dealStage, args.dealOwner ); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_deal_history": { const args = request.params.arguments as unknown as GetDealHistoryArgs; if (!args.dealId) { throw new Error("Missing required argument: dealId"); } const response = await hubspotClient.getDealHistory(args.dealId); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_deal_notes": { const args = request.params.arguments as unknown as GetDealNotesArgs; if (!args.dealId) { throw new Error("Missing required argument: dealId"); } const response = await hubspotClient.getDealNotes(args.dealId, args.limit, args.after); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_engagements_by_deal": { const args = request.params.arguments as unknown as GetEngagementsByDealArgs; if (!args.dealId) { throw new Error("Missing required argument: dealId"); } const response = await hubspotClient.getEngagementsByDeal(args.dealId, args.types, args.limit, args.after); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_sales_performance": { const args = request.params.arguments as unknown as GetSalesPerformanceArgs; if (!args.period || !args.startDate) { throw new Error("Missing required arguments: period and startDate"); } const response = await hubspotClient.getSalesPerformance(args.period, args.startDate, args.endDate, args.ownerIds, args.pipeline); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_pipeline_analytics": { const args = request.params.arguments as unknown as GetPipelineAnalyticsArgs; if (!args.pipelineId || !args.period || !args.startDate) { throw new Error("Missing required arguments: pipelineId, period, and startDate"); } const response = await hubspotClient.getPipelineAnalytics(args.pipelineId, args.period, args.startDate, args.endDate); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } case "hubspot_get_forecast_analytics": { const args = request.params.arguments as unknown as GetForecastAnalyticsArgs; if (!args.period) { throw new Error("Missing required argument: period"); } const response = await hubspotClient.getForecastAnalytics(args.period, args.numberOfPeriods, args.pipeline); return { content: [{ type: "text", text: JSON.stringify(response) }], }; } default: throw new Error(`Unknown tool: ${request.params.name}`); } } catch (error) { console.error("Error executing tool:", error); return { content: [ { type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error), }), }, ], }; } }); server.setRequestHandler(ListToolsRequestSchema, async () => { console.error("Received ListToolsRequest"); return { tools: [ searchContactsTool, getContactTool, createContactTool, updateContactTool, listDealsTool, getDealTool, createDealTool, updateDealTool, listCompaniesTool, getCompanyTool, getSalesAnalyticsTool, getDealHistoryTool, getDealNotesTool, getEngagementsByDealTool, getSalesPerformanceTool, getPipelineAnalyticsTool, getForecastAnalyticsTool, ], }; }); const transport = new StdioServerTransport(); console.error("Connecting server to transport..."); await server.connect(transport); console.error("HubSpot MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });

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/scopiousdigital/hubspot-mcp'

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