@clipwright/mcp-server 0.38.2 → 0.40.0

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/server.js CHANGED
@@ -5,11 +5,11 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { z } from "zod";
6
6
  // `mcpUgcInputShape`, not the full shape: fields the server rejects must not
7
7
  // be offered to a caller who would get a 400 for using them.
8
- import { mcpUgcInputShape, actorDefaultsInput, BROLL_POLICY_DESCRIPTION, CANDIDATE_CHARGE_TEXT, candidateRequest, CATALOG_LANGUAGES, CREATE_ACTOR_DESCRIPTION, createActorInputShape, DEFAULT_FACELESS_VOICE, FACELESS_VOICE_NAMES, generateImageRequest, generateVideoRequest, MODEL_VOICE_MODEL, MODEL_VOICE_NAMES, SCRIPT_LENGTH_DESCRIPTION, voicesQueryShape, agentRetryShape, runId, MAKE_UGC_DESCRIPTION, MAKE_UGC_AGENT_PROTOCOL, MAKE_FACELESS_DESCRIPTION, MAKE_FACELESS_AGENT_PROTOCOL, MCP_SERVER_INSTRUCTIONS, makeFacelessInput, offeredFacelessInput, REVISION_CHARGE_TEXT, revisionRequest, UPLOAD_IMAGE_DESCRIPTION, UPLOAD_MAX_BYTES, UPLOAD_MEDIA_TYPES, CREATE_UPLOAD_DESCRIPTION, GET_UPLOAD_DESCRIPTION, uploadId, sniffUploadMediaType, ASSET_KINDS, ASSET_LIST_MAX_LIMIT, ASSET_VIDEO_MAX_BYTES, pendingAssetRead, ASSET_ROLES, assetId, assetSource, CREATE_ASSET_DESCRIPTION, createAssetRequest, DELETE_ASSET_DESCRIPTION, GET_ASSET_DESCRIPTION, LIST_ASSETS_DESCRIPTION, UPDATE_ASSET_DESCRIPTION, ANALYZE_REFERENCE_DESCRIPTION, QUOTE_ANALYSIS_DESCRIPTION, analyzeReferenceRequest, ANALYZE_CREATOR_DESCRIPTION, GET_CREATOR_PROFILE_DESCRIPTION, QUOTE_CREATOR_PROFILE_DESCRIPTION, analyzeCreatorRequest, creatorProfileId, creatorProfileQuery, GET_REFERENCE_ANALYSIS_DESCRIPTION, referenceAnalysisId, referenceAnalysisQuery, REFERENCE_WAIT_MAX_SECONDS, withPriceLine, feedbackRequest, SEND_FEEDBACK_DESCRIPTION, ACCEPTED_UGC_FIELDS, rejectedFields, unknownFields, } from "@clipwright/core";
9
- import { formatGetRun, RUN_WAIT_MAX_SECONDS, waitForRun, withFailureReport } from "./format.js";
8
+ import { mcpUgcInputShape, actorDefaultsInput, extractBrandInput, mcpDesign, CANDIDATE_CHARGE_TEXT, candidateRequest, CATALOG_LANGUAGES, CREATE_ACTOR_DESCRIPTION, createActorInputShape, DEFAULT_FACELESS_VOICE, FACELESS_VOICE_NAMES, generateImageRequest, generateVideoRequest, MODEL_VOICE_MODEL, MODEL_VOICE_NAMES, voicesQueryShape, motionMovesQuery, agentRetryShape, runId, MAKE_UGC_DESCRIPTION, MAKE_UGC_AGENT_PROTOCOL, MAKE_FACELESS_DESCRIPTION, MAKE_FACELESS_AGENT_PROTOCOL, MCP_SERVER_INSTRUCTIONS, makeFacelessInput, offeredFacelessInput, REVISION_CHARGE_TEXT, revisionRequest, UPLOAD_IMAGE_DESCRIPTION, UPLOAD_MAX_BYTES, UPLOAD_MEDIA_TYPES, CREATE_UPLOAD_DESCRIPTION, GET_UPLOAD_DESCRIPTION, uploadId, sniffUploadMediaType, ASSET_KINDS, ASSET_LIST_MAX_LIMIT, ASSET_VIDEO_MAX_BYTES, pendingAssetRead, ASSET_ROLES, assetId, assetSource, CREATE_ASSET_DESCRIPTION, createAssetRequest, DELETE_ASSET_DESCRIPTION, DELETE_RUN_MEDIA_DESCRIPTION, DELETE_UPLOAD_DESCRIPTION, DELETE_CREATOR_PROFILE_DESCRIPTION, GET_ASSET_DESCRIPTION, LIST_ASSETS_DESCRIPTION, UPDATE_ASSET_DESCRIPTION, ANALYZE_REFERENCE_DESCRIPTION, QUOTE_ANALYSIS_DESCRIPTION, analyzeReferenceRequest, ANALYZE_CREATOR_DESCRIPTION, GET_CREATOR_PROFILE_DESCRIPTION, QUOTE_CREATOR_PROFILE_DESCRIPTION, FIND_TRENDS_DESCRIPTION, QUOTE_TRENDS_DESCRIPTION, findTrendsRequest, analyzeCreatorRequest, creatorProfileId, creatorProfileQuery, GET_REFERENCE_ANALYSIS_DESCRIPTION, referenceAnalysisId, referenceAnalysisQuery, REFERENCE_WAIT_MAX_SECONDS, withPriceLine, feedbackRequest, SEND_FEEDBACK_DESCRIPTION, ACCEPTED_UGC_FIELDS, rejectedFields, unknownFields, } from "@clipwright/core";
9
+ import { formatGetRun, RUN_WAIT_MAX_SECONDS, withFailureReport } from "./format.js";
10
10
  import { facelessQuoteMarkdown, RUN_DISPLAY_HINT, ugcQuoteMarkdown } from "./run-display.js";
11
11
  import { registerWorkflowTools } from './workflow-tools.js';
12
- import { advertiseSlimTools } from "./advertised-schema.js";
12
+ import { advertiseSlimTools, describeTool } from "./advertised-schema.js";
13
13
  import { answerInvalidArguments } from "./invalid-arguments.js";
14
14
  /** A key outside the video tools is named before the SDK is asked, as the API names it (rule 3): stripped, it would vanish silently. */
15
15
  function ugcKeyRefusal(key) {
@@ -28,6 +28,9 @@ const packageVersion = () => createRequire(import.meta.url)("../package.json").v
28
28
  export const REMOTE_UPLOAD_REFUSAL = "This Clipwright server is remote and cannot read files on your machine, so upload_image takes no path here. " +
29
29
  "Pass a public https URL as upload_image({url}), or call create_upload and PUT the file to the returned put_url " +
30
30
  "(for example with curl), then use the upload_id.";
31
+ /** tools/list says each tool in short (#1142); this read gives the rest. */
32
+ export const DESCRIBE_TOOL_DESCRIPTION = "Free. tools/list gives each tool in short; this answers a tool's full description and every field's guidance, or one field's " +
33
+ "with its subfields. Read it before the first call of a tool or field you have not used, such as quote_ugc templates.";
31
34
  /** What `create_asset` answers a path on a server that reads no files. */
32
35
  export const REMOTE_ASSET_PATH_REFUSAL = "This Clipwright server is remote and cannot read files on your machine, so create_asset takes no path here. " +
33
36
  "Pass source {url} (add kind: \"video\" for an MP4) or {base64, media_type}, or pass {put: {media_type, bytes, sha256}} " +
@@ -56,6 +59,21 @@ export function withPoll(analysis) {
56
59
  return { ...analysis, poll: { tool: "get_reference_analysis", arguments: { analysis_id: analysis.analysis_id, wait_seconds: REFERENCE_WAIT_MAX_SECONDS },
57
60
  retry_after_seconds: analysis.retry_after_seconds ?? 10 } };
58
61
  }
62
+ /** What an MCP analysis answer leaves out unless asked (#1172): the profile and the frame links, about 25 KB and 23 KB of a reel's answer. */
63
+ export const REFERENCE_INCLUDES = ["profile", "frames"];
64
+ export const REFERENCE_OMITTED_NOTE = "Left out to keep this answer small: get_reference_analysis with include: [\"profile\", \"frames\"] " +
65
+ "returns them. plan_reference_remake reads the profile by analysis_id.";
66
+ /** The answer without the parts not asked for, each named in `omitted`; one already absent is not named. */
67
+ export function lightAnalysis(analysis, include = []) {
68
+ const fields = [...(include.includes("profile") || analysis.reference_profile == null ? [] : ["reference_profile"]),
69
+ ...(include.includes("frames") || analysis.frames == null ? [] : ["frames"])];
70
+ if (fields.length === 0)
71
+ return analysis;
72
+ const light = { ...analysis };
73
+ for (const field of fields)
74
+ delete light[field];
75
+ return { ...light, omitted: { fields, note: REFERENCE_OMITTED_NOTE } };
76
+ }
59
77
  /** An unfinished creator profile names the read that waits for it. */
60
78
  export function withProfilePoll(profile) {
61
79
  if (profile.state !== "queued" && profile.state !== "analyzing")
@@ -63,8 +81,16 @@ export function withProfilePoll(profile) {
63
81
  return { ...profile, poll: { tool: "get_creator_profile", arguments: { profile_id: profile.profile_id, wait_seconds: REFERENCE_WAIT_MAX_SECONDS },
64
82
  retry_after_seconds: profile.retry_after_seconds ?? 5 } };
65
83
  }
84
+ /** An unfinished topic search names the same call again (#921): it replays the search in flight for free and waits for it. */
85
+ export function withTrendPoll(search, request) {
86
+ if (search.state !== "queued" && search.state !== "searching")
87
+ return search;
88
+ return { ...search, poll: { tool: "find_trends", arguments: request, retry_after_seconds: search.retry_after_seconds ?? 5 } };
89
+ }
66
90
  /** A start waits this long for its profile, inside an agent's usual 60 s tool timeout. */
67
91
  export const CREATOR_START_WAIT_SECONDS = 40;
92
+ /** How a paid edit or generation answers a repeat: one sentence, said the same way in every tool. */
93
+ const REPEAT_TEXT = (another) => `Repeating an identical request returns the run it started, starting nothing new; pass ${another} for another one.`;
68
94
  export const MAX_INLINE_FRAMES = 8;
69
95
  /** The read's query without `display`: the MCP read always asks for the breakdown. */
70
96
  const { display: _display, ...readQuery } = referenceAnalysisQuery.shape;
@@ -128,8 +154,8 @@ export function createServer(client, options = { fileAccess: "local" }) {
128
154
  inputSchema: { run_id: runId, wait_seconds: z.number().int().min(0).max(RUN_WAIT_MAX_SECONDS).optional()
129
155
  .describe(`Wait up to this many seconds (at most ${RUN_WAIT_MAX_SECONDS}) for the run to end before answering.`) },
130
156
  }, async ({ run_id, wait_seconds }, extra) => withFailureReport(async () => {
131
- // Terminal shape lives in the pure `formatGetRun`: both `status` and `state`.
132
- const run = await waitForRun(() => client.getRun(run_id), wait_seconds ?? 0, extra.signal);
157
+ // The API holds the read (#1036); the caller's cancel aborts it. Terminal shape lives in `formatGetRun`.
158
+ const run = await client.getRun(run_id, { ...(wait_seconds ? { wait_seconds } : {}), signal: extra.signal });
133
159
  return formatGetRun(run);
134
160
  }));
135
161
  // The editor's read tool; the draft of an edit stays on REST, the SDK and the CLI.
@@ -149,6 +175,15 @@ export function createServer(client, options = { fileAccess: "local" }) {
149
175
  }, async ({ run_id }) => withFailureReport(async () => ({
150
176
  content: [{ type: "text", text: JSON.stringify(await client.getRunShots(run_id)) }],
151
177
  })));
178
+ server.registerTool("delete_run_media", {
179
+ title: "Delete a run's files",
180
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
181
+ description: DELETE_RUN_MEDIA_DESCRIPTION,
182
+ inputSchema: { run_id: runId },
183
+ }, async ({ run_id }) => withFailureReport(async () => {
184
+ await client.deleteRunMedia(run_id);
185
+ return { content: [{ type: "text", text: JSON.stringify({ deleted: run_id }) }] };
186
+ }));
152
187
  // The revision pair: a flat object for tools/list, and the union parse refuses none or both of `changes` and `document`.
153
188
  const [byDocument, byChanges] = revisionRequest.options;
154
189
  const revisionShape = {
@@ -163,8 +198,7 @@ export function createServer(client, options = { fileAccess: "local" }) {
163
198
  title: "Quote a revision of a run",
164
199
  annotations: { readOnlyHint: true, destructiveHint: false },
165
200
  description: "Price a revision of a run WITHOUT spending credits: what it would change (summary) and what it costs " +
166
- "(billing.total_credits). Pass run_id and exactly one of changes or document, the same as revise_run. Always " +
167
- `call this first and show the user the price before revise_run. ${REVISION_CHARGE_TEXT} While paid edits are ` +
201
+ `(billing.total_credits). Pass run_id and exactly one of changes or document, the same as revise_run. ${REVISION_CHARGE_TEXT} While paid edits are ` +
168
202
  "closed it answers editing_disabled and charges nothing.",
169
203
  inputSchema: revisionShape,
170
204
  }, async ({ run_id, ...request }) => withFailureReport(async () => ({
@@ -175,13 +209,10 @@ export function createServer(client, options = { fileAccess: "local" }) {
175
209
  annotations: { readOnlyHint: false, destructiveHint: false },
176
210
  description: withPriceLine("revise_run", "Start a revision of a run made by make_faceless (paid): a NEW run that renders the video again with the " +
177
211
  "shots you change, by changes (a shot and what to generate for it) or by document (the whole edited " +
178
- "document); pass exactly one. Call quote_revision first and show the user the price. An identical request " +
179
- "returns the same run it started before, so a call repeated after a lost answer starts nothing new; to " +
180
- `start a second revision with the same request on purpose, pass attempt 2. ${REVISION_CHARGE_TEXT} An ` +
212
+ `document); pass exactly one. ${REPEAT_TEXT("attempt 2")} ${REVISION_CHARGE_TEXT} An ` +
181
213
  "original has a limit on revisions running at once and on revisions failed or unfinished, and paid edits " +
182
214
  "of it close some days after it finished; a refusal names which. While paid edits are closed it answers " +
183
- "editing_disabled and charges nothing. The result is a new run: call get_run with its poll.arguments; once it succeeded " +
184
- "it is editable under its own run_id."),
215
+ "editing_disabled and charges nothing. Poll the new run with get_run; once it succeeded it is editable under its own run_id."),
185
216
  inputSchema: { ...revisionShape, ...agentRetryShape },
186
217
  }, async ({ run_id, attempt, ...request }) => withFailureReport(async () => formatGetRun(await client.startRevision(run_id, revisionOf(request), { attempt }))));
187
218
  // The candidate pair; the list stays on REST, the SDK and the CLI, since an agent polls its own candidate with get_run.
@@ -199,8 +230,7 @@ export function createServer(client, options = { fileAccess: "local" }) {
199
230
  annotations: { readOnlyHint: true, destructiveHint: false },
200
231
  description: "Price one shot candidate of a run WITHOUT spending credits: one picture or clip made for one shot, to keep " +
201
232
  "or discard in a revision. Pass run_id, shot_id and operation, the same as create_candidate; the answer names " +
202
- "the unit, its credits and usable_until, the last moment the candidate can enter a revision. Always call this " +
203
- `first and show the user the price before create_candidate. ${CANDIDATE_CHARGE_TEXT} While shot candidates are ` +
233
+ `the unit, its credits and usable_until, the last moment the candidate can enter a revision. ${CANDIDATE_CHARGE_TEXT} While shot candidates are ` +
204
234
  "closed it answers editing_disabled and charges nothing.",
205
235
  inputSchema: candidateShape,
206
236
  }, async ({ run_id, ...request }) => withFailureReport(async () => ({
@@ -210,23 +240,20 @@ export function createServer(client, options = { fileAccess: "local" }) {
210
240
  title: "Make a shot candidate",
211
241
  annotations: { readOnlyHint: false, destructiveHint: false },
212
242
  description: withPriceLine("create_candidate", "Make a shot candidate of a run made by make_faceless (paid): a NEW run of skill edit_shot that generates one " +
213
- "picture or clip for one shot, to keep or discard in a later revision. Call quote_candidate first and show the " +
214
- `user the price. ${CANDIDATE_CHARGE_TEXT} An identical request returns the same run it started before, so a ` +
215
- "call repeated after a lost answer starts nothing new; to make another candidate with the same request on " +
216
- "purpose, pass take 2. An original has a limit on candidates running at once and on candidates failed or " +
243
+ `picture or clip for one shot, to keep or discard in a later revision. ${CANDIDATE_CHARGE_TEXT} ${REPEAT_TEXT("take 2")} ` +
244
+ "An original has a limit on candidates running at once and on candidates failed or " +
217
245
  "unfinished, apart from its revisions; a refusal names which. While shot candidates are closed it answers " +
218
- "editing_disabled and charges nothing. The result is a new run: call get_run with its poll.arguments; once it succeeded, its " +
219
- "candidate.preview_url shows what was made."),
246
+ "editing_disabled and charges nothing. Poll the new run with get_run; once it succeeded, its candidate.preview_url shows what was made."),
220
247
  inputSchema: candidateShape,
221
248
  }, async ({ run_id, ...request }) => withFailureReport(async () => formatGetRun(await client.createCandidate(run_id, candidateOf(request)))));
222
249
  // Single generations (clipwright#724): the tools are listed while the feature is closed and relay the API's refusal.
223
- const generationTerms = "A reference is {upload_id, rights_confirmed: true} for a picture from upload_image or create_upload (rights_confirmed states you may use " +
250
+ // The quote says what its input means; the paid tool points at it and says how a call is charged and repeated.
251
+ const generationCharge = "A generation that fails is charged nothing.";
252
+ const closedTerms = `${generationCharge} While generations are closed it answers generations_disabled or generations_marking_pending and charges nothing.`;
253
+ const quoteTerms = "A reference is {upload_id, rights_confirmed: true} for a picture from upload_image or create_upload (rights_confirmed states you may use " +
224
254
  "the people and works in it), {run_id} of a succeeded generate_image run of this account until its reusable_until, " +
225
- "or {asset_id} of a picture asset from create_asset, whose rights were confirmed when it was made. " +
226
- "A generation that fails is charged nothing. An identical request returns the " +
227
- "same run it started before, so a call repeated after a lost answer starts nothing new; to make another one with the " +
228
- "same request on purpose, pass attempt 2. While generations are closed it answers generations_disabled or " +
229
- "generations_marking_pending and charges nothing.";
255
+ `or {asset_id} of a picture asset from create_asset, whose rights were confirmed when it was made. ${closedTerms}`;
256
+ const generationTerms = `${generationCharge} ${REPEAT_TEXT("attempt 2")}`;
230
257
  // Strict objects: an unknown key is refused at the tool, not stripped. The clip's mode rules run in the handler.
231
258
  const imageTool = generateImageRequest, imageRunTool = generateImageRequest.extend(agentRetryShape);
232
259
  const videoTool = z.strictObject(generateVideoRequest.shape), videoRunTool = videoTool.extend(agentRetryShape);
@@ -244,8 +271,7 @@ export function createServer(client, options = { fileAccess: "local" }) {
244
271
  title: "Quote an image",
245
272
  annotations: { readOnlyHint: true, destructiveHint: false },
246
273
  description: "Price one generate_image call WITHOUT spending credits: its credits (one image at its model's unit_credits), " +
247
- "its width and height, and warnings. Always call this first and show the user the price before generate_image. " +
248
- generationTerms,
274
+ `its width and height, and warnings. ${quoteTerms}`,
249
275
  inputSchema: imageTool,
250
276
  }, async (args) => withFailureReport(async () => ({
251
277
  content: [{ type: "text", text: JSON.stringify(await client.quoteImage(args)) }],
@@ -254,16 +280,14 @@ export function createServer(client, options = { fileAccess: "local" }) {
254
280
  title: "Generate an image",
255
281
  annotations: { readOnlyHint: false, destructiveHint: false },
256
282
  description: withPriceLine("generate_image", "Generate one image from a prompt (paid): a NEW run of skill generate_image, charged the quoted credits per image " +
257
- "once it is delivered. Call quote_image first and show the user the price. Call get_run with its poll.arguments; once it " +
258
- "succeeded, generation.url is the image and its run_id can be a reference of a later generation. " + generationTerms),
283
+ "once it is delivered. Poll get_run; once it succeeded, generation.url is the image and its run_id can be a reference of a later generation. " + generationTerms),
259
284
  inputSchema: imageRunTool,
260
285
  }, async ({ attempt, ...request }) => withFailureReport(async () => formatGetRun(await client.generateImage(request, { attempt }))));
261
286
  server.registerTool("quote_video", {
262
287
  title: "Quote a clip",
263
288
  annotations: { readOnlyHint: true, destructiveHint: false },
264
289
  description: "Price one generate_video call WITHOUT spending credits: its credits (unit_credits per second of duration_s), " +
265
- "its width and height, and warnings. Always call this first and show the user the price before generate_video. " +
266
- generationTerms,
290
+ `its width and height, and warnings. A reference is as quote_image states it. ${closedTerms}`,
267
291
  inputSchema: videoTool,
268
292
  }, async (args) => withFailureReport(async () => ({
269
293
  content: [{ type: "text", text: JSON.stringify(await client.quoteVideo(generateVideoRequest.parse(args))) }],
@@ -273,15 +297,13 @@ export function createServer(client, options = { fileAccess: "local" }) {
273
297
  annotations: { readOnlyHint: false, destructiveHint: false },
274
298
  description: withPriceLine("generate_video", "Generate one silent clip (paid): a NEW run of skill generate_video, charged unit_credits per second of duration_s " +
275
299
  "once it is delivered. Modes: text; first_frame (first_frame, optional last_frame, of the requested aspect ratio " +
276
- "within 2%); reference (1 to 4 references). Call quote_video first and show the user the price. Call get_run with " +
277
- "its poll.arguments; once it succeeded, generation.url is the clip. " + generationTerms),
300
+ "within 2%); reference (1 to 4 references). Poll get_run; once it succeeded, generation.url is the clip. " + generationTerms),
278
301
  inputSchema: videoRunTool,
279
302
  }, async ({ attempt, ...request }) => withFailureReport(async () => formatGetRun(await client.generateVideo(generateVideoRequest.parse(request), { attempt }))));
280
303
  server.registerTool("quote_ugc", {
281
304
  title: "Quote a video",
282
305
  annotations: { readOnlyHint: true, destructiveHint: false },
283
- description: "Estimate the credit cost of a make_ugc call WITHOUT spending credits. " +
284
- "Always call this first and show the user the price before make_ugc.",
306
+ description: "Estimate the credit cost of a make_ugc call WITHOUT spending credits; show the user the price before make_ugc.",
285
307
  // Same core shape as make_ugc, so the quote prices exactly what make_ugc
286
308
  // would render.
287
309
  inputSchema: ugcTool,
@@ -339,6 +361,15 @@ export function createServer(client, options = { fileAccess: "local" }) {
339
361
  }, async ({ upload_id }) => withFailureReport(async () => ({
340
362
  content: [{ type: "text", text: JSON.stringify(await client.getUpload(upload_id)) }],
341
363
  })));
364
+ server.registerTool("delete_upload", {
365
+ title: "Delete an upload",
366
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
367
+ description: DELETE_UPLOAD_DESCRIPTION,
368
+ inputSchema: { upload_id: uploadId },
369
+ }, async ({ upload_id }) => withFailureReport(async () => {
370
+ await client.deleteUpload(upload_id);
371
+ return { content: [{ type: "text", text: JSON.stringify({ deleted: upload_id }) }] };
372
+ }));
342
373
  // Account assets (clipwright#724 F2). A path is read only by a server on the caller's machine, as with upload_image.
343
374
  const assetText = async (value) => ({ content: [{ type: "text", text: JSON.stringify(await value) }] });
344
375
  server.registerTool("create_asset", {
@@ -424,20 +455,22 @@ export function createServer(client, options = { fileAccess: "local" }) {
424
455
  description: withPriceLine("analyze_reference", ANALYZE_REFERENCE_DESCRIPTION),
425
456
  inputSchema: analyzeReferenceRequest.shape,
426
457
  }, async (request) => withFailureReport(async () => ({
427
- content: [{ type: "text", text: JSON.stringify(withPoll(await client.analyzeReference(request))) }],
458
+ content: [{ type: "text", text: JSON.stringify(withPoll(lightAnalysis(await client.analyzeReference(request)))) }],
428
459
  })));
429
460
  server.registerTool("get_reference_analysis", {
430
461
  title: "Get a reference analysis",
431
462
  annotations: { readOnlyHint: true, destructiveHint: false },
432
463
  description: GET_REFERENCE_ANALYSIS_DESCRIPTION,
433
464
  inputSchema: { analysis_id: referenceAnalysisId, ...readQuery,
434
- frames_as: z.enum(["links", "images"]).optional().describe(`images: also return up to ${MAX_INLINE_FRAMES} key frames as images in this answer`) },
435
- }, async ({ analysis_id, frames_as, ...query }) => withFailureReport(async () => {
465
+ frames_as: z.enum(["links", "images"]).optional().describe(`images: also return up to ${MAX_INLINE_FRAMES} key frames as images in this answer`),
466
+ include: z.array(z.enum(REFERENCE_INCLUDES)).max(REFERENCE_INCLUDES.length).optional()
467
+ .describe("profile: also the reference_profile; frames: also the frame links. Both are left out by default.") },
468
+ }, async ({ analysis_id, frames_as, include, ...query }) => withFailureReport(async () => {
436
469
  // Every MCP read carries the ready breakdown (#990): an agent shows the same table without composing its own.
437
470
  const analysis = await client.getReferenceAnalysis(analysis_id, { ...query, display: "markdown" });
438
471
  const images = frames_as === "images" ? await keyFrameImages(analysis.frames) : [];
439
472
  // The only tool answering image blocks; the shared response type keeps text-only for every other tool's callers.
440
- return { content: [{ type: "text", text: JSON.stringify(withPoll(analysis)) }, ...images] };
473
+ return { content: [{ type: "text", text: JSON.stringify(withPoll(lightAnalysis(analysis, include))) }, ...images] };
441
474
  }));
442
475
  // Creator profile (#999): paid per profile; the start waits for it, and an unfinished one is read with get_creator_profile.
443
476
  server.registerTool("quote_creator_profile", {
@@ -468,31 +501,57 @@ export function createServer(client, options = { fileAccess: "local" }) {
468
501
  }, async ({ profile_id, wait_seconds }) => withFailureReport(async () => ({
469
502
  content: [{ type: "text", text: JSON.stringify(withProfilePoll(await client.getCreatorProfile(profile_id, { ...(wait_seconds === undefined ? {} : { wait_seconds }), display: "markdown" }))) }],
470
503
  })));
504
+ // Topic search (#921): paid per search, the first step of a guided brief; the start waits for it like a creator profile.
505
+ server.registerTool("quote_trends", {
506
+ title: "Quote a trend search",
507
+ annotations: { readOnlyHint: true, destructiveHint: false },
508
+ description: QUOTE_TRENDS_DESCRIPTION,
509
+ inputSchema: findTrendsRequest.shape,
510
+ }, async (request) => withFailureReport(async () => ({
511
+ content: [{ type: "text", text: JSON.stringify(await client.quoteTrends(request)) }],
512
+ })));
513
+ server.registerTool("find_trends", {
514
+ title: "Find trending reels on a topic",
515
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
516
+ description: withPriceLine("find_trends", FIND_TRENDS_DESCRIPTION),
517
+ inputSchema: findTrendsRequest.shape,
518
+ },
519
+ // A repeat of the same request replays the search in flight for free and waits again, so no read tool is listed (tools/list budget).
520
+ async (request) => withFailureReport(async () => {
521
+ const started = await client.findTrends(request);
522
+ const done = started.state === "queued" || started.state === "searching"
523
+ ? await client.getTrendSearch(started.search_id, { wait_seconds: CREATOR_START_WAIT_SECONDS, display: "markdown" })
524
+ : await client.getTrendSearch(started.search_id, { display: "markdown" });
525
+ return { content: [{ type: "text", text: JSON.stringify(withTrendPoll(done, request)) }] };
526
+ }));
527
+ server.registerTool("delete_creator_profile", {
528
+ title: "Delete a creator profile",
529
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
530
+ description: DELETE_CREATOR_PROFILE_DESCRIPTION,
531
+ inputSchema: { profile_id: creatorProfileId },
532
+ }, async ({ profile_id }) => withFailureReport(async () => {
533
+ await client.deleteCreatorProfile(profile_id);
534
+ return { content: [{ type: "text", text: JSON.stringify({ deleted: profile_id }) }] };
535
+ }));
471
536
  server.registerTool("list_voices", {
472
537
  title: "List voices",
473
538
  annotations: { readOnlyHint: true, destructiveHint: false },
474
539
  description: "List the voices for the `voice` field of make_ugc, or of make_faceless with skill=make_faceless. " +
475
- "Without `skill` the list holds the make_ugc voices only. Each voice names the one skill that speaks it " +
476
- "in `skill`, and the other skill refuses it before any charge. skill=make_faceless lists " +
477
- `the ${FACELESS_VOICE_NAMES.length} narration voices of make_faceless (kind faceless_voice, each with a ` +
478
- `language and a gender; without voice it speaks ${DEFAULT_FACELESS_VOICE}; it takes no voice_id). ` +
479
- "The rest of this text is about the make_ugc voices: the presets first, then the " +
540
+ "Without `skill` the list holds the make_ugc voices only. Each voice names in `skill` the one skill that speaks it; " +
541
+ "the other refuses it before any charge. skill=make_faceless lists " +
542
+ `the ${FACELESS_VOICE_NAMES.length} narration voices of make_faceless (kind faceless_voice, with a ` +
543
+ `language and a gender; default ${DEFAULT_FACELESS_VOICE}; no voice_id). ` +
544
+ "make_ugc voices: the presets, then the " +
480
545
  `${MODEL_VOICE_NAMES.length} voices of ${MODEL_VOICE_MODEL} (kind model_voice), then catalog voices in ` +
481
- `${CATALOG_LANGUAGES.length} languages. Filter by \`language\` (the voice's native language; any voice ` +
482
- "speaks any supported language), `gender`, `age`, `use_case` and `model`; an unknown filter value is " +
483
- "refused with the allowed values. Pick a voice by `name` (e.g. voice=\"george\"); a name always means " +
484
- "the same voice. A model_voice voice is spoken only by its `model`: naming it selects that model, " +
485
- "it has no language of its own and matches the languages measured on the model, and its gender is " +
486
- "listed only where it is known. `voice_id` is an escape hatch for a raw vendor voice id that is not listed, such as a " +
487
- "cloned voice. Without voice or voice_id the default voice follows the actor's gender: sarah for a " +
488
- "woman, george for a man, taken from actor_id or from actor_gender next to image. With image and no " +
489
- "actor_gender the voice is george and a warning says so: pass the gender of the person in the photo as " +
490
- "actor_gender (ask the user only when you cannot tell), or pick a voice here. Keep any voice the user chose. A voice with `preview_url` has a short-lived audio sample " +
491
- "spoken by that voice with that model; play it to the user before a paid render instead of judging a " +
492
- "voice by its description. A model_voice sample is in English, or in the language you filter by. " +
493
- "It expires at `preview_expires_at`: call this tool again for a fresh link, " +
494
- "never store it. A voice without `preview_url` has no sample yet. `words_per_second` is an English voice's measured pace; " +
495
- "eleven_v4 has no speed setting, so to match a reference presenter pick the voice nearest its pace. Free. " + SCRIPT_LENGTH_DESCRIPTION,
546
+ `${CATALOG_LANGUAGES.length} languages. Filter by \`language\` (native; any voice speaks any supported ` +
547
+ "language), `gender`, `age`, `use_case` and `model`; an unknown value is refused with the allowed ones. " +
548
+ "Pick a voice by `name` (voice=\"george\"), always the same voice. " +
549
+ "A model_voice voice is spoken only by its `model`: naming it selects that model; its languages are those " +
550
+ "measured on the model, its gender listed only where known. `voice_id` takes an unlisted raw vendor id, such as a " +
551
+ "cloned voice. Keep any voice the user chose. `preview_url` is a short sample in that voice and model (a model_voice " +
552
+ "one in English or the filtered language): play it before a paid render. It expires at `preview_expires_at`: call " +
553
+ "again, never store it; none means no sample yet. `words_per_second` is an English voice's measured pace; eleven_v4 " +
554
+ "has no speed, so match a presenter by the nearest pace. Free. Script limits by speech model: the script field of quote_ugc.",
496
555
  inputSchema: voicesQueryShape,
497
556
  }, async (filters) => withFailureReport(async () => {
498
557
  // Relays /v1/voices: no vendor call and no vendor key on this surface.
@@ -510,10 +569,22 @@ export function createServer(client, options = { fileAccess: "local" }) {
510
569
  }, async () => withFailureReport(async () => ({
511
570
  content: [{ type: "text", text: JSON.stringify(await client.listTemplateSounds()) }],
512
571
  })));
572
+ server.registerTool("list_moves", {
573
+ title: "List moves",
574
+ annotations: { readOnlyHint: true, destructiveHint: false },
575
+ description: "The catalogue of moves: motion and graphics to put on lines of a script, for make_ugc, make_faceless and plan_reference_remake " +
576
+ "moves. Filter by beat (hook, problem, demo, proof, social_proof, offer, cta, transition), content (what the line has: number, list, " +
577
+ "quote, rating, photo, screenshot, footage, link, words, none), surface, mood, skill or q; next_cursor pages. Each line says what the " +
578
+ "move does for the viewer and its credits per use (0 is free). names=a,b gives those moves in full: look, data to pass, what must " +
579
+ "already be on the line, how often a video takes it and what pairs with it. A soon move is coming and cannot be picked yet. Free.",
580
+ inputSchema: motionMovesQuery.shape,
581
+ }, async (filters) => withFailureReport(async () => ({
582
+ content: [{ type: "text", text: JSON.stringify(await client.listMoves(filters)) }],
583
+ })));
513
584
  server.registerTool("list_actors", {
514
585
  title: "List actors",
515
586
  annotations: { readOnlyHint: true, destructiveHint: false },
516
- description: "List saved Clipwright actors with stable IDs, gender, age and verified image formats. Choose actor_id for quote_ugc and make_ugc. Preview URLs expire; actor IDs remain reusable. Do not combine actor_id with image, person or actor_gender. Without voice, the actor speaks the default voice of their gender.",
587
+ description: "List saved Clipwright actors with stable IDs, gender, age and verified image formats. Choose actor_id for quote_ugc and make_ugc. Show a preview_url as a markdown image; a personal actor's expires, so list again for a fresh one; actor IDs remain reusable. Do not combine actor_id with image, person or actor_gender. Without voice, the actor speaks the default voice of their gender.",
517
588
  inputSchema: {},
518
589
  }, async () => withFailureReport(async () => ({
519
590
  content: [{ type: "text", text: JSON.stringify({ actors: await client.listActors() }) }],
@@ -521,7 +592,7 @@ export function createServer(client, options = { fileAccess: "local" }) {
521
592
  server.registerTool("get_actor_defaults", {
522
593
  title: "Get actor B-roll default",
523
594
  annotations: { readOnlyHint: true, destructiveHint: false },
524
- description: `Read this account actor's saved B-roll default. ${BROLL_POLICY_DESCRIPTION}`,
595
+ description: "Read this account actor's saved B-roll default; set_actor_defaults' broll_policy says what each value does.",
525
596
  inputSchema: { actor_id: z.string().min(1) },
526
597
  }, async ({ actor_id }) => withFailureReport(async () => ({
527
598
  content: [{ type: "text", text: JSON.stringify(await client.getActorDefaults(actor_id)) }],
@@ -529,11 +600,29 @@ export function createServer(client, options = { fileAccess: "local" }) {
529
600
  server.registerTool("set_actor_defaults", {
530
601
  title: "Set actor B-roll default",
531
602
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
532
- description: `Save this account actor's B-roll default without spending credits. ${BROLL_POLICY_DESCRIPTION}`,
603
+ description: "Save this account actor's B-roll default without spending credits; broll_policy says what each value does.",
533
604
  inputSchema: { actor_id: z.string().min(1), ...actorDefaultsInput.shape },
534
605
  }, async ({ actor_id, ...input }) => withFailureReport(async () => ({
535
606
  content: [{ type: "text", text: JSON.stringify(await client.setActorDefaults(actor_id, input)) }],
536
607
  })));
608
+ server.registerTool("extract_brand", {
609
+ title: "Read a brand from a site",
610
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
611
+ description: "Free. A public site's colours, fonts, logo and corners as a proposed design; the user confirms it. " +
612
+ "logo_url: import with create_asset for design.logo_asset_id.",
613
+ inputSchema: extractBrandInput.shape,
614
+ }, async ({ url }) => withFailureReport(async () => ({
615
+ content: [{ type: "text", text: JSON.stringify(await client.extractBrand(url)) }],
616
+ })));
617
+ // One tool reads and saves the kit: a second would cost tools/list bytes no client has to spare (#1003).
618
+ server.registerTool("brand_kit", {
619
+ title: "Read or save the brand kit",
620
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
621
+ description: "Free. No design: the saved one (used when a video names none), our looks and faces. design: save the user's confirmed one.",
622
+ inputSchema: { design: mcpDesign.optional() },
623
+ }, async ({ design }) => withFailureReport(async () => ({
624
+ content: [{ type: "text", text: JSON.stringify(design === undefined ? await client.getBrandKit() : await client.setBrandKit(design)) }],
625
+ })));
537
626
  // Personal actor: the same schema the server checks, so offered and accepted
538
627
  // input cannot drift.
539
628
  server.registerTool("create_actor", {
@@ -552,7 +641,9 @@ export function createServer(client, options = { fileAccess: "local" }) {
552
641
  title: "Quote a personal actor",
553
642
  annotations: { readOnlyHint: true, destructiveHint: false },
554
643
  description: "Estimate the credit cost of create_actor WITHOUT spending credits. Call this first and show the " +
555
- "price before create_actor: each requested format is a separate charged image.",
644
+ "price before create_actor: each requested format is a separate charged image, except a portrait from your own " +
645
+ "image (portrait), which costs nothing. A portrait that is not your own image asset within 2% of 9:16 is " +
646
+ "refused here; one without exactly one well-framed face fails the run at no charge.",
556
647
  inputSchema: createActorInputShape,
557
648
  }, async (args) => withFailureReport(async () => ({
558
649
  content: [{ type: "text", text: JSON.stringify(await client.quoteActor(args)) }],
@@ -571,8 +662,7 @@ export function createServer(client, options = { fileAccess: "local" }) {
571
662
  server.registerTool("quote_faceless", {
572
663
  title: "Quote a faceless video",
573
664
  annotations: { readOnlyHint: true, destructiveHint: false },
574
- description: "Estimate the credit cost of make_faceless WITHOUT spending credits. " +
575
- "Always call this first and show the user the price before make_faceless.",
665
+ description: "Estimate the credit cost of make_faceless WITHOUT spending credits; show the user the price before make_faceless.",
576
666
  inputSchema: offeredFacelessInput,
577
667
  }, async (args) => withFailureReport(async () => {
578
668
  const quote = await client.quoteFaceless(makeFacelessInput.parse(args));
@@ -597,6 +687,8 @@ export function createServer(client, options = { fileAccess: "local" }) {
597
687
  "is refused with debt_outstanding whatever the balance, until the account buys credits; " +
598
688
  "with no debt, balance_credits is what a new run can spend. holds_credits are reserved for " +
599
689
  "runs still in progress and settle when they finish. grants lists unexpired credit grants. " +
690
+ "support_replies lists Clipwright's replies to this account's support requests and send_feedback, newest first; " +
691
+ "request_id is the feedback_id send_feedback returned. " +
600
692
  "quote_ugc already warns when make_ugc would be refused; this tool gives the numbers behind it.",
601
693
  inputSchema: {},
602
694
  }, async () => withFailureReport(async () => ({
@@ -621,7 +713,21 @@ export function createServer(client, options = { fileAccess: "local" }) {
621
713
  }, async (request) => withFailureReport(async () => ({
622
714
  content: [{ type: "text", text: JSON.stringify(await client.sendFeedback(request)) }],
623
715
  })));
624
- advertiseSlimTools(server);
716
+ server.registerTool("describe_tool", {
717
+ title: "Describe a tool",
718
+ annotations: { readOnlyHint: true, destructiveHint: false },
719
+ description: DESCRIBE_TOOL_DESCRIPTION,
720
+ inputSchema: {
721
+ tool: z.string().min(1).describe("The tool's name, such as quote_ugc."),
722
+ field: z.string().min(1).optional().describe("One field and its subfields, such as templates; omitted, every field."),
723
+ },
724
+ }, async ({ tool, field }) => {
725
+ const described = describeTool(await fullList(), tool, field);
726
+ return described === undefined
727
+ ? { content: [{ type: "text", text: `No tool is named "${tool}"; tools/list names every tool.` }], isError: true }
728
+ : { content: [{ type: "text", text: JSON.stringify(described) }] };
729
+ });
730
+ const fullList = advertiseSlimTools(server);
625
731
  return server;
626
732
  }
627
733
  //# sourceMappingURL=server.js.map