Skip to main content
Glama
index.ts47.7 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { WebSocket, WebSocketServer, VerifyClientCallbackSync } from "ws"; import { IncomingMessage } from "http"; // Toggle this to true to enable debug logging for websocket const DEBUG_WEBSOCKET = false; // Parse command line arguments const args = process.argv.slice(2); let port = 11041; // Default port let useStdio = false; // Process arguments for (let i = 0; i < args.length; i++) { if (args[i] === "--port" && i + 1 < args.length) { port = parseInt(args[i + 1], 10); i++; // Skip the next argument which is the port number if (isNaN(port)) { console.error("Invalid port number specified. Using default port 11041."); port = 11041; } } else if (args[i] === "--stdio") { useStdio = true; } } class PhonePiMCPServer { private server: Server; private wss: WebSocketServer | null = null; private phoneConnection: WebSocket | null = null; private pendingRequests: Map<string, (response: any) => void> = new Map(); private requestCounter = 0; private lastPing: number = 0; private _pingIntervalId: NodeJS.Timeout | null = null; constructor(private serverPort: number = 11041) { // Initialize MCP Server this.server = new Server( { name: "phone-mcp-server", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); // Try to initialize WebSocket Server, or connect to existing one this.initializeWebSocketServer(); this.setupMcpHandlers(); } private initializeWebSocketServer() { // Set up error handling this.setupErrorHandling(); try { // Initialize WebSocket Server with debug logging console.error(`Starting WebSocket server on port ${this.serverPort}...`); this.wss = new WebSocketServer({ port: this.serverPort, host: "0.0.0.0", // Explicitly listen on all interfaces verifyClient: (info: { req: IncomingMessage }) => { const address = info.req.socket.remoteAddress; console.error(`Connection attempt from ${address || "unknown"}`); return true; }, clientTracking: true, perMessageDeflate: { zlibDeflateOptions: { level: 6, // Higher level = more compression but slower }, zlibInflateOptions: { chunkSize: 10 * 1024, // Larger chunk size for faster inflation }, threshold: 1024, // Only compress messages larger than this }, }); this.wss.on("error", (error: any) => { if (error.code === "EADDRINUSE") { console.error( `Port ${this.serverPort} already in use, connecting to existing server...` ); // Close our server instance that failed if (this.wss) { this.wss.close(); this.wss = null; } this.connectToExistingServer(); } else { console.error("WebSocket Server Error:", error); } }); this.wss.on("connection", (ws, req) => { if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] New connection from ${ req.socket.remoteAddress || "unknown" }` ); } }); this.wss.on("listening", () => { console.error( `WebSocket server is listening on port ${this.serverPort}` ); this.setupWebSocket(); }); // Additional error handling for the server this.wss.on("close", () => { if (DEBUG_WEBSOCKET) { console.error("[WebSocket] Server closed"); } }); } catch (error) { console.error( "Failed to start WebSocket server, connecting to existing one...", error ); this.connectToExistingServer(); } } private connectToExistingServer() { console.error( `Attempting to connect to existing WebSocket server on port ${this.serverPort}...` ); // Create a WebSocket client to connect to the existing server const ws = new WebSocket(`ws://localhost:${this.serverPort}`, { handshakeTimeout: 10000, // 10 seconds timeout for initial handshake }); // Set a connection timeout const connectionTimeout = setTimeout(() => { if (ws.readyState !== WebSocket.OPEN) { if (DEBUG_WEBSOCKET) { console.error("[WebSocket] Connection attempt timed out"); } ws.terminate(); // After timeout, wait a bit and try to start our own server setTimeout(() => { if (DEBUG_WEBSOCKET) { console.error( "[WebSocket] Attempting to start our own server after connection timeout" ); } this.initializeWebSocketServer(); }, 5000); } }, 15000); ws.on("open", () => { clearTimeout(connectionTimeout); console.error("Connected to existing WebSocket server"); // Use the existing connection as if it were a server connection this.phoneConnection = ws; this.lastPing = Date.now(); ws.on("message", async (data) => { await this.processIncomingMessage(ws, data); }); ws.on("close", (code, reason) => { console.error( `[WebSocket] Connection to existing server closed (code: ${code}, reason: ${ reason || "unknown" })` ); this.phoneConnection = null; // Try to reconnect after a delay setTimeout(() => this.connectToExistingServer(), 5000); }); // Send initial ping ws.send(JSON.stringify({ type: "ping" })); // Start the ping interval for client mode too this.startPingInterval(); }); ws.on("error", (error) => { console.error("Error connecting to existing WebSocket server:", error); }); // Set up error handling this.setupErrorHandling(); } private setupErrorHandling() { // Error handling this.server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { await this.cleanup(); process.exit(0); }); } private setupWebSocket() { if (!this.wss) { console.error("WebSocket server not initialized"); return; } this.wss.on("connection", (ws) => { // If there's an existing connection, close it if (this.phoneConnection) { this.phoneConnection.close(); } this.phoneConnection = ws; this.lastPing = Date.now(); console.error("[WebSocket] Phone connected"); ws.on("message", async (data) => { await this.processIncomingMessage(ws, data); }); ws.on("close", (code, reason) => { console.error( `[WebSocket] Phone disconnected (code: ${code}, reason: ${ reason || "unknown" })` ); this.phoneConnection = null; }); ws.on("error", (error) => { console.error("[WebSocket] Phone connection error:", error); }); // Send initial ping ws.send(JSON.stringify({ type: "ping" })); }); // Start the ping interval this.startPingInterval(); } private async processIncomingMessage(ws: WebSocket, data: any) { try { // Convert the data to string properly based on its type const dataString = typeof data === "string" ? data : data instanceof Buffer ? data.toString("utf-8") : JSON.stringify(data); if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] Raw message received: ${dataString.substring(0, 200)}${ dataString.length > 200 ? "..." : "" }` ); } let message; try { message = JSON.parse(dataString); } catch (jsonError) { console.error( `[WebSocket] Failed to parse JSON: ${ jsonError instanceof Error ? jsonError.message : String(jsonError) }` ); // Send an error response if the message cannot be parsed try { ws.send( JSON.stringify({ type: "error", error: "Invalid JSON format", originalMessage: dataString.substring(0, 100), }) ); } catch (sendError) { console.error( `[WebSocket] Failed to send error response: ${sendError}` ); } return; } if(DEBUG_WEBSOCKET) { console.error(`[WebSocket] Parsed message: ${JSON.stringify(message)}`); } // Process different message types if (message.type === "pong") { this.lastPing = Date.now(); } else if (message.type === "response") { if (!message.requestId) { console.error( `[WebSocket] Response missing requestId: ${JSON.stringify(message)}` ); return; } const resolver = this.pendingRequests.get(message.requestId); if (resolver) { resolver(message.data); this.pendingRequests.delete(message.requestId); if (DEBUG_WEBSOCKET) { console.error(`[WebSocket] Resolved request: ${message.requestId}`); } } else { console.error( `[WebSocket] No pending request found for ID: ${message.requestId}` ); } } else if (message.type === "request") { console.error( `[WebSocket] Received request from phone: ${JSON.stringify(message)}` ); this.handleIncomingRequest(ws, message); } else if (message.type === "ping") { this.handleIncomingRequest(ws, { ...message, type: "ping", tool: "ping", }); } else { console.error(`[WebSocket] Unknown message type: ${message.type}`); } } catch (error) { console.error("[WebSocket] Error processing message:", error); } } private startPingInterval() { // Clear any existing ping interval if (this._pingIntervalId) { clearInterval(this._pingIntervalId); } // Ping phone every 15 seconds to check connection const pingInterval = setInterval(() => { if (this.phoneConnection) { const now = Date.now(); if (now - this.lastPing > 45000) { // 45 second timeout console.error("[WebSocket] Phone timed out - no pong received"); try { if (this.phoneConnection.readyState === WebSocket.OPEN) { console.error("[WebSocket] Closing timed out connection"); if (this.phoneConnection.terminate) { this.phoneConnection.terminate(); } else { this.phoneConnection.close(1000, "Connection timeout"); } } } catch (err) { console.error( "[WebSocket] Error closing timed out connection:", err ); } this.phoneConnection = null; } else if (this.phoneConnection.readyState === WebSocket.OPEN) { try { this.phoneConnection.send(JSON.stringify({ type: "ping" })); } catch (err) { console.error("[WebSocket] Error sending ping:", err); } } } }, 15000); // Store the interval ID for cleanup this._pingIntervalId = pingInterval; } private async handleIncomingRequest(ws: WebSocket, message: any) { // Handle incoming requests from the phone if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] Processing request from phone: ${JSON.stringify(message)}` ); } // Check if the request is properly formed if (!message.requestId) { if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] Request missing requestId: ${JSON.stringify(message)}` ); } return; } if (!message.tool) { if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] Request missing tool: ${JSON.stringify(message)}` ); } try { ws.send( JSON.stringify({ type: "response", requestId: message.requestId, data: { status: "error", error: "Missing tool name in request" }, }) ); } catch (sendError) { console.error( `[WebSocket] Failed to send error response: ${sendError}` ); } return; } // Process the request based on the tool requested try { // Handle different tool requests const toolName = message.tool; const params = message.params || {}; let responseData; // Example of how to handle specific tools if (toolName === "ping") { responseData = { status: "success", message: "pong", timestamp: Date.now(), }; } else if (toolName === "get_server_info") { responseData = { status: "success", info: { version: "0.1.0", uptime: process.uptime(), nodeVersion: process.version, platform: process.platform, }, }; } else { if (DEBUG_WEBSOCKET) { // Forward the request to the phone and wait for its actual response console.error(`[WebSocket] Forwarding request to phone: ${toolName}`); } // Create a promise to handle the response from the phone const phoneResponse = new Promise((resolve, reject) => { // Set a reasonable timeout for the phone to respond const timeout = setTimeout(() => { this.pendingRequests.delete(message.requestId); reject( new Error( `Request to phone timed out after 30 seconds: ${toolName}` ) ); }, 30000); // Store the request ID with a custom callback for this specific request this.pendingRequests.set(message.requestId, (response) => { clearTimeout(timeout); resolve(response); }); // Forward the request to the connected phone if ( this.phoneConnection && this.phoneConnection.readyState === WebSocket.OPEN ) { try { this.phoneConnection.send( JSON.stringify({ type: "request", requestId: message.requestId, tool: toolName, params: params, }) ); if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] Forwarded request to phone: ${toolName} (ID: ${message.requestId})` ); } } catch (sendError) { clearTimeout(timeout); this.pendingRequests.delete(message.requestId); reject( new Error( `Failed to forward request to phone: ${ sendError instanceof Error ? sendError.message : String(sendError) }` ) ); } } else { clearTimeout(timeout); this.pendingRequests.delete(message.requestId); reject(new Error("Phone not connected or connection not ready")); } }); try { // Wait for the phone's response responseData = await phoneResponse; if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] Received response from phone for ${ message.requestId }: ${JSON.stringify(responseData)}` ); } } catch (error) { console.error( `[WebSocket] Error waiting for phone response: ${error}` ); responseData = { status: "error", error: `Failed to get response from phone: ${ error instanceof Error ? error.message : String(error) }`, timestamp: Date.now(), }; } } // Send the response ws.send( JSON.stringify({ type: "response", requestId: message.requestId, data: responseData, }) ); if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] Sent response for request ${ message.requestId }: ${JSON.stringify(responseData)}` ); } } catch (processError) { console.error(`[WebSocket] Error processing request: ${processError}`); try { ws.send( JSON.stringify({ type: "response", requestId: message.requestId, data: { status: "error", error: `Failed to process request: ${ processError instanceof Error ? processError.message : String(processError) }`, }, }) ); } catch (sendError) { console.error( `[WebSocket] Failed to send error response: ${sendError}` ); } } } private setupMcpHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "get_battery_level", description: "Get the current battery level of the phone", inputSchema: { type: "object", properties: {}, required: [], }, }, // { // name: 'take_photo', // description: 'Take a photo using the phone camera', // inputSchema: { // type: 'object', // properties: { // camera: { // type: 'string', // enum: ['front', 'back'], // description: 'Which camera to use', // }, // }, // required: ['camera'], // }, // }, // { // name: 'get_location', // description: 'Get the current GPS location of the phone', // inputSchema: { // type: 'object', // properties: {}, // required: [], // }, // }, // Snippet tools { name: "add_snippet", description: "Add a new snippet to the phone", inputSchema: { type: "object", properties: { title: { type: "string", description: "Title of the snippet", }, content: { type: "string", description: "Content of the snippet", }, type: { type: "string", enum: ["note", "todo", "bookmark", "snippet", "draft"], description: "Type of the snippet", }, tags: { type: "string", description: "Comma-separated tags for the snippet", }, }, required: ["title", "content", "type"], }, }, { name: "get_all_snippets", description: "Get all snippets from the phone", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get_snippet_by_id", description: "Get a specific snippet by ID", inputSchema: { type: "object", properties: { id: { type: "number", description: "Snippet ID", }, }, required: ["id"], }, }, { name: "get_snippets_by_type", description: "Get snippets by type", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["note", "todo", "bookmark", "snippet", "draft"], description: "Type of snippets to retrieve", }, }, required: ["type"], }, }, { name: "search_snippets", description: "Search snippets by query", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query", }, }, required: ["query"], }, }, { name: "update_snippet", description: "Update an existing snippet", inputSchema: { type: "object", properties: { id: { type: "number", description: "Snippet ID", }, title: { type: "string", description: "Title of the snippet", }, content: { type: "string", description: "Content of the snippet", }, type: { type: "string", enum: ["note", "todo", "bookmark", "snippet", "draft"], description: "Type of the snippet", }, tags: { type: "string", description: "Comma-separated tags for the snippet", }, }, required: ["id", "title", "content", "type"], }, }, { name: "delete_snippet", description: "Delete a snippet from the phone", inputSchema: { type: "object", properties: { id: { type: "number", description: "Snippet ID to delete", }, }, required: ["id"], }, }, { name: "send_sms", description: "Send an SMS message", inputSchema: { type: "object", properties: { to: { type: "string", description: "Phone number to send to", }, message: { type: "string", description: "Message content", }, }, required: ["to", "message"], }, }, { name: "make_call", description: "Make a phone call", inputSchema: { type: "object", properties: { to: { type: "string", description: "Phone number to call", }, }, required: ["to"], }, }, { name: "find_phone", description: "Make the phone beep to help locate it", inputSchema: { type: "object", properties: { loop: { type: "boolean", description: "Whether to play the beep in a loop until dismissed", default: true, }, showDismissUI: { type: "boolean", description: "Whether to show a UI for dismissing the beep", default: true, }, notifyOnCompletion: { type: "boolean", description: "Whether to send a notification when the task is complete", default: false, }, }, required: [], }, }, // { // name: 'set_alarm', // description: 'Set an alarm for a specific time', // inputSchema: { // type: 'object', // properties: { // time: { // type: 'string', // description: 'Time to set the alarm for (ISO 8601 format)', // }, // }, // required: ['time'], // }, // }, { name: "set_timer", description: "Set a timer for a specific duration", inputSchema: { type: "object", properties: { seconds: { type: "number", description: "Duration of the timer in seconds", minimum: 1, }, }, required: ["seconds"], }, }, // { // name: 'cancel_alarm_or_timer', // description: 'Cancel a previously set alarm or timer', // inputSchema: { // type: 'object', // properties: { // identifier: { // type: 'string', // description: 'Identifier of the alarm/timer to cancel', // }, // }, // required: ['identifier'], // }, // }, // { // name: 'stop_alarm', // description: 'Stop the currently playing alarm sound', // inputSchema: { // type: 'object', // properties: {}, // required: [], // }, // }, { name: "copy_to_clipboard", description: "Copy text to the phone clipboard", inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to copy to clipboard", }, }, required: ["text"], }, }, { name: "get_contacts", description: "Get all contacts from the phone", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get_contact_by_id", description: "Get a specific contact by ID", inputSchema: { type: "object", properties: { id: { type: "string", description: "Contact ID", }, }, required: ["id"], }, }, { name: "add_contact", description: "Add a new contact to the phone", inputSchema: { type: "object", properties: { contact: { type: "object", description: "Contact data", properties: { name: { type: "string", description: "Full name of the contact", }, firstName: { type: "string", description: "First name of the contact", }, middleName: { type: "string", description: "Middle name of the contact", }, lastName: { type: "string", description: "Last name of the contact", }, phoneNumbers: { type: "array", description: "List of phone numbers", items: { type: "object", properties: { label: { type: "string", description: 'Label for the phone number (e.g., "home", "work", "mobile")', }, number: { type: "string", description: "The phone number", }, }, required: ["label", "number"], }, }, emails: { type: "array", description: "List of email addresses", items: { type: "object", properties: { label: { type: "string", description: 'Label for the email (e.g., "home", "work")', }, email: { type: "string", description: "The email address", }, }, required: ["label", "email"], }, }, addresses: { type: "array", description: "List of addresses", items: { type: "object", properties: { label: { type: "string", description: 'Label for the address (e.g., "home", "work")', }, street: { type: "string", description: "Street address", }, city: { type: "string", description: "City", }, region: { type: "string", description: "State/Province/Region", }, postalCode: { type: "string", description: "Postal/ZIP code", }, country: { type: "string", description: "Country", }, }, required: ["label"], }, }, company: { type: "string", description: "Company name", }, jobTitle: { type: "string", description: "Job title", }, note: { type: "string", description: "Additional notes about the contact", }, }, required: ["name"], }, }, required: ["contact"], }, }, { name: "update_contact", description: "Update an existing contact", inputSchema: { type: "object", properties: { contact: { type: "object", description: "Contact data with updates", properties: { id: { type: "string", description: "Contact ID (required for updating)", }, name: { type: "string", description: "Full name of the contact", }, firstName: { type: "string", description: "First name of the contact", }, middleName: { type: "string", description: "Middle name of the contact", }, lastName: { type: "string", description: "Last name of the contact", }, phoneNumbers: { type: "array", description: "List of phone numbers", items: { type: "object", properties: { id: { type: "string", description: "ID of the phone number (required for updating existing numbers)", }, label: { type: "string", description: 'Label for the phone number (e.g., "home", "work", "mobile")', }, number: { type: "string", description: "The phone number", }, }, required: ["label", "number"], }, }, emails: { type: "array", description: "List of email addresses", items: { type: "object", properties: { id: { type: "string", description: "ID of the email (required for updating existing emails)", }, label: { type: "string", description: 'Label for the email (e.g., "home", "work")', }, email: { type: "string", description: "The email address", }, }, required: ["label", "email"], }, }, addresses: { type: "array", description: "List of addresses", items: { type: "object", properties: { id: { type: "string", description: "ID of the address (required for updating existing addresses)", }, label: { type: "string", description: 'Label for the address (e.g., "home", "work")', }, street: { type: "string", description: "Street address", }, city: { type: "string", description: "City", }, region: { type: "string", description: "State/Province/Region", }, postalCode: { type: "string", description: "Postal/ZIP code", }, country: { type: "string", description: "Country", }, }, required: ["label"], }, }, company: { type: "string", description: "Company name", }, jobTitle: { type: "string", description: "Job title", }, note: { type: "string", description: "Additional notes about the contact", }, }, required: ["id", "name"], }, }, required: ["contact"], }, }, { name: "delete_contact", description: "Delete a contact from the phone", inputSchema: { type: "object", properties: { id: { type: "string", description: "Contact ID to delete", }, }, required: ["id"], }, }, { name: "share_snippet", description: "Share a snippet via messaging apps", inputSchema: { type: "object", properties: { id: { type: "number", description: "Snippet ID to share", }, method: { type: "string", enum: ["default", "sms", "whatsapp", "clipboard"], description: "Sharing method to use", default: "default", }, recipient: { type: "string", description: "Phone number for SMS or WhatsApp (required for those methods)", }, }, required: ["id"], }, }, { name: "send_message", description: "Send a message to the phone that requires user attention or response", inputSchema: { type: "object", properties: { title: { type: "string", description: "Title of the message", }, body: { type: "string", description: "Content of the message", }, priority: { type: "string", enum: ["low", "normal", "high", "urgent"], description: "Priority level of the message", default: "normal", }, responseOptions: { type: "array", description: "Optional response options for the user to choose from", items: { type: "string", }, }, requireResponse: { type: "boolean", description: "Whether a response is required from the user", default: false, }, expiresIn: { type: "number", description: "Optional expiration time in minutes (0 means no expiration)", default: 0, }, }, required: ["title", "body"], }, }, { name: "get_message_response", description: "Get the response to a previously sent message", inputSchema: { type: "object", properties: { messageId: { type: "string", description: "ID of the message to get the response for", }, }, required: ["messageId"], }, }, { name: "send_notification", description: "Send a notification to the phone", inputSchema: { type: "object", properties: { title: { type: "string", description: "Title of the notification", }, body: { type: "string", description: "Content of the notification", }, priority: { type: "string", enum: ["default", "high"], description: "Priority level of the notification", default: "default", }, data: { type: "object", description: "Additional data to include with the notification", }, }, required: ["title", "body"], }, }, ], })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!this.phoneConnection) { throw new McpError( ErrorCode.InvalidParams, "Phone not connected - please ensure the phone app is running and connected" ); } if (this.phoneConnection.readyState !== WebSocket.OPEN) { throw new McpError( ErrorCode.InvalidParams, "Phone connection is not open" ); } const requestId = `req-${++this.requestCounter}`; const message = { type: "request", requestId, tool: request.params.name, params: request.params.arguments || {}, }; try { const response = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(requestId); reject(new Error("Request timed out after 30 seconds")); }, 30000); this.pendingRequests.set(requestId, (response) => { clearTimeout(timeout); if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] Received response for request ${requestId}: ${JSON.stringify( response )}` ); } resolve(response); }); if ( this.phoneConnection && this.phoneConnection.readyState === WebSocket.OPEN ) { try { const messageStr = JSON.stringify(message); if (DEBUG_WEBSOCKET) { console.error( `[WebSocket] Sending request to phone: ${messageStr}` ); } this.phoneConnection.send(messageStr); } catch (err) { clearTimeout(timeout); this.pendingRequests.delete(requestId); console.error( `[WebSocket] Failed to send message: ${ err instanceof Error ? err.message : String(err) }` ); reject( new Error( `Failed to send message: ${ err instanceof Error ? err.message : String(err) }` ) ); } } else { clearTimeout(timeout); this.pendingRequests.delete(requestId); console.error( `[WebSocket] Cannot send message: Phone not connected (readyState: ${this.phoneConnection?.readyState})` ); reject(new Error("Phone not connected or connection not ready")); } }); // Check if this is a photo response with image data if ( request.params.name === "take_photo" && typeof response === "string" ) { try { const parsedResponse = JSON.parse(response); if ( parsedResponse.imageData && parsedResponse.imageData.startsWith("data:image/") ) { // Extract the base64 data and content type from the data URL const matches = parsedResponse.imageData.match( /^data:([^;]+);base64,(.+)$/ ); if (matches && matches.length === 3) { const contentType = matches[1]; // e.g., "image/jpeg" const base64Data = matches[2]; // Return both the image and the text response with a portion of the image data for analysis // Include the first 10000 characters of the base64 data, which should be enough for basic analysis // without making the response too large const analysisData = base64Data.length > 10000 ? base64Data.substring(0, 10000) + "..." : base64Data; return { content: [ { type: "image", mimeType: contentType, data: base64Data, }, { type: "text", text: JSON.stringify( { ...parsedResponse, // Include a portion of the image data for analysis imageData: `[${contentType} image data]`, imageDataForAnalysis: `data:${contentType};base64,${analysisData}`, }, null, 2 ), }, ], }; } } } catch (e) { console.error("Error parsing photo response:", e); // Fall back to default handling if parsing fails } } // Default handling for non-image responses return { content: [ { type: "text", text: JSON.stringify(response, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to execute ${request.params.name}: ${ error instanceof Error ? error.message : "Unknown error" }` ); } }); } private async cleanup() { console.error("Cleaning up server..."); // Clear ping interval if set if (this._pingIntervalId) { clearInterval(this._pingIntervalId); this._pingIntervalId = null; } // Close WebSocket server if it exists if (this.wss) { try { this.wss.close(); console.error("WebSocket server closed"); } catch (error) { console.error("Error closing WebSocket server:", error); } } // Close phone connection if it exists if (this.phoneConnection) { try { this.phoneConnection.close(); console.error("Phone connection closed"); } catch (error) { console.error("Error closing phone connection:", error); } } } async run() { // Register signal handlers for graceful shutdown process.on("SIGINT", async () => { console.error("Received SIGINT, shutting down..."); await this.cleanup(); process.exit(0); }); process.on("SIGTERM", async () => { console.error("Received SIGTERM, shutting down..."); await this.cleanup(); process.exit(0); }); // Start the server with the appropriate transport if (useStdio) { console.error("Using stdio transport"); const transport = new StdioServerTransport(); // Connect the server await this.server.connect(transport); console.error("Server ready with stdio transport"); } else { console.error("Using WebSocket transport only"); } console.error(`PhonePi MCP server running on port ${this.serverPort}...`); console.error( `Connect your phone to this server using port ${this.serverPort}` ); } } // Create server instance with the specified port const server = new PhonePiMCPServer(port); // Run the server async function main() { try { await server.run(); // If not using stdio, keep the process alive if (!useStdio) { console.error("Server running in standalone mode"); // This prevents the process from exiting setInterval(() => {}, 1000 * 60 * 60); // Keep alive once per hour } } catch (error) { console.error("Error running server:", error); process.exit(1); } } main().catch((error) => { console.error("Unhandled error in main:", error); process.exit(1); });

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