@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
431 lines • 22.7 kB
JavaScript
// 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