/**
* MCP Opportunity Tools for GoHighLevel Integration
* Exposes opportunity management capabilities to Claude Desktop
*/
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { GHLApiClient } from '../clients/ghl-api-client.js';
import {
MCPSearchOpportunitiesParams,
MCPCreateOpportunityParams,
MCPUpdateOpportunityParams,
MCPUpsertOpportunityParams,
MCPAddOpportunityFollowersParams,
MCPRemoveOpportunityFollowersParams,
GHLOpportunity,
GHLSearchOpportunitiesResponse,
GHLGetPipelinesResponse,
GHLUpsertOpportunityResponse
} from '../types/ghl-types.js';
/**
* Opportunity Tools Class
* Implements MCP tools for opportunity management
*/
export class OpportunityTools {
constructor(private ghlClient: GHLApiClient) {}
/**
* Get all opportunity tool definitions for MCP server
*/
getToolDefinitions(): Tool[] {
return [
{
name: 'search_opportunities',
description: 'Search for opportunities in GoHighLevel CRM using various filters like pipeline, stage, contact, status, etc.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'General search query (searches name, contact info)'
},
pipelineId: {
type: 'string',
description: 'Filter by specific pipeline ID'
},
pipelineStageId: {
type: 'string',
description: 'Filter by specific pipeline stage ID'
},
contactId: {
type: 'string',
description: 'Filter by specific contact ID'
},
status: {
type: 'string',
description: 'Filter by opportunity status',
enum: ['open', 'won', 'lost', 'abandoned', 'all']
},
assignedTo: {
type: 'string',
description: 'Filter by assigned user ID'
},
limit: {
type: 'number',
description: 'Maximum number of opportunities to return (default: 20, max: 100)',
minimum: 1,
maximum: 100,
default: 20
}
}
}
},
{
name: 'get_pipelines',
description: 'Get all sales pipelines configured in GoHighLevel',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_opportunity',
description: 'Get detailed information about a specific opportunity by ID',
inputSchema: {
type: 'object',
properties: {
opportunityId: {
type: 'string',
description: 'The unique ID of the opportunity to retrieve'
}
},
required: ['opportunityId']
}
},
{
name: 'create_opportunity',
description: 'Create a new opportunity in GoHighLevel CRM',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name/title of the opportunity'
},
pipelineId: {
type: 'string',
description: 'ID of the pipeline this opportunity belongs to'
},
contactId: {
type: 'string',
description: 'ID of the contact associated with this opportunity'
},
status: {
type: 'string',
description: 'Initial status of the opportunity (default: open)',
enum: ['open', 'won', 'lost', 'abandoned'],
default: 'open'
},
monetaryValue: {
type: 'number',
description: 'Monetary value of the opportunity in dollars'
},
assignedTo: {
type: 'string',
description: 'User ID to assign this opportunity to'
}
},
required: ['name', 'pipelineId', 'contactId']
}
},
{
name: 'update_opportunity_status',
description: 'Update the status of an opportunity (won, lost, etc.)',
inputSchema: {
type: 'object',
properties: {
opportunityId: {
type: 'string',
description: 'The unique ID of the opportunity'
},
status: {
type: 'string',
description: 'New status for the opportunity',
enum: ['open', 'won', 'lost', 'abandoned']
}
},
required: ['opportunityId', 'status']
}
},
{
name: 'delete_opportunity',
description: 'Delete an opportunity from GoHighLevel CRM',
inputSchema: {
type: 'object',
properties: {
opportunityId: {
type: 'string',
description: 'The unique ID of the opportunity to delete'
}
},
required: ['opportunityId']
}
},
{
name: 'update_opportunity',
description: 'Update an existing opportunity with new details (full update)',
inputSchema: {
type: 'object',
properties: {
opportunityId: {
type: 'string',
description: 'The unique ID of the opportunity to update'
},
name: {
type: 'string',
description: 'Updated name/title of the opportunity'
},
pipelineId: {
type: 'string',
description: 'Updated pipeline ID'
},
pipelineStageId: {
type: 'string',
description: 'Updated pipeline stage ID'
},
status: {
type: 'string',
description: 'Updated status of the opportunity',
enum: ['open', 'won', 'lost', 'abandoned']
},
monetaryValue: {
type: 'number',
description: 'Updated monetary value in dollars'
},
assignedTo: {
type: 'string',
description: 'Updated assigned user ID'
}
},
required: ['opportunityId']
}
},
{
name: 'upsert_opportunity',
description: 'Create or update an opportunity based on contact and pipeline (smart merge)',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name/title of the opportunity'
},
pipelineId: {
type: 'string',
description: 'ID of the pipeline this opportunity belongs to'
},
contactId: {
type: 'string',
description: 'ID of the contact associated with this opportunity'
},
status: {
type: 'string',
description: 'Status of the opportunity',
enum: ['open', 'won', 'lost', 'abandoned'],
default: 'open'
},
pipelineStageId: {
type: 'string',
description: 'Pipeline stage ID'
},
monetaryValue: {
type: 'number',
description: 'Monetary value of the opportunity in dollars'
},
assignedTo: {
type: 'string',
description: 'User ID to assign this opportunity to'
}
},
required: ['pipelineId', 'contactId']
}
},
{
name: 'add_opportunity_followers',
description: 'Add followers to an opportunity for notifications and tracking',
inputSchema: {
type: 'object',
properties: {
opportunityId: {
type: 'string',
description: 'The unique ID of the opportunity'
},
followers: {
type: 'array',
items: { type: 'string' },
description: 'Array of user IDs to add as followers'
}
},
required: ['opportunityId', 'followers']
}
},
{
name: 'remove_opportunity_followers',
description: 'Remove followers from an opportunity',
inputSchema: {
type: 'object',
properties: {
opportunityId: {
type: 'string',
description: 'The unique ID of the opportunity'
},
followers: {
type: 'array',
items: { type: 'string' },
description: 'Array of user IDs to remove as followers'
}
},
required: ['opportunityId', 'followers']
}
}
];
}
/**
* Execute opportunity tool based on tool name and arguments
*/
async executeTool(name: string, args: any): Promise<any> {
switch (name) {
case 'search_opportunities':
return this.searchOpportunities(args as MCPSearchOpportunitiesParams);
case 'get_pipelines':
return this.getPipelines();
case 'get_opportunity':
return this.getOpportunity(args.opportunityId);
case 'create_opportunity':
return this.createOpportunity(args as MCPCreateOpportunityParams);
case 'update_opportunity_status':
return this.updateOpportunityStatus(args.opportunityId, args.status);
case 'delete_opportunity':
return this.deleteOpportunity(args.opportunityId);
case 'update_opportunity':
return this.updateOpportunity(args as MCPUpdateOpportunityParams);
case 'upsert_opportunity':
return this.upsertOpportunity(args as MCPUpsertOpportunityParams);
case 'add_opportunity_followers':
return this.addOpportunityFollowers(args as MCPAddOpportunityFollowersParams);
case 'remove_opportunity_followers':
return this.removeOpportunityFollowers(args as MCPRemoveOpportunityFollowersParams);
default:
throw new Error(`Unknown opportunity tool: ${name}`);
}
}
/**
* SEARCH OPPORTUNITIES
*/
private async searchOpportunities(params: MCPSearchOpportunitiesParams): Promise<{ success: boolean; opportunities: GHLOpportunity[]; meta: any; message: string }> {
try {
// Build search parameters with correct API naming (underscores)
const searchParams: any = {
location_id: this.ghlClient.getConfig().locationId,
limit: params.limit || 20
};
// Only add parameters if they have values
if (params.query && params.query.trim()) {
searchParams.q = params.query.trim();
}
if (params.pipelineId) {
searchParams.pipeline_id = params.pipelineId;
}
if (params.pipelineStageId) {
searchParams.pipeline_stage_id = params.pipelineStageId;
}
if (params.contactId) {
searchParams.contact_id = params.contactId;
}
if (params.status) {
searchParams.status = params.status;
}
if (params.assignedTo) {
searchParams.assigned_to = params.assignedTo;
}
process.stderr.write(`[GHL MCP] Calling searchOpportunities with params: ${JSON.stringify(searchParams, null, 2)}\n`);
const response = await this.ghlClient.searchOpportunities(searchParams);
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
const data = response.data as GHLSearchOpportunitiesResponse;
const opportunities = Array.isArray(data.opportunities) ? data.opportunities : [];
return {
success: true,
opportunities,
meta: data.meta,
message: `Found ${opportunities.length} opportunities (${data.meta?.total || opportunities.length} total)`
};
} catch (error) {
process.stderr.write(`[GHL MCP] Search opportunities error: ${JSON.stringify(error, null, 2)}\n`);
throw new Error(`Failed to search opportunities: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* GET PIPELINES
*/
private async getPipelines(): Promise<{ success: boolean; pipelines: any[]; message: string }> {
try {
const response = await this.ghlClient.getPipelines();
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
const data = response.data as GHLGetPipelinesResponse;
const pipelines = Array.isArray(data.pipelines) ? data.pipelines : [];
return {
success: true,
pipelines,
message: `Retrieved ${pipelines.length} pipelines`
};
} catch (error) {
throw new Error(`Failed to get pipelines: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* GET OPPORTUNITY BY ID
*/
private async getOpportunity(opportunityId: string): Promise<{ success: boolean; opportunity: GHLOpportunity; message: string }> {
try {
const response = await this.ghlClient.getOpportunity(opportunityId);
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
return {
success: true,
opportunity: response.data,
message: 'Opportunity retrieved successfully'
};
} catch (error) {
throw new Error(`Failed to get opportunity: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* CREATE OPPORTUNITY
*/
private async createOpportunity(params: MCPCreateOpportunityParams): Promise<{ success: boolean; opportunity: GHLOpportunity; message: string }> {
try {
const opportunityData = {
locationId: this.ghlClient.getConfig().locationId,
name: params.name,
pipelineId: params.pipelineId,
contactId: params.contactId,
status: params.status || 'open' as const,
pipelineStageId: params.pipelineStageId,
monetaryValue: params.monetaryValue,
assignedTo: params.assignedTo,
customFields: params.customFields
};
const response = await this.ghlClient.createOpportunity(opportunityData);
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
return {
success: true,
opportunity: response.data,
message: `Opportunity created successfully with ID: ${response.data.id}`
};
} catch (error) {
throw new Error(`Failed to create opportunity: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* UPDATE OPPORTUNITY STATUS
*/
private async updateOpportunityStatus(opportunityId: string, status: string): Promise<{ success: boolean; message: string }> {
try {
const response = await this.ghlClient.updateOpportunityStatus(opportunityId, status as any);
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
return {
success: true,
message: `Opportunity status updated to ${status}`
};
} catch (error) {
throw new Error(`Failed to update opportunity status: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* DELETE OPPORTUNITY
*/
private async deleteOpportunity(opportunityId: string): Promise<{ success: boolean; message: string }> {
try {
const response = await this.ghlClient.deleteOpportunity(opportunityId);
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
return {
success: true,
message: 'Opportunity deleted successfully'
};
} catch (error) {
throw new Error(`Failed to delete opportunity: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* UPDATE OPPORTUNITY (FULL UPDATE)
*/
private async updateOpportunity(params: MCPUpdateOpportunityParams): Promise<{ success: boolean; opportunity: GHLOpportunity; message: string }> {
try {
const updateData: any = {};
// Only include fields that are provided
if (params.name) updateData.name = params.name;
if (params.pipelineId) updateData.pipelineId = params.pipelineId;
if (params.pipelineStageId) updateData.pipelineStageId = params.pipelineStageId;
if (params.status) updateData.status = params.status;
if (params.monetaryValue !== undefined) updateData.monetaryValue = params.monetaryValue;
if (params.assignedTo) updateData.assignedTo = params.assignedTo;
const response = await this.ghlClient.updateOpportunity(params.opportunityId, updateData);
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
return {
success: true,
opportunity: response.data,
message: `Opportunity updated successfully`
};
} catch (error) {
throw new Error(`Failed to update opportunity: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* UPSERT OPPORTUNITY
*/
private async upsertOpportunity(params: MCPUpsertOpportunityParams): Promise<{ success: boolean; opportunity: GHLOpportunity; isNew: boolean; message: string }> {
try {
const upsertData = {
locationId: this.ghlClient.getConfig().locationId,
pipelineId: params.pipelineId,
contactId: params.contactId,
name: params.name,
status: params.status || 'open' as const,
pipelineStageId: params.pipelineStageId,
monetaryValue: params.monetaryValue,
assignedTo: params.assignedTo
};
const response = await this.ghlClient.upsertOpportunity(upsertData);
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
const data = response.data as GHLUpsertOpportunityResponse;
return {
success: true,
opportunity: data.opportunity,
isNew: data.new,
message: `Opportunity ${data.new ? 'created' : 'updated'} successfully`
};
} catch (error) {
throw new Error(`Failed to upsert opportunity: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* ADD OPPORTUNITY FOLLOWERS
*/
private async addOpportunityFollowers(params: MCPAddOpportunityFollowersParams): Promise<{ success: boolean; followers: string[]; followersAdded: string[]; message: string }> {
try {
const response = await this.ghlClient.addOpportunityFollowers(params.opportunityId, params.followers);
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
return {
success: true,
followers: response.data.followers || [],
followersAdded: response.data.followersAdded || [],
message: `Added ${response.data.followersAdded?.length || 0} followers to opportunity`
};
} catch (error) {
throw new Error(`Failed to add opportunity followers: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* REMOVE OPPORTUNITY FOLLOWERS
*/
private async removeOpportunityFollowers(params: MCPRemoveOpportunityFollowersParams): Promise<{ success: boolean; followers: string[]; followersRemoved: string[]; message: string }> {
try {
const response = await this.ghlClient.removeOpportunityFollowers(params.opportunityId, params.followers);
if (!response.success || !response.data) {
const errorMsg = response.error?.message || 'Unknown API error';
throw new Error(`API request failed: ${errorMsg}`);
}
return {
success: true,
followers: response.data.followers || [],
followersRemoved: response.data.followersRemoved || [],
message: `Removed ${response.data.followersRemoved?.length || 0} followers from opportunity`
};
} catch (error) {
throw new Error(`Failed to remove opportunity followers: ${error instanceof Error ? error.message : String(error)}`);
}
}
}