Skip to main content
Glama
client.ts10.1 kB
import { Anthropic } from "@anthropic-ai/sdk"; import { MessageParam, Tool } from "@anthropic-ai/sdk/resources/messages/messages.mjs"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { PhonePiConfig, MCPTool, MCPToolResult, AIMessage } from "./types.js"; import chalk from "chalk"; import { spawn } from "child_process"; export class PhonePiClient { private mcp: Client; private anthropic: Anthropic; private transport: StdioClientTransport | null = null; private tools: Tool[] = []; private verbose: boolean; private messages: AIMessage[] = []; private readonly MAX_MESSAGE_HISTORY = 10; private readonly MODEL_VERSION = "claude-3-7-sonnet-20250219"; constructor(config: PhonePiConfig, verbose = false) { if (!config.anthropicApiKey) { throw new Error("ANTHROPIC_API_KEY is required"); } // Validate API key format (basic check) if (!config.anthropicApiKey.startsWith('sk-ant-') && !config.anthropicApiKey.startsWith('sk-')) { throw new Error("Invalid Anthropic API key format. API keys should start with 'sk-ant-' or 'sk-'"); } // Remove any whitespace or quotes that might have been accidentally included const cleanApiKey = config.anthropicApiKey.trim().replace(/^["']|["']$/g, ''); this.anthropic = new Anthropic({ apiKey: cleanApiKey, }); this.mcp = new Client({ name: "phonepi-cli", version: "1.0.0" }); this.verbose = verbose; } private log(message: string) { if (this.verbose) { console.log(chalk.gray(`[DEBUG] ${message}`)); } } async connectToServer(retries = 3): Promise<boolean> { // Suppress WebSocket logs before creating connection // Redirect the server's stdout and stderr to a file to hide WebSocket logs let serverProcess: ReturnType<typeof spawn> | null = null; for (let attempt = 1; attempt <= retries; attempt++) { try { // Cleanup any existing client connections await this.cleanup(); this.log(`Connecting to MCP server (attempt ${attempt}/${retries})...`); if (!this.verbose) { // Start server in a separate process and ignore its output this.log("Starting server in silent mode..."); serverProcess = spawn( process.platform === 'win32' ? 'npx.cmd' : 'npx', ['phonepi-mcp', 'start'], { stdio: 'ignore', detached: true } ); // Give the server a moment to start await new Promise(resolve => setTimeout(resolve, 2000)); } // Create transport to connect to the server this.transport = new StdioClientTransport({ command: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['phonepi-mcp', 'start'], env: { ...process.env, NODE_ENV: 'production', PHONEPI_VERBOSE: this.verbose ? 'true' : 'false' } }); // Connect with timeout const connectPromise = this.mcp.connect(this.transport); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Connection timeout")), 10000) ); await Promise.race([connectPromise, timeoutPromise]); // Get available tools const toolsResult = await this.mcp.listTools(); this.tools = toolsResult.tools.map((tool: any) => ({ name: tool.name, description: tool.description || "", input_schema: tool.inputSchema, })) as Tool[]; if (this.tools.length === 0) { throw new Error("No tools available from server"); } // We don't need the separate server process anymore if (serverProcess) { serverProcess.unref(); serverProcess = null; } this.log(`Connected to server with ${this.tools.length} tools available`); return true; } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); if (errorMessage.includes("Connection closed") || errorMessage.includes("Cannot find module") || errorMessage.includes("ENOENT") || errorMessage.includes("not found")) { console.error(chalk.red("\nError: Could not start phonepi-mcp server")); console.error(chalk.yellow("Please ensure it's installed:")); console.error(chalk.blue("\nnpm install -g phonepi-mcp")); process.exit(1); } console.error(chalk.yellow(`Connection attempt ${attempt}/${retries} failed:`), e); // On last retry, throw the error if (attempt === retries) { console.error(chalk.red("Failed to connect to MCP server after all retries")); await this.cleanup(); throw e; } // Progressive backoff between retries const retryDelay = Math.min(2000 * attempt, 10000); await new Promise(resolve => setTimeout(resolve, retryDelay)); continue; } } return false; } private pruneMessageHistory() { if (this.messages.length > this.MAX_MESSAGE_HISTORY * 2) { // Keep the first message (system context) and last MAX_MESSAGE_HISTORY messages this.messages = [ ...this.messages.slice(0, 1), ...this.messages.slice(-this.MAX_MESSAGE_HISTORY) ]; } } async processQuery(query: string): Promise<string> { this.messages.push({ role: "user", content: query }); this.pruneMessageHistory(); const messages: MessageParam[] = this.messages.map((msg) => ({ role: msg.role, content: msg.content, })); try { const response = await this.anthropic.messages.create({ model: this.MODEL_VERSION, max_tokens: 1000, messages, tools: this.tools, }); const finalText: string[] = []; for (const content of response.content) { if (content.type === "text") { finalText.push(content.text); } else if (content.type === "tool_use") { const toolName = content.name; const toolArgs = content.input as Record<string, unknown>; this.log(`Executing tool: ${toolName}`); try { const result = await this.mcp.callTool({ name: toolName, arguments: toolArgs, }); // Add tool call and result to message history this.messages.push({ role: "assistant", content: `Tool call: ${toolName}(${JSON.stringify(toolArgs)})` }); this.messages.push({ role: "user", content: JSON.stringify(result.content), }); this.pruneMessageHistory(); const followUpResponse = await this.anthropic.messages.create({ model: this.MODEL_VERSION, max_tokens: 1000, messages: this.messages.map((msg) => ({ role: msg.role, content: msg.content, })), }); if (followUpResponse.content[0].type === "text") { finalText.push(followUpResponse.content[0].text); } } catch (toolError) { this.log(`Error executing tool ${toolName}: ${toolError}`); finalText.push(`Error executing tool ${toolName}: ${toolError}`); // Add error to message history this.messages.push({ role: "user", content: `Error executing tool ${toolName}: ${toolError}` }); this.pruneMessageHistory(); } } } const finalResponse = finalText.join("\n"); this.messages.push({ role: "assistant", content: finalResponse }); this.pruneMessageHistory(); return finalResponse; } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); console.error(chalk.red("Error processing query:"), errorMessage); // Check for authentication errors if (errorMessage.includes("401") || errorMessage.includes("authentication_error") || errorMessage.includes("invalid x-api-key") || errorMessage.includes("unauthorized")) { console.error(chalk.red("\nAuthentication error with Anthropic API. Please check your API key:")); console.error(chalk.yellow("1. Ensure your API key is valid and active")); console.error(chalk.yellow("2. Make sure it starts with 'sk-ant-' or 'sk-'")); console.error(chalk.yellow("3. Check for extra spaces or quotes in your key")); console.error(chalk.yellow("4. Consider regenerating a new API key at https://console.anthropic.com/")); throw new Error("Anthropic API authentication failed"); } // Try to reconnect if we lost the server connection if (errorMessage.includes("ECONNRESET") || errorMessage.includes("disconnected") || errorMessage.includes("broken pipe")) { console.log(chalk.yellow("\nConnection to server lost. Attempting to reconnect...")); try { await this.connectToServer(1); console.log(chalk.green("Reconnected to server. Please try your query again.")); } catch (reconnectError) { console.error(chalk.red("Failed to reconnect: "), reconnectError); } } throw e; } } async cleanup() { try { // Close MCP connection if (this.transport) { try { await this.mcp.close(); } catch (e) { this.log(`Error closing MCP connection: ${e}`); } this.transport = null; } // Reset state this.tools = []; this.messages = []; } catch (e) { console.error(chalk.red("Error during cleanup:"), e); throw e; } } }

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/priyankark/phonepi-mcp'

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