UNPKG

@withpica/mcp-server

Version:

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

212 lines • 11.9 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatSuccess } from "@withpica/mcp-utils"; export class UsersTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ { definition: { name: "pica_users_find_by_handle", scope: "read:people", audiences: ["stdio", "http"], tier: "read", description: "Use when the user asks: 'find @josh-allen', 'is anyone on PICA called X', 'look up a producer by their PICA handle'. " + "Exact-match lookup of a public PICA handle across all organisations. Returns the public profile (display name, optional avatar, optional bio, handle_accepts_invites) on a match, or `{found: false}` for unclaimed handles. " + 'Never reveals email — for invite delivery use pica_collaborators_invite with `recipient: {kind: "handle", value: ...}` which resolves to email server-side. ' + "Rate-limited at 60 lookups/minute/agent_grant_id. " + "For your own profile, use pica_users_get_my_profile.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition.", inputSchema: { type: "object", properties: { handle: { type: "string", description: "The PICA handle to resolve (3-30 chars, [a-z0-9_-], case-insensitive). 'Josh-Allen' canonicalises to 'josh-allen'.", }, }, required: ["handle"], }, }, executor: this.findByHandle.bind(this), }, { definition: { name: "pica_users_set_handle", scope: "write:people", audiences: ["stdio", "http"], tier: "write", description: "Use when the user asks: 'claim handle X', 'set my PICA handle to X', 'I want to be @josh-allen'. " + "Claim or replace your public PICA handle. Validation: 3-30 chars, [a-z0-9_-], must start with a letter, no consecutive hyphens/underscores, no leading/trailing separator, not in the reserved list. " + "Storage is canonical lowercase. Returns the canonical form. " + "Failure shapes: handle_invalid (+ detail), handle_reserved, handle_already_taken. " + "Rate-limited at 10 attempts/hour/agent_grant_id (deters mass enumeration of which handles are taken). " + "To give a handle up, use pica_users_clear_handle.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas.", inputSchema: { type: "object", properties: { handle: { type: "string", description: "The handle to claim (3-30 chars).", }, }, required: ["handle"], }, }, executor: this.setHandle.bind(this), }, { definition: { name: "pica_users_clear_handle", scope: "write:people", audiences: ["stdio", "http"], tier: "write", description: "Use when the user asks: 'remove my handle', 'clear my PICA handle', 'I don't want to be @josh-allen anymore'. " + "Clear your public PICA handle. Idempotent. In-flight invites that resolved your handle at send-time stay valid because the invite holds your resolved email, not a reference to the handle. " + "To claim or replace a handle rather than remove it, use pica_users_set_handle.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas.", inputSchema: { type: "object", properties: {} }, }, executor: this.clearHandle.bind(this), }, { definition: { name: "pica_users_get_my_profile", scope: "read:people", audiences: ["stdio", "http"], tier: "read", description: "Use when the user asks: 'show my profile', 'what's my PICA handle', 'what bio do I have'. " + "Return the caller's full profile: workspace_email, full_name, public_handle, handle_accepts_invites, avatar_url, bio, linked_person_id. Caller's own profile only — to look up someone else use pica_users_find_by_handle.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas.", inputSchema: { type: "object", properties: {} }, }, executor: this.getMyProfile.bind(this), }, { definition: { name: "pica_users_set_bio", scope: "write:people", audiences: ["stdio", "http"], tier: "write", description: "Use when the user asks: 'set my bio to X', 'change my profile description', 'update my bio'. " + "Write your bio (plain text, max 280 chars). Server strips HTML/markdown and truncates. Empty string clears the bio. Returns the canonical stored value. " + "To copy the biography already held on your linked catalog person, use pica_users_import_bio_from_person.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas.", inputSchema: { type: "object", properties: { bio: { type: "string", description: "Bio text. Pass empty string to clear. Max 280 chars (truncated server-side).", }, }, required: ["bio"], }, }, executor: this.setBio.bind(this), }, { definition: { name: "pica_users_set_avatar_url", scope: "write:people", audiences: ["stdio", "http"], tier: "write", description: "Use when the user asks: 'set my avatar to X URL', 'change my profile picture', 'update my avatar'. " + "Write your avatar_url. URL must parse and match the trusted-host allowlist (Gravatar or PICA-managed S3). Pass null to clear. Returns the canonical stored value or `untrusted_avatar_host` on disallowed host. " + "To change your bio text rather than your picture, use pica_users_set_bio.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas.", inputSchema: { type: "object", properties: { avatar_url: { type: ["string", "null"], description: "Avatar URL (https Gravatar or PICA S3), or null to clear.", }, }, required: ["avatar_url"], }, }, executor: this.setAvatarUrl.bind(this), }, { definition: { name: "pica_users_import_bio_from_person", scope: "write:people", audiences: ["stdio", "http"], tier: "write", description: "Use when the user asks: 'pull my bio from my Spotify artist page', 'import bio from my catalog person', 'use the biography I already have on file'. " + "Copy the biography held on your linked catalog person into your own profile bio. Guarded by `overwrite` (default false): refuses if you already have a bio and surfaces both bios so you can confirm before re-calling with overwrite=true. Returns no_source_available if you have no linked person or an empty biography. " + "To write bio text directly rather than copy it, use pica_users_set_bio.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas.", inputSchema: { type: "object", properties: { overwrite: { type: "boolean", description: "Default false. Set true to overwrite an existing bio.", }, }, }, }, executor: this.importBioFromPerson.bind(this), }, ]; } async findByHandle(args) { const result = await this.pica.users.findByHandle(args.handle); if (result.found) { return formatSuccess(`Found @${result.profile.handle}${result.profile.display_name ? ` — ${result.profile.display_name}` : ""}`, result); } return formatSuccess("No PICA user found with that handle.", result); } async setHandle(args) { const result = await this.pica.users.setHandle(args.handle); return formatSuccess(`Handle claimed: @${result.handle}`, result); } async clearHandle() { await this.pica.users.clearHandle(); return formatSuccess("Handle cleared.", { cleared: true }); } async getMyProfile() { const profile = await this.pica.users.getMyProfile(); return formatSuccess(profile.public_handle ? `Your handle is @${profile.public_handle}.` : "You have not claimed a handle yet.", profile); } async setBio(args) { const result = await this.pica.users.setBio(args.bio); return formatSuccess(result.bio === null ? "Bio cleared." : `Bio updated (${result.bio.length} chars).`, result); } async setAvatarUrl(args) { const result = await this.pica.users.setAvatarUrl(args.avatar_url ?? null); return formatSuccess(result.avatar_url === null ? "Avatar cleared." : `Avatar updated to ${result.avatar_url}`, result); } async importBioFromPerson(args) { const result = await this.pica.users.importBioFromPerson({ overwrite: args.overwrite === true, }); return formatSuccess("Bio import completed.", result); } } //# sourceMappingURL=users.js.map