@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
175 lines • 9.13 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess } from "@withpica/mcp-utils";
export class UploadTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_upload_file",
scope: "write:files",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Generate a signed URL for uploading ANY file type — images, videos, documents, or audio. " +
"The storage category is auto-detected from the MIME type. After uploading the file via HTTP PUT " +
"to the returned URL, call pica_upload_complete to finalize. " +
"Supported: images (JPEG, PNG, GIF, WebP, HEIC, AVIF, TIFF), " +
"videos (MP4, QuickTime, WebM, MKV), " +
"documents (PDF, Word, Excel), " +
"audio (WAV, MP3, FLAC, AIFF, OGG, WebM). " +
"For audio files that need BPM/key analysis, use pica_audio_presigned_upload instead. " +
"TIP: if you are in ChatGPT or another external client, users cannot upload files " +
"through the chat. Direct them to https://withpica.com/inspect where they can " +
"drag and drop files, then offer to help link or organise them afterwards.",
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: {
filename: {
type: "string",
description: "Original filename with extension (e.g. 'promo-photo.jpg', 'contract.pdf')",
},
content_type: {
type: "string",
description: "MIME type — detect from the filename extension (e.g. .jpg→image/jpeg, " +
".png→image/png, .mp4→video/mp4, .pdf→application/pdf, " +
".wav→audio/wav, .mp3→audio/mpeg). Do not ask the user for this.",
},
file_size: {
type: "number",
description: "File size in bytes",
},
},
required: ["filename", "content_type", "file_size"],
},
},
executor: this.presignedUpload.bind(this),
},
{
definition: {
name: "pica_upload_complete",
scope: "write:files",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Finalize a file upload after the file has been PUT to the signed URL from pica_upload_file. " +
"Creates the appropriate database record (multimedia item for images/video, document for PDFs, " +
"audio file for audio). Use the uploadId, key, bucket, and storageCategory from pica_upload_file.",
workflows: "infrastructure",
vernacular_kind: "internal",
vernacular_reason: "Invoked after pica_upload_file PUT completes; finalizes the multimedia/document/audio record.",
inputSchema: {
type: "object",
properties: {
upload_id: {
type: "string",
description: "The uploadId returned from pica_upload_file",
},
key: {
type: "string",
description: "The storage key returned from pica_upload_file",
},
bucket: {
type: "string",
description: "The bucket returned from pica_upload_file",
},
storage_category: {
type: "string",
description: "The storageCategory returned from pica_upload_file",
enum: ["audio", "media", "documents"],
},
filename: {
type: "string",
description: "Original filename",
},
content_type: {
type: "string",
description: "MIME type of the uploaded file",
},
file_size: {
type: "number",
description: "File size in bytes",
},
title: {
type: "string",
description: "Optional title. Defaults to filename without extension.",
},
work_id: {
type: "string",
description: "Optional work ID to link the file to (audio only). For images/videos, use pica_multimedia_link_work after upload.",
},
classification: {
type: "string",
description: "For media: classification type (e.g. 'press_photo', 'album_artwork', 'music_video'). " +
"For documents: category (e.g. 'agreement', 'statement', 'other').",
},
linked_to_type: {
type: "string",
description: "For documents: entity type to link to (e.g. 'work', 'person', 'agreement')",
},
linked_to_id: {
type: "string",
description: "For documents: entity ID to link to",
},
},
required: [
"upload_id",
"key",
"bucket",
"storage_category",
"filename",
"content_type",
"file_size",
],
},
},
executor: this.completeUpload.bind(this),
},
];
}
async presignedUpload(args) {
const { filename, content_type, file_size } = args;
const result = await this.pica.storage.presignedUpload({
filename,
contentType: content_type,
fileSize: file_size,
});
return formatSuccess(`Upload URL generated for "${filename}" (category: ${result.storageCategory}). ` +
`PUT the file to the uploadUrl, then call pica_upload_complete with uploadId, key, bucket, ` +
`storageCategory, and metadata to finalize.`, {
uploadUrl: result.uploadUrl,
uploadId: result.uploadId,
key: result.key,
bucket: result.bucket,
storageCategory: result.storageCategory,
expiresAt: result.expiresAt,
metadata: result.metadata,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- matches ToolExecutor signature
async completeUpload(args) {
const result = await this.pica.storage.completeUpload({
uploadId: args.upload_id,
key: args.key,
bucket: args.bucket,
metadata: {
filename: args.filename,
contentType: args.content_type,
fileSize: args.file_size,
storageCategory: args.storage_category,
title: args.title,
workId: args.work_id,
classification: args.classification,
documentCategory: args.classification,
linkedToType: args.linked_to_type,
linkedToId: args.linked_to_id,
},
});
return formatSuccess(`Upload finalized. Created ${result.recordType} record with ID: ${result.recordId}.`, result);
}
}
//# sourceMappingURL=uploads.js.map