@contenthero/mcp 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +1 -1
- package/dist/format.d.ts +51 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +43 -4
- package/dist/format.js.map +1 -1
- package/dist/groups.js +1 -1
- package/dist/groups.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +123 -44
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -119,6 +119,72 @@ async function fetchSnapshotBase64(url) {
|
|
|
119
119
|
return null;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The asset itself, ready to attach to a finished generation.
|
|
124
|
+
*
|
|
125
|
+
* ## The rule, per medium
|
|
126
|
+
*
|
|
127
|
+
* ⭐⭐⭐ **BYTES FOR WHAT MCP CAN CARRY, A LINK FOR WHAT IT CANNOT.** Images and audio have first-class
|
|
128
|
+
* content blocks and modest sizes, so they are embedded: that is what makes them render in the chat, and it
|
|
129
|
+
* is also what makes them permanent, because bytes in a transcript cannot expire. Video has no block of its
|
|
130
|
+
* own and a ten-second 1080p clip would be megabytes of base64 in every future turn of the conversation, so
|
|
131
|
+
* it travels as a `resource_link` pointing at a capability url.
|
|
132
|
+
*
|
|
133
|
+
* ⛔ **FAILING TO FETCH IS NOT AN ERROR.** A generation that succeeded must never be reported as failed
|
|
134
|
+
* because we could not inline a preview of it. Every failure path returns no attachment and the text result
|
|
135
|
+
* stands on its own, which is exactly what the caller got before this existed.
|
|
136
|
+
*
|
|
137
|
+
* ⚠️ ONLY THE FIRST OUTPUT IS EMBEDDED WHEN THERE ARE MANY. A four-image batch as four base64 payloads is a
|
|
138
|
+
* large multiple of the same conversation cost, and the urls for the rest are already in the text. The cap
|
|
139
|
+
* is stated here rather than left implicit, because a silent truncation reads as "that is all there was".
|
|
140
|
+
*/
|
|
141
|
+
/**
|
|
142
|
+
* ⭐⭐⭐ **EVERY OUTPUT, AS A LINK, NOT THE FIRST ONE AS BYTES.**
|
|
143
|
+
*
|
|
144
|
+
* The first version embedded base64 for images and audio and capped at one attachment. Both halves were
|
|
145
|
+
* wrong, and they were wrong together:
|
|
146
|
+
*
|
|
147
|
+
* - **The cap made a four-variation batch show one variation.** Generating four and seeing one is not a
|
|
148
|
+
* smaller version of the feature, it is a broken one: the whole point of a batch is to compare them.
|
|
149
|
+
* - **Base64 is charged to the user's context on every subsequent turn.** A four-image batch embedded as
|
|
150
|
+
* bytes is a large multiple of the same cost, repeated for the rest of the conversation.
|
|
151
|
+
*
|
|
152
|
+
* ⭐ A `resource_link` costs a URL and renders in the host's UI, so ALL of them can come back. Verified
|
|
153
|
+
* against a working implementation: the Higgsfield MCP returns `resource_link` for its media and it renders.
|
|
154
|
+
*
|
|
155
|
+
* ## ⛔ THE THING THIS DELIBERATELY GIVES UP, AND WHERE IT WENT INSTEAD
|
|
156
|
+
*
|
|
157
|
+
* An `image` block feeds the MODEL's vision; a `resource_link` gives the HOST something to render for the
|
|
158
|
+
* human. Measured in this very session: when a link came back from another MCP, the model received text and
|
|
159
|
+
* could not see the picture.
|
|
160
|
+
*
|
|
161
|
+
* So the model can no longer critique a generation it just made from this result alone. That is the correct
|
|
162
|
+
* trade, because **`get_media` already exists to embed bytes for exactly that purpose** and an agent calls
|
|
163
|
+
* it when it actually needs to look. Deciding on every generation that the model probably wants to look was
|
|
164
|
+
* the wrong default: it spent the user's context to answer a question nobody asked.
|
|
165
|
+
*
|
|
166
|
+
* ⚠️ NO SSRF FETCH HAPPENS HERE ANY MORE. Nothing is downloaded, so the allowlist that guards
|
|
167
|
+
* `fetchSnapshotBase64` is not on this path; the url is handed to the host to fetch under its own rules.
|
|
168
|
+
*/
|
|
169
|
+
const LINK_MIME = {
|
|
170
|
+
image: 'image/png',
|
|
171
|
+
video: 'video/mp4',
|
|
172
|
+
audio: 'audio/mpeg',
|
|
173
|
+
};
|
|
174
|
+
function attachmentsFor(gen) {
|
|
175
|
+
const urls = (gen.outputUrls ?? []).filter((u) => typeof u === 'string' && u.length > 0);
|
|
176
|
+
const mimeType = LINK_MIME[gen.contentType];
|
|
177
|
+
if (!mimeType)
|
|
178
|
+
return [];
|
|
179
|
+
const ext = mimeType.split('/')[1];
|
|
180
|
+
return urls.map((uri, i) => ({
|
|
181
|
+
kind: 'link',
|
|
182
|
+
uri,
|
|
183
|
+
mimeType,
|
|
184
|
+
// Named per output so a batch reads as four distinct things rather than four copies of one name.
|
|
185
|
+
name: `${gen.outputId}${urls.length > 1 ? `-${i + 1}` : ''}.${ext}`,
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
122
188
|
/**
|
|
123
189
|
* True when an image URL is safe to fetch into an image block. SSRF allowlist:
|
|
124
190
|
* our storage hosts plus the finite set of generation-provider CDNs that our
|
|
@@ -134,6 +200,11 @@ function isAllowedImageHost(url) {
|
|
|
134
200
|
if (u.username || u.password)
|
|
135
201
|
return false;
|
|
136
202
|
return (u.host === 'cloud.contenthero.ai' ||
|
|
203
|
+
// ⭐ THE MEDIA GATEWAY. Generated assets now address through it with a capability token rather than a
|
|
204
|
+
// presigned R2 url, so without this every inline attachment would be silently dropped by the SSRF
|
|
205
|
+
// allowlist and the agent would be back to a bare link.
|
|
206
|
+
u.host === 'media.contenthero.ai' ||
|
|
207
|
+
u.host === 'cdn.contenthero.ai' ||
|
|
137
208
|
u.host.endsWith('.supabase.co') ||
|
|
138
209
|
u.host.endsWith('.fal.media') ||
|
|
139
210
|
u.host.endsWith('.cloudinary.com'));
|
|
@@ -297,7 +368,7 @@ export function registerTools(server, opts) {
|
|
|
297
368
|
server.registerTool('generate_image', {
|
|
298
369
|
title: 'Generate Image',
|
|
299
370
|
annotations: WRITE,
|
|
300
|
-
description: 'Generate one or more images from a text prompt (optionally image-to-image with reference images). Waits for the result and returns the image URLs. Optionally pass projectId to place the generated image onto that project in the same call, controlled by an optional placement: a VIDEO timeline places a clip on a track, a CANVAS design places a layer on a slide (defaulting to the slide the user is focused on). Omit projectId to save a standalone library output. SPENDS CREDITS: pass getCost to preview the price first, which runs nothing and charges nothing.',
|
|
371
|
+
description: 'Generate one or more images from a text prompt (optionally image-to-image with reference images). Waits for the result and returns the image URLs. Optionally pass projectId to place the generated image onto that project in the same call, controlled by an optional placement: a VIDEO timeline places a clip on a track, a CANVAS design places a layer on a slide (defaulting to the slide the user is focused on). Omit projectId to save a standalone library output. The result LINKS each output so the user sees it inline; to SEE it yourself (judge a face, check legibility, pick between variations) call get_media with the outputId. SPENDS CREDITS: pass getCost to preview the price first, which runs nothing and charges nothing.',
|
|
301
372
|
inputSchema: {
|
|
302
373
|
modelId: z.enum(models.image).describe(IMAGE_MODEL_GUIDANCE),
|
|
303
374
|
prompt: z
|
|
@@ -344,7 +415,7 @@ export function registerTools(server, opts) {
|
|
|
344
415
|
if (args.getCost)
|
|
345
416
|
return costResult(await client.estimateCost(request));
|
|
346
417
|
const gen = await client.generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
|
|
347
|
-
return completedResult(gen);
|
|
418
|
+
return completedResult(gen, attachmentsFor(gen));
|
|
348
419
|
}
|
|
349
420
|
catch (err) {
|
|
350
421
|
// A SUBMITTED generation is running and charged. Whether the wait timed out or a
|
|
@@ -399,7 +470,7 @@ export function registerTools(server, opts) {
|
|
|
399
470
|
if (args.getCost)
|
|
400
471
|
return costResult(await client.estimateBoardCost(request));
|
|
401
472
|
const gen = await client.generateBoardAndWait(request, { timeoutMs: SMART_WAIT_MS });
|
|
402
|
-
return completedResult(gen);
|
|
473
|
+
return completedResult(gen, attachmentsFor(gen));
|
|
403
474
|
}
|
|
404
475
|
catch (err) {
|
|
405
476
|
// A SUBMITTED generation is running and charged. Whether the wait timed out or a
|
|
@@ -415,7 +486,7 @@ export function registerTools(server, opts) {
|
|
|
415
486
|
server.registerTool('generate_video', {
|
|
416
487
|
title: 'Generate Video',
|
|
417
488
|
annotations: WRITE,
|
|
418
|
-
description: 'Generate a video from a text prompt (optionally from a start/end frame or reference images/videos/audio). Waits up to ~50s; if the render is still running it returns an outputId to poll with get_generation_status. Seedance 2.0 has two input modes selected by which references you pass: a startFrame (and optional endFrame) runs start/end-frame mode; referenceImages / referenceVideos / referenceAudio (without a startFrame) run references mode. Optionally pass projectId to place the generated video onto that project in the same call, controlled by an optional placement: a VIDEO timeline places a clip on a track, a CANVAS design places a layer on a slide (defaulting to the slide the user is focused on). Omit projectId to save a standalone library output. SPENDS CREDITS: pass getCost to preview the price first, which runs nothing and charges nothing.',
|
|
489
|
+
description: 'Generate a video from a text prompt (optionally from a start/end frame or reference images/videos/audio). Waits up to ~50s; if the render is still running it returns an outputId to poll with get_generation_status. Seedance 2.0 has two input modes selected by which references you pass: a startFrame (and optional endFrame) runs start/end-frame mode; referenceImages / referenceVideos / referenceAudio (without a startFrame) run references mode. Optionally pass projectId to place the generated video onto that project in the same call, controlled by an optional placement: a VIDEO timeline places a clip on a track, a CANVAS design places a layer on a slide (defaulting to the slide the user is focused on). Omit projectId to save a standalone library output. The result LINKS each output so the user sees it inline; to SEE it yourself (judge a face, check legibility, pick between variations) call get_media with the outputId. SPENDS CREDITS: pass getCost to preview the price first, which runs nothing and charges nothing.',
|
|
419
490
|
inputSchema: {
|
|
420
491
|
modelId: z.enum(models.video).describe(VIDEO_MODEL_GUIDANCE),
|
|
421
492
|
prompt: z
|
|
@@ -500,7 +571,7 @@ export function registerTools(server, opts) {
|
|
|
500
571
|
if (args.getCost)
|
|
501
572
|
return costResult(await client.estimateCost(request));
|
|
502
573
|
const gen = await client.generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
|
|
503
|
-
return completedResult(gen);
|
|
574
|
+
return completedResult(gen, attachmentsFor(gen));
|
|
504
575
|
}
|
|
505
576
|
catch (err) {
|
|
506
577
|
// A SUBMITTED generation is running and charged. Whether the wait timed out or a
|
|
@@ -516,7 +587,7 @@ export function registerTools(server, opts) {
|
|
|
516
587
|
server.registerTool('generate_audio', {
|
|
517
588
|
title: 'Generate Audio',
|
|
518
589
|
annotations: WRITE,
|
|
519
|
-
description: 'Generate audio with ElevenLabs: speech (TTS), music, or a sound effect. Returns the audio URL directly (synchronous, no polling). Optionally pass projectId to place the generated audio onto that editor project\'s timeline in the same call, controlled by an optional placement; omit projectId to save a standalone library output. SPENDS CREDITS: pass getCost to preview the price first, which runs nothing and charges nothing.',
|
|
590
|
+
description: 'Generate audio with ElevenLabs: speech (TTS), music, or a sound effect. Returns the audio URL directly (synchronous, no polling). Optionally pass projectId to place the generated audio onto that editor project\'s timeline in the same call, controlled by an optional placement; omit projectId to save a standalone library output. The result LINKS each output so the user sees it inline; to SEE it yourself (judge a face, check legibility, pick between variations) call get_media with the outputId. SPENDS CREDITS: pass getCost to preview the price first, which runs nothing and charges nothing.',
|
|
520
591
|
inputSchema: {
|
|
521
592
|
modelId: z.enum(models.audio).describe(AUDIO_MODEL_GUIDANCE),
|
|
522
593
|
prompt: z.string().optional().describe('For music / sfx: what to generate.'),
|
|
@@ -644,7 +715,7 @@ export function registerTools(server, opts) {
|
|
|
644
715
|
if (args.getCost)
|
|
645
716
|
return costResult(await client.estimateCost(request));
|
|
646
717
|
const gen = await client.generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
|
|
647
|
-
return completedResult(gen);
|
|
718
|
+
return completedResult(gen, attachmentsFor(gen));
|
|
648
719
|
}
|
|
649
720
|
catch (err) {
|
|
650
721
|
// A SUBMITTED generation is running and charged. Whether the wait timed out or a
|
|
@@ -705,7 +776,7 @@ export function registerTools(server, opts) {
|
|
|
705
776
|
if (args.getCost)
|
|
706
777
|
return costResult(await client.estimateCost(request));
|
|
707
778
|
const gen = await client.generateAndWait(request, { timeoutMs: SMART_WAIT_MS });
|
|
708
|
-
return completedResult(gen);
|
|
779
|
+
return completedResult(gen, attachmentsFor(gen));
|
|
709
780
|
}
|
|
710
781
|
catch (err) {
|
|
711
782
|
// A SUBMITTED generation is running and charged. Whether the wait timed out or a
|
|
@@ -720,6 +791,13 @@ export function registerTools(server, opts) {
|
|
|
720
791
|
// -- transcribe -----------------------------------------------------------
|
|
721
792
|
server.registerTool('transcribe', {
|
|
722
793
|
title: 'Transcribe Audio',
|
|
794
|
+
/*
|
|
795
|
+
⭐ ANNOTATED, NOT JUST ARGUED FOR. The comment below has said "NOT read-only" since this tool
|
|
796
|
+
shipped, and nobody ever wrote the annotation, so the tool went out carrying NEITHER hint. Claude
|
|
797
|
+
files an unannotated tool under "Other", which is how 2 of 88 ended up unclassified: a comment
|
|
798
|
+
describing an enforcement nobody built reads exactly like one that was built.
|
|
799
|
+
*/
|
|
800
|
+
annotations: WRITE,
|
|
723
801
|
// NOT read-only, despite only returning text. readOnlyHint is a host's signal that a
|
|
724
802
|
// tool is safe to call without asking the user, and this one is metered per minute of
|
|
725
803
|
// audio: annotated READ, an agent could transcribe a two-hour file repeatedly,
|
|
@@ -2496,19 +2574,44 @@ export function registerTools(server, opts) {
|
|
|
2496
2574
|
inputSchema: {
|
|
2497
2575
|
projectId: z.string().optional().describe('Scope to a specific project (editor/canvas). Omit for the user\'s most-recent-active surface anywhere. Required for render when no session is live.'),
|
|
2498
2576
|
capture: z.boolean().optional().describe("Also return a screenshot of the user's live viewport (their SCREEN), captured at read time. Default false returns structured context only. Request it only when the task depends on seeing the live, as-shown state including unsaved UI. To see the composed OUTPUT rather than the screen, use render instead."),
|
|
2499
|
-
render: z.boolean().optional().describe('Also
|
|
2500
|
-
mode: z.enum(['
|
|
2501
|
-
frame: z.number().int().min(0).optional().describe('
|
|
2502
|
-
slideId: z.string().optional().describe('
|
|
2503
|
-
slideIndex: z.number().int().min(1).optional().describe('
|
|
2504
|
-
fromFrame: z.number().int().min(0).optional().describe('
|
|
2505
|
-
toFrame: z.number().int().min(0).optional().describe('
|
|
2506
|
-
count: z.number().int().min(1).optional().describe('
|
|
2507
|
-
width: z.number().int().min(48).max(1440).optional().describe('
|
|
2577
|
+
render: z.boolean().optional().describe('Also render your work so you can visually verify edits. Ephemeral, stored nowhere, counts against no quota, works without a live tab. render=true alone renders the current focus point as one image; add count with fromFrame/toFrame for several across a range; mode=video returns a short playable clip of that range. Use this to check your work, not export_project, which produces a file the user KEEPS. To watch a RAW source clip instead of your composition, use get_media with a video item.'),
|
|
2578
|
+
mode: z.enum(['image', 'video']).optional().describe("What MEDIUM to render (default 'image'). 'image' returns composed frames INLINE: one by default, or several across a range when you pass count with fromFrame/toFrame, to judge motion, flow and cut placement. 'video' returns the range actually playing, as a short low-res composed clip, for timing a cut or a beat that separate frames cannot show; it is a JOB, returning a renderId to poll with get_preview, because it has to be rendered."),
|
|
2579
|
+
frame: z.number().int().min(0).optional().describe("mode='image' (editor): which single timeline frame to render. Omit to render the current playhead frame."),
|
|
2580
|
+
slideId: z.string().optional().describe("mode='image' (canvas): the id of the slide to render. Omit to render the focused slide."),
|
|
2581
|
+
slideIndex: z.number().int().min(1).optional().describe("mode='image' (canvas): the 1-based slide index to render (alternative to slideId)."),
|
|
2582
|
+
fromFrame: z.number().int().min(0).optional().describe('Start timeline frame of the range, for several frames or a video. Omit to start at the beginning.'),
|
|
2583
|
+
toFrame: z.number().int().min(0).optional().describe('End timeline frame of the range. Omit to run to the end.'),
|
|
2584
|
+
count: z.number().int().min(1).optional().describe("mode='image': how many frames to return across the range. Omit for one frame at the focus point, or a proportional default when a range is given."),
|
|
2585
|
+
width: z.number().int().min(48).max(1440).optional().describe("mode='image': render at an explicit DISPLAY width in pixels, to judge legibility at the size the output will actually be seen (a course tile, a thumbnail, a feed card) rather than at full resolution, where small type always looks fine. Height follows the composition aspect ratio and is not settable. Clamped; the size produced is reported back on rendered."),
|
|
2508
2586
|
},
|
|
2509
2587
|
}, async (args, extra) => {
|
|
2510
2588
|
try {
|
|
2511
2589
|
const client = await getClient(extra);
|
|
2590
|
+
/*
|
|
2591
|
+
⭐⭐⭐ **`video` IS THE THIRD RUNG OF THIS LADDER, NOT A SEPARATE TOOL.** It used to be
|
|
2592
|
+
`create_preview`, and the split was drawn on HOW the render is delivered (a job, not inline)
|
|
2593
|
+
rather than on WHAT the caller is asking for. Both answers to "let me look at my own work,
|
|
2594
|
+
ephemerally, without producing a deliverable" now live behind one question.
|
|
2595
|
+
|
|
2596
|
+
⛔ THE EVIDENCE THE OLD BOUNDARY WAS WRONG WAS IN THE DESCRIPTIONS. `get_context` ended with
|
|
2597
|
+
"for a composed VIDEO of a range use create_preview" and `create_preview` ended with "to see a
|
|
2598
|
+
single frame or a few frames use get_context render". Two tools each telling the agent when to
|
|
2599
|
+
use the other is routing work the schema should be doing. The CLI had already reached this
|
|
2600
|
+
conclusion: it exposes `context preview`, a sibling of `context`, while export lives under
|
|
2601
|
+
`project`.
|
|
2602
|
+
|
|
2603
|
+
⚠️ THE TRANSPORT IS UNCHANGED. This is a facade over the same client call the old tool made, so
|
|
2604
|
+
nothing moved server-side and the SDK needed no new field.
|
|
2605
|
+
*/
|
|
2606
|
+
if (args.mode === 'video') {
|
|
2607
|
+
const job = await client.createPreview({
|
|
2608
|
+
projectId: args.projectId ?? '',
|
|
2609
|
+
fromFrame: args.fromFrame,
|
|
2610
|
+
toFrame: args.toFrame,
|
|
2611
|
+
});
|
|
2612
|
+
return text(`Preview render started (frames ${job.fromFrame}-${job.toFrame}, ~${job.durationSeconds}s).\n` +
|
|
2613
|
+
`Poll get_preview with renderId="${job.renderId}" and bucketName="${job.bucketName}" until status is "done", then fetch the returned url.`);
|
|
2614
|
+
}
|
|
2512
2615
|
const result = await client.getContext({
|
|
2513
2616
|
projectId: args.projectId,
|
|
2514
2617
|
capture: args.capture,
|
|
@@ -2530,37 +2633,13 @@ export function registerTools(server, opts) {
|
|
|
2530
2633
|
return errorResult(err);
|
|
2531
2634
|
}
|
|
2532
2635
|
});
|
|
2533
|
-
server.registerTool('create_preview', {
|
|
2534
|
-
title: 'Create Preview',
|
|
2535
|
-
// NOT read-only. It does not charge the caller's credits (unlike transcribe), but it
|
|
2536
|
-
// STARTS A RENDER JOB: it returns a renderId you then poll, which is state that did
|
|
2537
|
-
// not exist before the call. readOnlyHint says a tool does not modify its
|
|
2538
|
-
// environment, and dispatching a Lambda render does. get_preview, which only reads
|
|
2539
|
-
// that job, stays READ.
|
|
2540
|
-
description: "Create an async PREVIEW of your work (ephemeral, never stored, not a deliverable). Currently a short low-res COMPOSED VIDEO of an editor range, so you can assess motion, cuts, transitions, and pacing that a still cannot show. This is a JOB: it returns a renderId + bucketName; poll get_preview with those until it is done, then fetch the returned url. To see a single frame or a few frames instead (cheaper, instant), use get_context render. Requires the context:read scope.",
|
|
2541
|
-
inputSchema: {
|
|
2542
|
-
projectId: z.string().describe('The editor project to preview.'),
|
|
2543
|
-
fromFrame: z.number().int().min(0).optional().describe('Start timeline frame of the range. Omit to start at the beginning.'),
|
|
2544
|
-
toFrame: z.number().int().min(0).optional().describe('End timeline frame. Omit to run to the end (capped to a short preview length).'),
|
|
2545
|
-
},
|
|
2546
|
-
}, async (args, extra) => {
|
|
2547
|
-
try {
|
|
2548
|
-
const client = await getClient(extra);
|
|
2549
|
-
const job = await client.createPreview({ projectId: args.projectId, fromFrame: args.fromFrame, toFrame: args.toFrame });
|
|
2550
|
-
return text(`Preview render started (frames ${job.fromFrame}-${job.toFrame}, ~${job.durationSeconds}s).\n` +
|
|
2551
|
-
`Poll get_preview with renderId="${job.renderId}" and bucketName="${job.bucketName}" until status is "done", then fetch the returned url.`);
|
|
2552
|
-
}
|
|
2553
|
-
catch (err) {
|
|
2554
|
-
return errorResult(err);
|
|
2555
|
-
}
|
|
2556
|
-
});
|
|
2557
2636
|
server.registerTool('get_preview', {
|
|
2558
2637
|
title: 'Get Preview',
|
|
2559
2638
|
annotations: READ,
|
|
2560
|
-
description: 'Poll a preview started with
|
|
2639
|
+
description: 'Poll a preview started by get_context with mode="video". While rendering, returns the progress; when done, returns a short-lived url to the ephemeral preview output (plus the estimated cost). Requires the context:read scope.',
|
|
2561
2640
|
inputSchema: {
|
|
2562
|
-
renderId: z.string().describe('The renderId returned by
|
|
2563
|
-
bucketName: z.string().describe('The bucketName returned by
|
|
2641
|
+
renderId: z.string().describe('The renderId returned by get_context with mode="video".'),
|
|
2642
|
+
bucketName: z.string().describe('The bucketName returned by get_context with mode="video".'),
|
|
2564
2643
|
},
|
|
2565
2644
|
}, async (args, extra) => {
|
|
2566
2645
|
try {
|