UNPKG

@withpica/mcp-server

Version:

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

431 lines • 22.7 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatList, formatSuccess } from "@withpica/mcp-utils"; export class MultimediaTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ { definition: { name: "pica_multimedia_search", scope: "read:files", audiences: ["stdio", "http", "public"], tier: "read", description: "Search and list multimedia assets — photos, videos, audio, YouTube, Spotify, SoundCloud. " + "Supports filtering by type, classification, published status, and linked work or person. " + "Read-only: to add an item rather than find one, use pica_multimedia_create.", 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: { query: { type: "string", description: "Full-text search across title, caption, and description", }, content_type: { type: "string", description: "Filter by content type", enum: [ "uploaded_photo", "uploaded_video", "uploaded_audio", "youtube_video", "spotify_track", "soundcloud_track", "live_performance", ], }, classification: { type: "string", description: "Filter by classification", enum: [ "sync_placement", "music_video", "behind_the_scenes", "live_performance", "promo_material", "album_artwork", "studio_session", "artist_showcase", "general", ], }, is_published: { type: "boolean", description: "Filter by published status", }, work_id: { type: "string", description: "Filter by linked work ID", }, person_id: { type: "string", description: "Filter by linked person ID", }, limit: { type: "number", minimum: 1, maximum: 200, description: "Maximum results (default: 50, max: 200)", }, offset: { type: "number", description: "Pagination offset (default: 0)", }, }, }, }, executor: this.searchMultimedia.bind(this), }, { definition: { name: "pica_multimedia_create", scope: "write:files", audiences: ["stdio", "http", "public"], tier: "write", description: "Create a multimedia item. Use for linking external content " + "(YouTube, Spotify, SoundCloud URLs) or registering uploaded media. " + "For actual file uploads use pica_upload (interactive) or pica_upload_file + " + "pica_upload_complete; for YouTube videos with auto-enriched metadata use " + "pica_multimedia_link_youtube.", workflows: ["multimedia-required"], 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: { title: { type: "string", description: "Title of the multimedia item.", }, content_type: { type: "string", description: "Type of content.", enum: [ "uploaded_photo", "uploaded_video", "uploaded_audio", "youtube_video", "spotify_track", "soundcloud_track", "live_performance", ], }, url: { type: "string", description: "URL of the media (YouTube URL, Spotify URI, SoundCloud URL, etc.).", }, classification: { type: "string", description: "Classification of the content.", enum: [ "sync_placement", "music_video", "behind_the_scenes", "live_performance", "promo_material", "album_artwork", "studio_session", "artist_showcase", "general", ], }, caption: { type: "string", description: "Caption or subtitle." }, description: { type: "string", description: "Detailed description.", }, tags: { type: "array", description: "Tags for categorisation.", items: { type: "string" }, }, credits: { type: "string", description: "Freeform credits string (e.g. 'Shot by Alice Nguyen, produced by Bob').", }, duration_seconds: { type: "number", description: "Duration in seconds (for video / audio).", }, // Collection / ordering / publish collection_id: { type: "string", description: "Collection UUID — attach this item to a multimedia collection.", }, display_order: { type: "number", description: "Sort position within its collection.", }, is_featured: { type: "boolean", description: "Mark as featured — surfaces in prominent positions on artist portals and directory.", }, is_published: { type: "boolean", description: "Publish to public surfaces on create. Default false; the service stamps `published_at` when this flips true.", }, // Event / performance context (audit row 21 add-candidates) venue: { type: "string", description: "Venue name (for live / performance content).", }, event_name: { type: "string", description: "Event / tour / show name (for live / performance content).", }, performance_date: { type: "string", description: "Date of the performance (YYYY-MM-DD) — used to timestamp live recordings.", }, setlist_position: { type: "number", description: "Song's position in the performance setlist (1-indexed).", }, sync_side: { type: "string", description: "Sync-side label for sync placements (e.g. 'master', 'publishing').", }, // External identifiers (enrichment pipelines may overwrite) spotify_url: { type: "string", description: "Spotify canonical URL.", }, spotify_track_uri: { type: "string", description: "Spotify URI (spotify:track:…).", }, spotify_track_id: { type: "string", description: "Spotify track ID (22 chars).", }, youtube_url: { type: "string", description: "YouTube canonical URL.", }, youtube_video_id: { type: "string", description: "YouTube video ID (e.g. 'dQw4w9WgXcQ').", }, soundcloud_url: { type: "string", description: "SoundCloud canonical URL.", }, thumbnail_url: { type: "string", description: "Externally-hosted thumbnail URL.", }, }, required: ["title", "content_type"], }, }, executor: this.createMultimedia.bind(this), }, { definition: { name: "pica_multimedia_import_url", scope: "write:files", audiences: ["stdio", "http", "public"], tier: "write", description: "Import an image or video from a public URL — downloads it and stores it in PICA. " + "Works with direct image links AND web pages that declare a preview image (og:image), " + "e.g. postimg, imgur, press pages. Pass person_id to set the imported image as that " + "person's profile image (artist photo, avatar, press photo, profile picture) in the " + "same call. For YouTube videos, use pica_multimedia_link_youtube instead.", workflows: ["multimedia-required"], 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: { url: { type: "string", description: "Public URL of the image or video to import", }, title: { type: "string", description: "Title for the multimedia item", }, source: { type: "string", description: "Source label (e.g. 'spotify', 'web', 'press_kit')", }, work_id: { type: "string", description: "Optional work ID to link the imported media to", }, person_id: { type: "string", description: "Optional person id — set the imported image as this person's profile image (artist photo / avatar). The person must belong to your organisation.", }, }, required: ["url"], }, }, executor: this.importFromUrl.bind(this), }, { definition: { name: "pica_multimedia_link_youtube", scope: "write:files", audiences: ["stdio", "http", "public"], tier: "write", description: "Link a YouTube video to PICA. Creates a multimedia item with YouTube metadata " + "(thumbnail, title, view count). Can optionally link to a work. " + "For any other image or video URL, use pica_multimedia_import_url instead.", workflows: ["multimedia-required"], 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: { youtube_video_id: { type: "string", description: "YouTube video ID (e.g. 'dQw4w9WgXcQ' from youtube.com/watch?v=dQw4w9WgXcQ)", }, title: { type: "string", description: "Title for the multimedia item", }, classification: { type: "string", description: "Classification type", enum: [ "sync_placement", "music_video", "behind_the_scenes", "live_performance", "promo_material", "general", ], }, work_id: { type: "string", description: "Optional work ID to link the video to", }, work_relationship_type: { type: "string", description: "Relationship type when linking to a work (e.g. 'music_video_for', 'sync_placement')", }, }, required: ["youtube_video_id", "title"], }, }, executor: this.linkYoutube.bind(this), }, { definition: { name: "pica_multimedia_link_work", scope: "write:files", audiences: ["stdio", "http", "public"], tier: "write", description: "Link an existing multimedia item to a work. Creates a relationship between " + "the multimedia (photo, video, audio) and a musical work. " + "The item must already exist — create one first with pica_multimedia_create.", workflows: ["multimedia-required"], 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: { multimedia_id: { type: "string", description: "The multimedia item ID", }, work_id: { type: "string", description: "The work ID to link to", }, relationship_type: { type: "string", description: "Type of relationship", enum: [ "featured_in", "music_video_for", "cover_performance", "album_artwork", "behind_the_scenes", "sync_placement", "general", ], }, notes: { type: "string", description: "Optional notes about the relationship", }, }, required: ["multimedia_id", "work_id", "relationship_type"], }, }, executor: this.linkWork.bind(this), }, ]; } async searchMultimedia(args) { const result = await this.pica.multimedia.search({ query: args.query, content_type: args.content_type, classification: args.classification, is_published: args.is_published, work_id: args.work_id, person_id: args.person_id, limit: args.limit, offset: args.offset, }); return formatList(result.data || [], { total: result.total || 0 }); } async createMultimedia(args) { const item = await this.pica.multimedia.create(args); return formatSuccess("Multimedia item created", item); } async importFromUrl(args) { const result = await this.pica.multimedia.importFromUrl({ url: args.url, title: args.title, source: args.source, work_id: args.work_id, person_id: args.person_id, }); // The receipts, not just the row. `via` says whether the URL was a // direct image or a page whose og:image was followed — the difference // an agent needs when a paste "worked" but pointed at a page. And when // person_id / work_id were passed, the follow-on writes can fail while // the import itself succeeds, so their outcome belongs in the summary // text an LLM reads, not only in the JSON below it. const { item, via, profile_image: profileImage, work_link: workLink, } = result; let summary = `Imported from URL (${via}): "${item.title || args.url}". Multimedia ID: ${item.id}`; if (profileImage) { summary += profileImage.set ? ` Profile image set on person ${profileImage.person_id}.` : ` Profile image NOT set (${profileImage.code ?? "UNKNOWN"}): ${profileImage.reason ?? "no reason given"}`; } if (workLink) { summary += workLink.linked ? ` Linked to work ${workLink.work_id}.` : ` Work link failed: ${workLink.reason ?? "no reason given"}`; } // Pass the WHOLE result, so structuredContent.data carries `via`, // `source_url`, `profile_image`, `work_link` and `item` for a machine // reader as well as the summary above for a human one. return formatSuccess(summary, result); } async linkYoutube(args) { const item = await this.pica.multimedia.linkYoutube({ youtube_video_id: args.youtube_video_id, title: args.title, classification: args.classification, work_id: args.work_id, work_relationship_type: args.work_relationship_type, }); return formatSuccess(`YouTube video linked: "${item.title || args.youtube_video_id}". Multimedia ID: ${item.id}`, item); } async linkWork(args) { const result = await this.pica.multimedia.linkWork(args.multimedia_id, args.work_id, args.relationship_type, args.notes); return formatSuccess(`Multimedia ${args.multimedia_id} linked to work ${args.work_id} as "${args.relationship_type}".`, result); } } //# sourceMappingURL=multimedia.js.map