#!/usr/bin/env node
/**
 * @maxy/outlook MCP server.
 *
 * Read-only Microsoft Graph access for Outlook.com / Microsoft 365 mailboxes.
 * Admin agent only. Public-agent surface is explicitly excluded.
 *
 * Auth: per-account OAuth Auth Code + PKCE. Tokens persist encrypted to
 *   {ACCOUNTS_DIR}/{accountId}/secrets/outlook/{tokens.enc, .key}
 *
 * Required environment:
 *   ACCOUNT_ID            — provided by getMcpServers when spawned per account.
 *   PLATFORM_ROOT         — repo root; used to derive ACCOUNTS_DIR.
 *   OUTLOOK_CLIENT_ID     — Azure AD app (client) ID. Operator-set per install.
 *   OUTLOOK_TENANT_ID     — Azure tenant ("common" for multi-tenant). Default "common".
 */
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { resolve } from "node:path";
import { z } from "zod";
import { TokenStore } from "./auth/token-store.js";
import { runAccountRegister } from "./tools/account-register.js";
import { runMailList } from "./tools/mail-list.js";
import { runMailSearch } from "./tools/mail-search.js";
import { runCalendarList } from "./tools/calendar-list.js";
import { runCalendarEvent } from "./tools/calendar-event.js";
import { runContactsList } from "./tools/contacts-list.js";
import { runMailboxInfo } from "./tools/mailbox-info.js";
// Plugin-system boot tolerance — see Task 202.
// Module-init must never exit; the plugin-system spawn enumerates tools/list
// before any per-account env is set. Per-call refusal preserves isolation.
const ACCOUNT_ID = process.env.ACCOUNT_ID ?? null;
const PLATFORM_ROOT = process.env.PLATFORM_ROOT ?? process.cwd();
const ACCOUNTS_DIR = resolve(PLATFORM_ROOT, "..", "data/accounts");
const OUTLOOK_CLIENT_ID = process.env.OUTLOOK_CLIENT_ID ?? "";
const OUTLOOK_TENANT_ID = process.env.OUTLOOK_TENANT_ID ?? "common";
process.stderr.write(`[outlook-mcp] boot accountId=${ACCOUNT_ID ?? "null"}\n`);
function refuseNoAccount(toolName) {
    process.stderr.write(`[outlook-mcp] tool=${toolName} refuse reason=no-account-context\n`);
    return {
        content: [
            {
                type: "text",
                text: `${toolName} cannot execute: this MCP instance was launched without ACCOUNT_ID (plugin-system metadata-only mode). Per-account access requires ACCOUNT_ID at spawn.`,
            },
        ],
        isError: true,
    };
}
function getStore() {
    if (!ACCOUNT_ID) {
        throw new Error("getStore() called without ACCOUNT_ID — handler guard missing");
    }
    return new TokenStore(ACCOUNT_ID, ACCOUNTS_DIR);
}
function getConfig() {
    if (!ACCOUNT_ID) {
        throw new Error("getConfig() called without ACCOUNT_ID — handler guard missing");
    }
    if (!OUTLOOK_CLIENT_ID) {
        throw new Error("OUTLOOK_CLIENT_ID is not configured. Set it in the operator's Maxy install before using outlook tools.");
    }
    return {
        clientId: OUTLOOK_CLIENT_ID,
        tenantId: OUTLOOK_TENANT_ID,
        accountId: ACCOUNT_ID,
        tokenStore: getStore(),
    };
}
function ok(payload) {
    return {
        content: [
            { type: "text", text: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2) },
        ],
    };
}
function fail(message) {
    return { content: [{ type: "text", text: message }], isError: true };
}
const server = new McpServer({
    name: "outlook",
    version: "0.1.0",
});
server.tool("outlook-account-register", "Initiate the OAuth Auth Code + PKCE flow for this account. Returns an authorization URL the operator opens in the VNC browser. Tool blocks until the operator completes consent (5-minute timeout) or fails loudly.", {}, async () => {
    if (!ACCOUNT_ID)
        return refuseNoAccount("outlook-account-register");
    try {
        const config = getConfig();
        const result = await runAccountRegister({
            accountId: ACCOUNT_ID,
            clientId: config.clientId,
            tenantId: config.tenantId,
            tokenStore: config.tokenStore,
        });
        return ok(result);
    }
    catch (err) {
        return fail(`outlook-account-register failed: ${err.message}`);
    }
});
server.tool("outlook-mail-list", "List recent messages in the mailbox. Defaults to top=25 ordered by receivedDateTime DESC. Folder defaults to Inbox; pass a folder displayName or id for other folders. Returns id, subject, from, receivedDateTime, bodyPreview, isRead.", {
    top: z.number().int().min(1).max(250).optional(),
    folder: z.string().optional(),
}, async ({ top, folder }) => {
    if (!ACCOUNT_ID)
        return refuseNoAccount("outlook-mail-list");
    try {
        const items = await runMailList(getConfig(), { top, folder });
        return ok(items);
    }
    catch (err) {
        return fail(`outlook-mail-list failed: ${err.message}`);
    }
});
server.tool("outlook-mail-search", "Search mail with Microsoft Graph $search. The query is enclosed in quotes and applied with ConsistencyLevel=eventual. Returns the same shape as outlook-mail-list.", {
    query: z.string().min(1),
    top: z.number().int().min(1).max(250).optional(),
}, async ({ query, top }) => {
    if (!ACCOUNT_ID)
        return refuseNoAccount("outlook-mail-search");
    try {
        const items = await runMailSearch(getConfig(), { query, top });
        return ok(items);
    }
    catch (err) {
        return fail(`outlook-mail-search failed: ${err.message}`);
    }
});
server.tool("outlook-calendar-list", "List calendar events in the next rangeDays days (default 7, max 365). Returns id, subject, start, end, location, attendees, isAllDay, organizer.", {
    rangeDays: z.number().int().min(1).max(365).optional(),
    top: z.number().int().min(1).max(500).optional(),
}, async ({ rangeDays, top }) => {
    if (!ACCOUNT_ID)
        return refuseNoAccount("outlook-calendar-list");
    try {
        const events = await runCalendarList(getConfig(), { rangeDays, top });
        return ok(events);
    }
    catch (err) {
        return fail(`outlook-calendar-list failed: ${err.message}`);
    }
});
server.tool("outlook-calendar-event", "Fetch a single calendar event by id, including the full body, attendees, and webLink.", {
    eventId: z.string().min(1),
}, async ({ eventId }) => {
    if (!ACCOUNT_ID)
        return refuseNoAccount("outlook-calendar-event");
    try {
        const detail = await runCalendarEvent(getConfig(), { eventId });
        return ok(detail);
    }
    catch (err) {
        return fail(`outlook-calendar-event failed: ${err.message}`);
    }
});
server.tool("outlook-contacts-list", "List contacts. Defaults to top=50, max 250. Returns id, displayName, emailAddresses, phones, jobTitle, companyName.", {
    top: z.number().int().min(1).max(250).optional(),
}, async ({ top }) => {
    if (!ACCOUNT_ID)
        return refuseNoAccount("outlook-contacts-list");
    try {
        const items = await runContactsList(getConfig(), { top });
        return ok(items);
    }
    catch (err) {
        return fail(`outlook-contacts-list failed: ${err.message}`);
    }
});
server.tool("outlook-mailbox-info", "Health probe — reports auth state, refresh-window status, and top-level folder count for this account. Useful for answering 'did account X auth?' without grepping logs.", {}, async () => {
    if (!ACCOUNT_ID)
        return refuseNoAccount("outlook-mailbox-info");
    try {
        const info = await runMailboxInfo(getConfig());
        return ok(info);
    }
    catch (err) {
        return fail(`outlook-mailbox-info failed: ${err.message}`);
    }
});
const transport = new StdioServerTransport();
await server.connect(transport);
process.stderr.write(`[outlook-mcp] ready account=${ACCOUNT_ID ?? "null"}\n`);
//# sourceMappingURL=index.js.map