@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
188 lines • 10.7 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatStructured, formatSuccess } from "@withpica/mcp-utils";
const MAX_PEOPLE = 200;
export class ProfileClaimsTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_profile_claims_invite",
scope: "write:people",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Invite people in this catalogue to claim their profile: each gets " +
"an email, at the address the catalogue holds for them, with a link " +
"to see and confirm (or correct) their own credits. A claim never " +
"lets them edit the catalogue. Use when the user asks: 'invite " +
"these writers to check their credits', 'ask everyone on this album " +
"to claim their profile'. ALWAYS call first WITHOUT confirm: that " +
"sends nothing and returns who would be emailed (and at which " +
"address) and who would be skipped and why. Show that to the user, " +
"and only call again with confirm: true once they agree. People " +
"with no email, already claimed, already holding a working invite, " +
"who asked not to be contacted, or whose address said 'not me' or " +
"'I manage them' are skipped. When confirming, pass " +
"expected_emails (each person id → the address the preview showed): " +
"anyone whose address has changed since is skipped rather than " +
"emailed somewhere the user never saw. Up to 200 people per call. " +
"→ first: pica_people_query (find person ids) " +
"→ then: pica_profile_claims_status (see who has answered)",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
person_ids: {
type: "array",
items: { type: "string" },
minItems: 1,
maxItems: MAX_PEOPLE,
description: "Ids of people in this catalogue to invite (from pica_people_query).",
},
confirm: {
type: "boolean",
description: "Leave out (or false) for a preview that sends nothing. Set true only after the user has seen the preview and agreed: then the emails are sent.",
},
expected_emails: {
type: "object",
additionalProperties: { type: "string" },
description: "With confirm: each person id → the address the preview showed for them. A person whose address has changed since is skipped (ADDRESS_CHANGED_SINCE_PREVIEW).",
},
},
required: ["person_ids"],
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
},
executor: this.invite.bind(this),
},
{
definition: {
name: "pica_profile_claims_status",
scope: "read:people",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Where people in this catalogue stand on claiming their profile. " +
"Use when the user asks: 'who has claimed their profile?', 'did " +
"Sam answer the invite?', 'who still hasn't checked their " +
"credits?'. States: " +
"claimed; invited and waiting (pending); invite expired; invited at " +
"an address the catalogue has since changed (address_changed); " +
"answered 'that's not me' (not_me) or 'I manage this person' " +
"(manager); withdrawn (revoked); or not invited. Also says whether " +
"the invite email went out (send_status: sent, held for approval, " +
"failed) and when it may be sent again (resend_after). Give " +
"person_ids to ask about specific people; leave it out to list " +
"everyone invited or claimed, newest first. Never returns an email " +
"address. " +
"→ then: pica_profile_claims_invite (invite or re-invite)",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
person_ids: {
type: "array",
items: { type: "string" },
maxItems: MAX_PEOPLE,
description: "Narrow to these people (ids from pica_people_query). A person never invited reads not_invited.",
},
state: {
type: "string",
enum: [
"claimed",
"not_invited",
"pending",
"expired",
"address_changed",
"person_removed",
"not_me",
"manager",
"revoked",
],
description: "Without person_ids: only people in this state (e.g. pending = invited, no answer yet).",
},
limit: {
type: "integer",
description: "Without person_ids: max people to list, most recent first (default 50, max 200). If `truncated` comes back true, more matched: narrow with state, or ask by person_ids.",
},
},
},
annotations: {
readOnlyHint: true,
},
},
executor: this.status.bind(this),
},
];
}
async invite(args) {
const personIds = Array.isArray(args.person_ids)
? args.person_ids.filter((id) => typeof id === "string")
: [];
const confirm = args.confirm === true;
const expected = args.expected_emails &&
typeof args.expected_emails === "object" &&
!Array.isArray(args.expected_emails)
? args.expected_emails
: undefined;
const result = await this.pica.profileClaims.invite({
person_ids: personIds,
confirm,
...(expected ? { expected_emails: expected } : {}),
});
if (!result) {
return formatSuccess("The invite call returned nothing. Call pica_profile_claims_status to see where these people stand before trying again.", { result: null });
}
const skipped = result.results.filter((r) => r.outcome === "skipped");
const skipLine = skipped.length > 0
? ` ${skipped.length} would be skipped: ${skipped
.map((r) => `${r.person_id} (${r.code})`)
.join(", ")}.`
: "";
if (!result.confirmed) {
return formatSuccess(`Preview only: nothing was sent. ${result.would_invite} would be emailed an invite to claim their profile.${skipLine} Show the user who would be emailed and at which address; if they agree, call pica_profile_claims_invite again with the same person_ids, confirm: true, and expected_emails mapping each person id to the address shown.`, result);
}
const held = result.results.filter((r) => r.outcome === "invited" && r.send_status === "held").length;
const notSent = result.results.filter((r) => r.outcome === "invited" && r.send_status === "failed").length;
const sent = result.invited - held - notSent;
const parts = [
sent > 0 ? `Emailed ${sent}.` : "",
held > 0 ? `${held} waiting for approval before they go out.` : "",
notSent > 0
? `${notSent} invite(s) made but the email didn't go out; invite them again.`
: "",
skipped.length > 0
? `Skipped ${skipped.length}: ${skipped.map((r) => `${r.person_id} (${r.code})`).join(", ")}.`
: "",
result.failed > 0 ? `${result.failed} failed; try those again.` : "",
].filter(Boolean);
return formatSuccess(parts.join(" "), result);
}
async status(args) {
const personIds = Array.isArray(args.person_ids)
? args.person_ids.filter((id) => typeof id === "string")
: undefined;
const { people, truncated } = await this.pica.profileClaims.status({
...(personIds && personIds.length > 0 ? { person_ids: personIds } : {}),
...(typeof args.state === "string" ? { state: args.state } : {}),
...(typeof args.limit === "number"
? { limit: Math.floor(args.limit) }
: {}),
});
const n = people.length;
const summary = truncated
? `Showing ${n} ${n === 1 ? "person" : "people"}, most recent first. More matched than are listed: narrow with state, or ask by person_ids. Do not treat this as everyone.`
: `Found ${n} ${n === 1 ? "person" : "people"}.`;
return formatStructured({ count: n, items: people, truncated }, summary);
}
}
//# sourceMappingURL=profile-claims.js.map