AkurAI Build
Menu

Public repository / 7a6a24a4e46f9f1d964c5593c076f9bdf638fea0

AkurAI-Mail / tools/akurai-mail-mcp.ts

Latest change 7a284f40b2637792d0599c38c4cac0342da676d7 - Merge live EC2 ops copies into scripts/ and tools/ by Ólafur Búi Ólafsson

#!/usr/bin/env bun
/**
 * AkurAI-Mail MCP Server — maintained, inspectable interface for mail operations.
 *
 * Permission tiers:
 *   "read"  — any agent
 *   "draft" — write/mutation (flag, archive, move, draft-create/update)
 *   "send"  — requires idempotencyKey + approvalProof (string token proving human approval)
 *
 * Every call is audited. Credentials and full message bodies are NEVER stored in audit.
 */
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, "..");
// AKURAI_MAIL_SERVER_CLI overrides the server CLI path (tests use a fake CLI).
const SERVER_CLI = process.env.AKURAI_MAIL_SERVER_CLI ?? join(REPO_ROOT, "scripts/akurai-mail-server");
const INDEX_CLI = join(REPO_ROOT, "tools/akurai-mail-index.ts");

// ── Permission scope (least-privilege gate) ─────────────────────────────────
// AKURAI_MAIL_MCP_SCOPE caps the maximum tier this server exposes, so a
// read-only agent can be deployed that cannot mutate:
//   "read"  — read-only (no draft/mutation, no send)
//   "draft" — read + draft/flag/archive/move, no external send
//   "send"  — full surface (send still needs idempotencyKey + approvalProof)
//   "admin" — user/alias/domain/setting management (root CLI via sudo)
// Defaults to "send" for backward compatibility.
type PermissionTier = "read" | "draft" | "send" | "admin";
const PERMISSION_RANK: Record<PermissionTier, number> = { read: 0, draft: 1, send: 2, admin: 3 };
const SCOPE: PermissionTier = ((): PermissionTier => {
  const raw = (process.env.AKURAI_MAIL_MCP_SCOPE ?? "send").toLowerCase();
  return raw === "read" || raw === "draft" || raw === "send" || raw === "admin" ? raw : "send";
})();

// ── CLI response types ─────────────────────────────────────────────────────
interface CliResult {
  ok: boolean;
  error?: string;
  raw?: string;
  [key: string]: unknown;
}

interface MessageResult {
  ok: boolean;
  message?: {
    id?: string;
    folder?: string;
    from?: string;
    to?: string;
    cc?: string;
    subject?: string;
    date?: string;
    bodyText?: string;
    bodyHtml?: string;
    attachments?: AttachmentMeta[];
    [key: string]: unknown;
  };
}

interface AttachmentMeta {
  index: number;
  filename: string;
  contentType: string;
  size: number;
}


interface DraftResult {
  ok: boolean;
  draft?: string;
}

// ── Audit log ───────────────────────────────────────────────────────────────
const AUDIT_LOG = process.env.AKURAI_MAIL_MCP_AUDIT_LOG ?? join(REPO_ROOT, ".akurai-mail-mcp-audit.jsonl");

type AuditEntry = {
  at: string;
  kind: "read" | "draft" | "send" | "admin";
  email: string;
  tool: string;
  detail: Record<string, unknown>;
};

async function appendAudit(kind: AuditEntry["kind"], email: string, tool: string, detail: Record<string, unknown>) {
  const entry: AuditEntry = {
    at: new Date().toISOString(),
    kind,
    email,
    tool,
    detail: redactForAudit(detail),
  };
  try {
    const existing = await Bun.file(AUDIT_LOG).text().catch(() => "");
    await Bun.write(AUDIT_LOG, existing + JSON.stringify(entry) + "\n");
  } catch {
    // audit failure must not halt the operation
  }
}

function redactForAudit(d: Record<string, unknown>): Record<string, unknown> {
  const sensitiveKeys: Record<string, true> = { password: true, secret: true, token: true, approvalProof: true, body: true, bodyHtml: true, bodyText: true, data: true };
  const r: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(d)) {
    if (sensitiveKeys[k]) {
      r[k] = typeof v === "string" && v.length > 4 ? v.slice(0, 4) + "…" : "[REDACTED]";
    } else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
      r[k] = redactForAudit(v as Record<string, unknown>);
    } else {
      r[k] = v;
    }
  }
  return r;
}

// Admin CLI calls need root on the VM; set AKURAI_MAIL_CLI_SUDO=1 to prefix sudo -n.
const CLI_PREFIX = process.env.AKURAI_MAIL_CLI_SUDO === "1" ? ["sudo", "-n"] : [];
// ── CLI invocation helpers ──────────────────────────────────────────────────

async function runServer(args: string[]): Promise<CliResult> {
  const proc = Bun.spawnSync([...CLI_PREFIX, SERVER_CLI, ...args], {
    cwd: REPO_ROOT,
  });
  if (proc.exitCode !== 0) {
    const stderr = proc.stderr.toString().trim();
    throw new Error(stderr || `server CLI exited ${proc.exitCode}`);
  }
  const stdout = proc.stdout.toString().trim();
  let result: CliResult;
  try {
    result = JSON.parse(stdout) as CliResult;
  } catch {
    result = { ok: true, raw: stdout };
  }
  return result;
}

async function runIndex(args: string[]): Promise<CliResult> {
  const proc = Bun.spawnSync(["bun", "run", INDEX_CLI, ...args], {
    cwd: join(__dirname),
    env: { ...process.env },
  });
  if (proc.exitCode !== 0) {
    const stderr = proc.stderr.toString().trim();
    throw new Error(stderr || `index CLI exited ${proc.exitCode}`);
  }
  const stdout = proc.stdout.toString().trim();
  let result: CliResult;
  try {
    result = JSON.parse(stdout) as CliResult;
  } catch {
    result = { ok: true, raw: stdout };
  }
  return result;
}

// ── Idempotency store (in-memory) ───────────────────────────────────────────
const _SetCtor: SetConstructor = Set;
const sentIds = new _SetCtor<string>();
function checkIdempotency(key: string): boolean {
  if (sentIds.has(key)) return false;
  sentIds.add(key);
  return true;
}

// ── Approval proof validation ───────────────────────────────────────────────
function validateApprovalProof(proof: string): boolean {
  if (!proof) return false;
  return /^(ALLOW|[0-9a-fA-F]{8,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/.test(proof);
}

// ── Recipient validation ────────────────────────────────────────────────────
function validateEmailList(raw: string): string[] {
  if (!raw || !raw.trim()) return [];
  const addresses = raw.split(/[,;]/).map(s => s.trim()).filter(Boolean);
  for (const addr of addresses) {
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(addr)) {
      throw new Error(`Invalid email address: ${addr}`);
    }
  }
  return addresses;
}

// ── MCP Protocol types ─────────────────────────────────────────────────────
type JsonRpcMessage = {
  jsonrpc: "2.0";
  id?: number | string | null;
  method?: string;
  params?: Record<string, unknown>;
  result?: unknown;
  error?: { code: number; message: string; data?: unknown };
};

type ToolMeta = {
  permission: "read" | "draft" | "send" | "admin";
  description: string;
};

type ToolDefinition = {
  name: string;
  meta: ToolMeta;
  inputSchema: {
    type: "object";
    properties: Record<string, unknown>;
    required: string[];
    additionalProperties: boolean;
  };
};

// ── Tool definitions ────────────────────────────────────────────────────────

const emailProp = { type: "string" as const, description: "Mailbox email address (e.g. user@olibuijr.com)" };
const messageIdProp = { type: "string" as const, description: "Base64-encoded message ID from a list/read response" };
const folderProp = {
  type: "string" as const,
  description: "Folder name: Inbox, Sent, Drafts, Archive, Spam, Trash",
  enum: ["Inbox", "Sent", "Drafts", "Archive", "Spam", "Trash"] as string[],
};

function toolDefinitions(): ToolDefinition[] {
  const readTools: ToolDefinition[] = [
    {
      name: "mail_accounts",
      meta: { permission: "read", description: "List all configured mail accounts" },
      inputSchema: { type: "object", properties: {}, required: [], additionalProperties: false },
    },
    {
      name: "mail_folder_list",
      meta: { permission: "read", description: "List folders and unread/total counts for a mailbox" },
      inputSchema: { type: "object", properties: { email: emailProp }, required: ["email"], additionalProperties: false },
    },
    {
      name: "mail_message_list",
      meta: { permission: "read", description: "List messages in a folder with cursor-based pagination" },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, folder: { ...folderProp, default: "Inbox" }, query: { type: "string", default: "" }, limit: { type: "number", default: 40 }, cursor: { type: "string", default: "" } },
        required: ["email"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_message_read",
      meta: { permission: "read", description: "Read a specific message with body text, HTML, and attachments" },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, id: messageIdProp, markRead: { type: "boolean", default: false } },
        required: ["email", "id"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_attachment_list",
      meta: { permission: "read", description: "List attachment metadata for a message" },
      inputSchema: { type: "object", properties: { email: emailProp, id: messageIdProp }, required: ["email", "id"], additionalProperties: false },
    },
    {
      name: "mail_attachment_download",
      meta: { permission: "read", description: "Download attachment data (base64-encoded)" },
      inputSchema: { type: "object", properties: { email: emailProp, id: messageIdProp, index: { type: "number" } }, required: ["email", "id", "index"], additionalProperties: false },
    },
    {
      name: "mail_thread_search",
      meta: { permission: "read", description: "Semantic search across mail using the indexed search engine" },
      inputSchema: {
        type: "object",
        properties: { query: { type: "string" }, account: { type: "string" }, limit: { type: "number", default: 10 } },
        required: ["query"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_signals",
      meta: { permission: "read", description: "Compute conversation signals (reply-needed, follow-up, priority)" },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, folder: { ...folderProp, default: "Inbox" } },
        required: ["email"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_audit_log",
      meta: { permission: "read", description: "Read the recent MCP audit log (no credentials or bodies)" },
      inputSchema: { type: "object", properties: { limit: { type: "number", default: 20 } }, required: [], additionalProperties: false },
    },
    { name: "mail_spam_policy_get", meta: { permission: "read", description: "Read anti-spam policy" }, inputSchema: { type: "object", properties: {}, required: [], additionalProperties: false } },
    { name: "mail_quarantine_list", meta: { permission: "read", description: "List quarantined messages" }, inputSchema: { type: "object", properties: { account: emailProp, cursor: { type: "string" }, limit: { type: "number", minimum: 1, maximum: 100 } }, required: [], additionalProperties: false } },
    { name: "mail_address_health", meta: { permission: "read", description: "Read mailbox and domain authentication health" }, inputSchema: { type: "object", properties: { account: emailProp, domain: { type: "string" } }, required: [], additionalProperties: false } },
    { name: "mail_reputation_status", meta: { permission: "read", description: "Read outbound reputation observations" }, inputSchema: { type: "object", properties: { domain: { type: "string" }, address: emailProp, days: { type: "number", minimum: 1, maximum: 90 } }, required: [], additionalProperties: false } },
    { name: "mail_spam_policy_change", meta: { permission: "draft", description: "Change anti-spam policy; requires confirmation" }, inputSchema: { type: "object", properties: { operation: { type: "string", enum: ["add", "remove", "update"] }, kind: { type: "string", enum: ["sender", "domain"] }, value: { type: "string" }, disposition: { type: "string", enum: ["allow", "block"] }, reason: { type: "string", minLength: 1 }, expiresAt: { type: ["string", "null"] }, expectedVersion: { type: "number" }, approvalProof: { type: "string" } }, required: ["operation", "kind", "value", "reason", "expectedVersion", "approvalProof"], additionalProperties: false } },
    { name: "mail_spam_scan", meta: { permission: "draft", description: "Scan mailbox for spam; requires confirmation" }, inputSchema: { type: "object", properties: { account: emailProp, approvalProof: { type: "string" } }, required: ["approvalProof"], additionalProperties: false } },
    { name: "mail_quarantine_release", meta: { permission: "draft", description: "Release quarantined messages; requires confirmation" }, inputSchema: { type: "object", properties: { account: emailProp, messageIds: { type: "array", items: messageIdProp, minItems: 1, maxItems: 100 }, allowSender: { type: "boolean" }, reason: { type: "string", minLength: 1 }, approvalProof: { type: "string" } }, required: ["account", "messageIds", "reason", "approvalProof"], additionalProperties: false } },
    { name: "mail_quarantine_confirm", meta: { permission: "draft", description: "Confirm quarantined messages as spam; requires confirmation" }, inputSchema: { type: "object", properties: { account: emailProp, messageIds: { type: "array", items: messageIdProp, minItems: 1, maxItems: 100 }, block: { type: "boolean" }, reason: { type: "string", minLength: 1 }, approvalProof: { type: "string" } }, required: ["account", "messageIds", "reason", "approvalProof"], additionalProperties: false } },
  ];
  const draftTools: ToolDefinition[] = [
    {
      name: "mail_draft_create",
      meta: { permission: "draft", description: "Create a new draft in the Drafts folder" },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, to: { type: "string" }, subject: { type: "string" }, body: { type: "string" }, cc: { type: "string" } },
        required: ["email"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_reply_prepare",
      meta: { permission: "draft", description: "Prepare a reply draft with proper threading headers" },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, messageId: messageIdProp, body: { type: "string" }, includeOriginal: { type: "boolean", default: true } },
        required: ["email", "messageId", "body"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_reply_all_prepare",
      meta: { permission: "draft", description: "Prepare a reply-all draft with all original recipients" },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, messageId: messageIdProp, body: { type: "string" }, includeOriginal: { type: "boolean", default: true } },
        required: ["email", "messageId", "body"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_flag",
      meta: { permission: "draft", description: "Star (flag) or unstar a message" },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, id: messageIdProp, flagged: { type: "boolean" } },
        required: ["email", "id", "flagged"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_archive",
      meta: { permission: "draft", description: "Move a message to Archive folder" },
      inputSchema: { type: "object", properties: { email: emailProp, id: messageIdProp }, required: ["email", "id"], additionalProperties: false },
    },
    {
      name: "mail_move",
      meta: { permission: "draft", description: "Move a message to a target folder" },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, id: messageIdProp, target: folderProp },
        required: ["email", "id", "target"],
        additionalProperties: false,
      },
    },
  ];

  const sendTools: ToolDefinition[] = [
    {
      name: "mail_send",
      meta: {
        permission: "send",
        description: "Send a new email. Requires idempotencyKey + approvalProof for external send gate.",
      },
      inputSchema: {
        type: "object",
        properties: {
          email: emailProp,
          to: { type: "string" },
          cc: { type: "string" },
          bcc: { type: "string" },
          subject: { type: "string" },
          body: { type: "string" },
          signature: { type: "string" },
          idempotencyKey: { type: "string" },
          approvalProof: { type: "string" },
        },
        required: ["email", "to", "subject", "body", "idempotencyKey", "approvalProof"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_reply",
      meta: {
        permission: "send",
        description: "Send a reply to an existing message. Requires idempotencyKey + approvalProof.",
      },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, messageId: messageIdProp, body: { type: "string" }, includeOriginal: { type: "boolean", default: true }, idempotencyKey: { type: "string" }, approvalProof: { type: "string" } },
        required: ["email", "messageId", "body", "idempotencyKey", "approvalProof"],
        additionalProperties: false,
      },
    },
    {
      name: "mail_reply_all",
      meta: {
        permission: "send",
        description: "Send a reply-all. Requires idempotencyKey + approvalProof.",
      },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, messageId: messageIdProp, body: { type: "string" }, includeOriginal: { type: "boolean", default: true }, idempotencyKey: { type: "string" }, approvalProof: { type: "string" } },
        required: ["email", "messageId", "body", "idempotencyKey", "approvalProof"],
        additionalProperties: false,
      },
    },
  ];

  const adminTools: ToolDefinition[] = [
    {
      name: "mail_status",
      meta: { permission: "admin", description: "Full mail system status: users, aliases, domains, queue, DNS health, anti-spam summary" },
      inputSchema: { type: "object", properties: {}, required: [], additionalProperties: false },
    },
    {
      name: "mail_user_add",
      meta: { permission: "admin", description: "Create a mailbox; returns the generated password when password is omitted" },
      inputSchema: { type: "object", properties: { email: emailProp, password: { type: "string" } }, required: ["email"], additionalProperties: false },
    },
    {
      name: "mail_password_set",
      meta: { permission: "admin", description: "Rotate a mailbox password; returns the generated password when password is omitted" },
      inputSchema: { type: "object", properties: { email: emailProp, password: { type: "string" } }, required: ["email"], additionalProperties: false },
    },
    {
      name: "mail_alias_add",
      meta: { permission: "admin", description: "Create an alias address that delivers to a target mailbox" },
      inputSchema: { type: "object", properties: { alias: { ...emailProp, description: "Alias address" }, target: emailProp }, required: ["alias", "target"], additionalProperties: false },
    },
    {
      name: "mail_domain_list",
      meta: { permission: "admin", description: "List mail domains and their DNS status" },
      inputSchema: { type: "object", properties: {}, required: [], additionalProperties: false },
    },
    {
      name: "mail_domain_check",
      meta: { permission: "admin", description: "Check a domain's mail DNS records (MX, SPF, DMARC, DKIM)" },
      inputSchema: { type: "object", properties: { domain: { type: "string" } }, required: ["domain"], additionalProperties: false },
    },
    {
      name: "mail_domain_add",
      meta: { permission: "admin", description: "Add a mail domain" },
      inputSchema: { type: "object", properties: { domain: { type: "string" } }, required: ["domain"], additionalProperties: false },
    },
    {
      name: "mail_domain_remove",
      meta: { permission: "admin", description: "Remove a mail domain" },
      inputSchema: { type: "object", properties: { domain: { type: "string" } }, required: ["domain"], additionalProperties: false },
    },
    {
      name: "mail_settings_get",
      meta: { permission: "read", description: "Read webmail settings store: contacts, signatures, vacation, rules, preferences" },
      inputSchema: { type: "object", properties: {}, required: [], additionalProperties: false },
    },
    {
      name: "mail_setting_set",
      meta: { permission: "admin", description: "Set one webmail setting: kind = contact | signature | vacation | rule | preferences, payload per kind" },
      inputSchema: {
        type: "object",
        properties: { email: emailProp, kind: { type: "string", enum: ["contact", "signature", "vacation", "rule", "preferences"] }, payload: { type: "object" } },
        required: ["email", "kind", "payload"],
        additionalProperties: false,
      },
    },
  ];

  return [...readTools, ...draftTools, ...sendTools, ...adminTools];
}

// ── Anti-spam / quarantine mutations (draft tier, approvalProof required) ──

const SPAM_FOLDER = "Spam";

function policyField(kind: string, disposition: string): "blocklist" | "allowlist" | "domainBlocklist" {
  if (kind === "domain") return "domainBlocklist";
  return disposition === "allow" ? "allowlist" : "blocklist";
}

function senderAddress(from: string): string {
  const bracketed = from.match(/<([^<>\s]+@[^<>\s]+)>/);
  return (bracketed ? bracketed[1]! : from).trim().toLowerCase();
}

async function messageSender(email: string, id: string): Promise<string> {
  const result = await runServer(["webmail-read", email, id]) as Record<string, unknown>;
  const message = (result.message ?? {}) as Record<string, unknown>;
  return senderAddress(String(message.from ?? ""));
}

async function changePolicyList(field: string, mode: string, values: string[], extra: Record<string, unknown>): Promise<CliResult> {
  return await runServer(["anti-spam-config", JSON.stringify({ field, mode, values, ...extra })]);
}

async function handleSpamMutation(name: string, rawArgs: Record<string, unknown>): Promise<unknown> {
  const reason = String(rawArgs.reason ?? "");
  if (name === "mail_spam_scan") {
    const email = rawArgs.account === undefined ? [] : ["--email", String(rawArgs.account)];
    const result = await runServer(["anti-spam-scan", ...email]);
    await appendAudit("draft", email[1] ?? "system", name, {});
    return result;
  }
  if (name === "mail_spam_policy_change") {
    const operation = String(rawArgs.operation);
    const kind = String(rawArgs.kind);
    const disposition = String(rawArgs.disposition ?? "block");
    const mode = operation === "remove" ? "remove" : "add";
    const result = await changePolicyList(policyField(kind, disposition), mode, [String(rawArgs.value)], {
      disposition, reason, expiresAt: rawArgs.expiresAt ?? null, expectedVersion: Number(rawArgs.expectedVersion), createdBy: "mcp",
    });
    await appendAudit("draft", "system", name, { operation, kind, disposition });
    return result;
  }
  const email = String(rawArgs.account);
  const ids = (rawArgs.messageIds as unknown[]).map(String);
  const senders = new Set<string>();
  const release = name === "mail_quarantine_release";
  const listSenders = release ? rawArgs.allowSender === true : rawArgs.block === true;
  for (const id of ids) {
    if (listSenders) senders.add(await messageSender(email, id));
    if (release) await runServer(["webmail-action", email, id, "move", "--target", "Inbox"]);
  }
  let policy: CliResult | undefined;
  const values = [...senders].filter(Boolean);
  if (values.length) {
    policy = await changePolicyList(release ? "allowlist" : "blocklist", "add", values, {
      disposition: release ? "allow" : "block", reason, source: "message-feedback", createdBy: "mcp",
    });
  }
  await appendAudit("draft", email, name, { count: ids.length, senders: values.length });
  return { ok: true, folder: release ? "Inbox" : SPAM_FOLDER, messages: ids.length, senders: values, policy };
}

// ── Tool handler ────────────────────────────────────────────────────────────

async function handleToolCall(name: string, rawArgs: Record<string, unknown>): Promise<unknown> {
  const def = toolDefinitions().find(t => t.name === name);
  if (!def) throw new Error(`Unknown tool: ${name}`);
  const permission = def.meta.permission;
  if (PERMISSION_RANK[permission] > PERMISSION_RANK[SCOPE]) throw new Error(`Tool "${name}" requires "${permission}" scope but server runs in "${SCOPE}" scope`);
  if (permission === "send") {
    const idempotencyKey = String(rawArgs.idempotencyKey ?? "");
    if (!idempotencyKey) throw new Error("idempotencyKey is required for send operations");
    if (!checkIdempotency(idempotencyKey)) return { ok: true, deduplicated: true };
    if (!validateApprovalProof(String(rawArgs.approvalProof ?? ""))) throw new Error("Invalid approvalProof");
  }
  switch (name) {
    case "mail_spam_policy_get": return await runServer(["spam-summary"]);
    case "mail_address_health":
    case "mail_reputation_status": return await runServer(["status"]);
    case "mail_quarantine_list": {
      const email = String(rawArgs.account ?? "");
      if (!email) return await runServer(["spam-summary"]);
      const limit = Math.min(100, Math.max(1, Number(rawArgs.limit ?? 40)));
      const result = await runServer(["webmail-state", email, "--folder", "Spam", "--limit", String(limit), "--cursor", String(rawArgs.cursor ?? "")]);
      await appendAudit("read", email, name, { limit });
      return result;
    }
    case "mail_spam_policy_change":
    case "mail_spam_scan":
    case "mail_quarantine_release":
    case "mail_quarantine_confirm":
      if (!validateApprovalProof(String(rawArgs.approvalProof ?? ""))) throw new Error("Invalid approvalProof");
      return await handleSpamMutation(name, rawArgs);
  }

  switch (name) {
    // ── Read tools ──
    case "mail_accounts": {
      const result = await runServer(["status"]) as Record<string, unknown>;
      await appendAudit("read", "system", name, {});
      const users = Array.isArray(result.users) ? result.users : [];
      const accounts = users
        .map((user) => user && typeof user === "object" ? (user as Record<string, unknown>).email : undefined)
        .filter((email): email is string => typeof email === "string");
      return { ok: true, accounts };
    }

    case "mail_folder_list": {
      const email = String(rawArgs.email);
      const state = await runServer(["webmail-state", email, "--folder", "Inbox", "--limit", "1"]);
      await appendAudit("read", email, name, { email });
      const folders = (state as Record<string, unknown>).folders;
      return { ok: true, folders: Array.isArray(folders) ? folders : [] };
    }

    case "mail_message_list": {
      const email = String(rawArgs.email);
      const folder = String(rawArgs.folder ?? "Inbox");
      const query = String(rawArgs.query ?? "");
      const limit = Math.min(Number(rawArgs.limit) || 40, 100);
      const cursor = String(rawArgs.cursor ?? "");
      const result = await runServer(["webmail-state", email, "--folder", folder, "--query", query, "--limit", String(limit), "--cursor", cursor]);
      await appendAudit("read", email, name, { email, folder, limit });
      return result;
    }

    case "mail_message_read": {
      const email = String(rawArgs.email);
      const id = String(rawArgs.id);
      const markRead = Boolean(rawArgs.markRead);
      const cliArgs = ["webmail-read", email, id];
      if (markRead) cliArgs.push("--mark-read");
      const result = await runServer(cliArgs);
      await appendAudit("read", email, name, { email, id, markRead });
      return result;
    }

    case "mail_attachment_list": {
      const email = String(rawArgs.email);
      const id = String(rawArgs.id);
      const result = await runServer(["webmail-read", email, id]) as unknown as MessageResult;
      const attachments = result.message?.attachments ?? [];
      await appendAudit("read", email, name, { email, id, attachmentCount: attachments.length });
      return { ok: true, attachments };
    }

    case "mail_attachment_download": {
      const email = String(rawArgs.email);
      const id = String(rawArgs.id);
      const index = Number(rawArgs.index);
      const result = await runServer(["webmail-attachment", email, id, String(index)]);
      await appendAudit("read", email, name, { email, id, index });
      return result;
    }

    case "mail_thread_search": {
      const query = String(rawArgs.query);
      const account = rawArgs.account ? String(rawArgs.account) : "";
      const limit = Math.min(Number(rawArgs.limit) || 10, 50);
      const indexArgs = ["search", query, "--json"];
      if (account) indexArgs.push("--account", account);
      if (limit) indexArgs.push("--limit", String(limit));
      const result = await runIndex(indexArgs);
      await appendAudit("read", account || "all", name, { query, limit });
      return result;
    }

    case "mail_signals": {
      const email = String(rawArgs.email);
      const folder = String(rawArgs.folder ?? "Inbox");
      const result = await runServer(["webmail-signals", email, "--folder", folder]);
      await appendAudit("read", email, name, { email, folder });
      return result;
    }

    case "mail_audit_log": {
      const limit = Math.min(Number(rawArgs.limit) || 20, 100);
      try {
        const file = Bun.file(AUDIT_LOG);
        const text = await file.text().catch(() => "");
        if (!text.trim()) return { ok: true, entries: [] };
        const lines = text.trim().split("\n").filter(Boolean);
        const entries = lines.slice(-limit).map(l => JSON.parse(l) as AuditEntry);
        return { ok: true, entries };
      } catch {
        return { ok: true, entries: [] };
      }
    }

    // ── Draft tools ──
    case "mail_draft_create": {
      const email = String(rawArgs.email);
      const payload = JSON.stringify({
        from: email,
        to: String(rawArgs.to ?? ""),
        cc: String(rawArgs.cc ?? ""),
        subject: String(rawArgs.subject ?? ""),
        body: String(rawArgs.body ?? ""),
      });
      const result = await runServer(["webmail-draft", email, payload]);
      await appendAudit("draft", email, name, { email, to: rawArgs.to, subject: rawArgs.subject });
      return result;
    }

    case "mail_reply_prepare": {
      const email = String(rawArgs.email);
      const messageId = String(rawArgs.messageId);
      const body = String(rawArgs.body);
      const includeOriginal = Boolean(rawArgs.includeOriginal);

      const original = await runServer(["webmail-read", email, messageId]) as unknown as MessageResult;
      const msg = original.message ?? {};
      const subject = msg.subject?.startsWith("Re:") ? msg.subject : `Re: ${msg.subject}`;
      const to = msg.from || "";

      let fullBody = body;
      if (includeOriginal && msg.bodyText) {
        fullBody = body + "\n\nOn " + (msg.date || "unknown") + ", " + (msg.from || "someone") + " wrote:\n> " + msg.bodyText.split("\n").join("\n> ");
      }

      const payload = JSON.stringify({ from: email, to, subject, body: fullBody });
      const result = await runServer(["webmail-draft", email, payload]) as unknown as DraftResult;
      await appendAudit("draft", email, name, { email, messageId, subject, to });
      return { ok: true, draft: result.draft, preparedReply: { to, subject, body: fullBody } };
    }

    case "mail_reply_all_prepare": {
      const email = String(rawArgs.email);
      const messageId = String(rawArgs.messageId);
      const body = String(rawArgs.body);
      const includeOriginal = Boolean(rawArgs.includeOriginal);

      const original = await runServer(["webmail-read", email, messageId]) as unknown as MessageResult;
      const msg = original.message ?? {};
      const subject = msg.subject?.startsWith("Re:") ? msg.subject : `Re: ${msg.subject}`;

      const selfEmail = email.toLowerCase();
      const from = msg.from || "";
      const originalTo = (msg.to || "").split(/[,;]/).map((s: string) => s.trim()).filter(Boolean);
      const originalCc = (msg.cc || "").split(/[,;]/).map((s: string) => s.trim()).filter(Boolean);

      const replyTo = [from, ...originalTo].filter((a: string) => a.toLowerCase() !== selfEmail).join(", ");
      const replyCc = originalCc.filter((a: string) => a.toLowerCase() !== selfEmail).join(", ");

      let fullBody = body;
      if (includeOriginal && msg.bodyText) {
        fullBody = body + "\n\nOn " + (msg.date || "unknown") + ", " + (msg.from || "someone") + " wrote:\n> " + msg.bodyText.split("\n").join("\n> ");
      }

      const payload = JSON.stringify({ from: email, to: replyTo, cc: replyCc, subject, body: fullBody });
      const result = await runServer(["webmail-draft", email, payload]) as unknown as DraftResult;
      await appendAudit("draft", email, name, { email, messageId, subject, to: replyTo, cc: replyCc });
      return { ok: true, draft: result.draft, preparedReply: { to: replyTo, cc: replyCc, subject, body: fullBody } };
    }

    case "mail_flag": {
      const email = String(rawArgs.email);
      const id = String(rawArgs.id);
      const flagged = Boolean(rawArgs.flagged);
      const result = await runServer(["webmail-action", email, id, flagged ? "star" : "unstar"]);
      await appendAudit("draft", email, name, { email, id, flagged });
      return result;
    }

    case "mail_archive": {
      const email = String(rawArgs.email);
      const id = String(rawArgs.id);
      const result = await runServer(["webmail-action", email, id, "archive"]);
      await appendAudit("draft", email, name, { email, id });
      return result;
    }

    case "mail_move": {
      const email = String(rawArgs.email);
      const id = String(rawArgs.id);
      const target = String(rawArgs.target);
      const result = await runServer(["webmail-action", email, id, "move", "--target", target]);
      await appendAudit("draft", email, name, { email, id, target });
      return result;
    }

    // ── Send tools ──
    case "mail_send": {
      const email = String(rawArgs.email);
      const to = String(rawArgs.to);
      const cc = String(rawArgs.cc ?? "");
      const bcc = String(rawArgs.bcc ?? "");
      const subject = String(rawArgs.subject);
      const body = String(rawArgs.body);
      const signature = String(rawArgs.signature ?? "");

      validateEmailList(to);
      if (cc) validateEmailList(cc);
      if (bcc) validateEmailList(bcc);

      const payload = JSON.stringify({ from: email, to, cc, bcc, subject, body, signature });
      const result = await runServer(["webmail-send", email, payload]);
      await appendAudit("send", email, name, { email, to, cc, bccCount: bcc ? bcc.split(/[,;]/).length : 0, subject });
      return result;
    }

    case "mail_reply":
    case "mail_reply_all": {
      const email = String(rawArgs.email);
      const messageId = String(rawArgs.messageId);
      const body = String(rawArgs.body);
      const includeOriginal = Boolean(rawArgs.includeOriginal);

      const original = await runServer(["webmail-read", email, messageId]) as unknown as MessageResult;
      const msg = original.message ?? {};
      const subject = msg.subject?.startsWith("Re:") ? msg.subject : `Re: ${msg.subject}`;

      let fullBody = body;
      if (includeOriginal && msg.bodyText) {
        fullBody = body + "\n\nOn " + (msg.date || "unknown") + ", " + (msg.from || "someone") + " wrote:\n> " + msg.bodyText.split("\n").join("\n> ");
      }

      const selfEmail = email.toLowerCase();
      const from = msg.from || "";
      const originalTo = (msg.to || "").split(/[,;]/).map((s: string) => s.trim()).filter(Boolean);
      const originalCc = (msg.cc || "").split(/[,;]/).map((s: string) => s.trim()).filter(Boolean);

      let toAddr: string;
      let ccAddr: string;

      if (name === "mail_reply_all") {
        toAddr = [from, ...originalTo].filter((a: string) => a.toLowerCase() !== selfEmail).join(", ");
        ccAddr = originalCc.filter((a: string) => a.toLowerCase() !== selfEmail).join(", ");
      } else {
        toAddr = from;
        ccAddr = "";
      }

      validateEmailList(toAddr);
      if (ccAddr) validateEmailList(ccAddr);

      const payload = JSON.stringify({ from: email, to: toAddr, cc: ccAddr, subject, body: fullBody });
      const result = await runServer(["webmail-send", email, payload]);
      await appendAudit("send", email, name, { email, messageId, to: toAddr, cc: ccAddr, subject });
      return result;
    }

    // ── Admin tools ──
    case "mail_status": {
      const result = await runServer(["status"]);
      await appendAudit("admin", "system", name, {});
      return result;
    }
    case "mail_user_add": {
      const email = String(rawArgs.email);
      const password = rawArgs.password === undefined ? [] : ["--password", String(rawArgs.password)];
      const result = await runServer(["add-user", email, ...password]);
      await appendAudit("admin", email, name, { email });
      return result;
    }
    case "mail_password_set": {
      const email = String(rawArgs.email);
      const password = rawArgs.password === undefined ? [] : ["--password", String(rawArgs.password)];
      const result = await runServer(["set-password", email, ...password]);
      await appendAudit("admin", email, name, { email });
      return result;
    }
    case "mail_alias_add": {
      const alias = String(rawArgs.alias);
      const target = String(rawArgs.target);
      const result = await runServer(["add-alias", alias, target]);
      await appendAudit("admin", alias, name, { alias, target });
      return result;
    }
    case "mail_domain_list": {
      const result = await runServer(["domain-list"]);
      await appendAudit("admin", "system", name, {});
      return result;
    }
    case "mail_domain_check": {
      const domain = String(rawArgs.domain);
      const result = await runServer(["domain-check", domain]);
      await appendAudit("admin", "system", name, { domain });
      return result;
    }
    case "mail_domain_add": {
      const domain = String(rawArgs.domain);
      const result = await runServer(["domain-add", domain]);
      await appendAudit("admin", "system", name, { domain });
      return result;
    }
    case "mail_domain_remove": {
      const domain = String(rawArgs.domain);
      const result = await runServer(["domain-remove", domain]);
      await appendAudit("admin", "system", name, { domain });
      return result;
    }
    case "mail_settings_get": {
      const result = await runServer(["webmail-export"]);
      await appendAudit("read", "system", name, {});
      return result;
    }
    case "mail_setting_set": {
      const email = rawArgs.email === undefined ? [] : [String(rawArgs.email)];
      const kind = String(rawArgs.kind);
      const payload = JSON.stringify(rawArgs.payload ?? {});
      const result = await runServer(["webmail-config", ...email, kind, payload]);
      await appendAudit("admin", email[0] ?? "system", name, { email: email[0], kind });
      return result;
    }

    default:
      throw new Error(`Unhandled tool: ${name}`);
  }
}

// ── Protocol I/O ───────────────────────────────────────────────────────────

const reader = Bun.stdin.stream();
const decoder = new TextDecoder();
let buffer = "";
// Transport mode of the connected client: framed (Content-Length) or
// line-delimited raw JSON. Responses mirror whichever the client used.
let lineMode = false;

async function sendMessage(msg: JsonRpcMessage) {
  const json = JSON.stringify(msg);
  if (lineMode) {
    await Bun.write(Bun.stdout, json + "\n");
    return;
  }
  const header = `Content-Length: ${Buffer.byteLength(json, "utf-8")}\r\n\r\n`;
  await Bun.write(Bun.stdout, header + json);
}

async function handleRequest(msg: JsonRpcMessage): Promise<void> {
  const method = msg.method ?? "";
  const id = msg.id;
  const params = (msg.params ?? {}) as Record<string, unknown>;

  try {
    switch (method) {
      case "initialize": {
        await sendMessage({
          jsonrpc: "2.0",
          id,
          result: {
            protocolVersion: "2025-03-26",
            capabilities: { tools: {}, resources: {} },
            serverInfo: { name: "akurai-mail-mcp", version: "0.5.0" },
          },
        });
        // Send initialized notification
        await sendMessage({
          jsonrpc: "2.0",
          method: "notifications/initialized",
        } as unknown as JsonRpcMessage);
        break;
      }

      case "tools/list": {
        await sendMessage({
          jsonrpc: "2.0",
          id,
          result: { tools: toolDefinitions().filter(t => PERMISSION_RANK[t.meta.permission] <= PERMISSION_RANK[SCOPE]) },
        });
        break;
      }

      case "tools/call": {
        const rawParams = params as { name?: string; arguments?: Record<string, unknown> };
        const toolName = String(rawParams.name ?? "");
        const toolArgs = rawParams.arguments ?? {};
        const result = await handleToolCall(toolName, toolArgs);
        await sendMessage({
          jsonrpc: "2.0",
          id,
          result: {
            content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
          },
        });
        break;
      }

      case "notifications/initialized":
        // Ignore — client acknowledging server init
        break;

      case "shutdown":
        await sendMessage({ jsonrpc: "2.0", id, result: null });
        process.exit(0);

      default: {
        if (id != null) {
          await sendMessage({
            jsonrpc: "2.0",
            id,
            error: { code: -32601, message: `Method not found: ${method}` },
          });
        }
      }
    }
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : String(err);
    if (id != null) {
      await sendMessage({
        jsonrpc: "2.0",
        id,
        error: { code: -32603, message },
      });
    }
  }
}

// ── Main loop ──────────────────────────────────────────────────────────────

for await (const chunk of reader) {
  buffer += decoder.decode(chunk, { stream: true });
  // Framed transport (LSP-style Content-Length headers).
  while (true) {
    const headerEnd = buffer.indexOf("\r\n\r\n");
    if (headerEnd === -1) break;
    const match = buffer.slice(0, headerEnd).match(/Content-Length:\s*(\d+)/i);
    if (!match) { buffer = buffer.slice(headerEnd + 4); continue; }
    const length = parseInt(match[1], 10);
    const bodyStart = headerEnd + 4;
    if (buffer.length < bodyStart + length) break;
    const body = buffer.slice(bodyStart, bodyStart + length);
    buffer = buffer.slice(bodyStart + length);

    try {
      const msg = JSON.parse(body) as JsonRpcMessage;
      handleRequest(msg); // fire-and-forget
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      await sendMessage({
        jsonrpc: "2.0",
        id: null,
        error: { code: -32700, message: `Parse error: ${message}` },
      });
    }
  }
  // Line-delimited fallback (newline-separated or bare JSON object, e.g.
  // zeroclaw stdio MCP sends one JSON object with no trailing newline)
  // — only when no partial framed message is pending in the buffer.
  if (!/content-length:/i.test(buffer)) {
    lineMode = true;
    let nl: number;
    while ((nl = buffer.indexOf("\n")) !== -1) {
      const line = buffer.slice(0, nl).trim();
      buffer = buffer.slice(nl + 1);
      if (!line.startsWith("{")) continue;
      try {
        handleRequest(JSON.parse(line) as JsonRpcMessage);
      } catch {
        // ignore malformed lines
      }
    }
    const trimmed = buffer.trim();
    if (trimmed.startsWith("{")) {
      try {
        handleRequest(JSON.parse(trimmed) as JsonRpcMessage);
        buffer = "";
      } catch {
        // incomplete JSON — wait for more bytes
      }
    }
  }
}