Add multi-account support and CORS/logging middleware
- Add garfield, sales, leads, founder accounts to IMAP and SMTP configs - Refactor fetcherpay config into shared helper functions - Add CORS middleware with wildcard origin - Add request logging middleware and MCP session lifecycle logs - Include package-lock.json and add @types/cors dependency Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2510
package-lock.json
generated
Normal file
2510
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,15 +11,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.18.0",
|
||||
"imapflow": "^1.0.0",
|
||||
"nodemailer": "^6.9.0",
|
||||
"dotenv": "^16.0.0"
|
||||
"nodemailer": "^6.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
|
||||
83
src/imap.ts
83
src/imap.ts
@@ -1,29 +1,58 @@
|
||||
import { ImapFlow } from 'imapflow';
|
||||
|
||||
export type Account = 'yahoo' | 'fetcherpay';
|
||||
export type Account = 'yahoo' | 'fetcherpay' | 'garfield' | 'sales' | 'leads' | 'founder';
|
||||
|
||||
const FETCHERPAY_IMAP_HOST = process.env['FETCHERPAY_IMAP_HOST'] ?? 'mail.fetcherpay.com';
|
||||
const FETCHERPAY_IMAP_PORT = parseInt(process.env['FETCHERPAY_IMAP_PORT'] ?? '30993');
|
||||
|
||||
function fetcherpayImapConfig(user: string, pass: string) {
|
||||
return {
|
||||
host: FETCHERPAY_IMAP_HOST,
|
||||
port: FETCHERPAY_IMAP_PORT,
|
||||
secure: true,
|
||||
auth: { user, pass },
|
||||
tls: { rejectUnauthorized: false },
|
||||
};
|
||||
}
|
||||
|
||||
function getConfig(account: Account = 'yahoo') {
|
||||
if (account === 'fetcherpay') {
|
||||
return {
|
||||
host: process.env['FETCHERPAY_IMAP_HOST'] ?? 'mail.fetcherpay.com',
|
||||
port: parseInt(process.env['FETCHERPAY_IMAP_PORT'] ?? '30993'),
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env['FETCHERPAY_EMAIL'] as string,
|
||||
pass: process.env['FETCHERPAY_PASSWORD'] as string,
|
||||
},
|
||||
tls: { rejectUnauthorized: false }, // self-signed cert on self-hosted server
|
||||
};
|
||||
switch (account) {
|
||||
case 'fetcherpay':
|
||||
return fetcherpayImapConfig(
|
||||
process.env['FETCHERPAY_EMAIL'] as string,
|
||||
process.env['FETCHERPAY_PASSWORD'] as string,
|
||||
);
|
||||
case 'garfield':
|
||||
return fetcherpayImapConfig(
|
||||
process.env['GARFIELD_EMAIL'] as string,
|
||||
process.env['GARFIELD_PASSWORD'] as string,
|
||||
);
|
||||
case 'sales':
|
||||
return fetcherpayImapConfig(
|
||||
process.env['SALES_EMAIL'] as string,
|
||||
process.env['SALES_PASSWORD'] as string,
|
||||
);
|
||||
case 'leads':
|
||||
return fetcherpayImapConfig(
|
||||
process.env['LEADS_EMAIL'] as string,
|
||||
process.env['LEADS_PASSWORD'] as string,
|
||||
);
|
||||
case 'founder':
|
||||
return fetcherpayImapConfig(
|
||||
process.env['FOUNDER_EMAIL'] as string,
|
||||
process.env['FOUNDER_PASSWORD'] as string,
|
||||
);
|
||||
default:
|
||||
return {
|
||||
host: 'imap.mail.yahoo.com',
|
||||
port: 993,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env['YAHOO_EMAIL'] as string,
|
||||
pass: process.env['YAHOO_APP_PASSWORD'] as string,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
host: 'imap.mail.yahoo.com',
|
||||
port: 993,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env['YAHOO_EMAIL'] as string,
|
||||
pass: process.env['YAHOO_APP_PASSWORD'] as string,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function withClient<T>(account: Account, fn: (client: ImapFlow) => Promise<T>): Promise<T> {
|
||||
@@ -159,9 +188,15 @@ export async function readMessage(uid: number, account: Account = 'yahoo'): Prom
|
||||
}
|
||||
|
||||
export async function getProfile(account: Account = 'yahoo'): Promise<{ email: string; name: string; account: string }> {
|
||||
const email = account === 'fetcherpay'
|
||||
? (process.env['FETCHERPAY_EMAIL'] ?? '')
|
||||
: (process.env['YAHOO_EMAIL'] ?? '');
|
||||
const emailMap: Record<Account, string> = {
|
||||
yahoo: process.env['YAHOO_EMAIL'] ?? '',
|
||||
fetcherpay: process.env['FETCHERPAY_EMAIL'] ?? '',
|
||||
garfield: process.env['GARFIELD_EMAIL'] ?? '',
|
||||
sales: process.env['SALES_EMAIL'] ?? '',
|
||||
leads: process.env['LEADS_EMAIL'] ?? '',
|
||||
founder: process.env['FOUNDER_EMAIL'] ?? '',
|
||||
};
|
||||
const email = emailMap[account] ?? '';
|
||||
return { email, name: email.split('@')[0], account };
|
||||
}
|
||||
|
||||
|
||||
39
src/index.ts
39
src/index.ts
@@ -1,5 +1,6 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
@@ -11,8 +12,24 @@ import {
|
||||
import { tools, handleToolCall } from './tools.js';
|
||||
|
||||
const app = express();
|
||||
app.use(cors({
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'mcp-session-id', 'Accept'],
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
|
||||
// Request logging middleware
|
||||
app.use((req, res, next) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}] ${req.method} ${req.path} - ${req.headers['user-agent'] || 'no-ua'}`);
|
||||
if (req.body && Object.keys(req.body).length > 0) {
|
||||
console.log(` Body: ${JSON.stringify(req.body).substring(0, 500)}`);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
function createMcpServer() {
|
||||
const server = new Server(
|
||||
{ name: 'hermes', version: '1.0.0' },
|
||||
@@ -33,11 +50,13 @@ const httpTransports = new Map<string, StreamableHTTPServerTransport>();
|
||||
|
||||
app.post('/mcp', async (req, res) => {
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
console.log(`[mcp] POST sessionId=${sessionId ?? 'none'}, isInit=${isInitializeRequest(req.body)}`);
|
||||
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && httpTransports.has(sessionId)) {
|
||||
// Known active session — reuse it
|
||||
console.log(`[mcp] Reusing existing session ${sessionId}`);
|
||||
transport = httpTransports.get(sessionId)!;
|
||||
} else if (isInitializeRequest(req.body)) {
|
||||
// Initialize request: create a new session.
|
||||
@@ -46,15 +65,23 @@ app.post('/mcp', async (req, res) => {
|
||||
if (sessionId) {
|
||||
console.warn(`[mcp] Stale session ${sessionId} re-initializing — pod may have restarted`);
|
||||
}
|
||||
console.log(`[mcp] Creating new session`);
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => crypto.randomUUID(),
|
||||
onsessioninitialized: (id) => { httpTransports.set(id, transport); },
|
||||
onsessioninitialized: (id) => {
|
||||
console.log(`[mcp] Session initialized: ${id}`);
|
||||
httpTransports.set(id, transport);
|
||||
},
|
||||
});
|
||||
transport.onclose = () => {
|
||||
if (transport.sessionId) httpTransports.delete(transport.sessionId);
|
||||
if (transport.sessionId) {
|
||||
console.log(`[mcp] Session closed: ${transport.sessionId}`);
|
||||
httpTransports.delete(transport.sessionId);
|
||||
}
|
||||
};
|
||||
const server = createMcpServer();
|
||||
await server.connect(transport);
|
||||
console.log(`[mcp] Server connected to transport`);
|
||||
} else {
|
||||
// Unknown session + non-initialize request: session expired (e.g. pod restarted).
|
||||
// Return 404 so MCP clients know to re-initialize rather than keep retrying.
|
||||
@@ -63,7 +90,13 @@ app.post('/mcp', async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
try {
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
console.log(`[mcp] Request handled successfully`);
|
||||
} catch (err) {
|
||||
console.error(`[mcp] Error handling request:`, err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/mcp', async (req, res) => {
|
||||
|
||||
92
src/smtp.ts
92
src/smtp.ts
@@ -1,34 +1,54 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import type { Account } from './imap.js';
|
||||
|
||||
function getSmtpTransport(account: Account = 'yahoo') {
|
||||
if (account === 'fetcherpay') {
|
||||
return nodemailer.createTransport({
|
||||
host: process.env['FETCHERPAY_SMTP_HOST'] ?? 'mail.fetcherpay.com',
|
||||
port: parseInt(process.env['FETCHERPAY_SMTP_PORT'] ?? '30587'),
|
||||
secure: false, // STARTTLS
|
||||
auth: {
|
||||
user: process.env['FETCHERPAY_EMAIL']!,
|
||||
pass: process.env['FETCHERPAY_PASSWORD']!,
|
||||
},
|
||||
tls: { rejectUnauthorized: false }, // self-signed cert
|
||||
});
|
||||
}
|
||||
const FETCHERPAY_SMTP_HOST = process.env['FETCHERPAY_SMTP_HOST'] ?? 'mail.fetcherpay.com';
|
||||
const FETCHERPAY_SMTP_PORT = parseInt(process.env['FETCHERPAY_SMTP_PORT'] ?? '30587');
|
||||
|
||||
function fetcherpaySmtpTransport(user: string, pass: string) {
|
||||
return nodemailer.createTransport({
|
||||
host: 'smtp.mail.yahoo.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env['YAHOO_EMAIL']!,
|
||||
pass: process.env['YAHOO_APP_PASSWORD']!,
|
||||
},
|
||||
host: FETCHERPAY_SMTP_HOST,
|
||||
port: FETCHERPAY_SMTP_PORT,
|
||||
secure: false, // STARTTLS
|
||||
auth: { user, pass },
|
||||
tls: { rejectUnauthorized: false },
|
||||
});
|
||||
}
|
||||
|
||||
function getSmtpTransport(account: Account = 'yahoo') {
|
||||
switch (account) {
|
||||
case 'fetcherpay':
|
||||
return fetcherpaySmtpTransport(process.env['FETCHERPAY_EMAIL']!, process.env['FETCHERPAY_PASSWORD']!);
|
||||
case 'garfield':
|
||||
return fetcherpaySmtpTransport(process.env['GARFIELD_EMAIL']!, process.env['GARFIELD_PASSWORD']!);
|
||||
case 'sales':
|
||||
return fetcherpaySmtpTransport(process.env['SALES_EMAIL']!, process.env['SALES_PASSWORD']!);
|
||||
case 'leads':
|
||||
return fetcherpaySmtpTransport(process.env['LEADS_EMAIL']!, process.env['LEADS_PASSWORD']!);
|
||||
case 'founder':
|
||||
return fetcherpaySmtpTransport(process.env['FOUNDER_EMAIL']!, process.env['FOUNDER_PASSWORD']!);
|
||||
default:
|
||||
return nodemailer.createTransport({
|
||||
host: 'smtp.mail.yahoo.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env['YAHOO_EMAIL']!,
|
||||
pass: process.env['YAHOO_APP_PASSWORD']!,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getSenderEmail(account: Account = 'yahoo'): string {
|
||||
return account === 'fetcherpay'
|
||||
? process.env['FETCHERPAY_EMAIL']!
|
||||
: process.env['YAHOO_EMAIL']!;
|
||||
const emailMap: Record<Account, string> = {
|
||||
yahoo: process.env['YAHOO_EMAIL'] ?? '',
|
||||
fetcherpay: process.env['FETCHERPAY_EMAIL'] ?? '',
|
||||
garfield: process.env['GARFIELD_EMAIL'] ?? '',
|
||||
sales: process.env['SALES_EMAIL'] ?? '',
|
||||
leads: process.env['LEADS_EMAIL'] ?? '',
|
||||
founder: process.env['FOUNDER_EMAIL'] ?? '',
|
||||
};
|
||||
return emailMap[account] ?? '';
|
||||
}
|
||||
|
||||
export async function sendEmail(
|
||||
@@ -55,17 +75,21 @@ export async function createDraft(
|
||||
): Promise<string> {
|
||||
const { ImapFlow } = await import('imapflow');
|
||||
|
||||
const imapConfig = account === 'fetcherpay'
|
||||
? {
|
||||
host: process.env['FETCHERPAY_IMAP_HOST'] ?? 'mail.fetcherpay.com',
|
||||
port: parseInt(process.env['FETCHERPAY_IMAP_PORT'] ?? '30993'),
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env['FETCHERPAY_EMAIL']!,
|
||||
pass: process.env['FETCHERPAY_PASSWORD']!,
|
||||
},
|
||||
tls: { rejectUnauthorized: false },
|
||||
}
|
||||
const fetcherpayImapBase = {
|
||||
host: process.env['FETCHERPAY_IMAP_HOST'] ?? 'mail.fetcherpay.com',
|
||||
port: parseInt(process.env['FETCHERPAY_IMAP_PORT'] ?? '30993'),
|
||||
secure: true,
|
||||
tls: { rejectUnauthorized: false },
|
||||
};
|
||||
const fetcherpayImapAccounts: Partial<Record<Account, { user: string; pass: string }>> = {
|
||||
fetcherpay: { user: process.env['FETCHERPAY_EMAIL']!, pass: process.env['FETCHERPAY_PASSWORD']! },
|
||||
garfield: { user: process.env['GARFIELD_EMAIL']!, pass: process.env['GARFIELD_PASSWORD']! },
|
||||
sales: { user: process.env['SALES_EMAIL']!, pass: process.env['SALES_PASSWORD']! },
|
||||
leads: { user: process.env['LEADS_EMAIL']!, pass: process.env['LEADS_PASSWORD']! },
|
||||
founder: { user: process.env['FOUNDER_EMAIL']!, pass: process.env['FOUNDER_PASSWORD']! },
|
||||
};
|
||||
const imapConfig = fetcherpayImapAccounts[account]
|
||||
? { ...fetcherpayImapBase, auth: fetcherpayImapAccounts[account]! }
|
||||
: {
|
||||
host: 'imap.mail.yahoo.com',
|
||||
port: 993,
|
||||
|
||||
@@ -5,8 +5,8 @@ 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".',
|
||||
enum: ['yahoo', 'fetcherpay', 'garfield', 'sales', 'leads', 'founder'],
|
||||
description: 'Which mailbox to use: "yahoo" (gheron01@yahoo.com), "fetcherpay" (garfield.heron@fetcherpay.com), "garfield" (garfield@fetcherpay.com), "sales" (sales@fetcherpay.com), "leads" (leads@fetcherpay.com), or "founder" (founder@fetcherpay.com). Defaults to "yahoo".',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user