Skip to main content
Glama

Apple MCP Server

index.ts46.5 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { runAppleScript } from "run-applescript"; import tools from "./tools"; // Safe mode implementation - lazy loading of modules let useEagerLoading = true; let loadingTimeout: ReturnType<typeof setTimeout> | null = null; let safeModeFallback = false; console.error("Starting apple-mcp server..."); // Placeholders for modules - will either be loaded eagerly or lazily let contacts: typeof import("./utils/contacts").default | null = null; let notes: typeof import("./utils/notes").default | null = null; let message: typeof import("./utils/message").default | null = null; let mail: typeof import("./utils/mail").default | null = null; let reminders: typeof import("./utils/reminders").default | null = null; let calendar: typeof import("./utils/calendar").default | null = null; let maps: typeof import("./utils/maps").default | null = null; // Type map for module names to their types type ModuleMap = { contacts: typeof import("./utils/contacts").default; notes: typeof import("./utils/notes").default; message: typeof import("./utils/message").default; mail: typeof import("./utils/mail").default; reminders: typeof import("./utils/reminders").default; calendar: typeof import("./utils/calendar").default; maps: typeof import("./utils/maps").default; }; // Helper function for lazy module loading async function loadModule< T extends | "contacts" | "notes" | "message" | "mail" | "reminders" | "calendar" | "maps", >(moduleName: T): Promise<ModuleMap[T]> { if (safeModeFallback) { console.error(`Loading ${moduleName} module on demand (safe mode)...`); } try { switch (moduleName) { case "contacts": if (!contacts) contacts = (await import("./utils/contacts")).default; return contacts as ModuleMap[T]; case "notes": if (!notes) notes = (await import("./utils/notes")).default; return notes as ModuleMap[T]; case "message": if (!message) message = (await import("./utils/message")).default; return message as ModuleMap[T]; case "mail": if (!mail) mail = (await import("./utils/mail")).default; return mail as ModuleMap[T]; case "reminders": if (!reminders) reminders = (await import("./utils/reminders")).default; return reminders as ModuleMap[T]; case "calendar": if (!calendar) calendar = (await import("./utils/calendar")).default; return calendar as ModuleMap[T]; case "maps": if (!maps) maps = (await import("./utils/maps")).default; return maps as ModuleMap[T]; default: throw new Error(`Unknown module: ${moduleName}`); } } catch (e) { console.error(`Error loading module ${moduleName}:`, e); throw e; } } // Set a timeout to switch to safe mode if initialization takes too long loadingTimeout = setTimeout(() => { console.error( "Loading timeout reached. Switching to safe mode (lazy loading...)", ); useEagerLoading = false; safeModeFallback = true; // Clear the references to any modules that might be in a bad state contacts = null; notes = null; message = null; mail = null; reminders = null; calendar = null; // Proceed with server setup initServer(); }, 5000); // 5 second timeout // Eager loading attempt async function attemptEagerLoading() { try { console.error("Attempting to eagerly load modules..."); // Try to import all modules contacts = (await import("./utils/contacts")).default; console.error("- Contacts module loaded successfully"); notes = (await import("./utils/notes")).default; console.error("- Notes module loaded successfully"); message = (await import("./utils/message")).default; console.error("- Message module loaded successfully"); mail = (await import("./utils/mail")).default; console.error("- Mail module loaded successfully"); reminders = (await import("./utils/reminders")).default; console.error("- Reminders module loaded successfully"); calendar = (await import("./utils/calendar")).default; console.error("- Calendar module loaded successfully"); maps = (await import("./utils/maps")).default; console.error("- Maps module loaded successfully"); // If we get here, clear the timeout and proceed with eager loading if (loadingTimeout) { clearTimeout(loadingTimeout); loadingTimeout = null; } console.error("All modules loaded successfully, using eager loading mode"); initServer(); } catch (error) { console.error("Error during eager loading:", error); console.error("Switching to safe mode (lazy loading)..."); // Clear any timeout if it exists if (loadingTimeout) { clearTimeout(loadingTimeout); loadingTimeout = null; } // Switch to safe mode useEagerLoading = false; safeModeFallback = true; // Clear the references to any modules that might be in a bad state contacts = null; notes = null; message = null; mail = null; reminders = null; calendar = null; maps = null; // Initialize the server in safe mode initServer(); } } // Attempt eager loading first attemptEagerLoading(); // Main server object let server: Server; // Initialize the server and set up handlers function initServer() { console.error( `Initializing server in ${safeModeFallback ? "safe" : "standard"} mode...`, ); server = new Server( { name: "Apple MCP tools", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools, })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (!args) { throw new Error("No arguments provided"); } switch (name) { case "contacts": { if (!isContactsArgs(args)) { throw new Error("Invalid arguments for contacts tool"); } try { const contactsModule = await loadModule("contacts"); if (args.name) { const numbers = await contactsModule.findNumber(args.name); return { content: [ { type: "text", text: numbers.length ? `${args.name}: ${numbers.join(", ")}` : `No contact found for "${args.name}". Try a different name or use no name parameter to list all contacts.`, }, ], isError: false, }; } else { const allNumbers = await contactsModule.getAllNumbers(); const contactCount = Object.keys(allNumbers).length; if (contactCount === 0) { return { content: [ { type: "text", text: "No contacts found in the address book. Please make sure you have granted access to Contacts.", }, ], isError: false, }; } const formattedContacts = Object.entries(allNumbers) .filter(([_, phones]) => phones.length > 0) .map(([name, phones]) => `${name}: ${phones.join(", ")}`); return { content: [ { type: "text", text: formattedContacts.length > 0 ? `Found ${contactCount} contacts:\n\n${formattedContacts.join("\n")}` : "Found contacts but none have phone numbers. Try searching by name to see more details.", }, ], isError: false, }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: errorMessage.includes("access") ? errorMessage : `Error accessing contacts: ${errorMessage}`, }, ], isError: true, }; } } case "notes": { if (!isNotesArgs(args)) { throw new Error("Invalid arguments for notes tool"); } try { const notesModule = await loadModule("notes"); const { operation } = args; switch (operation) { case "search": { if (!args.searchText) { throw new Error( "Search text is required for search operation", ); } const foundNotes = await notesModule.findNote(args.searchText); return { content: [ { type: "text", text: foundNotes.length ? foundNotes .map((note) => `${note.name}:\n${note.content}`) .join("\n\n") : `No notes found for "${args.searchText}"`, }, ], isError: false, }; } case "list": { const allNotes = await notesModule.getAllNotes(); return { content: [ { type: "text", text: allNotes.length ? allNotes .map((note) => `${note.name}:\n${note.content}`) .join("\n\n") : "No notes exist.", }, ], isError: false, }; } case "create": { if (!args.title || !args.body) { throw new Error( "Title and body are required for create operation", ); } const result = await notesModule.createNote( args.title, args.body, args.folderName, ); return { content: [ { type: "text", text: result.success ? `Created note "${args.title}" in folder "${result.folderName}"${result.usedDefaultFolder ? " (created new folder)" : ""}.` : `Failed to create note: ${result.message}`, }, ], isError: !result.success, }; } default: throw new Error(`Unknown operation: ${operation}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: errorMessage.includes("access") ? errorMessage : `Error accessing notes: ${errorMessage}`, }, ], isError: true, }; } } case "messages": { if (!isMessagesArgs(args)) { throw new Error("Invalid arguments for messages tool"); } try { const messageModule = await loadModule("message"); switch (args.operation) { case "send": { if (!args.phoneNumber || !args.message) { throw new Error( "Phone number and message are required for send operation", ); } await messageModule.sendMessage(args.phoneNumber, args.message); return { content: [ { type: "text", text: `Message sent to ${args.phoneNumber}`, }, ], isError: false, }; } case "read": { if (!args.phoneNumber) { throw new Error( "Phone number is required for read operation", ); } const messages = await messageModule.readMessages( args.phoneNumber, args.limit, ); return { content: [ { type: "text", text: messages.length > 0 ? messages .map( (msg) => `[${new Date(msg.date).toLocaleString()}] ${msg.is_from_me ? "Me" : msg.sender}: ${msg.content}`, ) .join("\n") : "No messages found", }, ], isError: false, }; } case "schedule": { if (!args.phoneNumber || !args.message || !args.scheduledTime) { throw new Error( "Phone number, message, and scheduled time are required for schedule operation", ); } const scheduledMsg = await messageModule.scheduleMessage( args.phoneNumber, args.message, new Date(args.scheduledTime), ); return { content: [ { type: "text", text: `Message scheduled to be sent to ${args.phoneNumber} at ${scheduledMsg.scheduledTime}`, }, ], isError: false, }; } case "unread": { const messages = await messageModule.getUnreadMessages( args.limit, ); // Look up contact names for all messages const contactsModule = await loadModule("contacts"); const messagesWithNames = await Promise.all( messages.map(async (msg) => { // Only look up names for messages not from me if (!msg.is_from_me) { const contactName = await contactsModule.findContactByPhone(msg.sender); return { ...msg, displayName: contactName || msg.sender, // Use contact name if found, otherwise use phone/email }; } return { ...msg, displayName: "Me", }; }), ); return { content: [ { type: "text", text: messagesWithNames.length > 0 ? `Found ${messagesWithNames.length} unread message(s):\n` + messagesWithNames .map( (msg) => `[${new Date(msg.date).toLocaleString()}] From ${msg.displayName}:\n${msg.content}`, ) .join("\n\n") : "No unread messages found", }, ], isError: false, }; } default: throw new Error(`Unknown operation: ${args.operation}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: errorMessage.includes("access") ? errorMessage : `Error with messages operation: ${errorMessage}`, }, ], isError: true, }; } } case "mail": { if (!isMailArgs(args)) { throw new Error("Invalid arguments for mail tool"); } try { const mailModule = await loadModule("mail"); switch (args.operation) { case "unread": { // If an account is specified, we'll try to search specifically in that account let emails; if (args.account) { console.error( `Getting unread emails for account: ${args.account}`, ); // Use AppleScript to get unread emails from specific account const script = ` tell application "Mail" set resultList to {} try set targetAccount to first account whose name is "${args.account.replace(/"/g, '\\"')}" -- Get mailboxes for this account set acctMailboxes to every mailbox of targetAccount -- If mailbox is specified, only search in that mailbox set mailboxesToSearch to acctMailboxes ${ args.mailbox ? ` set mailboxesToSearch to {} repeat with mb in acctMailboxes if name of mb is "${args.mailbox.replace(/"/g, '\\"')}" then set mailboxesToSearch to {mb} exit repeat end if end repeat ` : "" } -- Search specified mailboxes repeat with mb in mailboxesToSearch try set unreadMessages to (messages of mb whose read status is false) if (count of unreadMessages) > 0 then set msgLimit to ${args.limit || 10} if (count of unreadMessages) < msgLimit then set msgLimit to (count of unreadMessages) end if repeat with i from 1 to msgLimit try set currentMsg to item i of unreadMessages set msgData to {subject:(subject of currentMsg), sender:(sender of currentMsg), ¬ date:(date sent of currentMsg) as string, mailbox:(name of mb)} -- Try to get content if possible try set msgContent to content of currentMsg if length of msgContent > 500 then set msgContent to (text 1 thru 500 of msgContent) & "..." end if set msgData to msgData & {content:msgContent} on error set msgData to msgData & {content:"[Content not available]"} end try set end of resultList to msgData on error -- Skip problematic messages end try end repeat if (count of resultList) ≥ ${args.limit || 10} then exit repeat end if on error -- Skip problematic mailboxes end try end repeat on error errMsg return "Error: " & errMsg end try return resultList end tell`; try { const asResult = await runAppleScript(script); if (asResult && asResult.startsWith("Error:")) { throw new Error(asResult); } // Parse the results - similar to general getUnreadMails const emailData = []; const matches = asResult.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { for (const match of matches) { try { const props = match .substring(1, match.length - 1) .split(","); const email: any = {}; props.forEach((prop) => { const parts = prop.split(":"); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(":").trim(); email[key] = value; } }); if (email.subject || email.sender) { emailData.push({ subject: email.subject || "No subject", sender: email.sender || "Unknown sender", dateSent: email.date || new Date().toString(), content: email.content || "[Content not available]", isRead: false, mailbox: `${args.account} - ${email.mailbox || "Unknown"}`, }); } } catch (parseError) { console.error( "Error parsing email match:", parseError, ); } } } emails = emailData; } catch (error) { console.error( "Error getting account-specific emails:", error, ); // Fallback to general method if specific account fails emails = await mailModule.getUnreadMails(args.limit); } } else { // No account specified, use the general method emails = await mailModule.getUnreadMails(args.limit); } return { content: [ { type: "text", text: emails.length > 0 ? `Found ${emails.length} unread email(s)${args.account ? ` in account "${args.account}"` : ""}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ""}:\n\n` + emails .map( (email: any) => `[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 500)}${email.content.length > 500 ? "..." : ""}`, ) .join("\n\n") : `No unread emails found${args.account ? ` in account "${args.account}"` : ""}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ""}`, }, ], isError: false, }; } case "search": { if (!args.searchTerm) { throw new Error( "Search term is required for search operation", ); } const emails = await mailModule.searchMails( args.searchTerm, args.limit, ); return { content: [ { type: "text", text: emails.length > 0 ? `Found ${emails.length} email(s) for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ""}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ""}:\n\n` + emails .map( (email: any) => `[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? "..." : ""}`, ) .join("\n\n") : `No emails found for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ""}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ""}`, }, ], isError: false, }; } case "send": { if (!args.to || !args.subject || !args.body) { throw new Error( "Recipient (to), subject, and body are required for send operation", ); } const result = await mailModule.sendMail( args.to, args.subject, args.body, args.cc, args.bcc, ); return { content: [{ type: "text", text: result }], isError: false, }; } case "mailboxes": { if (args.account) { const mailboxes = await mailModule.getMailboxesForAccount( args.account, ); return { content: [ { type: "text", text: mailboxes.length > 0 ? `Found ${mailboxes.length} mailboxes for account "${args.account}":\n\n${mailboxes.join("\n")}` : `No mailboxes found for account "${args.account}". Make sure the account name is correct.`, }, ], isError: false, }; } else { const mailboxes = await mailModule.getMailboxes(); return { content: [ { type: "text", text: mailboxes.length > 0 ? `Found ${mailboxes.length} mailboxes:\n\n${mailboxes.join("\n")}` : "No mailboxes found. Make sure Mail app is running and properly configured.", }, ], isError: false, }; } } case "accounts": { const accounts = await mailModule.getAccounts(); return { content: [ { type: "text", text: accounts.length > 0 ? `Found ${accounts.length} email accounts:\n\n${accounts.join("\n")}` : "No email accounts found. Make sure Mail app is configured with at least one account.", }, ], isError: false, }; } case "latest": { let account = args.account; if (!account) { const accounts = await mailModule.getAccounts(); if (accounts.length === 0) { throw new Error( "No email accounts found. Make sure Mail app is configured with at least one account.", ); } account = accounts[0]; // Use the first account if not provided } const emails = await mailModule.getLatestMails( account, args.limit, ); return { content: [ { type: "text", text: emails.length > 0 ? `Found ${emails.length} latest email(s) in account "${account}":\n\n` + emails .map( (email: any) => `[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 500)}${email.content.length > 500 ? "..." : ""}`, ) .join("\n\n") : `No latest emails found in account "${account}"`, }, ], isError: false, }; } default: throw new Error(`Unknown operation: ${args.operation}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: errorMessage.includes("access") ? errorMessage : `Error with mail operation: ${errorMessage}`, }, ], isError: true, }; } } case "reminders": { if (!isRemindersArgs(args)) { throw new Error("Invalid arguments for reminders tool"); } try { const remindersModule = await loadModule("reminders"); const { operation } = args; if (operation === "list") { // List all reminders const lists = await remindersModule.getAllLists(); const allReminders = await remindersModule.getAllReminders(); return { content: [ { type: "text", text: `Found ${lists.length} lists and ${allReminders.length} reminders.`, }, ], lists, reminders: allReminders, isError: false, }; } else if (operation === "search") { // Search for reminders const { searchText } = args; const results = await remindersModule.searchReminders( searchText!, ); return { content: [ { type: "text", text: results.length > 0 ? `Found ${results.length} reminders matching "${searchText}".` : `No reminders found matching "${searchText}".`, }, ], reminders: results, isError: false, }; } else if (operation === "open") { // Open a reminder const { searchText } = args; const result = await remindersModule.openReminder(searchText!); return { content: [ { type: "text", text: result.success ? `Opened Reminders app. Found reminder: ${result.reminder?.name}` : result.message, }, ], ...result, isError: !result.success, }; } else if (operation === "create") { // Create a reminder const { name, listName, notes, dueDate } = args; const result = await remindersModule.createReminder( name!, listName, notes, dueDate, ); return { content: [ { type: "text", text: `Created reminder "${result.name}" ${listName ? `in list "${listName}"` : ""}.`, }, ], success: true, reminder: result, isError: false, }; } else if (operation === "listById") { // Get reminders from a specific list by ID const { listId, props } = args; const results = await remindersModule.getRemindersFromListById( listId!, props, ); return { content: [ { type: "text", text: results.length > 0 ? `Found ${results.length} reminders in list with ID "${listId}".` : `No reminders found in list with ID "${listId}".`, }, ], reminders: results, isError: false, }; } return { content: [ { type: "text", text: "Unknown operation", }, ], isError: true, }; } catch (error) { console.error("Error in reminders tool:", error); const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: errorMessage.includes("access") ? errorMessage : `Error in reminders tool: ${errorMessage}`, }, ], isError: true, }; } } case "calendar": { if (!isCalendarArgs(args)) { throw new Error("Invalid arguments for calendar tool"); } try { const calendarModule = await loadModule("calendar"); const { operation } = args; switch (operation) { case "search": { const { searchText, limit, fromDate, toDate } = args; const events = await calendarModule.searchEvents( searchText!, limit, fromDate, toDate, ); return { content: [ { type: "text", text: events.length > 0 ? `Found ${events.length} events matching "${searchText}":\n\n${events .map( (event) => `${event.title} (${new Date(event.startDate!).toLocaleString()} - ${new Date(event.endDate!).toLocaleString()})\n` + `Location: ${event.location || "Not specified"}\n` + `Calendar: ${event.calendarName}\n` + `ID: ${event.id}\n` + `${event.notes ? `Notes: ${event.notes}\n` : ""}`, ) .join("\n\n")}` : `No events found matching "${searchText}".`, }, ], isError: false, }; } case "open": { const { eventId } = args; const result = await calendarModule.openEvent(eventId!); return { content: [ { type: "text", text: result.success ? result.message : `Error opening event: ${result.message}`, }, ], isError: !result.success, }; } case "list": { const { limit, fromDate, toDate } = args; const events = await calendarModule.getEvents( limit, fromDate, toDate, ); const startDateText = fromDate ? new Date(fromDate).toLocaleDateString() : "today"; const endDateText = toDate ? new Date(toDate).toLocaleDateString() : "next 7 days"; return { content: [ { type: "text", text: events.length > 0 ? `Found ${events.length} events from ${startDateText} to ${endDateText}:\n\n${events .map( (event) => `${event.title} (${new Date(event.startDate!).toLocaleString()} - ${new Date(event.endDate!).toLocaleString()})\n` + `Location: ${event.location || "Not specified"}\n` + `Calendar: ${event.calendarName}\n` + `ID: ${event.id}`, ) .join("\n\n")}` : `No events found from ${startDateText} to ${endDateText}.`, }, ], isError: false, }; } case "create": { const { title, startDate, endDate, location, notes, isAllDay, calendarName, } = args; const result = await calendarModule.createEvent( title!, startDate!, endDate!, location, notes, isAllDay, calendarName, ); return { content: [ { type: "text", text: result.success ? `${result.message} Event scheduled from ${new Date(startDate!).toLocaleString()} to ${new Date(endDate!).toLocaleString()}${result.eventId ? `\nEvent ID: ${result.eventId}` : ""}` : `Error creating event: ${result.message}`, }, ], isError: !result.success, }; } default: throw new Error(`Unknown calendar operation: ${operation}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: errorMessage.includes("access") ? errorMessage : `Error in calendar tool: ${errorMessage}`, }, ], isError: true, }; } } case "maps": { if (!isMapsArgs(args)) { throw new Error("Invalid arguments for maps tool"); } try { const mapsModule = await loadModule("maps"); const { operation } = args; switch (operation) { case "search": { const { query, limit } = args; if (!query) { throw new Error( "Search query is required for search operation", ); } const result = await mapsModule.searchLocations(query, limit); return { content: [ { type: "text", text: result.success ? `${result.message}\n\n${result.locations .map( (location) => `Name: ${location.name}\n` + `Address: ${location.address}\n` + `${location.latitude && location.longitude ? `Coordinates: ${location.latitude}, ${location.longitude}\n` : ""}`, ) .join("\n\n")}` : `${result.message}`, }, ], isError: !result.success, }; } case "save": { const { name, address } = args; if (!name || !address) { throw new Error( "Name and address are required for save operation", ); } const result = await mapsModule.saveLocation(name, address); return { content: [ { type: "text", text: result.message, }, ], isError: !result.success, }; } case "pin": { const { name, address } = args; if (!name || !address) { throw new Error( "Name and address are required for pin operation", ); } const result = await mapsModule.dropPin(name, address); return { content: [ { type: "text", text: result.message, }, ], isError: !result.success, }; } case "directions": { const { fromAddress, toAddress, transportType } = args; if (!fromAddress || !toAddress) { throw new Error( "From and to addresses are required for directions operation", ); } const result = await mapsModule.getDirections( fromAddress, toAddress, transportType as "driving" | "walking" | "transit", ); return { content: [ { type: "text", text: result.message, }, ], isError: !result.success, }; } case "listGuides": { const result = await mapsModule.listGuides(); return { content: [ { type: "text", text: result.message, }, ], isError: !result.success, }; } case "addToGuide": { const { address, guideName } = args; if (!address || !guideName) { throw new Error( "Address and guideName are required for addToGuide operation", ); } const result = await mapsModule.addToGuide(address, guideName); return { content: [ { type: "text", text: result.message, }, ], isError: !result.success, }; } case "createGuide": { const { guideName } = args; if (!guideName) { throw new Error( "Guide name is required for createGuide operation", ); } const result = await mapsModule.createGuide(guideName); return { content: [ { type: "text", text: result.message, }, ], isError: !result.success, }; } default: throw new Error(`Unknown maps operation: ${operation}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: errorMessage.includes("access") ? errorMessage : `Error in maps tool: ${errorMessage}`, }, ], isError: true, }; } } default: return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Start the server transport console.error("Setting up MCP server transport..."); (async () => { try { console.error("Initializing transport..."); const transport = new StdioServerTransport(); // Ensure stdout is only used for JSON messages console.error("Setting up stdout filter..."); const originalStdoutWrite = process.stdout.write.bind(process.stdout); process.stdout.write = (chunk: any, encoding?: any, callback?: any) => { // Only allow JSON messages to pass through if (typeof chunk === "string" && !chunk.startsWith("{")) { console.error("Filtering non-JSON stdout message"); return true; // Silently skip non-JSON messages } return originalStdoutWrite(chunk, encoding, callback); }; console.error("Connecting transport to server..."); await server.connect(transport); console.error("Server connected successfully!"); } catch (error) { console.error("Failed to initialize MCP server:", error); process.exit(1); } })(); } // Helper functions for argument type checking function isContactsArgs(args: unknown): args is { name?: string } { return ( typeof args === "object" && args !== null && (!("name" in args) || typeof (args as { name: string }).name === "string") ); } function isNotesArgs(args: unknown): args is { operation: "search" | "list" | "create"; searchText?: string; title?: string; body?: string; folderName?: string; } { if (typeof args !== "object" || args === null) { return false; } const { operation } = args as { operation?: unknown }; if (typeof operation !== "string") { return false; } if (!["search", "list", "create"].includes(operation)) { return false; } // Validate fields based on operation if (operation === "search") { const { searchText } = args as { searchText?: unknown }; if (typeof searchText !== "string" || searchText === "") { return false; } } if (operation === "create") { const { title, body } = args as { title?: unknown; body?: unknown }; if (typeof title !== "string" || title === "" || typeof body !== "string") { return false; } // Check folderName if provided const { folderName } = args as { folderName?: unknown }; if ( folderName !== undefined && (typeof folderName !== "string" || folderName === "") ) { return false; } } return true; } function isMessagesArgs(args: unknown): args is { operation: "send" | "read" | "schedule" | "unread"; phoneNumber?: string; message?: string; limit?: number; scheduledTime?: string; } { if (typeof args !== "object" || args === null) return false; const { operation, phoneNumber, message, limit, scheduledTime } = args as any; if ( !operation || !["send", "read", "schedule", "unread"].includes(operation) ) { return false; } // Validate required fields based on operation switch (operation) { case "send": case "schedule": if (!phoneNumber || !message) return false; if (operation === "schedule" && !scheduledTime) return false; break; case "read": if (!phoneNumber) return false; break; case "unread": // No additional required fields break; } // Validate field types if present if (phoneNumber && typeof phoneNumber !== "string") return false; if (message && typeof message !== "string") return false; if (limit && typeof limit !== "number") return false; if (scheduledTime && typeof scheduledTime !== "string") return false; return true; } function isMailArgs(args: unknown): args is { operation: "unread" | "search" | "send" | "mailboxes" | "accounts" | "latest"; account?: string; mailbox?: string; limit?: number; searchTerm?: string; to?: string; subject?: string; body?: string; cc?: string; bcc?: string; } { if (typeof args !== "object" || args === null) return false; const { operation, account, mailbox, limit, searchTerm, to, subject, body, cc, bcc, } = args as any; if ( !operation || !["unread", "search", "send", "mailboxes", "accounts", "latest"].includes( operation, ) ) { return false; } // Validate required fields based on operation switch (operation) { case "search": if (!searchTerm || typeof searchTerm !== "string") return false; break; case "send": if ( !to || typeof to !== "string" || !subject || typeof subject !== "string" || !body || typeof body !== "string" ) return false; break; case "unread": case "mailboxes": case "accounts": case "latest": // No additional required fields break; } // Validate field types if present if (account && typeof account !== "string") return false; if (mailbox && typeof mailbox !== "string") return false; if (limit && typeof limit !== "number") return false; if (cc && typeof cc !== "string") return false; if (bcc && typeof bcc !== "string") return false; return true; } function isRemindersArgs(args: unknown): args is { operation: "list" | "search" | "open" | "create" | "listById"; searchText?: string; name?: string; listName?: string; listId?: string; props?: string[]; notes?: string; dueDate?: string; } { if (typeof args !== "object" || args === null) { return false; } const { operation } = args as any; if (typeof operation !== "string") { return false; } if (!["list", "search", "open", "create", "listById"].includes(operation)) { return false; } // For search and open operations, searchText is required if ( (operation === "search" || operation === "open") && (typeof (args as any).searchText !== "string" || (args as any).searchText === "") ) { return false; } // For create operation, name is required if ( operation === "create" && (typeof (args as any).name !== "string" || (args as any).name === "") ) { return false; } // For listById operation, listId is required if ( operation === "listById" && (typeof (args as any).listId !== "string" || (args as any).listId === "") ) { return false; } return true; } function isCalendarArgs(args: unknown): args is { operation: "search" | "open" | "list" | "create"; searchText?: string; eventId?: string; limit?: number; fromDate?: string; toDate?: string; title?: string; startDate?: string; endDate?: string; location?: string; notes?: string; isAllDay?: boolean; calendarName?: string; } { if (typeof args !== "object" || args === null) { return false; } const { operation } = args as { operation?: unknown }; if (typeof operation !== "string") { return false; } if (!["search", "open", "list", "create"].includes(operation)) { return false; } // Check that required parameters are present for each operation if (operation === "search") { const { searchText } = args as { searchText?: unknown }; if (typeof searchText !== "string") { return false; } } if (operation === "open") { const { eventId } = args as { eventId?: unknown }; if (typeof eventId !== "string") { return false; } } if (operation === "create") { const { title, startDate, endDate } = args as { title?: unknown; startDate?: unknown; endDate?: unknown; }; if ( typeof title !== "string" || typeof startDate !== "string" || typeof endDate !== "string" ) { return false; } } return true; } function isMapsArgs(args: unknown): args is { operation: | "search" | "save" | "directions" | "pin" | "listGuides" | "addToGuide" | "createGuide"; query?: string; limit?: number; name?: string; address?: string; fromAddress?: string; toAddress?: string; transportType?: string; guideName?: string; } { if (typeof args !== "object" || args === null) { return false; } const { operation } = args as { operation?: unknown }; if (typeof operation !== "string") { return false; } if ( ![ "search", "save", "directions", "pin", "listGuides", "addToGuide", "createGuide", ].includes(operation) ) { return false; } // Check that required parameters are present for each operation if (operation === "search") { const { query } = args as { query?: unknown }; if (typeof query !== "string" || query === "") { return false; } } if (operation === "save" || operation === "pin") { const { name, address } = args as { name?: unknown; address?: unknown }; if ( typeof name !== "string" || name === "" || typeof address !== "string" || address === "" ) { return false; } } if (operation === "directions") { const { fromAddress, toAddress } = args as { fromAddress?: unknown; toAddress?: unknown; }; if ( typeof fromAddress !== "string" || fromAddress === "" || typeof toAddress !== "string" || toAddress === "" ) { return false; } // Check transportType if provided const { transportType } = args as { transportType?: unknown }; if ( transportType !== undefined && (typeof transportType !== "string" || !["driving", "walking", "transit"].includes(transportType)) ) { return false; } } if (operation === "createGuide") { const { guideName } = args as { guideName?: unknown }; if (typeof guideName !== "string" || guideName === "") { return false; } } if (operation === "addToGuide") { const { address, guideName } = args as { address?: unknown; guideName?: unknown; }; if ( typeof address !== "string" || address === "" || typeof guideName !== "string" || guideName === "" ) { return false; } } return true; }

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/supermemoryai/apple-mcp'

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