import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
initStderrTee("contacts");
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { eagerTool } from "../../../../lib/mcp-eager/dist/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { contactCreate } from "./tools/contact-create.js";
import { contactLookup } from "./tools/contact-lookup.js";
import { contactUpdate } from "./tools/contact-update.js";
import { contactList } from "./tools/contact-list.js";
import { contactDelete } from "./tools/contact-delete.js";
import { contactExport } from "./tools/contact-export.js";
import { contactErase } from "./tools/contact-erase.js";
import { groupCreate } from "./tools/group-create.js";
import { groupManage } from "./tools/group-manage.js";
import { closeDriver } from "./lib/neo4j.js";
const server = new McpServer({
    name: "maxy-contacts",
    version: "0.1.0",
});
// Plugin-system boot tolerance — see Task 202.
// Module-init must never throw; plugin-system spawn enumerates tools/list
// before ACCOUNT_ID is set. Per-call refusal preserves account isolation.
const accountId = process.env.ACCOUNT_ID ?? null;
process.stderr.write(`[contacts] boot accountId=${accountId ?? "null"}\n`);
function refuseNoAccount(toolName) {
    process.stderr.write(`[contacts] 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 writes require ACCOUNT_ID at spawn.`,
            },
        ],
        isError: true,
    };
}
const agentSlug = process.env.AGENT_SLUG;
const sessionId = process.env.SESSION_ID;
// Task 476 — boot signal so a stale spawn-env contract is visible at the
// first byte of stderr.
process.stderr.write(`[contacts] boot accountId=${accountId ?? "-"} sessionId=${sessionId || "-"} agentSlug=${agentSlug || "-"}\n`);
eagerTool(server, "contact-create", "Create a new contact (schema:Person) in the graph. Any single identifying property — name, email, or telephone — suffices; the agent judges what to capture and dedup/conflicts are managed downstream. When email or telephone is supplied, contact-create deduplicates against existing live persons and returns the match instead of writing a duplicate. The new contact must link to at least one existing node (typically a Conversation or Organization) — pass elementIds found via memory-search.", {
    givenName: z.string().optional().describe("First name (optional — supply whichever identifying fields you have)"),
    familyName: z.string().optional().describe("Last name"),
    email: z.string().email().optional().describe("Email address — when present, used as the dedup key"),
    telephone: z.string().min(1).max(30).optional().describe("Phone number — when present, used as the dedup key"),
    jobTitle: z.string().optional().describe("Job title"),
    source: z
        .string()
        .describe("Where this contact came from (e.g. public.maxy.bot, telegram)"),
    status: z
        .string()
        .optional()
        .describe("Contact status (e.g. active, prospect, booked, archived)"),
    relationships: z
        .array(z.object({
        type: z.string().describe("Relationship type (e.g. PARTICIPATES_IN, WORKS_FOR, KNOWN_VIA)"),
        direction: z.enum(["outgoing", "incoming"]),
        targetNodeId: z.string().describe("elementId of the target node (from memory-search)"),
    }))
        .min(1)
        .describe("At least one relationship to an existing node — a contact without an adjacency is noise, not knowledge."),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("contact-create");
    try {
        const result = await contactCreate({
            ...params,
            accountId,
            createdBy: {
                agent: agentSlug,
                session: sessionId,
                tool: "contact-create",
            },
        });
        if (result.alreadyExists) {
            return {
                content: [
                    {
                        type: "text",
                        text: `Contact already exists with ${result.identifier} (ID: ${result.nodeId}). No duplicate created.`,
                    },
                ],
            };
        }
        return {
            content: [
                {
                    type: "text",
                    text: `Contact created: ${result.identifier} (ID: ${result.nodeId}, status: ${params.status ?? "prospect"})`,
                },
            ],
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Failed to create contact: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
eagerTool(server, "contact-lookup", "Search for contacts by email, telephone, name, or list recent contacts. Returns schema:Person nodes.", {
    email: z.string().optional().describe("Exact email match"),
    telephone: z.string().optional().describe("Exact phone number match"),
    name: z.string().optional().describe("Partial name search (first or last name)"),
    limit: z.number().int().optional().describe("Max results (default 10)"),
}, async ({ email, telephone, name, limit }) => {
    if (!accountId)
        return refuseNoAccount("contact-lookup");
    try {
        const results = await contactLookup({ email, telephone, name, accountId, limit });
        if (results.length === 0) {
            return {
                content: [
                    { type: "text", text: "No contacts found." },
                ],
            };
        }
        const formatted = results
            .map((c) => {
            const identifierPart = c.email
                ? `<${c.email}>`
                : c.telephone
                    ? `<${c.telephone}>`
                    : "";
            return `${c.givenName}${c.familyName ? ` ${c.familyName}` : ""} ${identifierPart} — ${c.status} (from ${c.source}, ${c.createdOn})`;
        })
            .join("\n");
        return {
            content: [{ type: "text", text: formatted }],
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Lookup failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
eagerTool(server, "contact-update", "Update properties of an existing contact. Identify by nodeId, email, or telephone. Cannot change email or accountId.", {
    email: z.string().optional().describe("Find contact by email"),
    telephone: z.string().optional().describe("Find contact by phone number"),
    nodeId: z.string().optional().describe("Find contact by node ID"),
    updates: z
        .record(z.string(), z.unknown())
        .describe("Properties to update (e.g. status, telephone, jobTitle)"),
}, async ({ email, telephone, nodeId, updates }) => {
    if (!accountId)
        return refuseNoAccount("contact-update");
    try {
        const result = await contactUpdate({
            email,
            telephone,
            nodeId,
            updates: updates,
            accountId,
        });
        return {
            content: [
                {
                    type: "text",
                    text: `Contact updated (ID: ${result.nodeId})`,
                },
            ],
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Update failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
eagerTool(server, "contact-list", "List contacts with optional filters. Returns summary of matching Person nodes.", {
    status: z
        .string()
        .optional()
        .describe("Filter by status (e.g. prospect)"),
    source: z
        .string()
        .optional()
        .describe("Filter by source (e.g. public.maxy.bot, telegram)"),
    since: z
        .string()
        .optional()
        .describe("ISO date — only contacts created after this date"),
    limit: z.number().int().optional().describe("Max results (default 50)"),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("contact-list");
    try {
        const result = await contactList({ ...params, accountId });
        if (result.contacts.length === 0) {
            return {
                content: [
                    { type: "text", text: "No contacts match the filters." },
                ],
            };
        }
        const header = `${result.total} total contacts${params.status ? ` with status '${params.status}'` : ""}:\n\n`;
        const formatted = result.contacts
            .map((c) => {
            const identifierPart = c.email
                ? `<${c.email}>`
                : c.telephone
                    ? `<${c.telephone}>`
                    : "";
            return `${c.givenName}${c.familyName ? ` ${c.familyName}` : ""} ${identifierPart} — ${c.status} (${c.source}, ${c.createdOn})`;
        })
            .join("\n");
        return {
            content: [{ type: "text", text: header + formatted }],
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `List failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
eagerTool(server, "contact-delete", "Soft-delete a contact (Person node). Adds the :Trashed label, snapshots the unique email/telephone " +
    "so a fresh contact with the same address can be created, and preserves relationships. The node " +
    "disappears from memory-search; restore via memory-restore (with the elementId) or hard-purge after " +
    "the grace period via memory-empty-trash. For GDPR Right-to-Erasure cascade, use contact-erase instead.", {
    email: z.string().optional().describe("Find contact by email"),
    telephone: z.string().optional().describe("Find contact by phone number"),
    nodeId: z.string().optional().describe("Find contact by node ID"),
    reason: z.string().optional().describe("Optional human-readable reason — persisted as trashReason for audit."),
}, async ({ email, telephone, nodeId, reason }) => {
    if (!accountId)
        return refuseNoAccount("contact-delete");
    try {
        const result = await contactDelete({ email, telephone, nodeId, reason, accountId });
        const label = result.alreadyTrashed ? "Contact already trashed" : "Contact trashed";
        return {
            content: [
                {
                    type: "text",
                    text: `${label}: ${result.identifier} (elementId=${result.elementId}). Restore via memory-restore.`,
                },
            ],
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Delete failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
eagerTool(server, "contact-export", "Export all data held on a contact for GDPR Subject Access Request (Article 15). Returns a structured JSON document containing the Person record, AccessGrants, attributed Conversations with Messages, and Emails — suitable for handing to the data subject as-is.", {
    email: z.string().optional().describe("Find contact by email"),
    telephone: z.string().optional().describe("Find contact by phone number"),
    nodeId: z.string().optional().describe("Find contact by node ID"),
}, async ({ email, telephone, nodeId }) => {
    if (!accountId)
        return refuseNoAccount("contact-export");
    try {
        const result = await contactExport({ email, telephone, nodeId, accountId });
        const lines = [
            `## Subject Access Export — ${result.person.properties.givenName ?? "Unknown"}`,
            "",
            `**Person:** ${result.person.properties.email ?? result.person.properties.telephone ?? result.person.nodeId}`,
            `**Status:** ${result.person.properties.status}`,
            `**Source:** ${result.person.properties.source}`,
            `**Created:** ${result.person.properties.createdOn}`,
            "",
            `### Summary`,
            `- Access grants: ${result.summary.accessGrantCount}`,
            `- Conversations: ${result.summary.conversationCount}`,
            `- Messages: ${result.summary.messageCount}`,
            `- Emails: ${result.summary.emailCount}`,
            "",
            "### Full Export Data",
            "",
            "```json",
            JSON.stringify(result, null, 2),
            "```",
        ];
        return {
            content: [{ type: "text", text: lines.join("\n") }],
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Export failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
eagerTool(server, "contact-erase", "Delete all data held on a contact for GDPR Right to Erasure (Article 17). Cascade-deletes the Person node, AccessGrants, attributed Conversations and Messages, and Emails. Without confirm=true, returns a preview of what would be deleted. With confirm=true, executes the deletion and returns a receipt.", {
    email: z.string().optional().describe("Find contact by email"),
    telephone: z.string().optional().describe("Find contact by phone number"),
    nodeId: z.string().optional().describe("Find contact by node ID"),
    confirm: z
        .boolean()
        .optional()
        .describe("Set to true to execute the deletion. Without this, returns a preview only."),
}, async ({ email, telephone, nodeId, confirm }) => {
    if (!accountId)
        return refuseNoAccount("contact-erase");
    try {
        const result = await contactErase({ email, telephone, nodeId, confirm, accountId });
        if (!result.confirmed) {
            const lines = [
                `## Erasure Preview — ${result.identifier}`,
                "",
                "The following data would be deleted:",
                `- Person node: ${result.counts.person}`,
                `- Access grants: ${result.counts.accessGrants}`,
                `- Conversations: ${result.counts.conversations}`,
                `- Messages: ${result.counts.messages}`,
                `- Emails: ${result.counts.emails}`,
                "",
                "**This action is irreversible.** To proceed, call this tool again with `confirm: true`.",
            ];
            return {
                content: [{ type: "text", text: lines.join("\n") }],
            };
        }
        const lines = [
            `## Erasure Receipt — ${result.identifier}`,
            "",
            "All data for this contact has been permanently deleted:",
            `- Person node: ${result.counts.person} deleted`,
            `- Access grants: ${result.counts.accessGrants} deleted`,
            `- Conversations: ${result.counts.conversations} deleted`,
            `- Messages: ${result.counts.messages} deleted`,
            `- Emails: ${result.counts.emails} deleted`,
            "",
            "Note: Server logs may contain residual PII (phone, email, names in error output). Manual log review is recommended for complete erasure.",
        ];
        return {
            content: [{ type: "text", text: lines.join("\n") }],
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Erase failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
eagerTool(server, "group-create", "Create a group conversation with named participants. Returns a shareable link for participants to join. Participants are resolved by name, email, or telephone from existing contacts. The group is bound to a specific public agent — that agent must use access-gated authentication.", {
    groupName: z.string().describe("Display name for the group (e.g. 'Property Viewing — 42 Oak Street')"),
    agentSlug: z.string().describe("The public agent slug this group is for (must be a gated agent)"),
    participants: z.array(z.string()).min(2).describe("Contact identifiers — full names, emails, or phone numbers (minimum 2)"),
}, async ({ groupName, agentSlug, participants }) => {
    if (!accountId)
        return refuseNoAccount("group-create");
    try {
        const result = await groupCreate({
            groupName,
            agentSlug,
            participants,
            accountId,
            createdBy: { agent: agentSlug, session: sessionId, tool: "group-create" },
        });
        const lines = [
            `Group created: **${groupName}**`,
            ``,
            `Participants joined: ${result.joined.join(", ")}`,
        ];
        if (result.notFound.length > 0) {
            lines.push(`Not found (create contacts first): ${result.notFound.join(", ")}`);
        }
        lines.push(``, `Shareable link: ${result.shareableUrl}`);
        return {
            content: [{ type: "text", text: lines.join("\n") }],
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Failed to create group: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
eagerTool(server, "group-manage", "Manage participants in an existing group conversation. Actions: add (add a contact to the group), remove (remove a participant), list (show current participants).", {
    groupSlug: z.string().describe("The group's URL slug (from group-create result)"),
    action: z.enum(["add", "remove", "list"]).describe("Action to perform"),
    participant: z.string().optional().describe("Contact identifier — name, email, or phone (required for add/remove)"),
}, async ({ groupSlug, action, participant }) => {
    if (!accountId)
        return refuseNoAccount("group-manage");
    try {
        const result = await groupManage({ groupSlug, action, participant, accountId });
        if (action === "list") {
            return {
                content: [
                    {
                        type: "text",
                        text: `Group "${result.groupName}" participants:\n${result.participants.map((p, i) => `${i + 1}. ${p}`).join("\n")}`,
                    },
                ],
            };
        }
        return {
            content: [
                {
                    type: "text",
                    text: `${action === "add" ? "Added" : "Removed"} ${participant} ${action === "add" ? "to" : "from"} group "${result.groupName}".\n\nCurrent participants: ${result.participants.join(", ")}`,
                },
            ],
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Group manage failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
process.on("SIGINT", async () => {
    await closeDriver();
    process.exit(0);
});
const transport = new StdioServerTransport();
await server.connect(transport);
//# sourceMappingURL=index.js.map