Skip to main content
Glama
relay-client.ts13.6 kB
/** * Cloud Relay Client * * Handles communication with the PartnerCore Cloud MCP server. * Uses JSON-RPC protocol to communicate with /mcp endpoint. */ import axios, { AxiosInstance, AxiosError } from 'axios'; import { getLogger } from '../utils/logger.js'; /** * JSON-RPC request structure */ interface JsonRpcRequest { jsonrpc: '2.0'; method: string; params?: Record<string, unknown>; id: number | string; } /** * JSON-RPC response structure */ interface JsonRpcResponse<T = unknown> { jsonrpc: '2.0'; result?: T; error?: { code: number; message: string; data?: unknown; }; id: number | string | null; } /** * Tool call request */ export interface ToolCallRequest { name: string; arguments: Record<string, unknown>; } /** * Tool call response */ export interface ToolCallResponse { success: boolean; result?: unknown; error?: string; } /** * Available cloud tools */ export interface CloudToolDefinition { name: string; description: string; inputSchema: Record<string, unknown>; } /** * Cloud Relay Client configuration */ export interface CloudRelayConfig { cloudUrl: string; apiKey: string; } /** * Cloud Relay Client * * Communicates with PartnerCore Cloud MCP server using JSON-RPC protocol. */ export class CloudRelayClient { private client: AxiosInstance; private logger = getLogger(); private toolsCache: CloudToolDefinition[] | null = null; private requestId = 0; private initialized = false; constructor(config: CloudRelayConfig | string, apiKey?: string) { // Support both old (cloudUrl, apiKey) and new (config object) signatures const cloudUrl = typeof config === 'string' ? config : config.cloudUrl; const key = typeof config === 'string' ? (apiKey ?? '') : config.apiKey; const headers: Record<string, string> = { 'Content-Type': 'application/json', 'X-API-Key': key, // PartnerCore uses X-API-Key header 'X-Client-Type': 'partnercore-proxy', }; this.client = axios.create({ baseURL: cloudUrl, timeout: 60000, // 60 second timeout for long operations headers, }); // Add request interceptor for logging this.client.interceptors.request.use( (reqConfig) => { this.logger.debug(`Cloud request: ${reqConfig.method?.toUpperCase()} ${reqConfig.url}`); return reqConfig; }, (error) => { this.logger.error('Cloud request error:', error); return Promise.reject(error); } ); // Add response interceptor for error handling this.client.interceptors.response.use( (response) => { return response; }, (error: AxiosError) => { if (error.response) { this.logger.error(`Cloud response error: ${error.response.status} ${error.response.statusText}`); } else if (error.request) { this.logger.error('Cloud request failed: No response received'); } else { this.logger.error('Cloud request failed:', error.message); } return Promise.reject(error); } ); } /** * Generate next request ID */ private nextId(): number { return ++this.requestId; } /** * Send JSON-RPC request to /mcp endpoint */ private async rpcCall<T>(method: string, params?: Record<string, unknown>): Promise<JsonRpcResponse<T>> { const request: JsonRpcRequest = { jsonrpc: '2.0', method, params, id: this.nextId(), }; try { const response = await this.client.post<JsonRpcResponse<T>>('/mcp', request); return response.data; } catch (error) { const axiosError = error as AxiosError; throw new Error(axiosError.message || 'RPC call failed'); } } /** * Initialize connection with cloud MCP server */ async initialize(): Promise<boolean> { if (this.initialized) { return true; } try { const response = await this.rpcCall<{ capabilities: Record<string, unknown> }>('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'partnercore-proxy', version: '0.1.2', }, }); if (response.error) { this.logger.error(`Initialize failed: ${response.error.message}`); return false; } this.initialized = true; this.logger.info('Cloud MCP initialized successfully'); return true; } catch (error) { this.logger.error('Failed to initialize cloud MCP:', error); return false; } } /** * Check if connection to cloud is available */ async checkConnection(): Promise<boolean> { try { // Use /health endpoint for connection check await this.client.get('/health'); this.logger.info('Cloud MCP connection verified'); return true; } catch { this.logger.warn('Cloud MCP connection unavailable'); return false; } } /** * Validate API key by trying to list tools */ async validateApiKey(): Promise<boolean> { try { // Initialize first if (!this.initialized) { const initOk = await this.initialize(); if (!initOk) { return false; } } // Try to list tools - this will fail if API key is invalid const response = await this.rpcCall<{ tools: CloudToolDefinition[] }>('tools/list'); if (response.error) { this.logger.warn(`API key validation failed: ${response.error.message}`); return false; } this.logger.info('API key validated successfully'); return true; } catch { this.logger.warn('Cloud connection available but API key invalid'); return false; } } /** * Get available tools from cloud server */ async getTools(): Promise<CloudToolDefinition[]> { if (this.toolsCache) { return this.toolsCache; } try { // Ensure initialized if (!this.initialized) { await this.initialize(); } const response = await this.rpcCall<{ tools: CloudToolDefinition[] }>('tools/list'); if (response.error) { this.logger.error(`Get tools failed: ${response.error.message}`); return []; } this.toolsCache = response.result?.tools || []; this.logger.info(`Loaded ${this.toolsCache.length} cloud tools`); return this.toolsCache; } catch (error) { this.logger.error('Failed to get cloud tools:', error); return []; } } /** * Call a tool on the cloud server */ async callTool(request: ToolCallRequest): Promise<ToolCallResponse> { this.logger.debug(`Calling cloud tool: ${request.name}`); try { // Ensure initialized if (!this.initialized) { await this.initialize(); } const response = await this.rpcCall<{ content: Array<{ type: string; text: string }> }>('tools/call', { name: request.name, arguments: request.arguments, }); if (response.error) { return { success: false, error: response.error.message, }; } // Extract text content from MCP response const content = response.result?.content; if (content && Array.isArray(content) && content.length > 0) { const textContent = content.find(c => c.type === 'text'); if (textContent) { return { success: true, result: textContent.text, }; } } return { success: true, result: response.result, }; } catch (error) { const axiosError = error as AxiosError<{ error?: string }>; return { success: false, error: axiosError.response?.data?.error || axiosError.message || 'Unknown error', }; } } /** * Search knowledge base using al-assistant learn action */ async searchKnowledgeBase(query: string, filters?: Record<string, unknown>): Promise<unknown[]> { try { const response = await this.callTool({ name: 'al-assistant', arguments: { action: 'learn', params: { query, ...filters, }, }, }); if (!response.success) { this.logger.error('KB search failed:', response.error); return []; } // Try to parse as JSON if it's a string if (typeof response.result === 'string') { try { const parsed = JSON.parse(response.result) as unknown; if (Array.isArray(parsed)) { return parsed as unknown[]; } return [parsed]; } catch { return [{ content: response.result }]; } } const result: unknown = response.result; if (Array.isArray(result)) { return result as unknown[]; } return [result]; } catch (error) { this.logger.error('KB search failed:', error); return []; } } /** * Get templates using al-assistant list-templates action */ async getTemplates(type?: string): Promise<unknown[]> { try { const response = await this.callTool({ name: 'al-assistant', arguments: { action: 'list-templates', params: type ? { category: type } : {}, }, }); if (!response.success) { this.logger.error('Get templates failed:', response.error); return []; } // Try to parse as JSON if it's a string if (typeof response.result === 'string') { try { const parsed = JSON.parse(response.result) as unknown; if (Array.isArray(parsed)) { return parsed as unknown[]; } return [parsed]; } catch { return [{ content: response.result }]; } } const result: unknown = response.result; if (Array.isArray(result)) { return result as unknown[]; } return [result]; } catch (error) { this.logger.error('Get templates failed:', error); return []; } } /** * Validate subscription using al-assistant status action */ async validateSubscription(): Promise<{ valid: boolean; tier?: string; features?: string[] }> { try { const response = await this.callTool({ name: 'al-assistant', arguments: { action: 'status', params: {}, }, }); if (!response.success) { return { valid: false }; } return { valid: true, tier: 'standard', features: ['learn', 'generate', 'process', 'review'], }; } catch { return { valid: false }; } } /** * Get available prompts from cloud MCP */ async getPrompts(): Promise<Array<{ name: string; description: string }>> { try { const response = await this.callTool({ name: 'al-assistant', arguments: { action: 'get-prompt', params: {}, // Empty params returns list of all prompts }, }); if (!response.success) { this.logger.debug('Get prompts failed:', response.error); return this.getDefaultPrompts(); } // Try to parse the result if (typeof response.result === 'string') { try { const parsed = JSON.parse(response.result) as unknown; if (Array.isArray(parsed)) { return parsed as Array<{ name: string; description: string }>; } // If it's a single prompt list object if (parsed && typeof parsed === 'object' && 'prompts' in parsed) { return (parsed as { prompts: Array<{ name: string; description: string }> }).prompts; } } catch { // Return default prompts if parsing fails } } return this.getDefaultPrompts(); } catch (error) { this.logger.debug('Get prompts error:', error); return this.getDefaultPrompts(); } } /** * Get default prompts when cloud is unavailable */ private getDefaultPrompts(): Array<{ name: string; description: string }> { return [ { name: 'al-create-table', description: 'Create a new AL table with best practices' }, { name: 'al-create-page', description: 'Create a new AL page (Card, List, API, etc.)' }, { name: 'al-create-codeunit', description: 'Create a new AL codeunit' }, { name: 'al-create-enum', description: 'Create a new AL enum' }, { name: 'al-create-report', description: 'Create a new AL report' }, { name: 'al-create-tableextension', description: 'Create a table extension' }, { name: 'al-create-pageextension', description: 'Create a page extension' }, { name: 'al-code-review', description: 'Review AL code for AppSource compliance' }, { name: 'al-generate-tests', description: 'Generate test codeunits' }, { name: 'al-plan-feature', description: 'Plan a new feature with phases' }, ]; } /** * Get server status including cloud connection info */ async getStatus(): Promise<{ connected: boolean; initialized: boolean; toolCount: number; promptCount: number; }> { const connected = await this.checkConnection(); const tools = await this.getTools(); const prompts = await this.getPrompts(); return { connected, initialized: this.initialized, toolCount: tools.length, promptCount: prompts.length, }; } /** * Clear tools cache (call when reconnecting) */ clearCache(): void { this.toolsCache = null; this.initialized = false; } }

Latest Blog Posts

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/ciellosinc/partnercore-proxy'

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