@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/README.md +38 -5
- package/SKILL.md +11 -5
- package/dist/advertised-schema.d.ts +15 -10
- package/dist/advertised-schema.js +102 -43
- package/dist/advertised-schema.js.map +1 -1
- package/dist/format.d.ts +8 -6
- package/dist/format.js +18 -33
- package/dist/format.js.map +1 -1
- package/dist/listed-texts.d.ts +4 -0
- package/dist/listed-texts.js +188 -0
- package/dist/listed-texts.js.map +1 -0
- package/dist/run-display.js +21 -3
- package/dist/run-display.js.map +1 -1
- package/dist/server.d.ts +26 -0
- package/dist/server.js +173 -67
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
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,
|
|
9
|
-
import { formatGetRun, RUN_WAIT_MAX_SECONDS,
|
|
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
|
-
//
|
|
132
|
-
const run = await
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
-
"
|
|
477
|
-
`the ${FACELESS_VOICE_NAMES.length} narration voices of make_faceless (kind faceless_voice,
|
|
478
|
-
`language and a gender;
|
|
479
|
-
"
|
|
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\` (
|
|
482
|
-
"
|
|
483
|
-
"
|
|
484
|
-
"
|
|
485
|
-
"
|
|
486
|
-
"
|
|
487
|
-
"
|
|
488
|
-
"
|
|
489
|
-
"
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|