@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
480 lines (450 loc) • 15.2 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { INSTRUMENT_SERIF_REGULAR_BASE64, INSTRUMENT_SERIF_ITALIC_BASE64, GEIST_SANS_LIGHT_BASE64, APP_BRIDGE_JS, } from "./generated/shared-bundle.js";
// ---------------------------------------------------------------------------
// Design system CSS — embedded fonts + full PICA dark/light theme
// ---------------------------------------------------------------------------
export const PICA_APP_CSS = `
/* ---- Fonts ---- */
@font-face {
font-family: 'Instrument Serif';
font-style: normal;
font-weight: 400;
src: url('data:font/woff2;base64,${INSTRUMENT_SERIF_REGULAR_BASE64}') format('woff2');
}
@font-face {
font-family: 'Instrument Serif';
font-style: italic;
font-weight: 400;
src: url('data:font/woff2;base64,${INSTRUMENT_SERIF_ITALIC_BASE64}') format('woff2');
}
@font-face {
font-family: 'Geist Sans';
font-style: normal;
font-weight: 300;
src: url('data:font/woff2;base64,${GEIST_SANS_LIGHT_BASE64}') format('woff2');
}
/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; -webkit-font-smoothing: antialiased; }
body { font-family: 'Geist Sans', system-ui, sans-serif; font-weight: 300; }
a { color: inherit; text-decoration: none; }
button { cursor: pointer; border: none; background: none; font-family: inherit; }
ul, ol { list-style: none; }
img, svg { display: block; max-width: 100%; }
/* ---- CSS Custom Properties: dark theme (default) ---- */
[data-theme="dark"], :root {
--bg: #0A0A0A;
--card-bg: rgba(255, 255, 255, 0.05);
--card-border: rgba(255, 255, 255, 0.10);
--text-primary: rgba(255, 255, 255, 1);
--text-secondary: rgba(255, 255, 255, 0.40);
--text-tertiary: rgba(255, 255, 255, 0.25);
--accent: #B87333;
--ghost-bg: rgba(255, 255, 255, 0.05);
--ghost-border: rgba(255, 255, 255, 0.07);
}
/* ---- CSS Custom Properties: light theme ---- */
[data-theme="light"] {
--bg: #F5F4F1;
--card-bg: rgba(0, 0, 0, 0.04);
--card-border: rgba(0, 0, 0, 0.10);
--text-primary: rgba(0, 0, 0, 0.90);
--text-secondary: rgba(0, 0, 0, 0.50);
--text-tertiary: rgba(0, 0, 0, 0.35);
--accent: #B87333;
--ghost-bg: rgba(0, 0, 0, 0.04);
--ghost-border: rgba(0, 0, 0, 0.07);
}
/* ---- Base ---- */
body {
background: var(--bg);
color: var(--text-primary);
min-height: 100vh;
}
/* ---- Typography ---- */
.heading, .heading-lg, .heading-md, .heading-sm {
font-family: 'Instrument Serif', Georgia, serif;
font-weight: 400;
color: var(--text-primary);
letter-spacing: 0;
}
.heading-lg { font-size: 2rem; line-height: 1.15; }
.heading-md { font-size: 1.5rem; line-height: 1.2; }
.heading-sm { font-size: 1.125rem; line-height: 1.3; }
.heading { font-size: 1.75rem; line-height: 1.2; }
.text-secondary { color: var(--text-secondary); }
.text-tertiary { color: var(--text-tertiary); }
.text-accent { color: var(--accent); }
.label {
font-family: 'Geist Sans', system-ui, sans-serif;
font-size: 0.6875rem;
font-weight: 300;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-tertiary);
}
/* ---- Card ---- */
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 24px;
backdrop-filter: blur(16px);
}
.card-flat {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 24px;
}
/* ---- Ghost pill button ---- */
.btn-ghost {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--ghost-bg);
border: 1px solid var(--ghost-border);
border-radius: 9999px;
padding: 8px 20px;
font-size: 0.9375rem;
font-weight: 300;
color: var(--text-secondary);
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.btn-ghost:hover {
background: rgba(184, 115, 51, 0.10);
border-color: rgba(184, 115, 51, 0.30);
color: var(--accent);
}
.btn-copper {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--accent);
border: 1px solid transparent;
border-radius: 9999px;
padding: 8px 20px;
font-size: 0.9375rem;
font-weight: 300;
color: #0A0A0A;
transition: opacity 0.15s;
}
.btn-copper:hover { opacity: 0.85; }
/* ---- Badge ---- */
.badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 300;
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text-secondary);
}
.badge-copper {
background: rgba(184, 115, 51, 0.15);
border-color: rgba(184, 115, 51, 0.30);
color: var(--accent);
}
/* ---- Health bar ---- */
.health-bar {
display: flex;
height: 6px;
border-radius: 9999px;
overflow: hidden;
gap: 2px;
}
.health-bar__complete { background: #87A96B; }
.health-bar__attention { background: var(--accent); }
.health-bar__incomplete { background: var(--card-border); flex: 1; }
/* ---- Layout helpers ---- */
.stack { display: flex; flex-direction: column; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
.gap-8 { gap: 2rem; }
.row { display: flex; flex-direction: row; align-items: center; }
.between { justify-content: space-between; }
.section { padding: 24px 0; border-top: 1px solid var(--card-border); }
.section:first-child { border-top: none; }
/* ---- Clickable / collapsible ---- */
.clickable {
cursor: pointer;
transition: opacity 0.15s;
user-select: none;
}
.clickable:hover { opacity: 0.80; }
[data-collapsed] .collapsible-content { display: none; }
/* ---- Responsive ---- */
@media (max-width: 500px) {
.heading-lg { font-size: 1.5rem; }
.heading { font-size: 1.375rem; }
.card, .card-flat { padding: 16px; }
}
`;
// ---------------------------------------------------------------------------
// Shared JS foundation — runs inside every iframe
// ---------------------------------------------------------------------------
export const PICA_APP_JS = `
(function () {
'use strict';
// Initialise bridge from injected ext-apps runtime
var app = (window.__mcpExtApps && window.__mcpExtApps.getInstance)
? window.__mcpExtApps.getInstance()
: null;
// Keep theme in sync with host
window.addEventListener('hostcontextchanged', function (e) {
var detail = e.detail || {};
if (detail.theme) {
document.documentElement.setAttribute('data-theme', detail.theme);
}
});
// Dispatch tool results to card handlers via ontoolresult callback
window.addEventListener('toolresult', function (e) {
var raw = (e.detail && e.detail.result) ? e.detail.result : null;
if (!raw) return;
var data;
try { data = (typeof raw === 'string') ? JSON.parse(raw) : raw; }
catch (_) { data = raw; }
if (typeof window.onPicaToolResult === 'function') {
window.onPicaToolResult(data);
}
});
// Public API: send a message to the host agent via sendMessage
window.picaAgentAction = function (message) {
if (app && typeof app.sendMessage === 'function') {
return app.sendMessage(message);
}
console.warn('[pica] bridge not available — message not sent:', message);
};
// Parse ADR-149 error envelope
window.picaParseError = function (err) {
if (!err) return null;
if (err && typeof err === 'object' && 'code' in err) {
return { code: err.code, message: err.message || 'unknown error', data: err.data || null };
}
return { code: -32603, message: String(err), data: null };
};
})();
`;
// ---------------------------------------------------------------------------
// Upload manager — reusable per-file state machine for card file uploads.
// Exposed as an ES5 JS string snippet that cards embed in their own script
// block. Instantiate with createUploadManager({ accept, onStateChange, app }).
//
// State machine per file: pending -> uploading -> uploaded | failed | network_error
// Routes audio uploads through pica_audio_presigned_upload + pica_audio_complete_upload;
// all other types go through pica_upload_file + pica_upload_complete.
//
// Methods returned: addFiles(FileList), removeFile(id), retryFile(id), getFiles()
// ---------------------------------------------------------------------------
export const PICA_UPLOAD_MANAGER_JS = `
function createUploadManager(config) {
config = config || {};
var accept = config.accept || "any";
var onStateChange = config.onStateChange || function () {};
var app = config.app;
var files = [];
var idCounter = 0;
function notify() {
try { onStateChange(files.slice()); } catch (_) {}
}
function findFile(id) {
for (var i = 0; i < files.length; i++) {
if (files[i].id === id) return files[i];
}
return null;
}
function setState(id, state, extra) {
var f = findFile(id);
if (!f) return;
f.state = state;
if (extra && extra.url) f.url = extra.url;
if (extra && extra.meta) {
f.mime_type = extra.meta.mime_type;
f.size = extra.meta.size;
f.uploaded_at = extra.meta.uploaded_at;
f.upload_id = extra.meta.upload_id;
}
if (extra && extra.error) f.error = extra.error;
notify();
}
function doUpload(fileRecord) {
var file = fileRecord.file;
var id = fileRecord.id;
var isAudio = accept === "audio";
var presignTool = isAudio ? "pica_audio_presigned_upload" : "pica_upload_file";
var completeTool = isAudio ? "pica_audio_complete_upload" : "pica_upload_complete";
app.callServerTool({
name: presignTool,
arguments: {
filename: file.name,
content_type: file.type,
file_size: file.size,
},
}).then(function (presignResult) {
if (presignResult && presignResult.isError) throw new Error("presign failed");
var presign;
try {
presign = JSON.parse(presignResult.content[0].text);
} catch (_) {
throw new Error("presign parse failed");
}
var presignData = presign.data || presign;
return fetch(presignData.upload_url, { method: "PUT", body: file })
.then(function (res) {
if (!res.ok) throw new Error("upload failed: " + res.status);
return presignData;
})
.catch(function (netErr) {
setState(id, "network_error", { error: netErr && netErr.message });
throw new Error("__network_error__");
});
}).then(function (presignData) {
return app.callServerTool({
name: completeTool,
arguments: {
upload_id: presignData.upload_id,
key: presignData.key,
bucket: presignData.bucket,
storage_category: presignData.storage_category,
filename: file.name,
content_type: file.type,
file_size: file.size,
},
});
}).then(function (completeResult) {
if (completeResult && completeResult.isError) throw new Error("complete failed");
var completed;
try {
completed = JSON.parse(completeResult.content[0].text);
} catch (_) {
throw new Error("complete parse failed");
}
var completedData = completed.data || completed;
setState(id, "uploaded", {
url: completedData.url,
meta: {
mime_type: file.type,
size: file.size,
uploaded_at: new Date().toISOString(),
upload_id: completedData.id || completedData.upload_id || null,
},
});
}).catch(function (err) {
if (err && err.message === "__network_error__") return;
setState(id, "failed", { error: err && err.message });
});
}
function addFiles(fileList) {
var added = [];
for (var i = 0; i < fileList.length; i++) {
var file = fileList[i];
var id = "f-" + (++idCounter);
var record = {
id: id,
state: "pending",
file: file,
filename: file.name,
mime_type: file.type,
size: file.size,
url: null,
};
files.push(record);
added.push(record);
}
notify();
added.forEach(function (r) {
r.state = "uploading";
notify();
doUpload(r);
});
}
function removeFile(id) {
files = files.filter(function (f) { return f.id !== id; });
notify();
}
function retryFile(id) {
var f = findFile(id);
if (!f || !f.file) return;
f.state = "uploading";
f.error = null;
notify();
doUpload(f);
}
function getFiles() {
return files.slice();
}
return {
addFiles: addFiles,
removeFile: removeFile,
retryFile: retryFile,
getFiles: getFiles,
};
}
`;
// ---------------------------------------------------------------------------
// HTML builder — assembles a complete iframe document
// ---------------------------------------------------------------------------
export function buildAppHtml(title, bodyHtml, appJs) {
return `<!DOCTYPE html>
<html data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${title}</title>
<style>${PICA_APP_CSS}</style>
</head>
<body>
${bodyHtml}
<script>${APP_BRIDGE_JS}</script>
<script>${PICA_APP_JS}</script>
<script>${PICA_UPLOAD_MANAGER_JS}</script>
<script>${appJs}</script>
</body>
</html>`;
}
/**
* CSP connect-src allowlist for MCP App cards that upload files to storage.
* Covers PICA-hosted media buckets and common BYOC (bring your own cloud)
* providers: AWS S3, Cloudflare R2, Wasabi, Backblaze B2.
*
* Hosts that honor wildcard entries get broader coverage. Hosts that don't
* still work for PICA defaults (exact entries first) and specific BYOC regions.
* Users on unlisted endpoints see a graceful "use web dashboard" fallback.
*/
export const PICA_STORAGE_CONNECT_DOMAINS = [
// PICA-hosted media bucket
"https://withpica-media-production.s3.amazonaws.com",
"https://withpica-media-production.s3.us-east-1.amazonaws.com",
"https://withpica-media-production.s3.eu-north-1.amazonaws.com",
// AWS S3 BYOC — common region endpoints (virtual-hosted style)
"https://*.s3.amazonaws.com",
"https://*.s3.us-east-1.amazonaws.com",
"https://*.s3.us-east-2.amazonaws.com",
"https://*.s3.us-west-1.amazonaws.com",
"https://*.s3.us-west-2.amazonaws.com",
"https://*.s3.eu-west-1.amazonaws.com",
"https://*.s3.eu-west-2.amazonaws.com",
"https://*.s3.eu-central-1.amazonaws.com",
"https://*.s3.eu-north-1.amazonaws.com",
"https://*.s3.ap-southeast-1.amazonaws.com",
"https://*.s3.ap-southeast-2.amazonaws.com",
"https://*.s3.ap-northeast-1.amazonaws.com",
// Cloudflare R2
"https://*.r2.cloudflarestorage.com",
// Wasabi
"https://s3.wasabisys.com",
"https://s3.us-east-1.wasabisys.com",
"https://s3.us-east-2.wasabisys.com",
"https://s3.us-west-1.wasabisys.com",
"https://s3.eu-central-1.wasabisys.com",
"https://s3.ap-northeast-1.wasabisys.com",
// Backblaze B2 S3-compatible
"https://s3.us-west-000.backblazeb2.com",
"https://s3.us-west-001.backblazeb2.com",
"https://s3.us-west-002.backblazeb2.com",
"https://s3.us-west-004.backblazeb2.com",
"https://s3.eu-central-003.backblazeb2.com",
];
//# sourceMappingURL=shared.js.map