- Build complete Next.js CRM for commercial real estate - Add authentication with JWT sessions and role-based access - Add GoHighLevel API integration for contacts, conversations, opportunities - Add AI-powered Control Center with tool calling - Add Setup page with onboarding checklist (/setup) - Add sidebar navigation with Setup menu item - Fix type errors in onboarding API, GHL services, and control center tools - Add Prisma schema with SQLite for local development - Add UI components with clay morphism design system Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
215 lines
5.4 KiB
TypeScript
215 lines
5.4 KiB
TypeScript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
import { ToolDefinition, ToolResult } from './types';
|
|
|
|
/**
|
|
* MCP Client for connecting to the GoHighLevel MCP Server.
|
|
* Uses the official MCP SDK with SSE transport.
|
|
*/
|
|
export class MCPClient {
|
|
private serverUrl: string;
|
|
private client: Client | null = null;
|
|
private transport: SSEClientTransport | null = null;
|
|
private connected: boolean = false;
|
|
private toolsCache: ToolDefinition[] | null = null;
|
|
|
|
constructor(serverUrl: string) {
|
|
this.serverUrl = serverUrl;
|
|
}
|
|
|
|
/**
|
|
* Connect to the MCP server
|
|
*/
|
|
async connect(): Promise<boolean> {
|
|
if (this.connected && this.client) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
// Create SSE transport
|
|
this.transport = new SSEClientTransport(
|
|
new URL(`${this.serverUrl}/sse`)
|
|
);
|
|
|
|
// Create MCP client
|
|
this.client = new Client(
|
|
{ name: 'cresync-control-center', version: '1.0.0' },
|
|
{ capabilities: {} }
|
|
);
|
|
|
|
// Connect
|
|
await this.client.connect(this.transport);
|
|
this.connected = true;
|
|
console.log('[MCP Client] Connected to server');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('[MCP Client] Connection failed:', error);
|
|
this.connected = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disconnect from the MCP server
|
|
*/
|
|
async disconnect(): Promise<void> {
|
|
if (this.client) {
|
|
await this.client.close();
|
|
this.client = null;
|
|
this.transport = null;
|
|
this.connected = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Health check - try to connect and list tools
|
|
*/
|
|
async healthCheck(): Promise<boolean> {
|
|
try {
|
|
// Simple HTTP check to /health endpoint
|
|
const response = await fetch(`${this.serverUrl}/health`);
|
|
return response.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get available tools from MCP server
|
|
* Uses REST endpoint for listing (faster than SSE for just getting list)
|
|
*/
|
|
async getTools(): Promise<ToolDefinition[]> {
|
|
if (this.toolsCache) {
|
|
return this.toolsCache;
|
|
}
|
|
|
|
try {
|
|
// Use REST endpoint for tool listing
|
|
const response = await fetch(`${this.serverUrl}/tools`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch tools: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const tools = (data.tools || []).map((tool: any) => ({
|
|
name: tool.name,
|
|
description: tool.description || '',
|
|
inputSchema: tool.inputSchema || { type: 'object', properties: {} }
|
|
}));
|
|
this.toolsCache = tools;
|
|
|
|
console.log(`[MCP Client] Loaded ${tools.length} tools`);
|
|
return tools;
|
|
} catch (error) {
|
|
console.error('[MCP Client] Failed to fetch tools:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute a tool via MCP protocol
|
|
*/
|
|
async executeTool(toolName: string, input: Record<string, unknown>): Promise<ToolResult> {
|
|
try {
|
|
// Ensure connected
|
|
if (!this.connected) {
|
|
const connected = await this.connect();
|
|
if (!connected) {
|
|
return {
|
|
toolCallId: '',
|
|
success: false,
|
|
error: 'Failed to connect to MCP server'
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!this.client) {
|
|
return {
|
|
toolCallId: '',
|
|
success: false,
|
|
error: 'MCP client not initialized'
|
|
};
|
|
}
|
|
|
|
// Call tool via MCP protocol
|
|
const result = await this.client.callTool({
|
|
name: toolName,
|
|
arguments: input
|
|
});
|
|
|
|
// Parse result - MCP returns content array
|
|
let resultData: unknown;
|
|
if (result.content && Array.isArray(result.content)) {
|
|
// Extract text content
|
|
const textContent = result.content.find((c: any) => c.type === 'text');
|
|
if (textContent && 'text' in textContent) {
|
|
try {
|
|
resultData = JSON.parse(textContent.text);
|
|
} catch {
|
|
resultData = textContent.text;
|
|
}
|
|
} else {
|
|
resultData = result.content;
|
|
}
|
|
} else {
|
|
resultData = result;
|
|
}
|
|
|
|
return {
|
|
toolCallId: '',
|
|
success: !result.isError,
|
|
result: resultData,
|
|
error: result.isError ? String(resultData) : undefined
|
|
};
|
|
} catch (error) {
|
|
console.error(`[MCP Client] Tool execution failed for ${toolName}:`, error);
|
|
return {
|
|
toolCallId: '',
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Tool execution failed'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear tool cache
|
|
*/
|
|
clearCache(): void {
|
|
this.toolsCache = null;
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let mcpClientInstance: MCPClient | null = null;
|
|
|
|
/**
|
|
* Get or create MCP client instance
|
|
*/
|
|
export async function createMCPClient(): Promise<MCPClient | null> {
|
|
if (mcpClientInstance) {
|
|
return mcpClientInstance;
|
|
}
|
|
|
|
// Import settings service
|
|
const { settingsService } = await import('@/lib/settings/settings-service');
|
|
const serverUrl = await settingsService.get('mcpServerUrl');
|
|
|
|
if (!serverUrl) {
|
|
console.warn('[MCP Client] Server URL not configured');
|
|
return null;
|
|
}
|
|
|
|
mcpClientInstance = new MCPClient(serverUrl);
|
|
return mcpClientInstance;
|
|
}
|
|
|
|
/**
|
|
* Close and reset MCP client
|
|
*/
|
|
export async function closeMCPClient(): Promise<void> {
|
|
if (mcpClientInstance) {
|
|
await mcpClientInstance.disconnect();
|
|
mcpClientInstance = null;
|
|
}
|
|
}
|