@clipform/mcp-server 1.47.1 → 1.48.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.
@@ -6,7 +6,7 @@ import {
6
6
  getWorkflowText,
7
7
  objectType,
8
8
  registerPrompts
9
- } from "./chunk-MV27JVSL.js";
9
+ } from "./chunk-BBNREY2Q.js";
10
10
  import {
11
11
  GUIDE_TYPES,
12
12
  QUIZ_VARIANTS,
@@ -14,7 +14,7 @@ import {
14
14
  getGuideUri,
15
15
  guideFallbackText,
16
16
  registerResources
17
- } from "./chunk-UH77CDNO.js";
17
+ } from "./chunk-WUORO245.js";
18
18
  import {
19
19
  ACTIVE_NODE_TYPES,
20
20
  BUSINESS,
@@ -33,8 +33,9 @@ import {
33
33
  callApi,
34
34
  errorResult,
35
35
  resolveFormType,
36
+ structuredResult,
36
37
  textResult
37
- } from "./chunk-PJNOCHKR.js";
38
+ } from "./chunk-4CY5EJCR.js";
38
39
  import {
39
40
  __commonJS,
40
41
  __export,
@@ -17325,6 +17326,21 @@ function registerListFormsTool(server) {
17325
17326
  sort: external_exports.enum(["created_at", "updated_at"]).optional().describe("Sort field (default: created_at)"),
17326
17327
  order: external_exports.enum(["asc", "desc"]).optional().describe("Sort order (default: desc, newest first)")
17327
17328
  },
17329
+ outputSchema: {
17330
+ forms: external_exports.array(
17331
+ external_exports.object({
17332
+ id: external_exports.string(),
17333
+ title: external_exports.string(),
17334
+ share_id: external_exports.string(),
17335
+ is_live: external_exports.boolean(),
17336
+ has_unpublished_changes: external_exports.boolean(),
17337
+ created_at: external_exports.string(),
17338
+ updated_at: external_exports.string(),
17339
+ tags: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), color: external_exports.string().nullable() }))
17340
+ })
17341
+ ).describe("Matching forms (empty if none)"),
17342
+ next_cursor: external_exports.string().nullable().describe("Pass as cursor to fetch the next page, or null when no more")
17343
+ },
17328
17344
  annotations: {
17329
17345
  readOnlyHint: true,
17330
17346
  destructiveHint: false,
@@ -17346,8 +17362,21 @@ function registerListFormsTool(server) {
17346
17362
  return errorResult(result.error);
17347
17363
  }
17348
17364
  const data = result.data;
17365
+ const structured = {
17366
+ forms: data.forms.map((f) => ({
17367
+ id: f.id,
17368
+ title: f.title,
17369
+ share_id: f.share_id,
17370
+ is_live: f.is_live,
17371
+ has_unpublished_changes: f.has_unpublished_changes,
17372
+ created_at: f.created_at,
17373
+ updated_at: f.updated_at,
17374
+ tags: f.tags.map((t) => ({ id: t.id, name: t.name, color: t.color }))
17375
+ })),
17376
+ next_cursor: data.next_cursor
17377
+ };
17349
17378
  if (data.forms.length === 0) {
17350
- return textResult("No forms found matching the criteria.");
17379
+ return structuredResult("No forms found matching the criteria.", structured);
17351
17380
  }
17352
17381
  const lines = [`Found ${data.forms.length} form(s):
17353
17382
  `];
@@ -17361,14 +17390,36 @@ function registerListFormsTool(server) {
17361
17390
  `More results available. Pass cursor: "${data.next_cursor}" to get the next page.`
17362
17391
  );
17363
17392
  }
17364
- return textResult(lines.join("\n"));
17393
+ return structuredResult(lines.join("\n"), structured);
17365
17394
  }
17366
17395
  );
17367
17396
  }
17368
17397
 
17369
17398
  // src/lib/format-form.ts
17399
+ function getFormNodes(data) {
17400
+ return data.node ?? data.nodes ?? [];
17401
+ }
17402
+ function projectFormState(data) {
17403
+ return {
17404
+ form_id: String(data.form_id ?? ""),
17405
+ title: String(data.title ?? ""),
17406
+ is_live: Boolean(data.is_live),
17407
+ has_unpublished_changes: Boolean(data.has_unpublished_changes),
17408
+ nodes: getFormNodes(data).map((q) => {
17409
+ const options = q.node_options ?? q.options;
17410
+ return {
17411
+ id: q.id,
17412
+ type: q.type,
17413
+ prompt: q.prompt ?? "",
17414
+ required: Boolean(q.required),
17415
+ ...q.config && Object.keys(q.config).length > 0 ? { config: q.config } : {},
17416
+ ...options && options.length > 0 ? { options: options.map((o) => ({ id: o.id, content: o.content })) } : {}
17417
+ };
17418
+ })
17419
+ };
17420
+ }
17370
17421
  function formatFormState(data) {
17371
- const nodes = data.node ?? data.nodes ?? [];
17422
+ const nodes = getFormNodes(data);
17372
17423
  const lines = [
17373
17424
  `Form: ${data.title}`,
17374
17425
  `Form ID: ${data.form_id}`,
@@ -17408,6 +17459,22 @@ function registerGetFormTool(server) {
17408
17459
  inputSchema: {
17409
17460
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)")
17410
17461
  },
17462
+ outputSchema: {
17463
+ form_id: external_exports.string().describe("Form UUID"),
17464
+ title: external_exports.string().describe("Form title"),
17465
+ is_live: external_exports.boolean().describe("Whether the form is published"),
17466
+ has_unpublished_changes: external_exports.boolean().describe("Live form has edits not yet republished"),
17467
+ nodes: external_exports.array(
17468
+ external_exports.object({
17469
+ id: external_exports.string(),
17470
+ type: external_exports.string(),
17471
+ prompt: external_exports.string(),
17472
+ required: external_exports.boolean(),
17473
+ config: external_exports.record(external_exports.unknown()).optional(),
17474
+ options: external_exports.array(external_exports.object({ id: external_exports.string(), content: external_exports.string() })).optional()
17475
+ })
17476
+ ).describe("Nodes in sequential order - IDs for update_node / upload_node_media")
17477
+ },
17411
17478
  annotations: {
17412
17479
  readOnlyHint: true,
17413
17480
  destructiveHint: false,
@@ -17420,7 +17487,7 @@ function registerGetFormTool(server) {
17420
17487
  if (!result.ok) {
17421
17488
  return errorResult(result.error);
17422
17489
  }
17423
- return textResult(formatFormState(result.data));
17490
+ return structuredResult(formatFormState(result.data), projectFormState(result.data));
17424
17491
  }
17425
17492
  );
17426
17493
  }
@@ -17597,6 +17664,10 @@ function registerAddNodeTool(server) {
17597
17664
  "Insert after this node ID. Omit to append before the end screen."
17598
17665
  )
17599
17666
  },
17667
+ outputSchema: {
17668
+ node_id: external_exports.string().describe("ID of the created node - pass to update_node / upload_node_media"),
17669
+ type: external_exports.string().describe("Node type that was added")
17670
+ },
17600
17671
  annotations: {
17601
17672
  readOnlyHint: false,
17602
17673
  destructiveHint: false,
@@ -17615,8 +17686,10 @@ function registerAddNodeTool(server) {
17615
17686
  if (!result.ok) {
17616
17687
  return errorResult(result.error);
17617
17688
  }
17618
- return textResult(
17619
- `Node added. ID: ${result.data.node_id} | Type: ${node.type}`
17689
+ const nodeId = result.data.node_id;
17690
+ return structuredResult(
17691
+ `Node added. ID: ${nodeId} | Type: ${node.type}`,
17692
+ { node_id: nodeId, type: node.type }
17620
17693
  );
17621
17694
  }
17622
17695
  );
@@ -17644,6 +17717,16 @@ function registerUpdateNodeTool(server) {
17644
17717
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17645
17718
  updates: external_exports.array(NodeUpdateSchema).min(1).max(20).describe("One or more node updates to apply")
17646
17719
  },
17720
+ outputSchema: {
17721
+ results: external_exports.array(
17722
+ external_exports.object({
17723
+ node_id: external_exports.string(),
17724
+ ok: external_exports.boolean(),
17725
+ warnings: external_exports.array(external_exports.string()).optional(),
17726
+ error: external_exports.string().optional()
17727
+ })
17728
+ ).describe("One result per requested update, in order")
17729
+ },
17647
17730
  annotations: {
17648
17731
  readOnlyHint: false,
17649
17732
  destructiveHint: false,
@@ -17653,6 +17736,7 @@ function registerUpdateNodeTool(server) {
17653
17736
  },
17654
17737
  async ({ form_id, updates }) => {
17655
17738
  const allLines = [];
17739
+ const results = [];
17656
17740
  for (const { node_id, prompt, label, type, required: required2, config: config2, options } of updates) {
17657
17741
  const body = {};
17658
17742
  if (prompt !== void 0) body.prompt = prompt;
@@ -17667,6 +17751,7 @@ function registerUpdateNodeTool(server) {
17667
17751
  );
17668
17752
  if (!result.ok) {
17669
17753
  allLines.push(`Node ${node_id}: FAILED \u2014 ${result.error}`);
17754
+ results.push({ node_id, ok: false, error: result.error });
17670
17755
  continue;
17671
17756
  }
17672
17757
  const changes = [];
@@ -17682,8 +17767,9 @@ function registerUpdateNodeTool(server) {
17682
17767
  if (warnings?.length) {
17683
17768
  for (const w of warnings) allLines.push(`Node ${node_id} warning: ${w}`);
17684
17769
  }
17770
+ results.push({ node_id, ok: true, ...warnings?.length ? { warnings } : {} });
17685
17771
  }
17686
- return textResult(allLines.join("\n"));
17772
+ return structuredResult(allLines.join("\n"), { results });
17687
17773
  }
17688
17774
  );
17689
17775
  }
@@ -17764,6 +17850,18 @@ When a public URL is provided, the media is fetched and stored automatically. On
17764
17850
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17765
17851
  items: external_exports.array(MediaItemSchema).min(1).max(10).describe("One or more media items to upload")
17766
17852
  },
17853
+ outputSchema: {
17854
+ results: external_exports.array(
17855
+ external_exports.object({
17856
+ node_id: external_exports.string(),
17857
+ ok: external_exports.boolean(),
17858
+ stored: external_exports.boolean().describe("Bytes already stored (URL-based); false when an upload_url is pending"),
17859
+ upload_url: external_exports.string().optional(),
17860
+ upload_method: external_exports.string().optional(),
17861
+ error: external_exports.string().optional()
17862
+ })
17863
+ ).describe("One result per item, in order")
17864
+ },
17767
17865
  annotations: {
17768
17866
  readOnlyHint: false,
17769
17867
  destructiveHint: false,
@@ -17774,15 +17872,16 @@ When a public URL is provided, the media is fetched and stored automatically. On
17774
17872
  async ({ form_id, items }) => {
17775
17873
  const workspaceId = getMcpAuth()?.workspace_id;
17776
17874
  const lines = [];
17875
+ const results = [];
17777
17876
  let successCount = 0;
17778
17877
  for (let i = 0; i < items.length; i++) {
17779
17878
  const item = items[i];
17780
17879
  if (items.length > 1) lines.push(`--- Item ${i + 1} (node ${item.node_id}) ---`);
17781
17880
  if (!workspaceId) {
17782
- lines.push(
17783
- "FAILED: no workspace in context - reconnect the MCP or set CLIPFORM_API_KEY so media can land in your library."
17784
- );
17881
+ const error2 = "no workspace in context - reconnect the MCP or set CLIPFORM_API_KEY so media can land in your library.";
17882
+ lines.push(`FAILED: ${error2}`);
17785
17883
  lines.push("");
17884
+ results.push({ node_id: item.node_id, ok: false, stored: false, error: error2 });
17786
17885
  continue;
17787
17886
  }
17788
17887
  const createBody = {
@@ -17795,12 +17894,15 @@ When a public URL is provided, the media is fetched and stored automatically. On
17795
17894
  if (!created.ok) {
17796
17895
  lines.push(`FAILED: ${created.error}`);
17797
17896
  lines.push("");
17897
+ results.push({ node_id: item.node_id, ok: false, stored: false, error: created.error });
17798
17898
  continue;
17799
17899
  }
17800
17900
  const mediaAssetId = created.data.media_asset_id;
17801
17901
  if (!mediaAssetId) {
17802
- lines.push(`FAILED: upload did not return a media asset id (route ${String(created.data.route ?? "unknown")}).`);
17902
+ const error2 = `upload did not return a media asset id (route ${String(created.data.route ?? "unknown")}).`;
17903
+ lines.push(`FAILED: ${error2}`);
17803
17904
  lines.push("");
17905
+ results.push({ node_id: item.node_id, ok: false, stored: false, error: error2 });
17804
17906
  continue;
17805
17907
  }
17806
17908
  const attached = await callApi(`/forms/${form_id}/nodes/${item.node_id}/media/attach`, {
@@ -17810,6 +17912,7 @@ When a public URL is provided, the media is fetched and stored automatically. On
17810
17912
  if (!attached.ok) {
17811
17913
  lines.push(`FAILED to attach the uploaded media to the node: ${attached.error}`);
17812
17914
  lines.push("");
17915
+ results.push({ node_id: item.node_id, ok: false, stored: false, error: attached.error });
17813
17916
  continue;
17814
17917
  }
17815
17918
  const presentation = {};
@@ -17826,6 +17929,15 @@ When a public URL is provided, the media is fetched and stored automatically. On
17826
17929
  }
17827
17930
  }
17828
17931
  successCount++;
17932
+ const uploadUrl = created.data.upload_url;
17933
+ const uploadMethod = created.data.upload_method;
17934
+ results.push({
17935
+ node_id: item.node_id,
17936
+ ok: true,
17937
+ stored: !uploadUrl,
17938
+ ...uploadUrl ? { upload_url: uploadUrl } : {},
17939
+ ...uploadUrl && uploadMethod ? { upload_method: uploadMethod } : {}
17940
+ });
17829
17941
  const resultLines = [];
17830
17942
  if (created.data.upload_url) {
17831
17943
  resultLines.push(
@@ -17848,7 +17960,7 @@ When a public URL is provided, the media is fetched and stored automatically. On
17848
17960
  `);
17849
17961
  }
17850
17962
  if (successCount === 0) return errorResult(lines.join("\n"));
17851
- return textResult(lines.join("\n"));
17963
+ return structuredResult(lines.join("\n"), { results });
17852
17964
  }
17853
17965
  );
17854
17966
  }
@@ -17864,6 +17976,14 @@ function registerGetNodeMediaTool(server) {
17864
17976
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17865
17977
  node_id: external_exports.string().describe("The node ID")
17866
17978
  },
17979
+ outputSchema: {
17980
+ media: external_exports.object({
17981
+ media_type: external_exports.string(),
17982
+ status: external_exports.string(),
17983
+ duration: external_exports.number().optional(),
17984
+ transcription_status: external_exports.string().nullable().optional()
17985
+ }).nullable().describe("Attached media (null if the node has none)")
17986
+ },
17867
17987
  annotations: {
17868
17988
  readOnlyHint: true,
17869
17989
  destructiveHint: false,
@@ -17880,15 +18000,22 @@ function registerGetNodeMediaTool(server) {
17880
18000
  }
17881
18001
  const media = result.data.media;
17882
18002
  if (!media) {
17883
- return textResult("No media attached to this node.");
18003
+ return structuredResult("No media attached to this node.", { media: null });
17884
18004
  }
17885
- return textResult(
18005
+ const projected = {
18006
+ media_type: String(media.media_type),
18007
+ status: String(media.status),
18008
+ ...typeof media.duration === "number" ? { duration: media.duration } : {},
18009
+ transcription_status: media.transcription_status == null ? null : String(media.transcription_status)
18010
+ };
18011
+ return structuredResult(
17886
18012
  [
17887
18013
  `Type: ${media.media_type}`,
17888
18014
  `Status: ${media.status}`,
17889
18015
  media.duration ? `Duration: ${media.duration}s` : "",
17890
18016
  `Transcription: ${media.transcription_status}`
17891
- ].filter(Boolean).join("\n")
18017
+ ].filter(Boolean).join("\n"),
18018
+ { media: projected }
17892
18019
  );
17893
18020
  }
17894
18021
  );
@@ -18042,6 +18169,20 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18042
18169
  query: external_exports.string().describe("News search query (e.g. 'Iran war 2026', 'Australian Open final', 'UK election')"),
18043
18170
  count: external_exports.number().min(1).max(15).default(5).optional().describe("Max results per provider (default 5)")
18044
18171
  },
18172
+ outputSchema: {
18173
+ query: external_exports.string(),
18174
+ articles: external_exports.array(
18175
+ external_exports.object({
18176
+ title: external_exports.string().optional(),
18177
+ source: external_exports.string().optional(),
18178
+ description: external_exports.string().optional(),
18179
+ author: external_exports.string().optional(),
18180
+ published_at: external_exports.string().optional(),
18181
+ url: external_exports.string(),
18182
+ image_url: external_exports.string().optional()
18183
+ })
18184
+ ).describe("Matching articles (empty if none)")
18185
+ },
18045
18186
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18046
18187
  },
18047
18188
  async ({ query, count }) => {
@@ -18050,7 +18191,16 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18050
18191
  });
18051
18192
  if (!result.ok) return errorResult(result.error);
18052
18193
  const results = result.data.results;
18053
- if (!results.length) return textResult(`No news articles found for "${query}".`);
18194
+ if (!results.length) return structuredResult(`No news articles found for "${query}".`, { query, articles: [] });
18195
+ const articles = results.map((article) => ({
18196
+ ...article.title ? { title: article.title } : {},
18197
+ ...article.source ? { source: article.source } : {},
18198
+ ...article.description ? { description: article.description } : {},
18199
+ ...article.author ? { author: article.author } : {},
18200
+ ...article.publishedAt ? { published_at: article.publishedAt } : {},
18201
+ url: article.url,
18202
+ ...article.imageUrl ? { image_url: article.imageUrl } : {}
18203
+ }));
18054
18204
  const lines = [`Found ${results.length} articles for "${query}":
18055
18205
  `];
18056
18206
  for (const article of results) {
@@ -18062,7 +18212,7 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18062
18212
  if (article.imageUrl) lines.push(` Image: ${article.imageUrl}`);
18063
18213
  lines.push("");
18064
18214
  }
18065
- return textResult(lines.join("\n"));
18215
+ return structuredResult(lines.join("\n"), { query, articles });
18066
18216
  }
18067
18217
  );
18068
18218
  }
@@ -18118,6 +18268,12 @@ Supports any public YouTube video with captions enabled. Does NOT work for priva
18118
18268
  }
18119
18269
 
18120
18270
  // src/tools/generate-tts.ts
18271
+ var CaptionSchema = external_exports.object({
18272
+ start: external_exports.number(),
18273
+ end: external_exports.number(),
18274
+ text: external_exports.string(),
18275
+ words: external_exports.array(external_exports.object({ word: external_exports.string(), start: external_exports.number(), end: external_exports.number() })).optional()
18276
+ });
18121
18277
  var TtsItemSchema = external_exports.object({
18122
18278
  text: external_exports.string().min(1).max(5e3).describe("Narration text"),
18123
18279
  voice: external_exports.enum(["ryan", "sonia", "andrew", "ava", "guy"]).optional().default("ryan").describe("Voice: ryan (British male, clear/articulate), sonia (British female, warm/bright), andrew (American male, smooth/neutral), ava (American female, vibrant/friendly), guy (American male, deep/authoritative). Pick based on the form's topic and audience."),
@@ -18138,6 +18294,16 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
18138
18294
  inputSchema: {
18139
18295
  items: external_exports.array(TtsItemSchema).min(1).max(10).describe("One or more TTS items to generate")
18140
18296
  },
18297
+ outputSchema: {
18298
+ results: external_exports.array(
18299
+ external_exports.object({
18300
+ ok: external_exports.boolean(),
18301
+ audio_url: external_exports.string().optional(),
18302
+ captions: external_exports.array(CaptionSchema).optional().describe("Pass as the captions param to upload_node_media"),
18303
+ error: external_exports.string().optional()
18304
+ })
18305
+ ).describe("One result per item, in order")
18306
+ },
18141
18307
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18142
18308
  },
18143
18309
  async ({ items }) => {
@@ -18151,6 +18317,7 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
18151
18317
  )
18152
18318
  );
18153
18319
  const lines = [];
18320
+ const structured = [];
18154
18321
  let successCount = 0;
18155
18322
  for (let i = 0; i < results.length; i++) {
18156
18323
  const r = results[i];
@@ -18167,11 +18334,17 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
18167
18334
  ...Array.isArray(c.words) ? { words: c.words.map((w) => ({ word: w.word, start: w.start, end: w.end })) } : {}
18168
18335
  }));
18169
18336
  lines.push(`Captions: ${JSON.stringify(captions)}`);
18337
+ structured.push({
18338
+ ok: true,
18339
+ ...data.audioUrl ? { audio_url: String(data.audioUrl) } : {},
18340
+ captions
18341
+ });
18170
18342
  } else {
18171
18343
  const error2 = r.status === "rejected" ? r.reason?.message || String(r.reason) : r.value.error;
18172
18344
  const status = r.status === "fulfilled" ? r.value.status : 0;
18173
18345
  const hint = status === 500 || status === 0 ? " (transient - retrying may help)" : "";
18174
18346
  lines.push(`FAILED: ${error2}${hint}`);
18347
+ structured.push({ ok: false, error: String(error2) });
18175
18348
  }
18176
18349
  lines.push("");
18177
18350
  }
@@ -18181,7 +18354,7 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
18181
18354
  }
18182
18355
  lines.push(`Pass the complete Captions JSON as the "captions" parameter when uploading.`);
18183
18356
  if (successCount === 0) return errorResult(lines.join("\n"));
18184
- return textResult(lines.join("\n"));
18357
+ return structuredResult(lines.join("\n"), { results: structured });
18185
18358
  }
18186
18359
  );
18187
18360
  }
@@ -18205,10 +18378,30 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
18205
18378
  })
18206
18379
  ).min(1).max(10).describe("One or more search queries to run")
18207
18380
  },
18381
+ outputSchema: {
18382
+ searches: external_exports.array(
18383
+ external_exports.object({
18384
+ query: external_exports.string(),
18385
+ kind: external_exports.enum(["image", "video"]),
18386
+ items: external_exports.array(
18387
+ external_exports.object({
18388
+ title: external_exports.string().optional(),
18389
+ url: external_exports.string(),
18390
+ width: external_exports.number().optional(),
18391
+ height: external_exports.number().optional(),
18392
+ duration: external_exports.number().optional(),
18393
+ source: external_exports.string().optional(),
18394
+ attribution: external_exports.string().optional()
18395
+ })
18396
+ )
18397
+ })
18398
+ ).describe("Results grouped by query (items empty if a query found nothing or failed)")
18399
+ },
18208
18400
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18209
18401
  },
18210
18402
  async ({ queries }) => {
18211
18403
  const allLines = [];
18404
+ const searches = [];
18212
18405
  for (const q of queries) {
18213
18406
  const { query, count, orientation } = q;
18214
18407
  const kind = q.kind ?? "image";
@@ -18218,16 +18411,32 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
18218
18411
  if (queries.length > 1) allLines.push(`--- "${query}" (${kind}) ---`);
18219
18412
  if (!result.ok) {
18220
18413
  allLines.push(`FAILED: ${result.error}`, "");
18414
+ searches.push({ query, kind, items: [] });
18221
18415
  continue;
18222
18416
  }
18223
18417
  const results = result.data.results;
18224
18418
  if (!results.length) {
18225
18419
  allLines.push(`No ${kind}s found.`, "");
18420
+ searches.push({ query, kind, items: [] });
18226
18421
  continue;
18227
18422
  }
18423
+ const shown = results.slice(0, 10);
18424
+ searches.push({
18425
+ query,
18426
+ kind,
18427
+ items: shown.map((item) => ({
18428
+ ...item.title ? { title: item.title } : {},
18429
+ url: item.url,
18430
+ ...typeof item.width === "number" ? { width: item.width } : {},
18431
+ ...typeof item.height === "number" ? { height: item.height } : {},
18432
+ ...kind === "video" && typeof item.duration === "number" ? { duration: item.duration } : {},
18433
+ ...item.source ? { source: item.source } : {},
18434
+ ...item.attribution ? { attribution: item.attribution } : {}
18435
+ }))
18436
+ });
18228
18437
  allLines.push(`Found ${results.length} ${kind}s:
18229
18438
  `);
18230
- for (const item of results.slice(0, 10)) {
18439
+ for (const item of shown) {
18231
18440
  allLines.push(`- ${item.title}`);
18232
18441
  allLines.push(` URL: ${item.url}`);
18233
18442
  const meta = [];
@@ -18242,7 +18451,7 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
18242
18451
  allLines.push("");
18243
18452
  }
18244
18453
  }
18245
- return textResult(allLines.join("\n"));
18454
+ return structuredResult(allLines.join("\n"), { searches });
18246
18455
  }
18247
18456
  );
18248
18457
  }
@@ -18378,6 +18587,17 @@ For multi-render builds (e.g. one clip per quiz question), pass everything in ON
18378
18587
  wait: external_exports.boolean().optional().default(true).describe("Single render only: true (default) blocks until the render is ready and returns its URL; false returns a job ID for clipform_check_render. Batch items always run fire-and-poll."),
18379
18588
  items: external_exports.array(RenderItemSchema).min(1).max(10).optional().describe("Batch mode: multiple renders in one call. All fire in parallel; returns one job ID per item - collect with clipform_check_render (job_ids). Use this whenever rendering more than one clip.")
18380
18589
  },
18590
+ outputSchema: {
18591
+ jobs: external_exports.array(
18592
+ external_exports.object({
18593
+ status: external_exports.enum(["rendering", "complete"]),
18594
+ job_id: external_exports.string().optional().describe("Present when status is 'rendering' - pass to check_render"),
18595
+ public_url: external_exports.string().optional().describe("Present when status is 'complete' - attach via upload_node_media (fit_media: true)"),
18596
+ composition_id: external_exports.string().optional(),
18597
+ output_format: external_exports.string().optional()
18598
+ })
18599
+ ).describe("One entry per render (single or batch)")
18600
+ },
18381
18601
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18382
18602
  },
18383
18603
  async ({ compositionId, outputFormat, inputProps, wait, items }) => {
@@ -18393,23 +18613,34 @@ For multi-render builds (e.g. one clip per quiz question), pass everything in ON
18393
18613
  }
18394
18614
  if (items?.length) {
18395
18615
  const jobs2 = items.map((item) => ({ item, job: fireRender(item) }));
18396
- return textResult(
18616
+ return structuredResult(
18397
18617
  [
18398
18618
  `${jobs2.length} renders started in parallel:`,
18399
18619
  ...jobs2.map(({ item, job }) => `- ${item.compositionId} (${item.outputFormat}): job ${job.id}`),
18400
18620
  ``,
18401
18621
  `Collect with ONE clipform_check_render call (job_ids: [...]). Renders typically take ${RENDER_TIMING.expectedRange}.`
18402
- ].join("\n")
18622
+ ].join("\n"),
18623
+ {
18624
+ jobs: jobs2.map(({ item, job }) => ({
18625
+ status: "rendering",
18626
+ job_id: job.id,
18627
+ composition_id: item.compositionId,
18628
+ output_format: item.outputFormat
18629
+ }))
18630
+ }
18403
18631
  );
18404
18632
  }
18405
18633
  if (wait === false) {
18406
18634
  const job = fireRender({ compositionId, outputFormat, inputProps });
18407
- return textResult(
18635
+ return structuredResult(
18408
18636
  [
18409
18637
  `Render started (${compositionId}).`,
18410
18638
  `Job ID: ${job.id}`,
18411
18639
  `Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
18412
- ].join("\n")
18640
+ ].join("\n"),
18641
+ {
18642
+ jobs: [{ status: "rendering", job_id: job.id, composition_id: compositionId, output_format: outputFormat }]
18643
+ }
18413
18644
  );
18414
18645
  }
18415
18646
  const result = await callApi("/internal/render", {
@@ -18418,11 +18649,21 @@ For multi-render builds (e.g. one clip per quiz question), pass everything in ON
18418
18649
  });
18419
18650
  if (!result.ok) return errorResult(result.error);
18420
18651
  const data = result.data;
18421
- return textResult(
18652
+ return structuredResult(
18422
18653
  [
18423
18654
  `Render complete. Public URL: ${data.public_url}`,
18424
18655
  `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18425
- ].join("\n")
18656
+ ].join("\n"),
18657
+ {
18658
+ jobs: [
18659
+ {
18660
+ status: "complete",
18661
+ ...data.public_url ? { public_url: data.public_url } : {},
18662
+ composition_id: compositionId,
18663
+ output_format: outputFormat
18664
+ }
18665
+ ]
18666
+ }
18426
18667
  );
18427
18668
  }
18428
18669
  );
@@ -18443,6 +18684,19 @@ function registerSearchMusicTool(server) {
18443
18684
  maxDuration: external_exports.number().optional().describe("Maximum duration in seconds"),
18444
18685
  tags: external_exports.array(external_exports.string()).optional().describe("Genre/mood tags to filter by")
18445
18686
  },
18687
+ outputSchema: {
18688
+ query: external_exports.string(),
18689
+ tracks: external_exports.array(
18690
+ external_exports.object({
18691
+ title: external_exports.string().optional(),
18692
+ artist: external_exports.string().optional(),
18693
+ url: external_exports.string(),
18694
+ duration: external_exports.number().optional(),
18695
+ source: external_exports.string().optional(),
18696
+ license: external_exports.string().optional()
18697
+ })
18698
+ ).describe("Matching tracks (empty if none); pass a url as the audio track to generate_video")
18699
+ },
18446
18700
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18447
18701
  },
18448
18702
  async ({ query, count, instrumentalOnly, minDuration, maxDuration, tags }) => {
@@ -18451,7 +18705,15 @@ function registerSearchMusicTool(server) {
18451
18705
  });
18452
18706
  if (!result.ok) return errorResult(result.error);
18453
18707
  const results = result.data.results;
18454
- if (!results.length) return textResult(`No music found for "${query}".`);
18708
+ if (!results.length) return structuredResult(`No music found for "${query}".`, { query, tracks: [] });
18709
+ const tracks = results.map((item) => ({
18710
+ ...item.title ? { title: item.title } : {},
18711
+ ...item.artist ? { artist: item.artist } : {},
18712
+ url: item.url,
18713
+ ...typeof item.duration === "number" ? { duration: item.duration } : {},
18714
+ ...item.source ? { source: item.source } : {},
18715
+ ...item.license ? { license: item.license } : {}
18716
+ }));
18455
18717
  const lines = [`Found ${results.length} tracks for "${query}":
18456
18718
  `];
18457
18719
  for (const item of results) {
@@ -18462,7 +18724,7 @@ function registerSearchMusicTool(server) {
18462
18724
  if (item.license) lines.push(` License: ${item.license}`);
18463
18725
  lines.push("");
18464
18726
  }
18465
- return textResult(lines.join("\n"));
18727
+ return structuredResult(lines.join("\n"), { query, tracks });
18466
18728
  }
18467
18729
  );
18468
18730
  }
@@ -18503,6 +18765,19 @@ function registerListCompositionsTool(server) {
18503
18765
  title: "List Compositions",
18504
18766
  description: `Browse available video compositions and their expected props schemas. Call this before using clipform_render_composition to discover visual styles and their input props. For narrated slideshows from images, use clipform_generate_video instead.`,
18505
18767
  inputSchema: {},
18768
+ outputSchema: {
18769
+ compositions: external_exports.array(
18770
+ external_exports.object({
18771
+ id: external_exports.string(),
18772
+ width: external_exports.number(),
18773
+ height: external_exports.number(),
18774
+ fps: external_exports.number(),
18775
+ durationInFrames: external_exports.number(),
18776
+ props: external_exports.string().describe("Compact props summary (name*: type)"),
18777
+ propsSchema: external_exports.record(external_exports.unknown()).optional().describe("Full JSON Schema for inputProps")
18778
+ })
18779
+ ).describe("Available compositions - pass an id to render_composition")
18780
+ },
18506
18781
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18507
18782
  },
18508
18783
  async () => {
@@ -18517,7 +18792,18 @@ function registerListCompositionsTool(server) {
18517
18792
  }
18518
18793
  const all = result.data.compositions;
18519
18794
  const compositions = all.filter((c) => EXPOSED_COMPOSITIONS.includes(c.id));
18520
- if (!compositions.length) return textResult("No compositions found.");
18795
+ if (!compositions.length) return structuredResult("No compositions found.", { compositions: [] });
18796
+ const structured = {
18797
+ compositions: compositions.map((comp) => ({
18798
+ id: comp.id,
18799
+ width: comp.width,
18800
+ height: comp.height,
18801
+ fps: comp.fps,
18802
+ durationInFrames: comp.durationInFrames,
18803
+ props: summarizeSchema(comp.propsSchema),
18804
+ ...comp.propsSchema ? { propsSchema: comp.propsSchema } : {}
18805
+ }))
18806
+ };
18521
18807
  const lines = [`Available compositions (${compositions.length}):
18522
18808
  `];
18523
18809
  for (const comp of compositions) {
@@ -18527,7 +18813,7 @@ function registerListCompositionsTool(server) {
18527
18813
  }
18528
18814
  lines.push("");
18529
18815
  lines.push("Use clipform_render_composition with a compositionId to render. inputProps are validated strictly against the schema - unknown or missing props fail.");
18530
- return textResult(lines.join("\n"));
18816
+ return structuredResult(lines.join("\n"), structured);
18531
18817
  }
18532
18818
  );
18533
18819
  }
@@ -18542,6 +18828,11 @@ function registerListAssetsTool(server) {
18542
18828
  inputSchema: {
18543
18829
  type: external_exports.enum(["sfx", "animation", "font", "all"]).default("all").optional().describe("Asset type to list (default: all)")
18544
18830
  },
18831
+ outputSchema: {
18832
+ sfx: external_exports.array(external_exports.object({ name: external_exports.string(), description: external_exports.string().nullable().optional(), path: external_exports.string().optional() })),
18833
+ animations: external_exports.array(external_exports.object({ name: external_exports.string(), description: external_exports.string().nullable().optional(), path: external_exports.string().optional() })),
18834
+ fonts: external_exports.array(external_exports.object({ name: external_exports.string(), weights: external_exports.array(external_exports.union([external_exports.string(), external_exports.number()])).optional() }))
18835
+ },
18545
18836
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18546
18837
  },
18547
18838
  async ({ type }) => {
@@ -18552,6 +18843,11 @@ function registerListAssetsTool(server) {
18552
18843
  if (!result.ok) return errorResult(result.error);
18553
18844
  const data = result.data;
18554
18845
  const lines = [];
18846
+ const structured = {
18847
+ sfx: (data.sfx ?? []).map((s) => ({ name: s.name, description: s.description ?? null, path: s.path })),
18848
+ animations: (data.animations ?? []).map((a) => ({ name: a.name, description: a.description ?? null, path: a.path })),
18849
+ fonts: (data.fonts ?? []).map((f) => ({ name: f.name, ...Array.isArray(f.weights) ? { weights: f.weights } : {} }))
18850
+ };
18555
18851
  if (data.sfx?.length) {
18556
18852
  lines.push(`## Sound Effects (${data.sfx.length})
18557
18853
  `);
@@ -18576,7 +18872,7 @@ function registerListAssetsTool(server) {
18576
18872
  }
18577
18873
  lines.push("");
18578
18874
  }
18579
- return textResult(lines.length ? lines.join("\n") : "No assets found.");
18875
+ return structuredResult(lines.length ? lines.join("\n") : "No assets found.", structured);
18580
18876
  }
18581
18877
  );
18582
18878
  }
@@ -18627,6 +18923,12 @@ For multi-question builds, pass wait: false on every render: each call returns a
18627
18923
  background_color: external_exports.string().optional().describe("Background color (default '#000')"),
18628
18924
  wait: external_exports.boolean().optional().default(true).describe("true (default) blocks until the video is ready and returns its URL. false returns a job ID immediately - fire all renders first, then poll clipform_check_render. Use false whenever rendering more than one video.")
18629
18925
  },
18926
+ outputSchema: {
18927
+ status: external_exports.enum(["rendering", "complete"]).describe("'rendering' when wait:false (poll check_render); 'complete' with a public_url when wait:true"),
18928
+ job_id: external_exports.string().optional().describe("Present when status is 'rendering' - pass to check_render"),
18929
+ public_url: external_exports.string().optional().describe("Present when status is 'complete' - attach via upload_node_media (fit_media: true)"),
18930
+ duration_seconds: external_exports.number().optional()
18931
+ },
18630
18932
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18631
18933
  },
18632
18934
  async ({ items, audio_url, background_audio_url, background_audio_volume, duration_seconds, random_effects, transition, style_preset, texture, duotone, background_color, wait }) => {
@@ -18658,30 +18960,52 @@ For multi-question builds, pass wait: false on every render: each call returns a
18658
18960
  if (r.ok) completeJob(job.id, r.data);
18659
18961
  else failJob(job.id, r.error);
18660
18962
  }).catch((err) => failJob(job.id, err instanceof Error ? err.message : String(err)));
18661
- return textResult(
18963
+ return structuredResult(
18662
18964
  [
18663
18965
  `Render started (${items.length} item${items.length > 1 ? "s" : ""}).`,
18664
18966
  `Job ID: ${job.id}`,
18665
18967
  `Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
18666
- ].join("\n")
18968
+ ].join("\n"),
18969
+ { status: "rendering", job_id: job.id }
18667
18970
  );
18668
18971
  }
18669
18972
  const result = await apiCall();
18670
18973
  if (!result.ok) return errorResult(result.error);
18671
18974
  const data = result.data;
18672
- return textResult(
18975
+ return structuredResult(
18673
18976
  [
18674
18977
  `Video rendered (${items.length} item${items.length > 1 ? "s" : ""}).`,
18675
18978
  `Public URL: ${data.public_url}`,
18676
18979
  data.duration_seconds ? `Duration: ${data.duration_seconds}s` : "",
18677
18980
  `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18678
- ].filter(Boolean).join("\n")
18981
+ ].filter(Boolean).join("\n"),
18982
+ {
18983
+ status: "complete",
18984
+ ...data.public_url ? { public_url: data.public_url } : {},
18985
+ ...typeof data.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {}
18986
+ }
18679
18987
  );
18680
18988
  }
18681
18989
  );
18682
18990
  }
18683
18991
 
18684
18992
  // src/tools/check-render.ts
18993
+ function projectJob(id, job) {
18994
+ if (!job) return { job_id: id, status: "not_found" };
18995
+ if (job.status === "rendering") {
18996
+ return { job_id: id, status: "rendering", elapsed_seconds: Math.round((Date.now() - job.createdAt) / 1e3) };
18997
+ }
18998
+ if (job.status === "failed") {
18999
+ return { job_id: id, status: "failed", ...job.error ? { error: job.error } : {} };
19000
+ }
19001
+ const data = job.result;
19002
+ return {
19003
+ job_id: id,
19004
+ status: "complete",
19005
+ ...data?.public_url ? { public_url: data.public_url } : {},
19006
+ ...typeof data?.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {}
19007
+ };
19008
+ }
18685
19009
  var LOST_JOB_RECOVERY = [
18686
19010
  `Jobs expire after 30 minutes and are lost if the server restarts - the render itself may still have completed.`,
18687
19011
  `To recover, re-run the original render call with identical arguments:`,
@@ -18718,6 +19042,18 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
18718
19042
  job_id: external_exports.string().uuid().optional().describe("A single job ID returned by the render tool"),
18719
19043
  job_ids: external_exports.array(external_exports.string().uuid()).min(1).max(20).optional().describe("Multiple job IDs - check the whole batch in one call instead of one call per job")
18720
19044
  },
19045
+ outputSchema: {
19046
+ jobs: external_exports.array(
19047
+ external_exports.object({
19048
+ job_id: external_exports.string(),
19049
+ status: external_exports.enum(["rendering", "complete", "failed", "not_found"]),
19050
+ public_url: external_exports.string().optional(),
19051
+ duration_seconds: external_exports.number().optional(),
19052
+ elapsed_seconds: external_exports.number().optional(),
19053
+ error: external_exports.string().optional()
19054
+ })
19055
+ ).describe("One entry per queried job; attach completed public_urls via upload_node_media")
19056
+ },
18721
19057
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18722
19058
  },
18723
19059
  async ({ job_id, job_ids }) => {
@@ -18733,38 +19069,41 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
18733
19069
  }
18734
19070
  if (job.status === "rendering") {
18735
19071
  const elapsed = Math.round((Date.now() - job.createdAt) / 1e3);
18736
- return textResult(
19072
+ return structuredResult(
18737
19073
  [
18738
19074
  `Status: rendering (${elapsed}s elapsed)`,
18739
19075
  `Tool: ${job.tool}`,
18740
19076
  ``,
18741
19077
  `Still in progress. Check again in ${RENDER_TIMING.pollDelay}.`
18742
- ].join("\n")
19078
+ ].join("\n"),
19079
+ { jobs: [projectJob(ids[0], job)] }
18743
19080
  );
18744
19081
  }
18745
19082
  if (job.status === "failed") {
18746
19083
  return errorResult(`Render failed: ${job.error}`);
18747
19084
  }
18748
19085
  const data = job.result;
18749
- return textResult(
19086
+ return structuredResult(
18750
19087
  [
18751
19088
  `Render complete.`,
18752
19089
  ...data.public_url ? [`Public URL: ${data.public_url}`] : [],
18753
19090
  ...data.duration_seconds ? [`Duration: ${data.duration_seconds}s`] : [],
18754
19091
  `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18755
- ].join("\n")
19092
+ ].join("\n"),
19093
+ { jobs: [projectJob(ids[0], job)] }
18756
19094
  );
18757
19095
  }
18758
19096
  const results = ids.map((id) => describeJob(id, getJob(id)));
18759
19097
  const pending = results.filter((r) => !r.done).length;
18760
19098
  const lost = results.some((r) => r.line.includes("NOT FOUND"));
18761
- return textResult(
19099
+ return structuredResult(
18762
19100
  [
18763
19101
  `Render batch: ${results.length - pending}/${results.length} finished${pending ? ` - check again in ${RENDER_TIMING.pollDelay}` : ""}.`,
18764
19102
  ...results.map((r) => r.line),
18765
19103
  ...pending === 0 ? [``, `Attach completed renders via clipform_upload_node_media with fit_media: true (composed 9:16 frames - contain, never crop).`] : [],
18766
19104
  ...lost ? [``, LOST_JOB_RECOVERY] : []
18767
- ].join("\n")
19105
+ ].join("\n"),
19106
+ { jobs: ids.map((id) => projectJob(id, getJob(id))) }
18768
19107
  );
18769
19108
  }
18770
19109
  );
@@ -19080,4 +19419,4 @@ export {
19080
19419
  JSONRPCMessageSchema,
19081
19420
  createServer
19082
19421
  };
19083
- //# sourceMappingURL=chunk-JT7YWEQP.js.map
19422
+ //# sourceMappingURL=chunk-3MTEM3VC.js.map