@contenthero/mcp 0.4.2 → 0.4.3

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
@@ -37,7 +37,7 @@ import { z } from 'zod';
37
37
  import { GenerationTimeoutError, pendingOutputId, } from '@contenthero/sdk';
38
38
  import { getClient as defaultGetClient } from './client.js';
39
39
  import { resolveModelEnums, BOARD_TYPES, BOARD_TYPE_GUIDANCE, IMAGE_MODEL_GUIDANCE, VIDEO_MODEL_GUIDANCE, AUDIO_MODEL_GUIDANCE, EDIT_AUDIO_MODEL_GUIDANCE, UPSCALE_MODEL_GUIDANCE, LIP_SYNC_MODEL_GUIDANCE, } from './models.js';
40
- import { audioResult, avatarListResult, avatarResult, balanceResult, brandKitListResult, brandKitResult, brandKnowledgeListResult, brandKnowledgeDetailResult, brandKnowledgeSearchResult, brandKnowledgeItemResult, completedResult, connectedAccountListResult, connectedAccountResult, costResult, accountDetailResult, inspirationContentResult, mediaListResult, mediaSearchResult, folderListResult, folderContentsResult, mediaBatchResult, mediaUploadResult, importedMediaResult, uploadedMediaResult, tagListResult, tagResult, tagDeletedResult, modelListResult, modelResult, platformListResult, platformResult, elementListResult, elementResult, elementDeletedResult, errorResult, generationBatchResult, outlierListResult, enhanceClipsResult, pendingResult, stageListResult, spaceDeletedResult, spaceListResult, spaceResult, cardListResult, cardResult, postSummaryResult, publishResult, statusActionResult, editorOpsResult, text, projectDetailResult, liveContextResult, projectListResult, projectCreatedResult, projectDeletedResult, layerTypesResult, timelineTypesResult, editorTranscriptResult, exportJobResult, exportFormatsResult, trackedAccountListResult, transcriptResult, voiceListResult, voiceResult, } from './format.js';
40
+ import { audioResult, avatarListResult, avatarResult, avatarPendingResult, balanceResult, brandKitListResult, brandKitResult, brandKnowledgeListResult, brandKnowledgeDetailResult, brandKnowledgeSearchResult, brandKnowledgeItemResult, completedResult, connectedAccountListResult, connectedAccountResult, costResult, accountDetailResult, inspirationContentResult, mediaListResult, mediaSearchResult, folderListResult, folderContentsResult, mediaBatchResult, mediaUploadResult, importedMediaResult, uploadedMediaResult, tagListResult, tagResult, tagDeletedResult, modelListResult, modelResult, platformListResult, platformResult, elementListResult, elementResult, elementDeletedResult, errorResult, generationBatchResult, outlierListResult, enhanceClipsResult, pendingResult, stageListResult, spaceDeletedResult, spaceListResult, spaceResult, cardListResult, cardResult, postSummaryResult, publishResult, statusActionResult, editorOpsResult, text, projectDetailResult, liveContextResult, projectListResult, projectCreatedResult, projectDeletedResult, layerTypesResult, timelineTypesResult, editorTranscriptResult, exportJobResult, exportFormatsResult, trackedAccountListResult, transcriptResult, voiceListResult, voiceResult, } from './format.js';
41
41
  /** Platforms a card or one of its posts may target. */
42
42
  const POST_PLATFORMS = [
43
43
  'youtube',
@@ -286,6 +286,10 @@ export function registerTools(server, opts) {
286
286
  .array(z.string())
287
287
  .optional()
288
288
  .describe('References for image-to-image / editing. Each may be a URL or a previous output id (e.g. "<id>" or "<id>-2") to chain from an earlier generation.'),
289
+ avatarId: z
290
+ .string()
291
+ .optional()
292
+ .describe('Optional avatar id from list_avatars. File the result onto that avatar as a new LOOK (one appearance of a reusable character: same person, different outfit, setting or framing) instead of saving a standalone library output. Combine with a referenceImage of the avatar to keep the subject on-model.'),
289
293
  ...PLACEMENT_INPUT_FIELDS,
290
294
  getCost: z.boolean().optional().describe('Return the credit cost estimate instead of generating (nothing runs, nothing is charged).'),
291
295
  },
@@ -302,6 +306,7 @@ export function registerTools(server, opts) {
302
306
  seed: args.seed,
303
307
  references: buildReferences({ images: args.referenceImages }),
304
308
  parameters: args.mode ? { mode: args.mode } : undefined,
309
+ avatarId: args.avatarId,
305
310
  projectId: args.projectId,
306
311
  placement: args.placement,
307
312
  playheadFrame: args.playheadFrame,
@@ -344,6 +349,10 @@ export function registerTools(server, opts) {
344
349
  .optional()
345
350
  .describe('Number of board variations (1-4). Defaults to 1.'),
346
351
  boardName: z.string().optional().describe('Optional name for the board.'),
352
+ avatarId: z
353
+ .string()
354
+ .optional()
355
+ .describe('Optional avatar id from list_avatars. Associate the board with that avatar, so a character sheet built for an avatar stays filed against it.'),
347
356
  getCost: z.boolean().optional().describe('Return the credit cost estimate instead of generating (nothing runs, nothing is charged).'),
348
357
  },
349
358
  }, async (args, extra) => {
@@ -355,6 +364,7 @@ export function registerTools(server, opts) {
355
364
  referenceImages: args.referenceImages,
356
365
  numImages: args.numImages,
357
366
  boardName: args.boardName,
367
+ avatarId: args.avatarId,
358
368
  });
359
369
  if (args.getCost)
360
370
  return costResult(await client.estimateBoardCost(request));
@@ -735,6 +745,115 @@ export function registerTools(server, opts) {
735
745
  return errorResult(err);
736
746
  }
737
747
  });
748
+ // -- create_avatar --------------------------------------------------------
749
+ server.registerTool('create_avatar', {
750
+ title: 'Create Avatar',
751
+ annotations: WRITE,
752
+ description: "Create a reusable character and start generating its first look. SPENDS CREDITS (pass getCost to preview the price without creating anything). Returns as soon as the record exists: the avatar is NOT usable yet, it sits at status 'processing' with no image until its first look finishes, so poll get_avatar until status is 'completed'. Supply referenceImageUrls to make the avatar a likeness of a real person from their photos; omit them to invent a character from the description and traits.",
753
+ inputSchema: {
754
+ name: z.string().describe('Avatar name, at least 3 characters.'),
755
+ age: z.string().describe("Apparent age, e.g. '20s', '35', 'middle-aged'. Required: the prompt writer describes the character from these traits."),
756
+ gender: z.string().describe('Gender presentation. Required, same reason as age.'),
757
+ ethnicity: z.string().optional().describe('Optional ethnicity, for a more specific likeness.'),
758
+ niche: z.array(z.string()).optional().describe('Content niches this character is for, e.g. ["fitness","nutrition"].'),
759
+ style: z.string().optional().describe('Visual style hint for the portrait, e.g. "editorial", "cinematic". Not stored on the avatar.'),
760
+ description: z
761
+ .string()
762
+ .optional()
763
+ .describe('Free-text description of the character. The strongest single input when no reference photos are given.'),
764
+ defaultVoiceId: z.string().optional().describe('A voiceId from list_voices, used as this avatar the default voice.'),
765
+ referenceImageUrls: z
766
+ .array(z.string())
767
+ .optional()
768
+ .describe('Photos of a REAL PERSON to anchor identity to: each a URL or a previous output id. Only use photos of someone who has agreed to being cloned.'),
769
+ getCost: z.boolean().optional().describe('Return the credit cost estimate instead of creating (nothing runs, nothing is charged).'),
770
+ },
771
+ }, async (args, extra) => {
772
+ try {
773
+ const client = await getClient(extra);
774
+ if (args.getCost) {
775
+ const creditsEstimate = await client.estimateAvatarCost();
776
+ // `modelId` is the first thing `costResult` names in its sentence, and `contentType` only
777
+ // admits image/video/audio. An avatar is none of those: the price is the fixed
778
+ // avatar-creation fee, not the cost of the portrait model, so say that rather than pick a
779
+ // media kind that would misdescribe it.
780
+ return costResult({ getCost: true, creditsEstimate, modelId: 'avatar creation' });
781
+ }
782
+ const created = await client.createAvatar({
783
+ name: args.name,
784
+ age: args.age,
785
+ gender: args.gender,
786
+ ethnicity: args.ethnicity,
787
+ niche: args.niche,
788
+ style: args.style,
789
+ description: args.description,
790
+ defaultVoiceId: args.defaultVoiceId,
791
+ referenceImageUrls: args.referenceImageUrls,
792
+ });
793
+ return avatarPendingResult(created);
794
+ }
795
+ catch (err) {
796
+ return errorResult(err);
797
+ }
798
+ });
799
+ // -- update_avatar --------------------------------------------------------
800
+ server.registerTool('update_avatar', {
801
+ title: 'Update Avatar',
802
+ annotations: WRITE,
803
+ description: "Update an avatar and/or change its looks. Fields: name, defaultLookId (also becomes the avatar's profile photo), defaultVoiceId. Looks are changed through ops, the same shape update_timeline and update_canvas use: add_look files images the account ALREADY OWNS onto the avatar, remove_look trashes one (recoverable for 30 days). Ops run before the fields, so one call can add a look and make it the default. To GENERATE a new look instead of filing an existing image, call generate_image with avatarId.",
804
+ inputSchema: {
805
+ avatarId: z.string().describe('The avatar id from list_avatars.'),
806
+ name: z.string().optional().describe('New name, at least 3 characters.'),
807
+ defaultLookId: z
808
+ .string()
809
+ .optional()
810
+ .describe("A look id from get_avatar. Becomes the avatar's default look AND its profile photo."),
811
+ defaultVoiceId: z.string().nullable().optional().describe('A voiceId from list_voices, or null to clear it.'),
812
+ ops: z
813
+ .array(z.union([
814
+ z.object({
815
+ op: z.literal('add_look'),
816
+ imageUrls: z
817
+ .array(z.string())
818
+ .describe('Images the account already owns, as URLs: an upload, a creation, an editor export, or another avatar look. Anything not owned by this account is skipped rather than failing the call.'),
819
+ }),
820
+ z.object({
821
+ op: z.literal('remove_look'),
822
+ lookId: z.string().describe('A look id from get_avatar.'),
823
+ }),
824
+ ]))
825
+ .optional()
826
+ .describe('Look changes, applied in order before the field updates. NOT a transaction: a failure part-way leaves earlier ops applied.'),
827
+ },
828
+ }, async (args, extra) => {
829
+ try {
830
+ const client = await getClient(extra);
831
+ const { avatarId, ...request } = args;
832
+ const updated = await client.updateAvatar(avatarId, request);
833
+ return avatarResult(updated.avatar);
834
+ }
835
+ catch (err) {
836
+ return errorResult(err);
837
+ }
838
+ });
839
+ // -- delete_avatar --------------------------------------------------------
840
+ server.registerTool('delete_avatar', {
841
+ title: 'Delete Avatar',
842
+ annotations: WRITE,
843
+ description: "Delete an avatar. Soft: the avatar stops appearing, but ITS LOOKS SURVIVE as library images and can be filed onto another avatar with update_avatar's add_look. Use this to retire a duplicate or an abandoned character, after moving any looks worth keeping.",
844
+ inputSchema: {
845
+ avatarId: z.string().describe('The avatar id from list_avatars.'),
846
+ },
847
+ }, async (args, extra) => {
848
+ try {
849
+ const client = await getClient(extra);
850
+ await client.deleteAvatar(args.avatarId);
851
+ return text(`Avatar ${args.avatarId} deleted. Its looks are retained and can be filed onto another avatar with update_avatar add_look.`);
852
+ }
853
+ catch (err) {
854
+ return errorResult(err);
855
+ }
856
+ });
738
857
  // -- list_voices ----------------------------------------------------------
739
858
  server.registerTool('list_voices', {
740
859
  title: 'List Voices',
@@ -1157,11 +1276,11 @@ export function registerTools(server, opts) {
1157
1276
  annotations: READ,
1158
1277
  description: "Return the contents of one folder. The folder id is either one of the account's own folder ids or a built-in derived-folder key. A manual folder returns exactly the items filed in it; a smart folder computes its members live from its saved query; a derived folder returns its built-in set. Items are media (with kind and a description) and, in manual folders, entities such as projects or posts.",
1159
1278
  inputSchema: {
1160
- folder_id: z.string().describe('A folder id, or a derived-folder key (recents, favorites, edits, canvas, cards).'),
1279
+ folderId: z.string().describe('A folder id, or a derived-folder key (recents, favorites, edits, canvas, cards).'),
1161
1280
  },
1162
1281
  }, async (args, extra) => {
1163
1282
  try {
1164
- const r = await (await getClient(extra)).getFolder(args.folder_id);
1283
+ const r = await (await getClient(extra)).getFolder(args.folderId);
1165
1284
  return folderContentsResult(r.folder, r.items);
1166
1285
  }
1167
1286
  catch (err) {
@@ -1176,11 +1295,11 @@ export function registerTools(server, opts) {
1176
1295
  name: z.string().describe('The folder name.'),
1177
1296
  type: z.enum(['manual', 'smart']).optional().describe("'manual' (a collection you file items into) or 'smart' (a saved live query). Defaults to manual."),
1178
1297
  query: smartQuerySchema,
1179
- parent_id: z.string().optional().describe('Nest the new folder under this parent folder id.'),
1298
+ parentId: z.string().optional().describe('Nest the new folder under this parent folder id.'),
1180
1299
  },
1181
1300
  }, async (args, extra) => {
1182
1301
  try {
1183
- const f = await (await getClient(extra)).createFolder({ name: args.name, type: args.type, query: args.query, parentId: args.parent_id });
1302
+ const f = await (await getClient(extra)).createFolder({ name: args.name, type: args.type, query: args.query, parentId: args.parentId });
1184
1303
  return text(`Created ${f.type} folder "${f.name}" (id ${f.id}).`);
1185
1304
  }
1186
1305
  catch (err) {
@@ -1188,32 +1307,32 @@ export function registerTools(server, opts) {
1188
1307
  }
1189
1308
  });
1190
1309
  /**
1191
- * One item's universal identity. NO folder_id: the folder is named by the tool's own folder_id /
1192
- * folder_ids now, which is what lets one call file many items into many folders.
1310
+ * One item's universal identity. NO folderId: the folder is named by the tool's own folderId /
1311
+ * folderIds now, which is what lets one call file many items into many folders.
1193
1312
  */
1194
1313
  const itemRefBodySchema = z.object({
1195
- source_table: z.string().describe("The item's source table (e.g. as returned by search_media)."),
1196
- source_record_id: z.string().describe("The item's source record id."),
1314
+ sourceTable: z.string().describe("The item's source table (e.g. as returned by list_media / get_media)."),
1315
+ sourceRecordId: z.string().describe("The item's source record id."),
1197
1316
  variant: z.number().int().optional().describe('The variation index (default 0 for single-asset items).'),
1198
1317
  });
1199
1318
  server.registerTool('update_folder', {
1200
1319
  title: 'Update Folder',
1201
1320
  annotations: WRITE,
1202
- description: "Update the account's own folders: rename one, MOVE folders under a different parent (or to the top level with a null parent), change a smart folder's saved query, and FILE or UNFILE items. addItems/removeItems are DELTAS of { source_table, source_record_id, variant? }, not a list to replace, because an item can sit in several folders at once and a replace would silently unfile it from the others. Filing never moves or copies anything: it adds a pointer, and only manual folders accept items (a smart folder computes its own membership). Pass folderIds to patch several folders at once, which crossed with addItems files the same items into all of them; renaming and re-querying still need exactly one folder. NOTE the asymmetry: nesting a FOLDER via parentId is a MOVE (a folder has one parent), while filing an ITEM is a pointer that leaves its other folders alone.",
1321
+ description: "Update the account's own folders: rename one, MOVE folders under a different parent (or to the top level with a null parent), change a smart folder's saved query, and FILE or UNFILE items. addItems/removeItems are DELTAS of { sourceTable, sourceRecordId, variant? }, not a list to replace, because an item can sit in several folders at once and a replace would silently unfile it from the others. Filing never moves or copies anything: it adds a pointer, and only manual folders accept items (a smart folder computes its own membership). Pass folderIds to patch several folders at once, which crossed with addItems files the same items into all of them; renaming and re-querying still need exactly one folder. NOTE the asymmetry: nesting a FOLDER via parentId is a MOVE (a folder has one parent), while filing an ITEM is a pointer that leaves its other folders alone.",
1203
1322
  inputSchema: {
1204
- folder_id: z.string().describe('The folder id to update.'),
1205
- folder_ids: z
1323
+ folderId: z.string().describe('The folder id to update.'),
1324
+ folderIds: z
1206
1325
  .array(z.string())
1207
1326
  .optional()
1208
1327
  .describe('Patch several folders at once. Attribute fields (name, query) still need exactly one.'),
1209
1328
  name: z.string().optional().describe('A new name.'),
1210
- parent_id: z.string().nullable().optional().describe('A new parent folder id, or null to move to the top level. MOVES the folder.'),
1329
+ parentId: z.string().nullable().optional().describe('A new parent folder id, or null to move to the top level. MOVES the folder.'),
1211
1330
  query: smartQuerySchema,
1212
- add_items: z
1331
+ addItems: z
1213
1332
  .array(itemRefBodySchema)
1214
1333
  .optional()
1215
1334
  .describe('File these items into the folder(s). A delta: their other folders are untouched.'),
1216
- remove_items: z
1335
+ removeItems: z
1217
1336
  .array(itemRefBodySchema)
1218
1337
  .optional()
1219
1338
  .describe('Unfile these items. Only the pointer goes; the asset is never deleted.'),
@@ -1223,17 +1342,17 @@ export function registerTools(server, opts) {
1223
1342
  const client = await getClient(extra);
1224
1343
  const patch = {
1225
1344
  name: args.name,
1226
- parentId: args.parent_id,
1345
+ parentId: args.parentId,
1227
1346
  query: args.query,
1228
- addItems: args.add_items?.map((r) => ({ sourceTable: r.source_table, sourceRecordId: r.source_record_id, variant: r.variant })),
1229
- removeItems: args.remove_items?.map((r) => ({ sourceTable: r.source_table, sourceRecordId: r.source_record_id, variant: r.variant })),
1347
+ addItems: args.addItems,
1348
+ removeItems: args.removeItems,
1230
1349
  };
1231
- const targets = args.folder_ids?.length ? args.folder_ids : [args.folder_id];
1350
+ const targets = args.folderIds?.length ? args.folderIds : [args.folderId];
1232
1351
  const folders = targets.length > 1
1233
1352
  ? await client.updateFolders(targets, patch)
1234
1353
  : [await client.updateFolder(targets[0], patch)];
1235
- const filed = args.add_items?.length ?? 0;
1236
- const unfiled = args.remove_items?.length ?? 0;
1354
+ const filed = args.addItems?.length ?? 0;
1355
+ const unfiled = args.removeItems?.length ?? 0;
1237
1356
  const what = [
1238
1357
  filed ? `filed ${filed} item(s)` : null,
1239
1358
  unfiled ? `unfiled ${unfiled} item(s)` : null,
@@ -1249,11 +1368,11 @@ export function registerTools(server, opts) {
1249
1368
  title: 'Delete Folder',
1250
1369
  annotations: WRITE,
1251
1370
  description: "Delete one of the account's own folders and everything nested under it. This removes the folder structure only; the media and entities inside are pointers, so the underlying assets are never deleted. Confirm intent before deleting a folder that contains items.",
1252
- inputSchema: { folder_id: z.string().describe('The folder id to delete.') },
1371
+ inputSchema: { folderId: z.string().describe('The folder id to delete.') },
1253
1372
  }, async (args, extra) => {
1254
1373
  try {
1255
- await (await getClient(extra)).deleteFolder(args.folder_id);
1256
- return text(`Deleted folder ${args.folder_id}.`);
1374
+ await (await getClient(extra)).deleteFolder(args.folderId);
1375
+ return text(`Deleted folder ${args.folderId}.`);
1257
1376
  }
1258
1377
  catch (err) {
1259
1378
  return errorResult(err);
@@ -1462,11 +1581,11 @@ export function registerTools(server, opts) {
1462
1581
  title: 'Get Element',
1463
1582
  annotations: READ,
1464
1583
  description: "Get one saved reference element by id: its name, category, description, and images.",
1465
- inputSchema: { id: z.string().describe('The element id.') },
1584
+ inputSchema: { elementId: z.string().describe('The element id.') },
1466
1585
  }, async (args, extra) => {
1467
1586
  try {
1468
1587
  const client = await getClient(extra);
1469
- return elementResult(await client.getElement(args.id));
1588
+ return elementResult(await client.getElement(args.elementId));
1470
1589
  }
1471
1590
  catch (err) {
1472
1591
  return errorResult(err);
@@ -1505,7 +1624,7 @@ export function registerTools(server, opts) {
1505
1624
  annotations: WRITE,
1506
1625
  description: "Update a saved element's name, description, or category.",
1507
1626
  inputSchema: {
1508
- id: z.string().describe('The element id.'),
1627
+ elementId: z.string().describe('The element id.'),
1509
1628
  name: z.string().optional(),
1510
1629
  description: z.string().optional(),
1511
1630
  category: z.enum(['auto', 'character', 'location', 'prop']).optional(),
@@ -1513,7 +1632,7 @@ export function registerTools(server, opts) {
1513
1632
  }, async (args, extra) => {
1514
1633
  try {
1515
1634
  const client = await getClient(extra);
1516
- return elementResult(await client.updateElement(args.id, { name: args.name, description: args.description, category: args.category }), 'Updated');
1635
+ return elementResult(await client.updateElement(args.elementId, { name: args.name, description: args.description, category: args.category }), 'Updated');
1517
1636
  }
1518
1637
  catch (err) {
1519
1638
  return errorResult(err);
@@ -1524,12 +1643,12 @@ export function registerTools(server, opts) {
1524
1643
  title: 'Delete Element',
1525
1644
  annotations: WRITE,
1526
1645
  description: 'Delete a saved reference element.',
1527
- inputSchema: { id: z.string().describe('The element id.') },
1646
+ inputSchema: { elementId: z.string().describe('The element id.') },
1528
1647
  }, async (args, extra) => {
1529
1648
  try {
1530
1649
  const client = await getClient(extra);
1531
- await client.deleteElement(args.id);
1532
- return elementDeletedResult(args.id);
1650
+ await client.deleteElement(args.elementId);
1651
+ return elementDeletedResult(args.elementId);
1533
1652
  }
1534
1653
  catch (err) {
1535
1654
  return errorResult(err);
@@ -1585,12 +1704,16 @@ export function registerTools(server, opts) {
1585
1704
  server.registerTool('list_cards', {
1586
1705
  title: 'List Cards',
1587
1706
  annotations: READ,
1588
- description: "List the account's content-pipeline posts (newest-updated first). Filter by status, platform, stage (id/slug/name), folder, favorite, or a title search. Call get_card for one post's full detail (posts + assets).",
1707
+ description: "List one SPACE's cards (newest-updated first). ⚠️ SCOPED, NOT COMPLETE: without spaceId this lists the account's DEFAULT space only, and cards on any other board are absent with nothing in the response saying so (search misses them too). Call list_spaces FIRST and pass spaceId unless you specifically mean the default board. Filter by status, platform, stage (id/slug/name), favorite, or a title search. Call get_card for one card's full detail (posts + assets).",
1589
1708
  inputSchema: {
1709
+ spaceId: z
1710
+ .string()
1711
+ .optional()
1712
+ .describe("Which space's board to list, from list_spaces. Omit ONLY when you mean the account's default space; omitting it does not search every board."),
1590
1713
  status: z.enum(['draft', 'active', 'completed', 'archived']).optional().describe('Filter by lifecycle status.'),
1591
1714
  platform: z.enum(POST_PLATFORMS).optional().describe('Filter by the post platform.'),
1592
- stage: z.string().optional().describe('Filter by a stage id, slug, or name.'),
1593
- search: z.string().optional().describe('Case-insensitive title search.'),
1715
+ stage: z.string().optional().describe('Filter by a stage id, slug, or name. Resolved within the chosen space.'),
1716
+ search: z.string().optional().describe('Case-insensitive title search, scoped to the chosen space.'),
1594
1717
  limit: z.number().int().min(1).max(100).optional().describe('How many to return (default 50).'),
1595
1718
  offset: z.number().int().min(0).optional().describe('Pagination offset.'),
1596
1719
  },
@@ -1598,6 +1721,7 @@ export function registerTools(server, opts) {
1598
1721
  try {
1599
1722
  const client = await getClient(extra);
1600
1723
  return cardListResult(await client.listCards({
1724
+ spaceId: args.spaceId,
1601
1725
  status: args.status,
1602
1726
  platform: args.platform,
1603
1727
  stage: args.stage,
@@ -1733,11 +1857,17 @@ export function registerTools(server, opts) {
1733
1857
  server.registerTool('list_stages', {
1734
1858
  title: 'List Pipeline Stages',
1735
1859
  annotations: READ,
1736
- description: "List the account's stages, in order. Stages are user-customizable (renamed, reordered, added, removed), so call this to discover the real stages before placing a post; pass a stage's id (most stable), slug, or name to create_card / update_card.",
1737
- }, async (extra) => {
1860
+ description: "List one SPACE's stages, in order. ⚠️ STAGES ARE PER-SPACE: without spaceId this is the account's DEFAULT space, and two spaces can each hold a stage named 'Published' with different ids, so a stage name resolved against the wrong space is a different column. Stages are user-customizable (renamed, reordered, added, removed), so call this to discover the real stages before placing a card; pass a stage's id (most stable), slug, or name to create_card / update_card.",
1861
+ inputSchema: {
1862
+ spaceId: z
1863
+ .string()
1864
+ .optional()
1865
+ .describe("Which space's stages, from list_spaces. Omit only when you mean the account's default space."),
1866
+ },
1867
+ }, async (args, extra) => {
1738
1868
  try {
1739
1869
  const client = await getClient(extra);
1740
- return stageListResult(await client.listStages());
1870
+ return stageListResult(await client.listStages({ spaceId: args.spaceId }));
1741
1871
  }
1742
1872
  catch (err) {
1743
1873
  return errorResult(err);
@@ -1833,7 +1963,7 @@ export function registerTools(server, opts) {
1833
1963
  ...(posts !== undefined ? { posts: posts } : {}),
1834
1964
  ...(assets !== undefined ? { assets: assets } : {}),
1835
1965
  };
1836
- // `cardIds` widens the path id, matching update_folder's folder_id / folder_ids. One card still
1966
+ // `cardIds` widens the path id, matching update_folder's folderId / folderIds. One card still
1837
1967
  // goes through updateCard so the single-card response shape is unchanged for every caller.
1838
1968
  const targets = cardIds?.length ? cardIds : [cardId];
1839
1969
  if (targets.length > 1) {