Update to ACE Carousel Creator
Snapshot Nov 6, 2026 · 00:15 UTC · version 0.2.0
Package or technical metadata updated
Package or technical metadata updated
Observed in package metadata. These changes alone do not establish a new customer-facing feature.
Up to 3 plugins free · no card required.
All fields and original values.
These are saved file metadata. Open a changed file to read its text edits.
Package file
Oct 24, 2026 · 13:48 UTC
Not present
Nov 6, 2026 · 00:15 UTC
{"sha256":"5f2025d9087cd8dc93ebe395791fda81379d9a438862e6e87a69b5faf4cc2b77","size":47}
Package file
Oct 24, 2026 · 13:48 UTC
Not present
Nov 6, 2026 · 00:15 UTC
{"sha256":"fd48d4fdbb81718d2dfbffa6314dee7c56d042aa5dd96d34d45ee3b38ba91e49","size":315}
Package file
Oct 24, 2026 · 13:48 UTC
Not present
Nov 6, 2026 · 00:15 UTC
{"sha256":"29942ea87db4340c9a272091ada1b037d8b8b02a2e76dce75509cb8a1e418059","size":2313}
Package file
Oct 24, 2026 · 13:48 UTC
Not present
Nov 6, 2026 · 00:15 UTC
{"sha256":"2014b20842e56188734f68b1490b3cbb5a8dd5365993de5a9935fdc353ff30fc","size":1112}
Compare saved observations
Download comparison JSONChanged files
Choose a file to see its text changes. Binary or large files show metadata only.
LICENSE →
examples/contact-gallery.yaml →
scripts/setup-native-renderer.mjs →
skills/create-carousel/SKILL.md →
skills/create-carousel/references/DESIGN_GUIDE.md →
skills/create-carousel/references/DSL_LAYOUT_SPEC.md →
skills/create-carousel/references/WRITING_GUIDE.md →
src/server.mjs
− removed · + added
--- before +++ after @@ -4,8 +4,13 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { z } from 'zod'; -import { buildCarousel, imageLimit } from './archive.mjs'; -import { DocumentError, getSpec, parseLayout, referencedAssets, validateDocument } from './document.mjs'; +import { buildCarousel, imageLimit, photoFormat } from './archive.mjs'; +import { DocumentError, parseLayout, referencedAssets, validateDocument } from './document.mjs'; + +import { getSpecResponse, designGuide, packageVersion, capabilities } from './spec.mjs'; +import { categories } from './catalog.mjs'; +import { diagnoseDocument } from './diagnostics.mjs'; +import { nativeRenderer } from './renderer.mjs'; const fileInput = z.object({ download_url: z.string().url(), file_id: z.string().min(1), mime_type: z.string().optional(), file_name: z.string().optional(), asset_name: z.string().min(1).max(128).optional() }).strict(); const fileName = name => (name.normalize('NFC').replace(/[\u0000-\u001f\u007f/\\<>:"|?*]/g, '-').replace(/\.carousel$/i, '').trim().slice(0, 64) || 'carousel') + '.carousel'; @@ -18,18 +23,71 @@ prune() { for (const [id, entry] of this.files) if (entry.expires <= this.now()) this.files.delete(id); } - add(name, bytes) { + add(name, bytes) { return this.addMany([{ name, bytes, mimeType: 'application/octet-stream' }])[0]; } + addMany(items) { this.prune(); const total = [...this.files.values()].reduce((n, f) => n + f.bytes.byteLength, 0); - if (total + bytes.byteLength > this.capacity || this.files.size >= 200) throw new DocumentError('Temporary storage is full. Wait for old download links to expire.'); - const id = randomBytes(24).toString('hex'); - const entry = { name: fileName(name), bytes, expires: this.now() + this.ttl }; - this.files.set(id, entry); - return { id, ...entry }; + if (total + items.reduce((n, item) => n + item.bytes.byteLength, 0) > this.capacity || this.files.size + items.length > 200) throw new DocumentError('Temporary storage is full. Wait for old download links to expire.'); + const prepared = items.map(({ name, bytes, mimeType }) => { + if (!['image/png', 'application/octet-stream'].includes(mimeType)) throw new DocumentError('Unsupported artifact type'); + const safeName = mimeType === 'image/png' ? fileName(name.replace(/\.png$/i, '')).replace(/\.carousel$/, '.png') : fileName(name); + return { id: randomBytes(24).toString('hex'), name: safeName, bytes, mimeType, expires: this.now() + this.ttl }; + }); + for (const { id, ...entry } of prepared) this.files.set(id, entry); + return prepared; } get(id) { this.prune(); return this.files.get(id); } } +class UploadError extends DocumentError { + constructor(message, status) { super(message); this.status = status; } +} + +// One-time links for photographs that exist only on the user's computer. Bytes are only packaged into projects, never served back. +export class UploadStore { + constructor({ ttl = 3_600_000, capacity = 256 * 1024 * 1024, limit = imageLimit, now = Date.now } = {}) { + this.ttl = ttl; this.capacity = capacity; this.limit = limit; this.now = now; this.slots = new Map(); + } + prune() { + for (const [id, slot] of this.slots) if (slot.expires <= this.now()) this.slots.delete(id); + } + reserve(names) { + this.prune(); + if (new Set(names).size !== names.length) throw new DocumentError('asset_names: each photograph needs a distinct asset_name; duplicate names'); + if (this.slots.size + names.length > 200) throw new DocumentError('Temporary storage is full. Wait for old upload links to expire.'); + const expires = this.now() + this.ttl; + return names.map(name => { + const id = randomBytes(24).toString('hex'); + this.slots.set(id, { name, expires, bytes: null }); + return { id, name, expires }; + }); + } + async receive(id, bytes) { + const slot = this.get(id); + if (!slot) throw new UploadError('Upload link expired or not found; call prepare_photo_upload again', 404); + if (slot.bytes) throw new UploadError('This upload link was already used; call prepare_photo_upload for a new one', 409); + if (bytes.byteLength > this.limit) throw new UploadError(`Photograph exceeds ${this.limit} bytes`, 413); + const stored = [...this.slots.values()].reduce((n, entry) => n + (entry.bytes?.byteLength ?? 0), 0); + if (stored + bytes.byteLength > this.capacity) throw new UploadError('Temporary storage is full. Try again later.', 507); + const format = await photoFormat(bytes); + if (!format) throw new UploadError('Send one still photograph per link: JPEG, PNG, WebP or AVIF', 415); + // Another request may have filled or expired the slot while the header was read. + if (slot.bytes || this.get(id) !== slot) throw new UploadError('This upload link was already used; call prepare_photo_upload for a new one', 409); + slot.bytes = bytes; + return { upload_id: id, asset_name: slot.name, format, bytes: bytes.byteLength }; + } + get(id) { this.prune(); return this.slots.get(id); } +} + +const readUpload = (request, limit) => new Promise((resolve, reject) => { + const tooLarge = () => new UploadError(`Photograph exceeds ${limit} bytes`, 413); + if (Number(request.headers['content-length']) > limit) { reject(tooLarge()); return; } + const chunks = []; let length = 0; + request.on('data', chunk => { length += chunk.byteLength; if (length <= limit) chunks.push(chunk); }); + request.on('end', () => length > limit ? reject(tooLarge()) : resolve(Buffer.concat(chunks))); + request.on('error', reject); +}); + export async function downloadImage(file, fetcher = fetch) { const url = new URL(file.download_url); const allowed = url.hostname === 'chatgpt.com' || url.hostname === 'oaiusercontent.com' || url.hostname.endsWith('.oaiusercontent.com'); @@ -54,53 +112,116 @@ return Buffer.concat(chunks); } -export function createMCPServer({ publicURL, store = new ArtifactStore(), download = downloadImage } = {}) { +export function createMCPServer({ publicURL, store = new ArtifactStore(), uploads = new UploadStore(), download = downloadImage, renderer = null } = {}) { const url = new URL(publicURL); if (url.username || url.password || url.search || url.hash || !['https:', 'http:'].includes(url.protocol) || (url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) throw new Error('CAROUSEL_PUBLIC_URL must be an HTTPS origin, or localhost for development'); const baseURL = url.href.replace(/\/$/, ''); - const server = new McpServer({ name: 'carousel-creator', version: '0.1.0' }, { - instructions: 'Create editable projects for ACE: Aesthetic Carousel Editor. First read get_carousel_spec. Write the copy and native layout yourself, then call create_carousel to validate and package a downloadable .carousel file. Use only real catalog names and supplied images. Preserve explicit line breaks; text never auto-wraps. Return the download link and tell the user to import it from Projects or Templates in ACE. Preserve the language requested by the user for carousel content. Schema checks do not confirm visual appearance.' + const versions = { ...capabilities, native_renderer: Boolean(renderer), renderer: renderer?.metadata ?? null, preview_formats: renderer ? ['image/png'] : [], photo_uploads: true }; + const artifactURL = entry => `${baseURL}/downloads/${entry.id}/${encodeURIComponent(entry.name)}`; + const buildInput = async (yaml, asset_files = [], photo_uploads = []) => { + const draft = validateDocument(parseLayout(yaml), { allowUnresolvedAssets: true }); + const images = new Map(), used = referencedAssets(draft); + const uploaded = photo_uploads.map(id => { + const slot = uploads.get(id); + if (!slot) throw new DocumentError(`photo_uploads: upload ${id} expired or was never prepared; call prepare_photo_upload again`); + if (!slot.bytes) throw new DocumentError(`photo_uploads: no file was sent for ${slot.name}; PUT the photograph to its upload_url first`); + return [slot.name, slot.bytes]; + }); + const names = [...asset_files.map((file, index) => file.asset_name ?? `photo_${index + 1}`), ...uploaded.map(([name]) => name)]; + const duplicate = names.find((name, index) => names.indexOf(name) !== index); + if (duplicate !== undefined) throw new DocumentError(`asset_files/photo_uploads: duplicate asset_name ${duplicate}`); + for (const [name, bytes] of uploaded) if (used.has(name)) images.set(name, bytes); + for (const [index, file] of asset_files.entries()) if (used.has(names[index])) images.set(names[index], await download(file)); + return buildCarousel(yaml, images); + }; + const uploadInput = z.array(z.string().regex(/^[a-f0-9]{48}$/)).max(20).optional(); + const renderPreview = async (project, width) => { + const result = await renderer.render(project.bytes, { width, slideCount: project.document.slides }); + const entries = store.addMany([ + { name: 'preview.png', bytes: result.sheet, mimeType: 'image/png' }, + ...result.slides.map(slide => ({ name: slide.file, bytes: slide.bytes, mimeType: 'image/png' })) + ]); + return { + preview: { status: 'rendered', engine: result.engine, sheet_url: artifactURL(entries[0]), expires_at: new Date(entries[0].expires).toISOString(), warnings: result.warnings, + slides: result.slides.map(({ bytes, file, ...slide }, index) => ({ ...slide, url: artifactURL(entries[index + 1]) })) }, + content: [ + { type: 'image', mimeType: 'image/png', data: Buffer.from(result.sheet).toString('base64') }, + ...entries.map(entry => ({ type: 'resource_link', name: entry.name, uri: artifactURL(entry), mimeType: 'image/png', description: 'Native ACE preview' })) + ] + }; + }; + const server = new McpServer({ name: 'carousel-creator', version: packageVersion }, { + instructions: 'Create editable ACE projects. Read get_carousel_spec overview, then rules and relevant catalogs/examples. When writing or revising copy, also read section: writing. Compose before create_carousel. Use real catalog IDs and supplied photographs. For photographs on the user’s computer, when you can run shell commands, call prepare_photo_upload, PUT each file to its upload_url, and pass the upload IDs as photo_uploads. Photographs attached only in a chat without file references cannot reach this server: leave intentional empty photo slots (source: null) and say that the photos are added in ACE. Preserve language and line breaks. Review warnings; do not flatten content to suppress them. When native_renderer is true, create_carousel automatically returns a native contact sheet as image content and per-slide PNG links. Display that image in the chat alongside the editable file. Inspect every slide and use render_carousel to iterate; rendering alone is not a visual review. Report preview failures instead of claiming it was checked. Import via ACE → Projects or Templates → Import.\n\n' + designGuide }); server.registerTool('get_carousel_spec', { title: 'Carousel Format and Resources', - description: 'Read the native carousel format before creating a layout. rules contains layout rules and recipes; catalogs lists exact built-in stickers, frames and fonts. Supply query to search a keyword or resource name.', - inputSchema: { section: z.enum(['rules', 'catalogs', 'full']).default('rules'), query: z.string().max(200).default('') }, + description: 'Read overview for design decisions and capability versions, writing before composing or revising copy (grounded angles, voice, storytelling and editorial review), rules for the native contract, catalogs for complete resources/geometry, or examples for diverse editable compositions. Catalog category avoids ambiguous substring matches; query supports exact IDs and category synonyms. Follow next_offset for more matches. Examples and schemas are not visual previews or ready-to-use copy.', + inputSchema: { section: z.enum(['overview', 'writing', 'rules', 'catalogs', 'examples', 'full']).default('rules'), query: z.string().max(200).default(''), category: z.enum(categories).optional(), offset: z.number().int().nonnegative().default(0), limit: z.number().int().min(1).max(50).default(12) }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } - }, async ({ section, query }) => ({ content: [{ type: 'text', text: getSpec(section, query) }] })); + }, async ({ section, query, ...options }) => { + try { + const result = getSpecResponse(section, query, options); + const { text, ...metadata } = result; + return { content: [{ type: 'text', text: section === 'overview' ? text + '\n\nConnected server capabilities:\n' + JSON.stringify(versions) : text }], structuredContent: { ...metadata, capabilities: versions } }; + } catch (error) { return failure(error); } + }); server.registerTool('validate_carousel', { - title: 'Validate Carousel', description: 'Check a completed native YAML layout without creating a download. Missing extension tags are inferred. Image-bearing layouts are checked during create_carousel after real image assets are supplied.', - inputSchema: { yaml: z.string().max(4 * 1024 * 1024) }, + title: 'Validate Carousel', description: 'Validate native YAML and return actionable, non-blocking design/portability warnings. Set allow_unresolved_assets for a draft with real asset keys; the result explicitly lists unresolved references. Creation still requires real image bytes. No native visual or text measurement check is performed.', + inputSchema: { yaml: z.string().max(4 * 1024 * 1024), allow_unresolved_assets: z.boolean().default(false) }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } - }, async ({ yaml }) => { + }, async ({ yaml, allow_unresolved_assets }) => { try { - const document = validateDocument(parseLayout(yaml)); - return { content: [{ type: 'text', text: JSON.stringify({ valid: true, slides: document.slides, layers: document.layers.length, requires: document.requires, visual_check: 'not performed' }) }] }; + const document = validateDocument(parseLayout(yaml), { allowUnresolvedAssets: allow_unresolved_assets }); + const unresolved_assets = [...referencedAssets(document)].filter(name => !Object.hasOwn(document.assets ?? {}, name)); + const result = { valid: true, slides: document.slides, layers: document.layers.length, requires: document.requires, asset_references_resolved: unresolved_assets.length === 0, unresolved_assets, asset_bytes_checked: false, ...diagnoseDocument(document), visual_check: 'not performed', versions }; + return { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result }; } catch (error) { return failure(error); } }); server.registerTool('create_carousel', { title: 'Create Carousel File', - description: 'Package complete editable YAML into a downloadable .carousel project for ACE: Aesthetic Carousel Editor. Export the file and import it into ACE to continue editing. You must compose content and layout first using get_carousel_spec. For photographs, omit draft assets and use source keys matching asset_files[].asset_name (default photo_1, photo_2...). Only referenced images are downloaded and included; metadata is stripped and hashes generated automatically. Empty slots use source: null. This version supports still images, not video. Download links expire after one hour.', - inputSchema: { yaml: z.string().max(4 * 1024 * 1024), filename: z.string().min(1).max(100).default('carousel'), asset_files: z.array(fileInput).max(20).optional() }, + description: 'Package complete editable YAML into a downloadable ACE .carousel project. When native_renderer is available, automatically return a native contact-sheet image for display in chat and PNG links for every slide. Compose content and layout first using get_carousel_spec. For photographs, omit draft assets and use source keys matching asset_files[].asset_name (default photo_1, photo_2...) or the asset_name of each upload listed in photo_uploads. Only referenced images are included; metadata is stripped and hashes generated automatically. Empty slots use source: null. Supports still images, not video. Preview failure is reported without losing the editable file. Links expire after one hour.', + inputSchema: { yaml: z.string().max(4 * 1024 * 1024), filename: z.string().min(1).max(100).default('carousel'), asset_files: z.array(fileInput).max(20).optional(), photo_uploads: uploadInput, preview: z.boolean().default(true), preview_width: z.number().int().min(360).max(1080).default(720) }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }, _meta: { 'openai/fileParams': ['asset_files'] } - }, async ({ yaml, filename, asset_files = [] }) => { + }, async ({ yaml, filename, asset_files = [], photo_uploads = [], preview, preview_width }) => { try { - const draft = validateDocument(parseLayout(yaml), { allowUnresolvedAssets: true }); - // Reject invalid layouts and determine usage before downloading any photographs. - const images = new Map(); - const names = new Set(); - const used = referencedAssets(draft); - for (const [index, file] of asset_files.entries()) { - const name = file.asset_name ?? `photo_${index + 1}`; - if (names.has(name)) throw new DocumentError(`asset_files: duplicate asset_name ${name}`); - names.add(name); - if (used.has(name)) images.set(name, await download(file)); - } - const result = await buildCarousel(yaml, images); + const result = await buildInput(yaml, asset_files, photo_uploads); const entry = store.add(filename, result.bytes); - const downloadURL = `${baseURL}/downloads/${entry.id}/${encodeURIComponent(entry.name)}`; - const summary = { filename: entry.name, download_url: downloadURL, expires_at: new Date(entry.expires).toISOString(), slides: result.document.slides, layers: result.document.layers.length, bytes: result.bytes.byteLength, visual_check: 'not performed', import: 'ACE: Aesthetic Carousel Editor → Projects (or Templates) → Import → choose the downloaded .carousel file' }; - return { content: [{ type: 'text', text: JSON.stringify(summary) }, { type: 'resource_link', name: entry.name, uri: downloadURL, mimeType: 'application/octet-stream', description: 'Editable ACE project; download and import into ACE: Aesthetic Carousel Editor' }], structuredContent: summary }; + const downloadURL = artifactURL(entry); + let rendered = { preview: { status: preview ? 'unavailable' : 'skipped', reason: preview ? 'Native renderer is not configured on this server' : 'Preview was disabled by the caller' }, content: [] }; + if (preview && renderer) { + try { rendered = await renderPreview(result, preview_width); } + catch (error) { rendered = { preview: { status: 'failed', reason: error.message }, content: [] }; } + } + const summary = { filename: entry.name, download_url: downloadURL, expires_at: new Date(entry.expires).toISOString(), slides: result.document.slides, layers: result.document.layers.length, bytes: result.bytes.byteLength, ...result.diagnostics, versions, preview: rendered.preview, visual_check: rendered.preview.status === 'rendered' ? 'native preview rendered; visual review pending' : 'not performed', import: 'ACE: Aesthetic Carousel Editor → Projects (or Templates) → Import → choose the downloaded .carousel file' }; + return { content: [{ type: 'text', text: JSON.stringify(summary) }, { type: 'resource_link', name: entry.name, uri: downloadURL, mimeType: 'application/octet-stream', description: 'Editable ACE project; download and import into ACE: Aesthetic Carousel Editor' }, ...rendered.content], structuredContent: summary }; + } catch (error) { return failure(error); } + }); + server.registerTool('render_carousel', { + title: 'Render Native Carousel Preview', + description: 'Render every slide with ACE SlideRasterizer, including native fonts, frames, grids, blend modes, masks and multi-composition. Returns a contact sheet as image content for display in chat and individual PNG URLs. Requires capabilities.native_renderer=true and real supplied assets. Use for visual iteration before final delivery; this does not certify visual quality.', + inputSchema: { yaml: z.string().max(4 * 1024 * 1024), asset_files: z.array(fileInput).max(20).optional(), photo_uploads: uploadInput, width: z.number().int().min(360).max(1080).default(720) }, + annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }, + _meta: { 'openai/fileParams': ['asset_files'] } + }, async ({ yaml, asset_files, photo_uploads, width }) => { + try { + if (!renderer) throw new DocumentError('Native rendering is unavailable on this server. Configure CAROUSEL_RENDERER_CONFIG on macOS.'); + const project = await buildInput(yaml, asset_files, photo_uploads); + const rendered = await renderPreview(project, width); + const summary = { slides: project.document.slides, ...project.diagnostics, versions, preview: rendered.preview, visual_check: 'native preview rendered; visual review pending' }; + return { content: [{ type: 'text', text: JSON.stringify(summary) }, ...rendered.content], structuredContent: summary }; + } catch (error) { return failure(error); } + }); + server.registerTool('prepare_photo_upload', { + title: 'Prepare Photo Upload', + description: 'Reserve one-time HTTPS upload links for photographs stored on the user’s computer, for clients that can run shell commands. Send each file with an HTTP PUT, for example curl -sS --fail-with-body -T photo.jpg "<upload_url>", then pass the upload IDs as photo_uploads to create_carousel or render_carousel and use each asset_name as an image layer source. Each link takes one still JPEG, PNG, WebP or AVIF photograph up to max_bytes; convert HEIC to JPEG first. Uploaded photographs are only packaged into projects, never served back, and expire after one hour.', + inputSchema: { asset_names: z.array(z.string().min(1).max(128)).min(1).max(20) }, + annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false } + }, async ({ asset_names }) => { + try { + const slots = uploads.reserve(asset_names); + const result = { uploads: slots.map(slot => ({ asset_name: slot.name, upload_id: slot.id, upload_url: `${baseURL}/uploads/${slot.id}` })), method: 'PUT', max_bytes: uploads.limit, expires_at: new Date(slots[0].expires).toISOString() }; + return { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result }; } catch (error) { return failure(error); } }); return server; @@ -108,23 +229,35 @@ export function httpServer(configuration) { const store = configuration.store ?? new ArtifactStore(); + const uploads = configuration.uploads ?? new UploadStore(); + const renderer = configuration.renderer ?? null; return createServer(async (request, response) => { const path = new URL(request.url, 'http://localhost').pathname; if (path === '/health' && request.method === 'GET') { response.writeHead(200, { 'Content-Type': 'application/json' }); response.end('{"ok":true}'); return; } + const upload = /^\/uploads\/([a-f0-9]{48})$/.exec(path); + if (upload) { + if (!['PUT', 'POST'].includes(request.method)) { response.writeHead(405, { Allow: 'PUT, POST' }); response.end(); return; } + let status = 200, body; + try { body = await uploads.receive(upload[1], await readUpload(request, uploads.limit)); } + catch (error) { status = error.status ?? 400; body = { error: error.message }; } + // An oversized body is left unread, so the connection cannot be reused. + response.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...(status === 413 ? { Connection: 'close' } : {}) }); + response.end(JSON.stringify(body)); return; + } const match = /^\/downloads\/([a-f0-9]{48})\/([^/]+)$/.exec(path); if (match && ['GET', 'HEAD'].includes(request.method)) { const entry = store.get(match[1]); let name; try { name = decodeURIComponent(match[2]); } catch { /* invalid URI is a missing file */ } if (!entry || name !== entry.name) { response.writeHead(404); response.end('Download expired or not found'); return; } - response.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': entry.bytes.byteLength, 'Content-Disposition': `attachment; filename="carousel.carousel"; filename*=UTF-8''${encodeURIComponent(entry.name)}`, 'Cache-Control': 'private, no-store', 'X-Content-Type-Options': 'nosniff' }); + response.writeHead(200, { 'Content-Type': entry.mimeType, 'Content-Length': entry.bytes.byteLength, 'Content-Disposition': `${entry.mimeType === 'image/png' ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(entry.name)}`, 'Cache-Control': 'private, no-store', 'X-Content-Type-Options': 'nosniff' }); response.end(request.method === 'HEAD' ? undefined : entry.bytes); return; } if (path !== '/mcp' || request.method !== 'POST') { response.writeHead(path === '/mcp' ? 405 : 404); response.end(); return; } const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }); - const server = createMCPServer({ publicURL: configuration.publicURL, store }); + const server = createMCPServer({ publicURL: configuration.publicURL, store, uploads, renderer, download: configuration.download }); response.on('close', () => { void transport.close(); void server.close(); }); try { await server.connect(transport); await transport.handleRequest(request, response); } catch { if (!response.headersSent) response.writeHead(500); response.end(); } @@ -135,8 +268,9 @@ const port = Number(process.env.CAROUSEL_PORT ?? 8787); const publicURL = process.env.CAROUSEL_PUBLIC_URL ?? `http://127.0.0.1:${port}`; // Validate configuration at startup, instead of failing during the first tool call. - const check = createMCPServer({ publicURL }); await check.close(); - const app = httpServer({ publicURL }); + const renderer = nativeRenderer(); + const check = createMCPServer({ publicURL, renderer }); await check.close(); + const app = httpServer({ publicURL, renderer }); app.listen(port, process.env.CAROUSEL_HOST ?? '127.0.0.1', () => console.log(`Carousel Creator MCP: ${publicURL}/mcp`)); for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => app.close(() => process.exit(0))); }
Full technical diff · 55 changed fields
added /files/.dockerignore
Field was absent
{
"sha256": "5f2025d9087cd8dc93ebe395791fda81379d9a438862e6e87a69b5faf4cc2b77",
"size": 47
}added /files/Dockerfile
Field was absent
{
"sha256": "fd48d4fdbb81718d2dfbffa6314dee7c56d042aa5dd96d34d45ee3b38ba91e49",
"size": 315
}added /files/LICENSE
Field was absent
{
"sha256": "29942ea87db4340c9a272091ada1b037d8b8b02a2e76dce75509cb8a1e418059",
"size": 2313
}added /files/README.md
Field was absent
{
"sha256": "2014b20842e56188734f68b1490b3cbb5a8dd5365993de5a9935fdc353ff30fc",
"size": 1112
}added /files/data~1catalog.json
Field was absent
{
"sha256": "93dcbf982d26c83ffef1134e7dd95115ce7337cd741ff963f27310becb4fff88",
"size": 425901
}added /files/data~1examples.json
Field was absent
{
"sha256": "9300369fb5b6ed73beb1db3352df1c6fb08e7c891589f6cb4e834beac9ecf124",
"size": 3675
}added /files/examples~1contact-gallery.yaml
Field was absent
{
"sha256": "31383e1bfa7f27aaf6b8d5fd6c450ec039e4be05390398b27341c2d82670733f",
"size": 1626
}added /files/examples~1effects-study.yaml
Field was absent
{
"sha256": "709a9449fa409ef272e58f07764eb2661961888a41329284a9b9aaf4fec1796c",
"size": 1210
}added /files/examples~1free-collage.yaml
Field was absent
{
"sha256": "82e9ea3801309187184a41c6a0490d4af1fa792abca303f462ae9d1a0a9b48a0",
"size": 1139
}added /files/examples~1photo-story.yaml
Field was absent
{
"sha256": "10b1f45fe3eab32b55740bfb3526831a23374ae41120ff05fead7a8761fd9d47",
"size": 1060
}added /files/examples~1smart-stamp.yaml
Field was absent
{
"sha256": "b039ec2ed3e17f9c1757f4380f7abe5ce366bb71d06153e087053cf0f21ae019",
"size": 914
}added /files/examples~1type-led.yaml
Field was absent
{
"sha256": "4220649308cf1b1b3418409b1d880b516df55e3e81dd87a9a44b3226b63d55da",
"size": 1200
}added /files/native~1RendererApp.swift
Field was absent
{
"sha256": "90f7699ffa9b44cb1263dbfebab1f0c1062ebf934cb97a4e718cc577fc66539c",
"size": 4434
}changed /files/package-lock.json/sha256
"ad8658df058ed6e12856e442e18591e49ee0dae7af722e8482940b43fa6d0432"
"1ff211f79a56954cb9b076c2eeedd85b56a361584f8205f11b84764f4f4fdf75"
changed /files/package.json/sha256
"2d43a17f4b9852e8cea13f32c903536b8fc8132607045a72d1f3c710c8de3efc"
"ea3ade412901851f814ecda42c8a32e00e72052bba153bc76f5ff515a30be224"
changed /files/package.json/size
503
908
changed /files/plugin.json/sha256
"9171fcb956c5e86ca839303912e290b0db31660efcd7017698fb540d21bd9ba5"
"8cdbe9970f20ed8120bd20a37b5e1c0604b48aa49e099bd00920e372734e2566"
changed /files/plugin.json/size
11337
10022
changed /files/scripts~1build-carousel.mjs/sha256
"0ec532932ce99c8e02f0f2b57730efd590f17b5c822c18ccb35aaaf43f0dab3f"
"8496a1368be3dbf2ef9245f449ac7a5158dec37184fa30c638ed127e663e65b2"
changed /files/scripts~1build-carousel.mjs/size
1161
1184
added /files/scripts~1check-server.mjs
Field was absent
{
"sha256": "42674a0f1e3797f36247f0093dae0cba1d4b8c407690bd929902d881605767ff",
"size": 4394
}added /files/scripts~1get-spec.mjs
Field was absent
{
"sha256": "771b4044e7bb24fa563b169e8c1880b41709c76c35c5778279004b47f85861d9",
"size": 311
}changed /files/scripts~1package-plugin.mjs/sha256
"698df0d5de5c1a076b100f47f0e00705467a85b77d31b9fae15e5bc36ebb7fa8"
"7df454a476fa80d28753c85541082b193a6b0aa1156bbadc3b80e7a3783ce7a3"
changed /files/scripts~1package-plugin.mjs/size
5594
6810
added /files/scripts~1render-carousel.mjs
Field was absent
{
"sha256": "ae7b739231767f5e17dd7957c179bbdc617fe9d4718437967733df67ad031537",
"size": 1450
}added /files/scripts~1setup-native-renderer.mjs
Field was absent
{
"sha256": "f18984ec4a2fa0587d737db082e4cfa33ea09adda828b4cce915bc61b812d8a6",
"size": 4180
}added /files/scripts~1sync-catalog.mjs
Field was absent
{
"sha256": "315dccd2343306cef52dedbd87798d495f5b0c5ad9e66bdeb9e56bc4e692fae8",
"size": 626
}changed /files/skills~1create-carousel~1SKILL.md/sha256
"9151fb427fee5e69f407c0f1e2ee2f33d77d2b0cc99f4f6409730ad62cd6de25"
"0d4c5ec38abdeeae533cdd8985685be4c0f532a0637f95767d2b999e605fa9e7"
changed /files/skills~1create-carousel~1SKILL.md/size
3939
10609
added /files/skills~1create-carousel~1references~1DESIGN_GUIDE.md
Field was absent
{
"sha256": "2ffd410e74f7028499999a3373315b076c5b1cea0930664fe14726810fc1b618",
"size": 4266
}changed /files/skills~1create-carousel~1references~1DSL_LAYOUT_SPEC.md/sha256
"1278b5f6f81afe42e29433479c5c102e3f75ae16eb93e23dd0eb2a70a15125d1"
"3394ff0ae25f338c282c55fe53dc580ae2c596b62f09da77f24452a5724e6cb0"
changed /files/skills~1create-carousel~1references~1DSL_LAYOUT_SPEC.md/size
212874
218069
added /files/skills~1create-carousel~1references~1WRITING_GUIDE.md
Field was absent
{
"sha256": "fcf68ef35f4e3a858d3d6236e4f3dd182fd52b018c24cd663d257ed1b4a2f477",
"size": 5902
}changed /files/src~1archive.mjs/sha256
"a7c8b4ed23371010e755a3898f926f6bed4d2dd5d257d4a7c98a3041a048b46d"
"f082bea90b8ff5e2655cccd7044bff2248d551e8989c734ee24ce4a8b14b64e9"
changed /files/src~1archive.mjs/size
2908
3498
added /files/src~1catalog.mjs
Field was absent
{
"sha256": "41e0108752aff449b825d15a46808ccdbc43649ba4998143118dca4b3e6ca962",
"size": 6068
}added /files/src~1diagnostics.mjs
Field was absent
{
"sha256": "a620e5631de644ab4a5586127c4612a94695152202b26145336f1d8cf70cacd5",
"size": 7803
}changed /files/src~1document.mjs/sha256
"a38e271507e7b55fb606ad619d907a6836ecdce8dafd502b8f042d155d3a6f7c"
"97a4868aafefe954717d9058d5d910639076613b8e157f80f2f47c42db8c46ec"
changed /files/src~1document.mjs/size
13586
12413
added /files/src~1errors.mjs
Field was absent
{
"sha256": "d1f236c2bff72432b73e3aa27ad675ee1038c80ccf8b457fbb4f717898061660",
"size": 44
}added /files/src~1recipes.mjs
Field was absent
{
"sha256": "660b575c97fc2ee70c0668c482d8f949185cf748acc7caee62b739fc44bbdcd1",
"size": 4103
}added /files/src~1renderer.mjs
Field was absent
{
"sha256": "44b8f9cad2feabe0b99cce1e8a6a6e830b8788ad8112b58c0444eedf720720da",
"size": 7706
}changed /files/src~1server.mjs/sha256
"711db8dfd4f596dcf673d6edd728f96ebc6177fb05284310b40748ffb3777679"
"67a96be7fcf04f603c900ce5151b73943c671f7cdbe65e82ef1be6d494008ca4"
changed /files/src~1server.mjs/size
10916
23198
added /files/src~1spec.mjs
Field was absent
{
"sha256": "2564a2e59d6abbc363d6622741e0c47e0958013bde2472295b8b4586d2858fcf",
"size": 4491
}changed /files/tests~1carousel.test.mjs/sha256
"d006e3346166652fa2104ebe75ec621f545255cd58924f86f4f922065dcac398"
"3d2bef07c979ebb12149442ecc81704d5ead4822ec58bea2d43a64c8b15db026"
changed /files/tests~1carousel.test.mjs/size
8150
8364
added /files/tests~1diagnostics.test.mjs
Field was absent
{
"sha256": "b3baa3352b4a57b38e13e674ec4a96f6a7d8547b251f2a1f6474327773a46cd8",
"size": 5280
}added /files/tests~1discovery.test.mjs
Field was absent
{
"sha256": "d6b7f4e6447e9d398b1b88feead7644d6f29be721a9175d8a29fcdb40e9cec75",
"size": 8147
}changed /files/tests~1http.integration.mjs/sha256
"f77721afd3db1659dda0874702326c703b627185729fb2f462c1ec3dd94aa166"
"d2f14621194cf28139f2e04dd77df988d7423f6c0b2b070e9e231796c383d7ae"
changed /files/tests~1http.integration.mjs/size
2053
2995
added /files/tests~1native.integration.mjs
Field was absent
{
"sha256": "37bc67dd03fa1931a1a52c9ee0860a3b9e056619d0f8a735ee029ac3ae33412d",
"size": 7909
}added /files/tests~1package.test.mjs
Field was absent
{
"sha256": "c7d100facf765bd64763a2825e65a2a2f662f94fc5ded114f524aabf039b11c9",
"size": 2933
}added /files/tests~1preview.test.mjs
Field was absent
{
"sha256": "a3b1c7317dc71d4d065156a89014b131b997e7c0c06203b8b4deac0d24fe9c89",
"size": 5386
}added /files/tests~1uploads.test.mjs
Field was absent
{
"sha256": "3e7dc647b2c2268e0c4cd71e119af9531079aced921011a081d1d5a7448d323b",
"size": 6352
}Full snapshot data
{
"files": {
".app.json": {
"sha256": "5e21add253e73f295f8dd9638efdad6133db89cea1afc6eb8bef86fcd9dc9d29",
"size": 108
},
".dockerignore": {
"sha256": "5f2025d9087cd8dc93ebe395791fda81379d9a438862e6e87a69b5faf4cc2b77",
"size": 47
},
"Dockerfile": {
"sha256": "fd48d4fdbb81718d2dfbffa6314dee7c56d042aa5dd96d34d45ee3b38ba91e49",
"size": 315
},
"LICENSE": {
"sha256": "29942ea87db4340c9a272091ada1b037d8b8b02a2e76dce75509cb8a1e418059",
"size": 2313
},
"README.md": {
"sha256": "2014b20842e56188734f68b1490b3cbb5a8dd5365993de5a9935fdc353ff30fc",
"size": 1112
},
"assets/icon.png": {
"sha256": "6dfad6349d9c99ac11613f4abbe19fe6e608f89357e0c2ff87abf7079072cb83",
"size": 31011
},
"data/catalog.json": {
"sha256": "93dcbf982d26c83ffef1134e7dd95115ce7337cd741ff963f27310becb4fff88",
"size": 425901
},
"data/examples.json": {
"sha256": "9300369fb5b6ed73beb1db3352df1c6fb08e7c891589f6cb4e834beac9ecf124",
"size": 3675
},
"examples/contact-gallery.yaml": {
"sha256": "31383e1bfa7f27aaf6b8d5fd6c450ec039e4be05390398b27341c2d82670733f",
"size": 1626
},
"examples/creator-demo-preview.png": {
"sha256": "72f9bdd64e2f4bbbeee007d645a75ea38e90713f99079c5fbafe74f8c3ea306c",
"size": 69512
},
"examples/creator-demo.carousel": {
"sha256": "4a16b05ee1c671d4e57d58136058838c15b542456f55d0e25df92941f9a64465",
"size": 5679
},
"examples/creator-demo.yaml": {
"sha256": "ac375143bd9958012deed3c38cb84c212a585c40a96c051f5503df17b74f642a",
"size": 3908
},
"examples/effects-study.yaml": {
"sha256": "709a9449fa409ef272e58f07764eb2661961888a41329284a9b9aaf4fec1796c",
"size": 1210
},
"examples/free-collage.yaml": {
"sha256": "82e9ea3801309187184a41c6a0490d4af1fa792abca303f462ae9d1a0a9b48a0",
"size": 1139
},
"examples/photo-story.yaml": {
"sha256": "10b1f45fe3eab32b55740bfb3526831a23374ae41120ff05fead7a8761fd9d47",
"size": 1060
},
"examples/smart-stamp.yaml": {
"sha256": "b039ec2ed3e17f9c1757f4380f7abe5ce366bb71d06153e087053cf0f21ae019",
"size": 914
},
"examples/type-led.yaml": {
"sha256": "4220649308cf1b1b3418409b1d880b516df55e3e81dd87a9a44b3226b63d55da",
"size": 1200
},
"mcp.json": {
"sha256": "828f2d5c8663ded2b9b701aa0fd0f0848bd1fbd865c1e8a170bb1b13148e7ccb",
"size": 210
},
"native/RendererApp.swift": {
"sha256": "90f7699ffa9b44cb1263dbfebab1f0c1062ebf934cb97a4e718cc577fc66539c",
"size": 4434
},
"package-lock.json": {
"sha256": "1ff211f79a56954cb9b076c2eeedd85b56a361584f8205f11b84764f4f4fdf75",
"size": 61504
},
"package.json": {
"sha256": "ea3ade412901851f814ecda42c8a32e00e72052bba153bc76f5ff515a30be224",
"size": 908
},
"plugin.json": {
"sha256": "8cdbe9970f20ed8120bd20a37b5e1c0604b48aa49e099bd00920e372734e2566",
"size": 10022
},
"scripts/build-carousel.mjs": {
"sha256": "8496a1368be3dbf2ef9245f449ac7a5158dec37184fa30c638ed127e663e65b2",
"size": 1184
},
"scripts/check-server.mjs": {
"sha256": "42674a0f1e3797f36247f0093dae0cba1d4b8c407690bd929902d881605767ff",
"size": 4394
},
"scripts/get-spec.mjs": {
"sha256": "771b4044e7bb24fa563b169e8c1880b41709c76c35c5778279004b47f85861d9",
"size": 311
},
"scripts/package-plugin.mjs": {
"sha256": "7df454a476fa80d28753c85541082b193a6b0aa1156bbadc3b80e7a3783ce7a3",
"size": 6810
},
"scripts/render-carousel.mjs": {
"sha256": "ae7b739231767f5e17dd7957c179bbdc617fe9d4718437967733df67ad031537",
"size": 1450
},
"scripts/setup-native-renderer.mjs": {
"sha256": "f18984ec4a2fa0587d737db082e4cfa33ea09adda828b4cce915bc61b812d8a6",
"size": 4180
},
"scripts/sync-catalog.mjs": {
"sha256": "315dccd2343306cef52dedbd87798d495f5b0c5ad9e66bdeb9e56bc4e692fae8",
"size": 626
},
"skills/create-carousel/SKILL.md": {
"sha256": "0d4c5ec38abdeeae533cdd8985685be4c0f532a0637f95767d2b999e605fa9e7",
"size": 10609
},
"skills/create-carousel/references/DESIGN_GUIDE.md": {
"sha256": "2ffd410e74f7028499999a3373315b076c5b1cea0930664fe14726810fc1b618",
"size": 4266
},
"skills/create-carousel/references/DSL_LAYOUT_SPEC.md": {
"sha256": "3394ff0ae25f338c282c55fe53dc580ae2c596b62f09da77f24452a5724e6cb0",
"size": 218069
},
"skills/create-carousel/references/WRITING_GUIDE.md": {
"sha256": "fcf68ef35f4e3a858d3d6236e4f3dd182fd52b018c24cd663d257ed1b4a2f477",
"size": 5902
},
"src/archive.mjs": {
"sha256": "f082bea90b8ff5e2655cccd7044bff2248d551e8989c734ee24ce4a8b14b64e9",
"size": 3498
},
"src/catalog.mjs": {
"sha256": "41e0108752aff449b825d15a46808ccdbc43649ba4998143118dca4b3e6ca962",
"size": 6068
},
"src/diagnostics.mjs": {
"sha256": "a620e5631de644ab4a5586127c4612a94695152202b26145336f1d8cf70cacd5",
"size": 7803
},
"src/document.mjs": {
"sha256": "97a4868aafefe954717d9058d5d910639076613b8e157f80f2f47c42db8c46ec",
"size": 12413
},
"src/errors.mjs": {
"sha256": "d1f236c2bff72432b73e3aa27ad675ee1038c80ccf8b457fbb4f717898061660",
"size": 44
},
"src/recipes.mjs": {
"sha256": "660b575c97fc2ee70c0668c482d8f949185cf748acc7caee62b739fc44bbdcd1",
"size": 4103
},
"src/renderer.mjs": {
"sha256": "44b8f9cad2feabe0b99cce1e8a6a6e830b8788ad8112b58c0444eedf720720da",
"size": 7706
},
"src/server.mjs": {
"sha256": "67a96be7fcf04f603c900ce5151b73943c671f7cdbe65e82ef1be6d494008ca4",
"size": 23198
},
"src/spec.mjs": {
"sha256": "2564a2e59d6abbc363d6622741e0c47e0958013bde2472295b8b4586d2858fcf",
"size": 4491
},
"tests/carousel.test.mjs": {
"sha256": "3d2bef07c979ebb12149442ecc81704d5ead4822ec58bea2d43a64c8b15db026",
"size": 8364
},
"tests/diagnostics.test.mjs": {
"sha256": "b3baa3352b4a57b38e13e674ec4a96f6a7d8547b251f2a1f6474327773a46cd8",
"size": 5280
},
"tests/discovery.test.mjs": {
"sha256": "d6b7f4e6447e9d398b1b88feead7644d6f29be721a9175d8a29fcdb40e9cec75",
"size": 8147
},
"tests/http.integration.mjs": {
"sha256": "d2f14621194cf28139f2e04dd77df988d7423f6c0b2b070e9e231796c383d7ae",
"size": 2995
},
"tests/native.integration.mjs": {
"sha256": "37bc67dd03fa1931a1a52c9ee0860a3b9e056619d0f8a735ee029ac3ae33412d",
"size": 7909
},
"tests/package.test.mjs": {
"sha256": "c7d100facf765bd64763a2825e65a2a2f662f94fc5ded114f524aabf039b11c9",
"size": 2933
},
"tests/preview.test.mjs": {
"sha256": "a3b1c7317dc71d4d065156a89014b131b997e7c0c06203b8b4deac0d24fe9c89",
"size": 5386
},
"tests/uploads.test.mjs": {
"sha256": "3e7dc647b2c2268e0c4cd71e119af9531079aced921011a081d1d5a7448d323b",
"size": 6352
}
}
}SHA-256 of public snapshot: 64959cf6937ca474b78ee128969c5afdcb3064e41f5dc2d8e4ef072e2c240fab