import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
initStderrTee("loop");
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { keyRegister } from "./tools/key-register.js";
import { keyDeregister } from "./tools/key-deregister.js";
import { keyList } from "./tools/key-list.js";
import { propertySearch } from "./tools/property-search.js";
import { propertyDetail } from "./tools/property-detail.js";
import { propertyListed } from "./tools/property-listed.js";
import { propertyRequest } from "./tools/property-request.js";
import { peopleSearch } from "./tools/people-search.js";
import { peopleDetail } from "./tools/people-detail.js";
import { viewingSearch } from "./tools/viewing-search.js";
import { viewingDetail } from "./tools/viewing-detail.js";
import { viewingCreate } from "./tools/viewing-create.js";
import { viewingUpdate } from "./tools/viewing-update.js";
import { feedbackGet, feedbackSubmit } from "./tools/feedback.js";
import { teamInfo } from "./tools/team-info.js";
import { teamAvailability } from "./tools/team-availability.js";
import { marketingMatchDetail } from "./tools/marketing-match.js";
import { marketingMatchBatch } from "./tools/marketing-match-batch.js";
import { marketingMatchRequest } from "./tools/marketing-match-request.js";
import { marketingEnquiry } from "./tools/marketing-enquiry.js";
import { customerPreferences } from "./tools/customer-preferences.js";
import { supplier } from "./tools/supplier.js";
import { closeDriver } from "./lib/neo4j.js";
const accountId = process.env.ACCOUNT_ID;
if (!accountId) {
    throw new Error("ACCOUNT_ID environment variable is required");
}
const server = new McpServer({
    name: "maxy-real-agency-loop",
    version: "0.2.0",
});
const ALL_PERMISSIONS = z.enum([
    "properties", "people", "viewings", "feedback", "team",
    "marketing", "customer", "supplier",
]);
console.error(`[loop] server started, account=${accountId}`);
// Helper: wrap a tool function in standard error handling
function toolHandler(fn) {
    return async (params) => {
        try {
            const text = await fn({ ...params, accountId });
            return { content: [{ type: "text", text }] };
        }
        catch (err) {
            return {
                content: [{
                        type: "text",
                        text: `Error: ${err instanceof Error ? err.message : String(err)}`,
                    }],
                isError: true,
            };
        }
    };
}
// ─────────────────────────────────────────────────────────────────
// Key Management Tools (3)
// ─────────────────────────────────────────────────────────────────
server.tool("loop-key-register", "Register a Loop CRM API key for a team. Validates the key against Loop's API, encrypts it, and stores it in the graph. Each key maps to one team.", {
    teamName: z.string().min(1).describe("Human-readable name for this team (e.g. 'Muvin Main Office')"),
    apiKey: z.string().min(10).describe("Loop API key (X-Api-Key header value)"),
    permissions: z
        .array(ALL_PERMISSIONS)
        .optional()
        .describe("Endpoint groups this key can access (default: all 8 groups)"),
}, async (params) => {
    try {
        const result = await keyRegister({ ...params, accountId });
        let text = `Team "${params.teamName}" registered successfully.`;
        if (result.warning)
            text += `\n\nWarning: ${result.warning}`;
        return { content: [{ type: "text", text }] };
    }
    catch (err) {
        return {
            content: [{
                    type: "text",
                    text: `Failed to register key: ${err instanceof Error ? err.message : String(err)}`,
                }],
            isError: true,
        };
    }
});
server.tool("loop-key-deregister", "Remove a registered Loop CRM team key. The key is permanently deleted from the graph.", { teamName: z.string().min(1).describe("Name of the team to remove") }, async ({ teamName }) => {
    try {
        await keyDeregister({ teamName, accountId });
        return { content: [{ type: "text", text: `Team "${teamName}" deregistered.` }] };
    }
    catch (err) {
        return {
            content: [{
                    type: "text",
                    text: `Failed to deregister: ${err instanceof Error ? err.message : String(err)}`,
                }],
            isError: true,
        };
    }
});
server.tool("loop-key-list", "List all registered Loop CRM teams for this account. Shows team names, addresses, and permissions. Never reveals API key values.", {}, toolHandler(async (p) => keyList({ accountId: p.accountId })));
// ─────────────────────────────────────────────────────────────────
// People Tools (2) — permission: people
// ─────────────────────────────────────────────────────────────────
server.tool("loop-people-search", "Search people in Loop CRM. Without a role, searches all contacts. With a role (buyers/sellers/renters/landlords), returns role-specific results with rich filters.", {
    role: z.enum(["buyers", "sellers", "renters", "landlords"]).optional().describe("Role to search (omit for all people)"),
    searchTerm: z.string().optional().describe("Name or contact search"),
    maxPrice: z.number().optional().describe("Max price (buyers) or max price filter (sellers)"),
    minPrice: z.number().optional().describe("Min price (sellers)"),
    minBeds: z.number().optional().describe("Min bedrooms (buyers/renters)"),
    maxRent: z.number().optional().describe("Max rent (renters only)"),
    searchAreas: z.string().optional().describe("Comma-separated outcodes e.g. 'SN7,OX12' (buyers/renters)"),
    propertyTypes: z.string().optional().describe("Comma-separated property types (buyers/renters)"),
    startDate: z.string().optional().describe("Start date filter (sellers/landlords)"),
    endDate: z.string().optional().describe("End date filter (sellers/landlords)"),
    teamName: z.string().optional().describe("Query a specific team only"),
    limit: z.number().int().optional().describe("Max results (default 200)"),
}, toolHandler(async (p) => peopleSearch(p)));
server.tool("loop-people-detail", "Get full details for a specific person by ID. Optionally specify a role (buyers/sellers/renters/landlords) for role-specific detail.", {
    personId: z.number().int().describe("Person ID"),
    role: z.enum(["buyers", "sellers", "renters", "landlords"]).optional().describe("Role for role-specific detail view"),
    teamName: z.string().optional().describe("Query a specific team only"),
}, toolHandler(async (p) => peopleDetail(p)));
// ─────────────────────────────────────────────────────────────────
// Property Tools (4) — permission: properties
// ─────────────────────────────────────────────────────────────────
server.tool("loop-property-search", "Search properties in Loop CRM. Queries /property/residential/sales and /property/residential/lettings. Use department to narrow to one type.", {
    department: z.enum(["sales", "lettings", "both"]).optional().describe("Department (default: both)"),
    searchTerm: z.string().optional().describe("Address or keyword search"),
    minPrice: z.number().optional().describe("Minimum price"),
    maxPrice: z.number().optional().describe("Maximum price"),
    minBedrooms: z.number().int().optional().describe("Minimum bedrooms"),
    maxBedrooms: z.number().int().optional().describe("Maximum bedrooms"),
    propertyStatuses: z.string().optional().describe("Comma-separated statuses (e.g. 'forSale,underOffer')"),
    propertyTypes: z.string().optional().describe("Comma-separated property types"),
    teamName: z.string().optional().describe("Query a specific team only"),
    limit: z.number().int().optional().describe("Max results (default 200)"),
}, toolHandler(async (p) => propertySearch(p)));
server.tool("loop-property-detail", "Get full details for a specific property by ID and department. Optionally include a preview hash for the public preview.", {
    propertyId: z.number().int().describe("Property ID"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
    previewHash: z.number().int().optional().describe("Preview hash for public preview URL"),
    teamName: z.string().optional().describe("Query a specific team only"),
}, toolHandler(async (p) => propertyDetail(p)));
server.tool("loop-property-listed", "Get properties listed on a specific channel (Rightmove, Zoopla, OnTheMarket, website). Optionally include sold properties.", {
    channel: z.enum(["rightmove", "onTheMarket", "zoopla", "website"]).describe("Listing channel"),
    department: z.enum(["sales", "lettings", "both"]).optional().describe("Department (default: both)"),
    includeSold: z.boolean().optional().describe("Include sold gallery (default: false)"),
    teamName: z.string().optional().describe("Query a specific team only"),
    limit: z.number().int().optional().describe("Max results (default 200)"),
}, toolHandler(async (p) => propertyListed(p)));
server.tool("loop-property-request", "Submit a viewing, callback, or information request for a property. Requires a specific team.", {
    teamName: z.string().min(1).describe("Team to submit the request through"),
    propertyId: z.number().int().describe("Property ID"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
    action: z.enum(["viewing", "call-back", "information"]).describe("Request type"),
    name: z.string().optional().describe("Requester name"),
    email: z.string().optional().describe("Requester email"),
    phone: z.string().optional().describe("Requester phone"),
    message: z.string().optional().describe("Additional message"),
}, toolHandler(async (p) => propertyRequest(p)));
// ─────────────────────────────────────────────────────────────────
// Viewing Tools (4) — permission: viewings
// ─────────────────────────────────────────────────────────────────
server.tool("loop-viewing-search", "Search viewings in Loop CRM. Queries /residential/sales/viewings and /residential/lettings/viewings.", {
    department: z.enum(["sales", "lettings", "both"]).optional().describe("Department (default: both)"),
    searchTerm: z.string().optional().describe("Search viewings"),
    appointmentStartDate: z.string().optional().describe("Filter from date (ISO format)"),
    appointmentEndDate: z.string().optional().describe("Filter to date (ISO format)"),
    status: z.string().optional().describe("Viewing status filter"),
    teamName: z.string().optional().describe("Query a specific team only"),
    limit: z.number().int().optional().describe("Max results (default 200)"),
}, toolHandler(async (p) => viewingSearch(p)));
server.tool("loop-viewing-detail", "Get full details for a specific viewing by ID and department.", {
    viewingId: z.number().int().describe("Viewing ID"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
    teamName: z.string().optional().describe("Query a specific team only"),
}, toolHandler(async (p) => viewingDetail(p)));
server.tool("loop-viewing-create", "Create a new viewing in Loop CRM. Creates the viewing and associated buyer/renter record.", {
    teamName: z.string().min(1).describe("Team to create the viewing for"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
    propertyId: z.number().int().describe("Property ID"),
    date: z.string().describe("Viewing date (YYYY-MM-DD)"),
    time: z.string().describe("Viewing time (HH:mm)"),
    attendeeName: z.string().describe("Attendee full name"),
    attendeeEmail: z.string().optional().describe("Attendee email"),
    attendeePhone: z.string().optional().describe("Attendee phone"),
}, toolHandler(async (p) => viewingCreate(p)));
server.tool("loop-viewing-update", "Add a note or record feedback for a viewing. For feedback, specify the party: buyer/seller (sales) or renter/landlord (lettings).", {
    teamName: z.string().min(1).describe("Team that owns the viewing"),
    viewingId: z.number().int().describe("Viewing ID"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
    action: z.enum(["note", "feedback"]).describe("note or feedback"),
    text: z.string().describe("Note text or feedback content"),
    feedbackParty: z.enum(["buyer", "seller", "renter", "landlord"]).optional().describe("Required for feedback: whose feedback"),
}, toolHandler(async (p) => viewingUpdate(p)));
// ─────────────────────────────────────────────────────────────────
// Feedback Tools (2) — permission: feedback
// ─────────────────────────────────────────────────────────────────
server.tool("loop-feedback-get", "Get feedback for a specific viewing. Reads from /feedback/residential/{department}/viewings/{id}.", {
    teamName: z.string().min(1).describe("Team that owns the viewing"),
    viewingId: z.number().int().describe("Viewing ID"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
}, toolHandler(async (p) => feedbackGet(p)));
server.tool("loop-feedback-submit", "Submit feedback for a viewing. Writes to /feedback/residential/{department}/viewings/{id}/feedback.", {
    teamName: z.string().min(1).describe("Team that owns the viewing"),
    viewingId: z.number().int().describe("Viewing ID"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
    feedback: z.string().describe("Feedback text"),
}, toolHandler(async (p) => feedbackSubmit(p)));
// ─────────────────────────────────────────────────────────────────
// Team Tools (2) — permission: team
// ─────────────────────────────────────────────────────────────────
server.tool("loop-team-info", "Get team details from Loop CRM. Returns team name, address, phone, and email.", {
    teamName: z.string().optional().describe("Query a specific team only (omit for all teams)"),
}, toolHandler(async (p) => teamInfo(p)));
server.tool("loop-team-availability", "Get available time slots for a specific agent on a given date.", {
    agentId: z.string().describe("Agent GUID identifier"),
    searchDate: z.string().describe("Date to check availability (YYYY-MM-DD)"),
    teamName: z.string().optional().describe("Query a specific team only"),
}, toolHandler(async (p) => teamAvailability(p)));
// ─────────────────────────────────────────────────────────────────
// Marketing Tools (4) — permission: marketing
// ─────────────────────────────────────────────────────────────────
server.tool("loop-marketing-match", "Get matching property detail and optionally the team profile for that match.", {
    propertyId: z.number().int().describe("Property ID"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
    includeTeamProfile: z.boolean().optional().describe("Include the team profile for this match (default: false)"),
    teamName: z.string().optional().describe("Query a specific team only"),
}, toolHandler(async (p) => marketingMatchDetail(p)));
server.tool("loop-marketing-match-batch", "Get batch matching results for multiple property IDs.", {
    propertyIds: z.array(z.number().int()).describe("Array of property IDs to match"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
    teamName: z.string().optional().describe("Query a specific team only"),
}, toolHandler(async (p) => marketingMatchBatch(p)));
server.tool("loop-marketing-match-request", "Submit a viewing, information, or callback request for a matching property.", {
    teamName: z.string().min(1).describe("Team to submit the request through"),
    propertyId: z.number().int().describe("Matching property ID"),
    department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
    action: z.enum(["viewing", "information", "callback"]).describe("Request type"),
    name: z.string().optional().describe("Requester name"),
    email: z.string().optional().describe("Requester email"),
    phone: z.string().optional().describe("Requester phone"),
    message: z.string().optional().describe("Additional message"),
}, toolHandler(async (p) => marketingMatchRequest(p)));
server.tool("loop-marketing-enquiry", "Marketing enquiry operations: submit seller enquiries, manage auto-responder interactions.", {
    teamName: z.string().min(1).describe("Team to submit through"),
    action: z.enum([
        "seller-enquiry", "autoresponder-get", "autoresponder-answers",
        "autoresponder-details", "autoresponder-refer",
    ]).describe("Enquiry action"),
    sellerEnquiryData: z.record(z.string(), z.unknown()).optional().describe("Seller enquiry request body (for seller-enquiry)"),
    autoResponderId: z.number().int().optional().describe("Auto-responder enquiry ID"),
    autoResponderKey: z.string().optional().describe("Auto-responder key (UUID)"),
    answers: z.array(z.unknown()).optional().describe("Auto-responder answers array"),
    details: z.record(z.string(), z.unknown()).optional().describe("Auto-responder details object"),
}, toolHandler(async (p) => marketingEnquiry(p)));
// ─────────────────────────────────────────────────────────────────
// Customer Tools (1) — permission: customer
// ─────────────────────────────────────────────────────────────────
server.tool("loop-customer-preferences", "Read or write customer preferences for a person. Action 'read' returns current preferences, 'write' updates them.", {
    teamName: z.string().min(1).describe("Team for this operation"),
    personCode: z.number().int().describe("Person code (ID)"),
    action: z.enum(["read", "write"]).describe("read or write"),
    preferences: z.record(z.string(), z.unknown()).optional().describe("Preferences object (required for write)"),
}, toolHandler(async (p) => customerPreferences(p)));
// ─────────────────────────────────────────────────────────────────
// Supplier Tools (1) — permission: supplier
// ─────────────────────────────────────────────────────────────────
server.tool("loop-supplier", "Supplier operations: maintenance jobs, quotes, board contractor jobs. Supports listing, completing, and quoting.", {
    teamName: z.string().min(1).describe("Team for this operation"),
    action: z.enum([
        "maintenance-jobs", "maintenance-complete", "maintenance-quotes",
        "maintenance-submit-quote", "board-jobs", "board-complete",
    ]).describe("Supplier action"),
    code: z.string().describe("Unique code assigned to the contractor"),
    jobId: z.number().int().optional().describe("Job ID (required for all actions except maintenance-submit-quote)"),
    quoteId: z.number().int().optional().describe("Quote ID (for maintenance-submit-quote)"),
    quoteData: z.record(z.string(), z.unknown()).optional().describe("Quote data (for maintenance-submit-quote)"),
    completionData: z.record(z.string(), z.unknown()).optional().describe("Completion data (for board-complete)"),
}, toolHandler(async (p) => supplier(p)));
// ─────────────────────────────────────────────────────────────────
// Lifecycle
// ─────────────────────────────────────────────────────────────────
process.on("SIGINT", async () => {
    await closeDriver();
    process.exit(0);
});
const transport = new StdioServerTransport();
await server.connect(transport);
//# sourceMappingURL=index.js.map