UNPKG

@withpica/mcp-server

Version:

MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog

992 lines • 54.5 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { elicitBulkConfirmation, formatSuccess, formatList, } from "@withpica/mcp-utils";
/**
 * What to tell an agent whose invite email did not arrive. The invite and
 * resend responses carry the link once (`invite_url`; the server keeps only
 * its hash), so the link itself goes in the message. A response without one
 * (an older server) falls back to the claim code.
 */
function shareTheLink(result) {
    const url = result && typeof result === "object"
        ? result.invite_url
        : undefined;
    return typeof url === "string" && url.length > 0
        ? `share this link with them directly (it is shown only this once): ${url}`
        : "share the invite's claim code";
}
function capitalise(text) {
    return text.charAt(0).toUpperCase() + text.slice(1);
}
export class CollaboratorsTools {
    pica;
    constructor(pica) {
        this.pica = pica;
    }
    getTools() {
        return [
            {
                definition: {
                    name: "pica_collaborators_invite",
                    scope: "write:people",
                    audiences: ["stdio", "http", "public"],
                    tier: "write",
                    description: "Invite someone to verify their credit on a work. Use when the " +
                        "user asks: 'let's notify the people on this song', 'ask everyone " +
                        "to verify', 'get my collaborators to confirm'. Reach them by " +
                        "email, or by their PICA handle if they're on another org — with " +
                        "a handle, PICA finds the email itself and the sending agent " +
                        "never sees it. Creates the person if they're new, sends the " +
                        "invite, and sets up their credit. If it's unclear which the user " +
                        "has, ask: 'do you have their email, or their PICA handle?' " +
                        "Targets a work, a recording, or a whole release. " +
                        "Prefer pica_collaborators_invite_bulk for several people on the " +
                        "same work, by email.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            work_id: {
                                type: "string",
                                description: "The work ID to invite the collaborator to. Exactly one of work_id / recording_id / release_id.",
                            },
                            recording_id: {
                                type: "string",
                                description: "Recording ID — invite someone to verify their credit on this recording (Producer, Mixer, etc.). Exactly one of work_id / recording_id / release_id.",
                            },
                            release_id: {
                                type: "string",
                                description: "Release ID — invite someone to verify ALL their credits across this release's tracks (both recording credits and composition credits) in one action. Exactly one of work_id / recording_id / release_id.",
                            },
                            name: {
                                type: "string",
                                description: "Full name of the collaborator",
                            },
                            email: {
                                type: "string",
                                description: "Email address of the collaborator. Either this OR `handle` must be set. When `handle` is set, leave this empty — PICA resolves server-side.",
                            },
                            handle: {
                                type: "string",
                                description: "PICA public handle (e.g. 'josh-allen'). Resolved server-side to the recipient's email — sender's agent never learns the email. Use `pica_users_find_by_handle` first to confirm the handle and the recipient's `handle_accepts_invites` flag. Either this OR `email` must be set.",
                            },
                            role: {
                                type: "string",
                                description: "Role on the work (e.g. 'producer', 'songwriter', 'drummer', 'vocalist')",
                            },
                            invited_by: {
                                type: "string",
                                description: "Person ID of the person sending the invite (the catalog owner). Use pica_people_query to find this ID.",
                            },
                            credit_type: {
                                type: "string",
                                enum: [
                                    "writer",
                                    "composer",
                                    "lyricist",
                                    "arranger",
                                    "producer",
                                    "performer",
                                    "vocalist",
                                    "instrumentalist",
                                    "engineer",
                                    "mixer",
                                    "mastering",
                                    "programmer",
                                    "remixer",
                                    "conductor",
                                ],
                                description: "Type of credit. Use one of the enum values (e.g. 'producer', 'writer', 'vocalist', 'instrumentalist'). Common synonyms are normalised server-side (production→producer, songwriting→writer, drums/guitar/bass→instrumentalist, vocals→vocalist, mixing→mixer), but prefer the canonical value.",
                            },
                            percentage_split: {
                                type: "number",
                                description: "Percentage split for this credit (0-100)",
                            },
                        },
                        required: ["invited_by", "credit_type"],
                    },
                },
                executor: this.inviteCollaborator.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_invites_list",
                    scope: "read:people",
                    audiences: ["stdio", "http", "public"],
                    tier: "read",
                    description: "See the invites the user has sent and where each one stands. Use " +
                        "when the user asks: 'who's pending invites?', 'who hasn't " +
                        "accepted yet?', 'outstanding invites'. Filter by status (pending " +
                        "/ verified / confirmed / expired / cancelled / disputed) or by " +
                        "work. For invites sent TO the user, see " +
                        "pica_collaborators_incoming_list instead.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            status: {
                                type: "string",
                                description: "Filter by status (default: 'pending')",
                                enum: [
                                    "pending",
                                    "verified",
                                    "confirmed",
                                    "expired",
                                    "cancelled",
                                    "disputed",
                                ],
                            },
                            work_id: {
                                type: "string",
                                description: "Filter invites for a specific work",
                            },
                            limit: {
                                type: "number",
                                description: "Maximum results to return (default: 50, max: 100)",
                            },
                        },
                    },
                },
                executor: this.listInvites.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_incoming_list",
                    scope: "read:people",
                    audiences: ["stdio", "http", "public"],
                    tier: "read",
                    description: "List collaboration invites addressed to YOUR email across every inviting org. Returns pending invites by default — pass status to see disputed/confirmed history. Each row includes work_title, inviter_org_name, credit_type, and percentage_split so you can decide whether to accept. " +
                        "For invites the user sent instead, use " +
                        "pica_collaborators_invites_list.",
                    workflows: "infrastructure",
                    vernacular_kind: "specialist",
                    vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
                    inputSchema: {
                        type: "object",
                        properties: {
                            status: {
                                type: "string",
                                description: "Filter by status (default: 'pending')",
                                enum: [
                                    "pending",
                                    "verified",
                                    "confirmed",
                                    "expired",
                                    "cancelled",
                                    "disputed",
                                ],
                            },
                            limit: {
                                type: "number",
                                description: "Maximum results to return (default: 50, max: 100)",
                            },
                        },
                    },
                },
                executor: this.listIncoming.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_accept",
                    scope: "write:people",
                    audiences: ["stdio", "http", "public"],
                    tier: "write",
                    description: "Accept a collaboration invite addressed to your email. Writes a real collaborator credit row (work_collaborators) on the inviter's work and notifies them. Fails if the invite is not addressed to your authenticated email. Use pica_collaborators_incoming_list to find the invite_id first.",
                    workflows: "infrastructure",
                    vernacular_kind: "specialist",
                    vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
                    inputSchema: {
                        type: "object",
                        properties: {
                            invite_id: {
                                type: "string",
                                description: "The invite id to accept",
                            },
                        },
                        required: ["invite_id"],
                    },
                },
                executor: this.acceptInvite.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_dispute",
                    scope: "write:people",
                    audiences: ["stdio", "http", "public"],
                    tier: "write",
                    description: "Push back on an invite that isn't right — 'that's not me' or 'I " +
                        "wasn't on this work'. Use when the user asks: 'I disagree with " +
                        "this', 'dispute that', 'contest this credit'. Marks the invite " +
                        "as disputed and lets the sender know. No credit is created. " +
                        "Prefer pica_collaborators_decline_by_code when the user has a " +
                        "claim code rather than an invite id.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            invite_id: {
                                type: "string",
                                description: "The invite id to dispute",
                            },
                            reason: {
                                type: "string",
                                description: "Optional freeform reason for the dispute",
                            },
                        },
                        required: ["invite_id"],
                    },
                },
                executor: this.disputeInvite.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_invite_bulk",
                    scope: "write:people",
                    audiences: ["stdio", "http", "public"],
                    tier: "write",
                    description: "Send multiple collaboration invites in one call. Every row is a " +
                        "work invite by email — work_id, name, email, invited_by and " +
                        "credit_type, with role and percentage_split optional. It cannot " +
                        "take a recording, a release or a PICA handle, so for those send " +
                        "one at a time with pica_collaborators_invite. Returns per-row " +
                        "{ ok, email, invite?, error? } so partial failures are visible.",
                    workflows: "infrastructure",
                    vernacular_kind: "specialist",
                    vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
                    inputSchema: {
                        type: "object",
                        properties: {
                            invites: {
                                type: "array",
                                description: "Array of invites to send",
                                items: {
                                    type: "object",
                                    properties: {
                                        work_id: {
                                            type: "string",
                                            description: "UUID of the work to invite the collaborator to",
                                        },
                                        name: {
                                            type: "string",
                                            description: "Full name of the collaborator",
                                        },
                                        email: {
                                            type: "string",
                                            description: "Email address to send the invite to",
                                        },
                                        role: {
                                            type: "string",
                                            description: "Role on the work (e.g. writer, producer)",
                                        },
                                        invited_by: {
                                            type: "string",
                                            description: "Name of the person sending the invite",
                                        },
                                        credit_type: {
                                            type: "string",
                                            enum: [
                                                "writer",
                                                "composer",
                                                "lyricist",
                                                "arranger",
                                                "producer",
                                                "performer",
                                                "vocalist",
                                                "instrumentalist",
                                                "engineer",
                                                "mixer",
                                                "mastering",
                                                "programmer",
                                                "remixer",
                                                "conductor",
                                            ],
                                            description: "Type of credit. Use one of the enum values (e.g. 'producer', 'writer'). Synonyms are normalised server-side but prefer the canonical value.",
                                        },
                                        percentage_split: {
                                            type: "number",
                                            description: "Split percentage (0-100)",
                                        },
                                    },
                                    required: [
                                        "work_id",
                                        "name",
                                        "email",
                                        "invited_by",
                                        "credit_type",
                                    ],
                                },
                            },
                            dry_run: {
                                type: "boolean",
                                description: "When true, returns { dry_run: true, would_affect, sample } without sending any invite emails. Bulk invites send real email — preview before committing.",
                            },
                        },
                        required: ["invites"],
                    },
                    previewMode: "dry_run",
                },
                executor: this.inviteBulk.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_invite_resend",
                    scope: "write:people",
                    audiences: ["stdio", "http", "public"],
                    tier: "write",
                    description: "Resend a pending collaboration invite. Regenerates the invite token and sends a new email. " +
                        "Prefer pica_collaborators_invite_revoke when the user wants to " +
                        "withdraw the invite rather than send it again.",
                    workflows: "infrastructure",
                    vernacular_kind: "specialist",
                    vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
                    inputSchema: {
                        type: "object",
                        properties: {
                            invite_id: {
                                type: "string",
                                description: "The invite ID to resend",
                            },
                        },
                        required: ["invite_id"],
                    },
                },
                executor: this.resendInvite.bind(this),
            },
            // ADR-236 — code-based recipient claim / decline + sender status/revoke
            {
                definition: {
                    name: "pica_collaborators_claim_by_code",
                    scope: "write:people",
                    audiences: ["stdio", "http"],
                    tier: "write",
                    description: "Preview or accept an invite the user received, using its claim " +
                        "code. Use when the user asks: 'I have a code from someone', " +
                        "'accept the invite ABCD-23N7', 'preview the invite, then " +
                        "accept'. Call with accept: false first to show the proposed " +
                        "credit, then again with accept: true to confirm — the user " +
                        "should see what they're agreeing to before they say yes. " +
                        "Prefer pica_collaborators_accept when the user has an invite id " +
                        "from pica_collaborators_incoming_list rather than a code.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            code: {
                                type: "string",
                                description: "8-char claim code (hyphenated form like DKZH-4N7P is fine; normalised server-side)",
                            },
                            accept: {
                                type: "boolean",
                                description: "false (default) → preview only, no mutation; true → atomically accept the invite and write the collaborator credit in the inviter's org scope.",
                            },
                        },
                        required: ["code"],
                    },
                },
                executor: this.claimByCode.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_decline_by_code",
                    scope: "write:people",
                    audiences: ["stdio", "http"],
                    tier: "write",
                    description: "Decline an invite the user received, using its claim code. Use " +
                        "when the user asks: 'decline the invite ABCD-23N7', 'I don't " +
                        "want this credit'. You can pass a short reason (up to 500 " +
                        "chars), which the sender will see. " +
                        "Prefer pica_collaborators_counter_propose when the user wants " +
                        "different terms rather than to refuse outright.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            code: {
                                type: "string",
                                description: "8-char claim code",
                            },
                            reason: {
                                type: "string",
                                description: "Optional free-text reason for declining (max 500 chars). Sent to the inviter; helps avoid re-invitation loops.",
                            },
                        },
                        required: ["code"],
                    },
                },
                executor: this.declineByCode.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_invite_status",
                    scope: "read:people",
                    audiences: ["stdio", "http"],
                    tier: "read",
                    description: "Check where a single invite the user sent has got to. Use when " +
                        "the user asks: 'has Josh accepted yet?', 'what's the status of " +
                        "the invite I just sent?', 'when does that invite expire?', 'show " +
                        "me the negotiation history'. Pass invite_id, or work_id to get " +
                        "that work's most recent invite. Add include_history: true to see " +
                        "the full back-and-forth of counter-proposals. " +
                        "For every outstanding invite at once, use " +
                        "pica_collaborators_invites_list; this one covers a single one.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            invite_id: {
                                type: "string",
                                description: "The invite ID to look up",
                            },
                            work_id: {
                                type: "string",
                                description: "Work ID — returns the most recently created invite for this work in the caller's org",
                            },
                            include_history: {
                                type: "boolean",
                                default: false,
                                description: "When true, response includes `counters` — the chronological counter-proposal chain (each entry: proposed_by_user_id, proposed_at, credit_type, percentage_split, message, status, decided_at, decided_by_user_id). Defaults to false for backwards compatibility.",
                            },
                        },
                    },
                },
                executor: this.inviteStatus.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_invite_revoke",
                    scope: "write:people",
                    audiences: ["stdio", "http"],
                    tier: "destructive",
                    description: "Pull back an invite the user already sent. Use when the user " +
                        "asks: 'cancel that invite', 'revoke the invite I just sent', " +
                        "'pull the invite to Josh'. Two steps, since it can't be undone: " +
                        "the first call returns a confirmation token and a summary to " +
                        "show the user, the second call (with that token) revokes it and " +
                        "kills both the link and the code. If the recipient is already a " +
                        "PICA user, they're told it was withdrawn. " +
                        "Prefer pica_collaborators_invite_resend when the user only wants " +
                        "to nudge the recipient again.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            invite_id: {
                                type: "string",
                                description: "The invite ID to revoke",
                            },
                            confirmation_token: {
                                type: "string",
                                description: "Second-call confirmation token returned by the first call. Required to actually mutate.",
                            },
                        },
                        required: ["invite_id"],
                    },
                },
                executor: this.revokeInvite.bind(this),
            },
            // ADR-238 — agent-mediated counter-proposals on collaboration invites
            {
                definition: {
                    name: "pica_collaborators_counter_propose",
                    scope: "write:people",
                    audiences: ["stdio", "http"],
                    tier: "write",
                    description: "Propose different terms on an invite the user received. Use when " +
                        "the user asks: 'I want 40 not 50', 'counter that invite at 35 " +
                        "percent', 'propose a different split'. Pass the new credit_type, " +
                        "percentage_split, and an optional note. The sender can then " +
                        "accept, decline, or counter back — up to 5 rounds between the " +
                        "two parties. For the sender's reply to a counter, use " +
                        "pica_collaborators_counter_back; this one is the recipient's.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            code: {
                                type: "string",
                                description: "8-char claim code from the invite (hyphenated form like DKZH-4N7P is fine; normalised server-side)",
                            },
                            credit_type: {
                                type: "string",
                                description: "The counter-proposed credit role (writer, composer, lyricist, arranger, producer, performer, engineer, etc.). May differ from the original.",
                            },
                            percentage_split: {
                                type: "number",
                                description: "Counter-proposed split, 0-100",
                            },
                            message: {
                                type: "string",
                                description: "Optional free-text reasoning (max 500 chars). Surfaced to the sender alongside the counter values.",
                            },
                        },
                        required: ["code", "credit_type", "percentage_split"],
                    },
                },
                executor: this.proposeCounter.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_accept_counter",
                    scope: "write:people",
                    audiences: ["stdio", "http"],
                    tier: "write",
                    description: "Accept the latest counter-offer and lock in the credit. Use when " +
                        "the user asks: 'accept their counter', 'yes, agree to 40 " +
                        "percent', 'take the counter from Josh'. Writes the credit using " +
                        "the counter's terms (not the original invite's) and confirms it. " +
                        "The original recipient stays the one who holds the credit, " +
                        "whoever proposed the final terms. Prefer " +
                        "pica_collaborators_decline_counter when the user rejects it.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            counter_id: {
                                type: "string",
                                description: "UUID of the pending counter to accept (returned by counter_propose / counter_back; also surfaced in pica_collaborators_invite_status with include_history)",
                            },
                        },
                        required: ["counter_id"],
                    },
                },
                executor: this.acceptCounter.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_decline_counter",
                    scope: "write:people",
                    audiences: ["stdio", "http"],
                    tier: "write",
                    description: "Turn down the latest counter-offer. Use when the user asks: " +
                        "'decline that counter', 'no, 35 is too low', 'reject the counter " +
                        "from Josh'. The invite goes back to pending, so the original " +
                        "terms are on the table again and either side can move next " +
                        "(counter back, accept the original, or revoke). A short reason " +
                        "(up to 500 chars) reaches the other party. Prefer " +
                        "pica_collaborators_counter_back when the user wants to answer " +
                        "with new terms rather than just refuse.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            counter_id: {
                                type: "string",
                                description: "UUID of the pending counter to decline",
                            },
                            reason: {
                                type: "string",
                                description: "Optional free-text reason (max 500 chars). Surfaced to the proposer via discovery event.",
                            },
                        },
                        required: ["counter_id"],
                    },
                },
                executor: this.declineCounter.bind(this),
            },
            {
                definition: {
                    name: "pica_collaborators_counter_back",
                    scope: "write:people",
                    audiences: ["stdio", "http"],
                    tier: "write",
                    description: "Come back with a new offer after the recipient countered. Use " +
                        "when the user asks: 'counter back at 45', 'meet them halfway, " +
                        "propose 45 percent', 'come back at 42'. Replaces the recipient's " +
                        "counter with the user's new one and notifies them — up to 5 " +
                        "rounds between the two parties. Prefer " +
                        "pica_collaborators_accept_counter when the user is happy with " +
                        "the recipient's terms as they stand.",
                    workflows: "infrastructure",
                    vernacular_kind: "creator-entry",
                    inputSchema: {
                        type: "object",
                        properties: {
                            counter_id: {
                                type: "string",
                                description: "UUID of the recipient's counter to respond to (pending or recently declined)",
                            },
                            credit_type: {
                                type: "string",
                                description: "Sender's new proposed credit role (writer, composer, producer, etc.)",
                            },
                            percentage_split: {
                                type: "number",
                                description: "Sender's new proposed split, 0-100",
                            },
                            message: {
                                type: "string",
                                description: "Optional free-text reasoning (max 500 chars). Surfaced to the recipient alongside the counter values.",
                            },
                        },
                        required: ["counter_id", "credit_type", "percentage_split"],
                    },
                },
                executor: this.counterBack.bind(this),
            },
        ];
    }
    async inviteCollaborator(args) {
        // ADR-237: discriminated-union recipient. Exactly one of `handle`
        // or `email` must be set. When `handle` is set, the SDK forwards
        // it to the route handler which resolves to email server-side.
        const handle = typeof args.handle === "string" && args.handle.trim() !== ""
            ? args.handle.trim()
            : null;
        const email = typeof args.email === "string" && args.email.trim() !== ""
            ? args.email.trim()
            : null;
        if (handle && email) {
            throw new Error("Pass either `handle` or `email`, not both. Use `handle` for ADR-237 cross-org discovery (PICA resolves to email server-side).");
        }
        if (!handle && !email) {
            throw new Error("One of `handle` or `email` is required.");
        }
        const targetCount = [
            args.work_id,
            args.recording_id,
            args.release_id,
        ].filter(Boolean).length;
        if (targetCount !== 1) {
            throw new Error("Exactly one of `work_id`, `recording_id`, `release_id` is required.");
        }
        const inviteParams = {
            workId: args.work_id,
            recordingId: args.recording_id,
            releaseId: args.release_id,
            personData: {
                name: args.name,
                // Email is intentionally omitted when handle is the source —
                // sendInvite resolves via handleService and splices in the
                // canonical workspace_email.
                ...(email ? { email } : {}),
                role: args.role,
            },
            invitedBy: args.invited_by,
            creditData: {
                credit_type: args.credit_type,
                percentage_split: args.percentage_split,
            },
        };
        if (handle) {
            inviteParams.recipient = { kind: "handle", value: handle };
        }
        else if (email) {
            inviteParams.recipient = { kind: "email", value: email };
        }
        const result = await this.pica.collaborators.invite(inviteParams);
        // ADR-264 W2d: emit the collaborator_missing_identifiers hint ONLY when
        // the invited person currently lacks BOTH IPI and ISNI in person_identifiers.
        //
        // The route now appends person_id + has_ipi + has_isni to the response so
        // this check works in production (not just in mocked tests). Three cases:
        //   1. person_id null (brand-new person, not yet in people table) —
        //      conservatively emit the standard hint.
        //   2. person_id set, has_ipi || has_isni true — suppress hint.
        //   3. person_id set, both false — emit the standard hint.
        // If the response doesn't carry the identity fields (unexpected shape),
        // fall back to the conservative "emit" path — never wrongly suppress.
        const hints = (() => {
            const r = result;
            const personId = r.person_id;
            const hasIpi = r.has_ipi;
            const hasIsni = r.has_isni;
            if (!personId || hasIpi === undefined || hasIsni === undefined) {
                // Fix 5: unexpected shape (brand-new person or route didn't supply
                // identity fields) — use softer "could not confirm" wording and
                // "nice_to_have" severity to distinguish this read-failure / uncertain path (CompletionHint has no "info")
                // from the confirmed-gap path below (which has "important" severity
                // and asserts the absence positively).
                return [
                    {
                        gap: "collaborator_missing_identifiers",
                        suggestion: `could not confirm whether ${args.name ?? "the recipient"} has an IPI/ISNI — check before enriching (pica_resolve_person)`,
                        severity: "nice_to_have",
                    },
                ];
            }
            if (hasIpi || hasIsni) {
                // Person already has an industry identifier — hint not needed.
                return [];
            }
            // Confirmed gap: person exists and has neither IPI nor ISNI.
            return [
                {
                    gap: "collaborator_missing_identifiers",
                    suggestion: `Enrich ${args.name ?? "the recipient"} with ISNI or IPI for royalty collection (pica_resolve_person)`,
                    severity: "important",
                },
            ];
        })();
        // ADR-237: when handle was the source, never echo email back in
        // the success message — the sender's agent must not learn the
        // recipient's email. The route handler is responsible for
        // stripping email from `result` too; we belt-and-brace at the
        // tool layer.
        const recipientLabel = handle
            ? `@${handle}`
            : `${args.name ?? "recipient"} (${email})`;
        if (handle && result && typeof result === "object") {
            // Defensive scrub of common email fields the underlying invite
            // payload may carry. Display fields (handle, display_name)
            // stay; PII does not.
            const r = result;
            delete r.email;
            if (r.person_data && typeof r.person_data === "object") {
                delete r.person_data.email;
            }
        }
        const target = args.work_id
            ? `work ${args.work_id}`
            : args.recording_id
                ? `recording ${args.recording_id}`
                : `release ${args.release_id}`;
        // Don't claim "sent" when the email actually failed to deliver. The
        // invite row is persisted either way; email_delivered:false means the
        // recipient got no email (Postmark suppression / rate-limit / bounce /
        // circuit-breaker) and needs a resend or the link shared directly.
        // `undefined` (older route without the field) keeps the plain message.
        const emailDelivered = result && typeof result === "object"
            ? result.email_delivered
            : undefined;
        // The invite response carries the link once (`invite_url`); only its
        // hash is stored, so this is the one moment it can be handed on.
        const message = emailDelivered === false
            ? `Invite created for ${recipientLabel} on ${target} (credit "${args.credit_type}"), but the invitation email could not be delivered. Resend with pica_collaborators_invite_resend, or ${shareTheLink(result)}.`
            : `Invite sent to ${recipientLabel} for credit "${args.credit_type}" on ${target}.`;
        return formatSuccess(message, result, hints);
    }
    async listInvites(args) {
        // ADR-157 sub-step 4b (superseded form): the original plan surfaced
        // an "events since last review" watermark hint, but sub-step 4a
        // already delivers confirmed/disputed receipts instantly via
        // Telegram — making the duplicated completion-hint surface
        // redundant. ADR-160 reinforces the receipts-layer principle. All
        // that remains is nudging unpaired orgs towards Telegram, via a
        // single parallel call to the existing telegram status endpoint.
        // Status-check failure degrades to "no nudge" so the primary read
        // path stays resilient.
        const [invites, telegramStatus] = await Promise.all([
            this.pica.collaborators.listInvites({
                status: args.status,
                workId: args.work_id,
                limit: args.limit,
            }),
            this.pica.telegram.getStatus().catch(() => null),
        ]);
        const hints = [];
        if (telegramStatus && telegramStatus.connected === false) {
            hints.push({
                gap: "telegram_not_connected",
                suggestion: "Want collaborator accept/dispute receipts delivered instantly? Pair Telegram with pica_telegram_connect — the message body is the receipt, no dashboard trip.",
                severity: "nice_to_have",
            });
        }
        return formatList(invites, {
            status: args.status || "pending",
            completion_hints: hints,
        });
    }
    async resendInvite(args) {
        const result = await this.pica.collaborators.resendInvite(args.invite_id);
        const delivered = result && typeof result === "object"
            ? result.email_delivered
            : undefined;
        return formatSuccess(delivered === false
            ? `Invite ${args.invite_id} was renewed, but the reminder email could not be delivered. ${capitalise(shareTheLink(result))}.`
            : `Invite ${args.invite_id} resent successfully.`, result);
    }
    async listIncoming(args) {
        const invites = await this.pica.collaborators.listIncoming({
            status: args.status,
            limit: args.limit,
        });
        const hints = [];
        if (invites.length > 0) {
            hints.push({
                gap: "pending_incoming_invites",
                suggestion: `You have ${invites.length} pending invite${invites.length === 1 ? "" : "s"}. Accept with pica_collaborators_accept or reject with pica_collaborators_dispute.`,
                severity: "important",
            });
        }
        return formatList(invites, {
            status: args.status || "pending",
            count: invites.length,
            completion_hints: hints,
        });
    }
    async acceptInvite(args) {
        const result = await this.pica.collaborators.accept(args.invite_id);
        const hints = [
            {
                gap: "credit_attested",
                suggestion: "Your credit is now live on the inviter's work. Use pica_works_query to see works you're credited on.",
                severity: "nice_to_have",
            },
        ];
        return formatSuccess(`Invite ${args.invite_id} accepted. Credit written.`, result, hints);
    }
    async disputeInvite(args) {
        const result = await this.pica.collaborators.dispute(args.invite_id, args.reason);
        return formatSuccess(`Invite ${args.invite_id} disputed. The inviter has been notified.`, result);
    }
    async inviteBulk(args, ctx) {
        // ADR-199 Stream 5b — dry_run preview before invite-email send.
        // Bulk invites send real email through Postmark; dry_run lets the
        // caller verify the recipient list before triggering the send.
        if (args.dry_run === true) {
            const invites = args.invites || [];
            const sample = invites.slice(0, 3).map((i) => ({
                email: i.email,
                name: i.name,
                work_id: i.work_id,
                credit_type: i.credit_type,
            }));
            return formatSuccess(`Dry run — would send ${invites.length} collaboration invite(s)`, {
                dry_run: true,
                would_affect: invites.map((i) => i.email),
                sample,
                target_count: invites.length,
            });
        }
        // ADR-200 Phase 1 — Class D form-confirmation gate. Bulk invites
        // dispatch real email through Postmark; once sent, they cannot be
        // unsent. Surface the recipient count and a sample for the user to
        // confirm before the batch fires.
        const invitesArr = args.invites || [];
        const sampleEmails = invitesArr
            .slice(0, 3)
            .map((i) => i.email)
            .join(", ");
        const tail = invitesArr.length > 3 ? `, +${invitesArr.length - 3} more` : "";
        const gate = await elicitBulkConfirmation({
            server: ctx?.server,
            args,
            action: "Send bulk collaboration invites",
            impactSummary: `${invitesArr.length} invite(s) (${sampleEmails}${tail})`,
            warning: "Each invite triggers a real email; sent emails cannot be recalled.",
        });
        if (gate.kind === "abort")
            return gate.result;
        const results = await this.pica.collaborators.inviteBulk(invitesArr.map((i) => ({
            workId: i.work_id,
            personData: { name: i.name, email: i.email, role: i.role },
            invitedBy: i.invited_by,
            creditData: {
                credit_type: i.credit_type,
                percentage_split: i.percentage_split,
            },
        })));
        // Classify per-row outcomes instead of a flat sent/failed split. Two
        // distinct non-success shapes were previously conflated: an invite that
        // was CREATED but whose email bounced comes back ok:true with
        // email_delivered:false (delivery is non-fatal — the invite link still
        // works), while a recipient that could not be resolved at all throws
        // recipient_not_found and used to be reported as a bare "failed" with a
        // raw error string.
        const rows = results;
        const undeliverable = rows.filter((r) => r.ok && r.invite?.email_delivered === false);
        const sent = rows.filter((r) => r.ok && r.invite?.email_delivered !== false);
        const notFound = rows.filter((r) => !r.ok && /recipient_not_found|handle not found/i.test(r.error ?? ""));
        const failed = rows.filter((r) => !r.ok && !/recipient_not_found|handle not found/i.test(r.error ?? ""));
        const parts = [`${sent.length} sent`];
        if (undeliverable.length > 0) {
            parts.push(`${undeliverable.length} created but the email couldn't be delivered (resend with pica_collaborators_invite_resend, or share each invite's link directly: invite_url in its result, shown only this once)`);
        }
        if (notFound.length > 0) {
            parts.push(`${notFound.length} recipient(s) not found (${notFound
                .map((r) => r.email)
                .filter(Boolean)
                .join(", ")}) — check the handle/email or create the person first`);
        }
        if (failed.length > 0)
            parts.push(`${failed.length} failed`);
        return formatSuccess(`Bulk invite complete: ${parts.join("; ")}.`, {
            results,
            ok: sent.length + undeliverable.length,
            sent: sent.length,
            undeliverable: undeliverable.length,
            recipient_not_found: notFound.length,
            failed: failed.length,
        });
    }
    // ADR-236 — code-based recipient claim / decline + sender status/revoke
    async claimByCode(args) {
        const accept = args.accept === true;
        if (!accept) {
            const preview = await this.pica.collaborators.previewByCode(args.code);
            return formatSuccess(`Invite preview for code ${args.code}: "${preview.work_title ?? "(work)"}" — ${preview.credit_type}${preview.percentage_split ? ` at ${preview.percentage_split}%` : ""} from ${preview.inviter_display_name ?? "the inviter"}. Call again with accept=true to confirm.`, preview);
        }
        const result = await this.pica.collaborators.acceptByCode(args.code);
        const hints = [
            {
                gap: "credit_attested",
                suggestion: "Your credit is now live on the inviter's work. Use pica_collaborations_received to see all works you're credited on across orgs.",
                severity: "nice_to_have",
            },
        ];
        return formatSuccess(`Invite claimed by code ${args.code}. Credit written in the inviter's catalog.`, result, hints);
    }
    async declineByCode(args) {
        const result = await this.pica.collaborators.declineByCode(args.code, args.reason);
        return formatSuccess(`Invite declined by code ${args.code}. The inviter's agent will surface the decline on their next prompt.`, result);
    }
    async inviteStatus(args) {
        const result = await this.pica.collaborators.inviteStatus({
            invite_id: args.invite_id,
            work_id: args.work_id,
            include_history: args.include_history === true,
        });
        return formatSuccess(`Invite status retrieved.`, result);
    }
    async revokeInvite(args) {
        // The two-step confirmation_token gate is enforced upstream in the
        // tool dispatcher (mcp-server/src/tools/index.ts) for any tool with
        // tier: "destructive". By the time this executor runs, the token has
        // been validated and consumed; we just do the mutation.
        const result = await this.pica.collaborators.revokeInvite(args.invite_id);
        return formatSuccess(`Invite ${args.invite_id} revoked. Both URL and code claim paths are dead.`, result);
    }
    // ADR-238 — counter-proposal executors. Each delegates to the matching
    // SDK method, which POSTs to the corresponding /counters/* route. Error
    // mapping (invalid_code / not_recipient / not_counter_party /
    // counter_no_longer_pending / counter_not_actionable / not_sender /
    // invite_no_longer_valid / negotiation_limit_reached) is owned by the
    // routes; recovery hints in mcp-server/src/tools/recovery-hints.ts pair
    // each error_code with an agent-friendly suggestion + next_tool.
    async proposeCounter(args) {
        const result = await this.pica.collaborators.proposeCounter({
            code: args.code,
            credit_type: args.credit_type,
            percentage_split: args.percentage_split,
            message: args.message,
        });
        return formatSuccess(`Counter proposed on invite (${args.credit_type} · ${args.percentage_split}%). The sender has been notified — they can accept, decline, or counter back.`, result);
    }
    async acceptCounter(args) {
        const result = await this.pica.collaborators.acceptCounter(args.counter_id);
        return formatSuccess(`Counter ${args.counter_id} accepted. Credit written with the counter's values; the invite is now confirmed.`, result);
    }
    async declineCounter(args) {
        const result = await this.pica.collaborators.declineCounter(args.counter_id, args.reason);
        return formatSuccess(`Counter ${args.counter_id} declined. The invite reverts to pending — the original proposal is back on the table.`, result);
    }
    async counterBack(args) {
        const result = await this.pica.collaborators.counterBack({
            counter_id: args.counter_id,
            credit_type: args.credit_type,
            percentage_split: args.percentage_split,
            message: args.message,
        });
        return formatSuccess(`Counter-back proposed (${args.credit_type} · ${args.percentage_split}%). The recipient has been notified.`, result);
    }
}
//# sourceMappingURL=collaborators.js.map