- Multi-account email support (Yahoo + self-hosted IMAP/SMTP) - MCP tools: get_profile, search_messages, read_message, list_folders, create_draft, send_email - Streamable HTTP transport with session recovery - Docker + Kubernetes deployment configuration - Express server with health endpoint 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
161 lines
4.8 KiB
TypeScript
161 lines
4.8 KiB
TypeScript
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
import { searchMessages, readMessage, getProfile, listFolders, type Account } from './imap.js';
|
|
import { sendEmail, createDraft } from './smtp.js';
|
|
|
|
const ACCOUNT_PARAM = {
|
|
account: {
|
|
type: 'string',
|
|
enum: ['yahoo', 'fetcherpay'],
|
|
description: 'Which mailbox to use: "yahoo" (gheron01@yahoo.com) or "fetcherpay" (garfield.heron@fetcherpay.com). Defaults to "yahoo".',
|
|
},
|
|
};
|
|
|
|
export const tools: Tool[] = [
|
|
{
|
|
name: 'get_profile',
|
|
description: 'Get the email account profile (email address and name)',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { ...ACCOUNT_PARAM },
|
|
},
|
|
},
|
|
{
|
|
name: 'search_messages',
|
|
description: 'Search email messages by keyword, sender, or subject',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
q: { type: 'string', description: 'Search query (keyword, from:email, subject:text)' },
|
|
maxResults: { type: 'number', description: 'Max messages to return (default 20)' },
|
|
...ACCOUNT_PARAM,
|
|
},
|
|
required: ['q'],
|
|
},
|
|
},
|
|
{
|
|
name: 'read_message',
|
|
description: 'Read a full email message by UID',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
uid: { type: 'number', description: 'Message UID from search results' },
|
|
...ACCOUNT_PARAM,
|
|
},
|
|
required: ['uid'],
|
|
},
|
|
},
|
|
{
|
|
name: 'list_folders',
|
|
description: 'List all email folders/mailboxes',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { ...ACCOUNT_PARAM },
|
|
},
|
|
},
|
|
{
|
|
name: 'create_draft',
|
|
description: 'Create a draft email',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
to: { type: 'string', description: 'Recipient email address' },
|
|
subject: { type: 'string', description: 'Email subject' },
|
|
body: { type: 'string', description: 'Email body (plain text)' },
|
|
...ACCOUNT_PARAM,
|
|
},
|
|
required: ['to', 'subject', 'body'],
|
|
},
|
|
},
|
|
{
|
|
name: 'send_email',
|
|
description: 'Send an email',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
to: { type: 'string', description: 'Recipient email address' },
|
|
subject: { type: 'string', description: 'Email subject' },
|
|
body: { type: 'string', description: 'Email body (plain text)' },
|
|
...ACCOUNT_PARAM,
|
|
},
|
|
required: ['to', 'subject', 'body'],
|
|
},
|
|
},
|
|
];
|
|
|
|
function acct(args: Record<string, unknown>): Account {
|
|
return (args.account as Account) ?? 'yahoo';
|
|
}
|
|
|
|
export async function handleToolCall(
|
|
name: string,
|
|
args: Record<string, unknown>
|
|
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
console.log(`[tool] ${name}`, JSON.stringify(args));
|
|
const t0 = Date.now();
|
|
try {
|
|
let result: unknown;
|
|
|
|
switch (name) {
|
|
case 'get_profile':
|
|
result = await getProfile(acct(args));
|
|
break;
|
|
|
|
case 'search_messages':
|
|
result = await searchMessages(args.q as string, (args.maxResults as number) ?? 20, acct(args));
|
|
break;
|
|
|
|
case 'read_message':
|
|
result = await readMessage(args.uid as number, acct(args));
|
|
break;
|
|
|
|
case 'list_folders':
|
|
result = await listFolders(acct(args));
|
|
break;
|
|
|
|
case 'create_draft':
|
|
result = await createDraft(args.to as string, args.subject as string, args.body as string, acct(args));
|
|
break;
|
|
|
|
case 'send_email':
|
|
result = await sendEmail(args.to as string, args.subject as string, args.body as string, acct(args));
|
|
break;
|
|
|
|
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
|
|
case 'yahoo_get_profile':
|
|
result = await getProfile('yahoo');
|
|
break;
|
|
case 'yahoo_search_messages':
|
|
result = await searchMessages(args.q as string, (args.maxResults as number) ?? 20, 'yahoo');
|
|
break;
|
|
case 'yahoo_read_message':
|
|
result = await readMessage(args.uid as number, 'yahoo');
|
|
break;
|
|
case 'yahoo_list_folders':
|
|
result = await listFolders('yahoo');
|
|
break;
|
|
case 'yahoo_create_draft':
|
|
result = await createDraft(args.to as string, args.subject as string, args.body as string, 'yahoo');
|
|
break;
|
|
case 'yahoo_send_email':
|
|
result = await sendEmail(args.to as string, args.subject as string, args.body as string, 'yahoo');
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
|
|
console.log(`[tool] ${name} OK (${Date.now() - t0}ms)`);
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
};
|
|
} catch (error) {
|
|
const msg = (error as Error).message;
|
|
const stack = (error as Error).stack ?? '';
|
|
console.error(`[tool] ${name} ERROR (${Date.now() - t0}ms):`, msg);
|
|
console.error(stack);
|
|
return {
|
|
content: [{ type: 'text', text: `Error: ${msg}` }],
|
|
};
|
|
}
|
|
}
|