Apple MCP Server
by Dhravya
Verified
#!/usr/bin/env bun
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";
interface WebSearchArgs {
query: string;
}
// Safe mode implementation - lazy loading of modules
let useEagerLoading = true;
let loadingTimeout: NodeJS.Timeout | 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 webSearch: typeof import('./utils/webSearch').default | null = null;
let calendar: typeof import('./utils/calendar').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;
webSearch: typeof import('./utils/webSearch').default;
calendar: typeof import('./utils/calendar').default;
};
// Helper function for lazy module loading
async function loadModule<T extends 'contacts' | 'notes' | 'message' | 'mail' | 'reminders' | 'webSearch' | 'calendar'>(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 'webSearch':
if (!webSearch) webSearch = (await import('./utils/webSearch')).default;
return webSearch as ModuleMap[T];
case 'calendar':
if (!calendar) calendar = (await import('./utils/calendar')).default;
return calendar 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;
webSearch = 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");
webSearch = (await import('./utils/webSearch')).default;
console.error("- WebSearch module loaded successfully");
calendar = (await import('./utils/calendar')).default;
console.error("- Calendar 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;
webSearch = null;
calendar = 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) {
return {
content: [{
type: "text",
text: `Error accessing contacts: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
case "notes": {
if (!isNotesArgs(args)) {
throw new Error("Invalid arguments for notes tool");
}
try {
const notesModule = await loadModule('notes');
if (args.searchText) {
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
};
} else {
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
};
}
} catch (error) {
return {
content: [{
type: "text",
text: `Error accessing notes: ${error instanceof Error ? error.message : String(error)}`
}],
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) {
return {
content: [{
type: "text",
text: `Error with messages operation: ${error instanceof Error ? error.message : String(error)}`
}],
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
};
}
default:
throw new Error(`Unknown operation: ${args.operation}`);
}
} catch (error) {
return {
content: [{
type: "text",
text: `Error with mail operation: ${error instanceof Error ? error.message : String(error)}`
}],
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);
return {
content: [{
type: "text",
text: `Error in reminders tool: ${error}`
}],
isError: true
};
}
}
case "webSearch": {
if (!isWebSearchArgs(args)) {
throw new Error("Invalid arguments for web search tool");
}
const webSearchModule = await loadModule('webSearch');
const result = await webSearchModule.webSearch(args.query);
return {
content: [{
type: "text",
text: result.results.length > 0 ?
`Found ${result.results.length} results for "${args.query}". ${result.results.map(r => `[${r.displayUrl}] ${r.title} - ${r.snippet} \n content: ${r.content}`).join("\n")}` :
`No results found for "${args.query}".`
}],
isError: false
};
}
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) {
return {
content: [{
type: "text",
text: `Error in calendar tool: ${error instanceof Error ? error.message : String(error)}`
}],
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 { searchText?: string } {
return (
typeof args === "object" &&
args !== null &&
(!("searchText" in args) || typeof (args as { searchText: string }).searchText === "string")
);
}
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";
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"].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":
// 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 isWebSearchArgs(args: unknown): args is WebSearchArgs {
return (
typeof args === "object" &&
args !== null &&
typeof (args as WebSearchArgs).query === "string"
);
}
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;
}