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;
}