@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
207 lines • 10.4 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess, formatStructuredList } from "@withpica/mcp-utils";
const ROLE_ENUM = [
"member",
"founder",
"lead",
"touring_member",
"session",
"guest",
"producer_in_residence",
];
export class GroupsTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_group_add_member",
scope: "write:people",
audiences: ["stdio", "http"],
tier: "write",
description: "Add a person as a member of a group person row. The group must already have is_group=true on its people record. " +
"Defaults: role='member', receive_notifications=true. " +
"→ before: pica_people_query (find both people) → then: pica_group_list_members (verify roster)",
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: {
group_person_id: {
type: "string",
description: "ID of the group person row (must have is_group=true)",
},
member_person_id: {
type: "string",
description: "ID of the member person row",
},
role: {
type: "string",
enum: [...ROLE_ENUM],
description: "Constrained role within the group. Default 'member'.",
},
role_label: {
type: "string",
description: "Free-text label for variants outside the enum (e.g. 'lead vocalist for tour 2025'). Optional.",
},
joined_at: {
type: "string",
description: "When the member joined (ISO 8601). Omit for unknown.",
},
receive_notifications: {
type: "boolean",
description: "Whether the member's linked user account (if any) gets notifications about group activity. Default true.",
},
notification_filter: {
type: "array",
items: { type: "string" },
description: "Optional subset of original event types to be notified about (e.g. ['work_credit_added','agreement_party_linked']). Omit for all.",
},
},
required: ["group_person_id", "member_person_id"],
},
},
executor: this.addMember.bind(this),
},
{
definition: {
name: "pica_group_remove_member",
scope: "write:people",
audiences: ["stdio", "http"],
tier: "write",
description: "Remove a member from a group by setting left_at on the active row. The row is preserved — never deleted — so member history survives. " +
"→ then: pica_group_list_members (with include_inactive=true to see history)",
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: {
group_person_id: {
type: "string",
description: "Group person ID",
},
member_person_id: {
type: "string",
description: "Member person ID",
},
left_at: {
type: "string",
description: "When they left (ISO 8601). Defaults to now.",
},
},
required: ["group_person_id", "member_person_id"],
},
},
executor: this.removeMember.bind(this),
},
{
definition: {
name: "pica_group_list_members",
scope: "read:people",
audiences: ["stdio", "http"],
tier: "read",
description: "List members of a group person. Active members only by default; pass include_inactive=true for history. " +
"To change the roster use pica_group_add_member or pica_group_remove_member; this only reads it. " +
"→ then: pica_people_inspect (get details for a specific member)",
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: {
group_person_id: {
type: "string",
description: "Group person ID (must have is_group=true)",
},
include_inactive: {
type: "boolean",
description: "Include rows with left_at set (past members). Default false.",
},
},
required: ["group_person_id"],
},
},
executor: this.listMembers.bind(this),
},
{
definition: {
name: "pica_person_list_groups",
scope: "read:people",
audiences: ["stdio", "http"],
tier: "read",
description: "List the groups a person is (or was) a member of. Active memberships only by default. " +
"→ then: pica_people_inspect (get group details)",
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: {
person_id: {
type: "string",
description: "Person ID",
},
include_inactive: {
type: "boolean",
description: "Include rows with left_at set (past memberships). Default false.",
},
},
required: ["person_id"],
},
},
executor: this.listGroups.bind(this),
},
];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- args bag from MCP transport
async addMember(args) {
const result = await this.pica.groups.addMember({
group_person_id: args.group_person_id,
member_person_id: args.member_person_id,
role: args.role,
role_label: args.role_label,
joined_at: args.joined_at,
receive_notifications: args.receive_notifications,
notification_filter: args.notification_filter,
});
const data = result?.data ?? result;
const role = data?.role ?? "member";
return formatSuccess(`Member added to group as ${role}${data?.role_label ? ` (${data.role_label})` : ""}.`, data);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- args bag from MCP transport
async removeMember(args) {
const result = await this.pica.groups.removeMember({
group_person_id: args.group_person_id,
member_person_id: args.member_person_id,
left_at: args.left_at,
});
const data = result?.data ?? result;
return formatSuccess(`Member removed from group (left_at=${data?.left_at ?? "now"}). Row preserved for history.`, data);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- args bag from MCP transport
async listMembers(args) {
const result = await this.pica.groups.listMembers(args.group_person_id, {
include_inactive: !!args.include_inactive,
});
const items = (result?.data ?? result ?? []);
return formatStructuredList(items, "group_membership", {
total: items.length,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- args bag from MCP transport
async listGroups(args) {
const result = await this.pica.groups.listGroupsForPerson(args.person_id, {
include_inactive: !!args.include_inactive,
});
const items = (result?.data ?? result ?? []);
return formatStructuredList(items, "group_membership", {
total: items.length,
});
}
}
//# sourceMappingURL=groups.js.map