@contenthero/mcp 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -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, uploadedMediaResult, tagListResult, tagResult, tagDeletedResult, modelListResult, modelResult, platformListResult, platformResult, elementListResult, elementResult, elementDeletedResult, errorResult, generationBatchResult, outlierListResult, enhanceClipsResult, pendingResult, pipelineStageListResult, postListResult, postResult, 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, balanceResult, brandKitListResult, brandKitResult, brandKnowledgeListResult, brandKnowledgeDetailResult, brandKnowledgeSearchResult, brandKnowledgeItemResult, completedResult, connectedAccountListResult, connectedAccountResult, costResult, accountDetailResult, inspirationContentResult, mediaListResult, mediaSearchResult, folderListResult, folderContentsResult, mediaBatchResult, mediaUploadResult, 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 post or destination may target. */
42
42
  const POST_PLATFORMS = [
43
43
  'youtube',
@@ -207,6 +207,62 @@ function buildReferences(parts) {
207
207
  */
208
208
  export function registerTools(server, opts) {
209
209
  const { getClient, models } = opts;
210
+ /**
211
+ * ⚠️ THESE SHAPES ARE DECLARED, NOT LEFT AS `z.unknown()`. An array of unknown serialises to
212
+ * `{"type":"array","items":{}}`, which tells a client NOTHING about what may go inside it. The server
213
+ * accepted every shape when called directly, and Claude Desktop rejected all of them before they left,
214
+ * because a validator cannot check a value against an empty schema and a model cannot pattern an argument
215
+ * on one either. Measured 2026-08-23: 12 fields across 4 tools were advertised that way.
216
+ *
217
+ * Every entry below stays permissive at the EDGES (optional fields, free-form payload objects) because the
218
+ * server does the real validation. The point is to describe the shape, not to duplicate the rules.
219
+ */
220
+ const logoEntrySchema = z.object({
221
+ url: z.string().optional().describe('A url the kit already has.'),
222
+ outputId: z.string().optional().describe('A generation to copy in: "<id>", or "<id>-2" for variation 2.'),
223
+ name: z.string().optional(),
224
+ is_primary: z.boolean().optional().describe("Make this the kit's cover. Exactly one logo ends up primary."),
225
+ layout: z.enum(['horizontal', 'stacked', 'icon', 'wordmark']).optional(),
226
+ colorMode: z.enum(['full_color', 'light', 'dark', 'grayscale']).optional(),
227
+ });
228
+ const assetEntrySchema = z.object({
229
+ url: z.string().optional().describe('A url the kit already has.'),
230
+ outputId: z.string().optional().describe('A generation to copy in.'),
231
+ name: z.string().optional(),
232
+ });
233
+ const sectionEntrySchema = z.object({
234
+ tab: z.string().describe('The tab this section belongs to, e.g. "voice", "overview". Part of the key.'),
235
+ sectionName: z.string().describe('The section title. Part of the key.'),
236
+ sortOrder: z.number().int().optional(),
237
+ fields: z.array(z.record(z.string(), z.unknown())).optional().describe('Field objects: { key, label, type, value }.'),
238
+ });
239
+ /** An existing tracked-account id, OR a profile to add by handle/url. */
240
+ const accountEntrySchema = z.union([
241
+ z.string().describe('A tracked-account id, or a full profile url.'),
242
+ z.object({
243
+ platform: z
244
+ .enum(['youtube', 'instagram', 'facebook', 'tiktok', 'x', 'threads', 'linkedin'])
245
+ .optional()
246
+ .describe('Only needed for a bare handle; a full profile url carries its own platform.'),
247
+ handleOrUrl: z.string().describe('A profile url, or a handle when platform is given.'),
248
+ }),
249
+ ]);
250
+ const postDestinationSchema = z.object({
251
+ platform: z.enum(POST_PLATFORMS).describe('The platform. This is the KEY: one destination per platform.'),
252
+ format: z.string().optional(),
253
+ connectedAccountId: z.string().nullable().optional(),
254
+ scheduledAt: z.string().nullable().optional().describe("Per-destination override of the post's schedule."),
255
+ platformSpecificData: z.record(z.string(), z.unknown()).optional().describe('The publish payload for this platform.'),
256
+ status: z.string().optional(),
257
+ });
258
+ const postAssetSchema = z.object({
259
+ id: z.string().optional().describe('Keep an existing asset, at this position in the order.'),
260
+ assetUrl: z.string().optional().describe('A public url for a NEW asset.'),
261
+ outputId: z.string().optional().describe('A generation for a NEW asset.'),
262
+ assetType: z.string().optional().describe('Required with assetUrl; inferred from outputId.'),
263
+ displayName: z.string().optional(),
264
+ metadata: z.record(z.string(), z.unknown()).nullable().optional(),
265
+ });
210
266
  // -- generate_image -------------------------------------------------------
211
267
  server.registerTool('generate_image', {
212
268
  title: 'Generate Image',
@@ -770,18 +826,18 @@ export function registerTools(server, opts) {
770
826
  visualStyle: z.string().optional(),
771
827
  designPrinciples: z.array(z.string()).optional(),
772
828
  contentStrategy: z.record(z.string(), z.unknown()).optional().describe('Content strategy object (free-form).'),
773
- logos: z.array(z.unknown()).optional().describe("The kit's logos, each { url | outputId, name?, is_primary?, layout?, colorMode? }. Use outputId to bring in a generation."),
774
- assets: z.array(z.unknown()).optional().describe("The kit's brand assets, each { url | outputId, name? }."),
829
+ logos: z.array(logoEntrySchema).optional().describe("The kit's logos, each { url | outputId, name?, is_primary?, layout?, colorMode? }. Use outputId to bring in a generation."),
830
+ assets: z.array(assetEntrySchema).optional().describe("The kit's brand assets, each { url | outputId, name? }."),
775
831
  sections: z
776
- .array(z.unknown())
832
+ .array(sectionEntrySchema)
777
833
  .optional()
778
834
  .describe("The kit's curated sections, each { tab, sectionName, sortOrder?, fields? }. Array position is the default order."),
779
835
  brandAccounts: z
780
- .array(z.unknown())
836
+ .array(accountEntrySchema)
781
837
  .optional()
782
838
  .describe("The account owner's OWN profiles. A tracked-account id, or { platform?, handleOrUrl } to ADD one and start ingesting it."),
783
839
  inspirationAccounts: z
784
- .array(z.unknown())
840
+ .array(accountEntrySchema)
785
841
  .optional()
786
842
  .describe('Competitor/creator profiles they watch. Same entry shape as brandAccounts.'),
787
843
  },
@@ -825,19 +881,19 @@ export function registerTools(server, opts) {
825
881
  .boolean()
826
882
  .optional()
827
883
  .describe('Re-run website extraction for this kit. Returns immediately; poll extractionStatus.'),
828
- logos: z.array(z.unknown()).optional().describe('The kit\'s logos, each { url | outputId, name?, is_primary?, layout?: horizontal|stacked|icon|wordmark, colorMode?: full_color|light|dark|grayscale }. REPLACES the list; [] clears it. Exactly one ends up primary (the kit\'s cover); name none and the first wins.'),
829
- assets: z.array(z.unknown()).optional().describe('The kit\'s brand assets, each { url | outputId, name? }. REPLACES the list; [] clears it.'),
884
+ logos: z.array(logoEntrySchema).optional().describe('The kit\'s logos, each { url | outputId, name?, is_primary?, layout?: horizontal|stacked|icon|wordmark, colorMode?: full_color|light|dark|grayscale }. REPLACES the list; [] clears it. Exactly one ends up primary (the kit\'s cover); name none and the first wins.'),
885
+ assets: z.array(assetEntrySchema).optional().describe('The kit\'s brand assets, each { url | outputId, name? }. REPLACES the list; [] clears it.'),
830
886
  sections: z
831
- .array(z.unknown())
887
+ .array(sectionEntrySchema)
832
888
  .optional()
833
889
  .describe("The kit's curated sections, each { tab, sectionName, sortOrder?, fields? }. REPLACES the set, keyed by (tab, sectionName); a section left out is ARCHIVED, never deleted. Array position is the default order."),
834
890
  isDefault: z.literal(true).optional().describe('Make this the default kit, un-defaulting every other.'),
835
891
  brandAccounts: z
836
- .array(z.unknown())
892
+ .array(accountEntrySchema)
837
893
  .optional()
838
894
  .describe("The account owner's OWN profiles. Each entry is a tracked-account id, or { platform?, handleOrUrl } to ADD one and start ingesting it. REPLACES the list; [] clears it."),
839
895
  inspirationAccounts: z
840
- .array(z.unknown())
896
+ .array(accountEntrySchema)
841
897
  .optional()
842
898
  .describe('Competitor/creator profiles they watch. Same entry shape as brandAccounts. REPLACES the list; [] clears it.'),
843
899
  name: z.string().optional(),
@@ -1086,7 +1142,7 @@ export function registerTools(server, opts) {
1086
1142
  server.registerTool('list_folders', {
1087
1143
  title: 'List Folders',
1088
1144
  annotations: READ,
1089
- description: "List the account's library folders (their own manual and smart folders, as a flat list with parent links for nesting) together with the built-in derived folders (recents, favorites, edits, canvas, posts). Use this to see how the library is organized before browsing or filing items.",
1145
+ description: "List the account's library folders (their own manual and smart folders, as a flat list with parent links for nesting) together with the built-in derived folders (recents, favorites, edits, canvas, cards). Use this to see how the library is organized before browsing or filing items.",
1090
1146
  inputSchema: {},
1091
1147
  }, async (_args, extra) => {
1092
1148
  try {
@@ -1101,7 +1157,7 @@ export function registerTools(server, opts) {
1101
1157
  annotations: READ,
1102
1158
  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.",
1103
1159
  inputSchema: {
1104
- folder_id: z.string().describe('A folder id, or a derived-folder key (recents, favorites, edits, canvas, posts).'),
1160
+ folder_id: z.string().describe('A folder id, or a derived-folder key (recents, favorites, edits, canvas, cards).'),
1105
1161
  },
1106
1162
  }, async (args, extra) => {
1107
1163
  try {
@@ -1207,12 +1263,12 @@ export function registerTools(server, opts) {
1207
1263
  server.registerTool('get_media', {
1208
1264
  title: 'Get Media',
1209
1265
  annotations: READ,
1210
- description: 'SEE specific media. Pass a batch of items (up to 10) to view them at once: each item is either a { url } (e.g. a URL threaded from get_context, a layer/asset URL from get_project / get_post, or an upload URL from list_media source=uploads) or an { mediaId, variation? } (a studio output id, full or first-8; omit variation to get the primary one). Returns light metadata per item plus an IMAGE block for each image so you can actually see it. For a VIDEO, set frames (and optionally fromSec/toSec) on the item to get low-res KEYFRAMES across that source-time window, so you can watch the raw footage (judge B-roll relevance, take quality) without editing it; audio still returns metadata + the url. An mediaId without a variation returns ONLY the primary variation and lists the others; request a specific variation to see it. Use this to inspect the actual pixels, not just URLs.',
1266
+ description: 'SEE specific media. Pass a batch of items (up to 10) to view them at once: each item is either a { url } (e.g. a URL threaded from get_context, a layer/asset URL from get_project / get_card, or an upload URL from list_media source=uploads) or an { mediaId, variation? } (a studio output id, full or first-8; omit variation to get the primary one). Returns light metadata per item plus an IMAGE block for each image so you can actually see it. For a VIDEO, set frames (and optionally fromSec/toSec) on the item to get low-res KEYFRAMES across that source-time window, so you can watch the raw footage (judge B-roll relevance, take quality) without editing it; audio still returns metadata + the url. An mediaId without a variation returns ONLY the primary variation and lists the others; request a specific variation to see it. Use this to inspect the actual pixels, not just URLs.',
1211
1267
  inputSchema: {
1212
1268
  items: z
1213
1269
  .array(z.union([
1214
1270
  z.object({
1215
- url: z.string().describe('A media URL on our storage (from get_context / get_project / get_post).'),
1271
+ url: z.string().describe('A media URL on our storage (from get_context / get_project / get_card).'),
1216
1272
  fromSec: z.number().min(0).optional().describe('Video keyframes: start of the source-time window (seconds). Omit for the whole clip.'),
1217
1273
  toSec: z.number().min(0).optional().describe('Video keyframes: end of the source-time window (seconds).'),
1218
1274
  frames: z.number().int().min(1).optional().describe('Video keyframes: how many to return across the window. Set this (or fromSec/toSec) to watch the raw footage.'),
@@ -1525,11 +1581,11 @@ export function registerTools(server, opts) {
1525
1581
  return errorResult(err);
1526
1582
  }
1527
1583
  });
1528
- // -- list_posts -----------------------------------------------------------
1529
- server.registerTool('list_posts', {
1584
+ // -- list_cards -----------------------------------------------------------
1585
+ server.registerTool('list_cards', {
1530
1586
  title: 'List Posts',
1531
1587
  annotations: READ,
1532
- description: "List the account's content-pipeline posts (newest-updated first). Filter by status, platform, pipeline_stage (id/slug/name), folder, favorite, or a title search. Call get_post for one post's full detail (destinations + assets).",
1588
+ description: "List the account's content-pipeline posts (newest-updated first). Filter by status, platform, pipeline_stage (id/slug/name), folder, favorite, or a title search. Call get_card for one post's full detail (destinations + assets).",
1533
1589
  inputSchema: {
1534
1590
  status: z.enum(['draft', 'active', 'completed', 'archived']).optional().describe('Filter by lifecycle status.'),
1535
1591
  platform: z.enum(POST_PLATFORMS).optional().describe('Filter by the post platform.'),
@@ -1541,7 +1597,7 @@ export function registerTools(server, opts) {
1541
1597
  }, async (args, extra) => {
1542
1598
  try {
1543
1599
  const client = await getClient(extra);
1544
- return postListResult(await client.listPosts({
1600
+ return cardListResult(await client.listCards({
1545
1601
  status: args.status,
1546
1602
  platform: args.platform,
1547
1603
  pipelineStage: args.pipelineStage,
@@ -1554,46 +1610,147 @@ export function registerTools(server, opts) {
1554
1610
  return errorResult(err);
1555
1611
  }
1556
1612
  });
1557
- // -- get_post -------------------------------------------------------------
1558
- server.registerTool('get_post', {
1613
+ // -- get_card -------------------------------------------------------------
1614
+ server.registerTool('get_card', {
1559
1615
  title: 'Get Post',
1560
1616
  annotations: READ,
1561
1617
  description: "Get one post in full: its fields (title, description, script, notes, status, stage, schedule), plus its publish destinations and attached assets.",
1562
1618
  inputSchema: {
1563
- postId: z.string().describe('The post id from list_posts.'),
1619
+ postId: z.string().describe('The post id from list_cards.'),
1620
+ },
1621
+ }, async (args, extra) => {
1622
+ try {
1623
+ const client = await getClient(extra);
1624
+ return cardResult(await client.getCard(args.postId));
1625
+ }
1626
+ catch (err) {
1627
+ return errorResult(err);
1628
+ }
1629
+ });
1630
+ // -- list_spaces ----------------------------------------------------------
1631
+ server.registerTool('list_spaces', {
1632
+ title: 'List Spaces',
1633
+ annotations: READ,
1634
+ description: "List the account's SPACES. A space is the planner's top-level container: Space > Stage > Card > Post. Each space has its own stages, so two spaces can both hold a stage called 'Published'. Call this FIRST to discover which board to work in, then pass a space id to list_stages, list_cards or create_card. Archived spaces are excluded unless includeArchived is set.",
1635
+ inputSchema: {
1636
+ includeArchived: z
1637
+ .boolean()
1638
+ .optional()
1639
+ .describe('Include archived spaces. Default false, matching the grid in the app.'),
1640
+ },
1641
+ }, async (args, extra) => {
1642
+ try {
1643
+ const client = await getClient(extra);
1644
+ return spaceListResult(await client.listSpaces({ includeArchived: args.includeArchived }));
1645
+ }
1646
+ catch (err) {
1647
+ return errorResult(err);
1648
+ }
1649
+ });
1650
+ // -- get_space ------------------------------------------------------------
1651
+ server.registerTool('get_space', {
1652
+ title: 'Get Space',
1653
+ annotations: READ,
1654
+ description: 'Return one space with its live card count. Use list_spaces to discover ids. The count excludes archived cards, and is the same number list_spaces reports for that space.',
1655
+ inputSchema: { spaceId: z.string().describe('The space id.') },
1656
+ }, async (args, extra) => {
1657
+ try {
1658
+ const client = await getClient(extra);
1659
+ return spaceResult(await client.getSpace(args.spaceId));
1660
+ }
1661
+ catch (err) {
1662
+ return errorResult(err);
1663
+ }
1664
+ });
1665
+ // -- create_space ---------------------------------------------------------
1666
+ server.registerTool('create_space', {
1667
+ title: 'Create Space',
1668
+ annotations: WRITE,
1669
+ description: "Create a space: a new planner board with its own stages. duplicateFrom copies another space's STAGES, never its cards, so the new board arrives with the columns and none of the work. Requires the pipeline:write scope.",
1670
+ inputSchema: {
1671
+ name: z.string().describe('The space name.'),
1672
+ coverUrl: z.string().optional().describe('A cover image URL for the space tile.'),
1673
+ duplicateFrom: z
1674
+ .string()
1675
+ .optional()
1676
+ .describe("Copy this space's stages into the new one. Cards are never copied."),
1677
+ },
1678
+ }, async (args, extra) => {
1679
+ try {
1680
+ const client = await getClient(extra);
1681
+ return spaceResult(await client.createSpace({
1682
+ name: args.name,
1683
+ coverUrl: args.coverUrl,
1684
+ duplicateFrom: args.duplicateFrom,
1685
+ }));
1686
+ }
1687
+ catch (err) {
1688
+ return errorResult(err);
1689
+ }
1690
+ });
1691
+ // -- update_space ---------------------------------------------------------
1692
+ server.registerTool('update_space', {
1693
+ title: 'Update Space',
1694
+ annotations: WRITE,
1695
+ description: "Rename a space or change its cover. This is a PATCH: a field you omit is left alone, so renaming does not disturb the cover. Pass coverUrl as an empty string to REMOVE the cover. To favorite or archive a space, use the `favorite` and `archive` tools with assetType 'space' instead. Requires the pipeline:write scope.",
1696
+ inputSchema: {
1697
+ spaceId: z.string().describe('The space id to update.'),
1698
+ name: z.string().optional().describe('A new name.'),
1699
+ coverUrl: z
1700
+ .string()
1701
+ .optional()
1702
+ .describe('A new cover image URL. Pass an empty string to remove the cover entirely.'),
1564
1703
  },
1565
1704
  }, async (args, extra) => {
1566
1705
  try {
1567
1706
  const client = await getClient(extra);
1568
- return postResult(await client.getPost(args.postId));
1707
+ // An empty string is how a tool caller says "remove it": JSON Schema has no way to
1708
+ // distinguish an omitted string from an explicit null in a plain string field.
1709
+ const coverUrl = args.coverUrl === undefined ? undefined : args.coverUrl === '' ? null : args.coverUrl;
1710
+ return spaceResult(await client.updateSpace(args.spaceId, { name: args.name, coverUrl }));
1569
1711
  }
1570
1712
  catch (err) {
1571
1713
  return errorResult(err);
1572
1714
  }
1573
1715
  });
1574
- // -- list_pipeline_stages -------------------------------------------------
1575
- server.registerTool('list_pipeline_stages', {
1716
+ // -- delete_space ---------------------------------------------------------
1717
+ server.registerTool('delete_space', {
1718
+ title: 'Delete Space',
1719
+ annotations: WRITE,
1720
+ description: 'Delete a space. The server REFUSES a space that still holds cards and names the count, because the delete cascades to every card in it along with their covers, captions, destinations and schedules. Archive the space instead if you want it out of the way. Requires the pipeline:write scope.',
1721
+ inputSchema: { spaceId: z.string().describe('The space id to delete.') },
1722
+ }, async (args, extra) => {
1723
+ try {
1724
+ const client = await getClient(extra);
1725
+ await client.deleteSpace(args.spaceId);
1726
+ return spaceDeletedResult(args.spaceId);
1727
+ }
1728
+ catch (err) {
1729
+ return errorResult(err);
1730
+ }
1731
+ });
1732
+ // -- list_stages -------------------------------------------------
1733
+ server.registerTool('list_stages', {
1576
1734
  title: 'List Pipeline Stages',
1577
1735
  annotations: READ,
1578
- description: "List the account's pipeline 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_post / update_post.",
1736
+ description: "List the account's pipeline 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.",
1579
1737
  }, async (extra) => {
1580
1738
  try {
1581
1739
  const client = await getClient(extra);
1582
- return pipelineStageListResult(await client.listPipelineStages());
1740
+ return stageListResult(await client.listStages());
1583
1741
  }
1584
1742
  catch (err) {
1585
1743
  return errorResult(err);
1586
1744
  }
1587
1745
  });
1588
- // -- create_post ----------------------------------------------------------
1589
- server.registerTool('create_post', {
1746
+ // -- create_card ----------------------------------------------------------
1747
+ server.registerTool('create_card', {
1590
1748
  title: 'Create Post',
1591
1749
  annotations: WRITE,
1592
- description: "Create a content-pipeline post. The post is the container; attach platforms with add_post_destination and media with add_post_asset, then schedule_post or publish_post. `stage` accepts a stage id/slug/name (defaults to the first stage). Requires a key with the pipeline:write scope.",
1750
+ description: "Create a content-pipeline post. The post is the container; attach platforms with add_post_destination and media with add_post_asset, then schedule_post or publish_card. `stage` accepts a stage id/slug/name (defaults to the first stage). Requires a key with the pipeline:write scope.",
1593
1751
  inputSchema: {
1594
1752
  title: z.string().describe('Post title (required).'),
1595
1753
  platform: z.enum(POST_PLATFORMS).describe('Primary platform for the post.'),
1596
- description: z.string().optional().describe('Optional description / caption draft.'),
1597
1754
  stage: z.string().optional().describe('Pipeline stage id, slug, or name. Defaults to the first stage.'),
1598
1755
  coverUrl: z.string().optional().describe('Public URL for the post cover (the card thumbnail).'),
1599
1756
  coverOutputId: z
@@ -1608,10 +1765,9 @@ export function registerTools(server, opts) {
1608
1765
  }, async (args, extra) => {
1609
1766
  try {
1610
1767
  const client = await getClient(extra);
1611
- return postSummaryResult(await client.createPost({
1768
+ return postSummaryResult(await client.createCard({
1612
1769
  title: args.title,
1613
1770
  platform: args.platform,
1614
- description: args.description,
1615
1771
  stage: args.stage,
1616
1772
  coverUrl: args.coverUrl,
1617
1773
  coverOutputId: args.coverOutputId,
@@ -1622,15 +1778,14 @@ export function registerTools(server, opts) {
1622
1778
  return errorResult(err);
1623
1779
  }
1624
1780
  });
1625
- // -- update_post ----------------------------------------------------------
1626
- server.registerTool('update_post', {
1781
+ // -- update_card ----------------------------------------------------------
1782
+ server.registerTool('update_card', {
1627
1783
  title: 'Update Post',
1628
1784
  annotations: WRITE,
1629
- description: "Update a post: its fields (title, description, script, notes, status, platform, cover, pipeline stage), its DESTINATIONS (which platforms it publishes to), its ASSETS (the media on it, in order), and its SCHEDULE. destinations and assets are DECLARATIVE: pass the WHOLE set, because anything you leave out is removed. Destinations key on platform. Assets key on id, and THE ARRAY ORDER IS THE carousel ORDER, so reordering is just sending the same ids in a different order; keep an existing asset by id, add a new one by assetUrl or outputId. scheduledAt sets the time on the post AND every destination (pass null to clear); give a destination its own scheduledAt to override it for that platform. To publish NOW, use publish_post. Requires the pipeline:write scope.",
1785
+ description: "Update a post: its fields (title, description, script, notes, status, platform, cover, pipeline stage), its DESTINATIONS (which platforms it publishes to), its ASSETS (the media on it, in order), and its SCHEDULE. destinations and assets are DECLARATIVE: pass the WHOLE set, because anything you leave out is removed. Destinations key on platform. Assets key on id, and THE ARRAY ORDER IS THE carousel ORDER, so reordering is just sending the same ids in a different order; keep an existing asset by id, add a new one by assetUrl or outputId. scheduledAt sets the time on the post AND every destination (pass null to clear); give a destination its own scheduledAt to override it for that platform. To publish NOW, use publish_card. Requires the pipeline:write scope.",
1630
1786
  inputSchema: {
1631
1787
  postId: z.string().describe('The post id.'),
1632
1788
  title: z.string().optional(),
1633
- description: z.string().optional(),
1634
1789
  platform: z.enum(POST_PLATFORMS).optional(),
1635
1790
  status: z.enum(['draft', 'active', 'completed', 'archived']).optional(),
1636
1791
  stage: z.string().optional().describe('Move the post to this stage (id, slug, or name).'),
@@ -1651,11 +1806,11 @@ export function registerTools(server, opts) {
1651
1806
  .optional()
1652
1807
  .describe('ISO time to publish. Sets the post AND every destination. null clears the schedule.'),
1653
1808
  destinations: z
1654
- .array(z.unknown())
1809
+ .array(postDestinationSchema)
1655
1810
  .optional()
1656
1811
  .describe("The post's destinations, each { platform, format?, connectedAccountId?, platformSpecificData?, scheduledAt?, status? }. REPLACES the set, keyed by platform; [] detaches all."),
1657
1812
  assets: z
1658
- .array(z.unknown())
1813
+ .array(postAssetSchema)
1659
1814
  .optional()
1660
1815
  .describe("The post's assets IN ORDER, each { id } to keep an existing one or { assetUrl | outputId, assetType?, displayName? } to add. REPLACES the list; [] clears it."),
1661
1816
  },
@@ -1665,7 +1820,7 @@ export function registerTools(server, opts) {
1665
1820
  const { postId, destinations, assets, ...input } = args;
1666
1821
  // The two declarative arrays are `unknown[]` in the schema (their entries are free-form objects the
1667
1822
  // server validates), so they are cast at this one boundary rather than duplicating the shape in zod.
1668
- return postSummaryResult(await client.updatePost(postId, {
1823
+ return postSummaryResult(await client.updateCard(postId, {
1669
1824
  ...input,
1670
1825
  ...(destinations !== undefined ? { destinations: destinations } : {}),
1671
1826
  ...(assets !== undefined ? { assets: assets } : {}),
@@ -1679,7 +1834,7 @@ export function registerTools(server, opts) {
1679
1834
  server.registerTool('list_tags', {
1680
1835
  title: 'List Tags',
1681
1836
  annotations: READ,
1682
- description: "List the account's tags (the organizational tag library). Set a post's tags with the `tags` field on create_post / update_post. A tag is just a lowercase name.",
1837
+ description: "List the account's tags (the organizational tag library). Set a post's tags with the `tags` field on create_card / update_card. A tag is just a lowercase name.",
1683
1838
  inputSchema: {},
1684
1839
  }, async (extra) => {
1685
1840
  try {
@@ -1694,7 +1849,7 @@ export function registerTools(server, opts) {
1694
1849
  server.registerTool('create_tag', {
1695
1850
  title: 'Create Tag',
1696
1851
  annotations: WRITE,
1697
- description: "Create a tag in the account's tag library (the name is lowercased). Tags organize posts; apply them with the `tags` field on create_post / update_post. Requires the pipeline:write scope.",
1852
+ description: "Create a tag in the account's tag library (the name is lowercased). Tags organize posts; apply them with the `tags` field on create_card / update_card. Requires the pipeline:write scope.",
1698
1853
  inputSchema: {
1699
1854
  name: z.string().describe('The tag name (lowercased on save).'),
1700
1855
  },
@@ -1711,7 +1866,7 @@ export function registerTools(server, opts) {
1711
1866
  server.registerTool('update_tag', {
1712
1867
  title: 'Update Tag',
1713
1868
  annotations: WRITE,
1714
- description: 'Rename a tag (preserves its assignments on all posts). To detach a tag from one post, set that post\'s `tags` without it via update_post. Requires the pipeline:write scope.',
1869
+ description: 'Rename a tag (preserves its assignments on all posts). To detach a tag from one post, set that post\'s `tags` without it via update_card. Requires the pipeline:write scope.',
1715
1870
  inputSchema: {
1716
1871
  tagId: z.string().describe('The tag id (from list_tags).'),
1717
1872
  name: z.string().describe('The new tag name (lowercased on save).'),
@@ -1729,7 +1884,7 @@ export function registerTools(server, opts) {
1729
1884
  server.registerTool('delete_tag', {
1730
1885
  title: 'Delete Tag',
1731
1886
  annotations: WRITE,
1732
- description: "Delete a tag from the account's library. This DESTROYS the tag and removes it from every post it was on. To just detach a tag from one post, set that post's `tags` without it via update_post instead. Requires the pipeline:write scope.",
1887
+ description: "Delete a tag from the account's library. This DESTROYS the tag and removes it from every post it was on. To just detach a tag from one post, set that post's `tags` without it via update_card instead. Requires the pipeline:write scope.",
1733
1888
  inputSchema: {
1734
1889
  tagId: z.string().describe('The tag id (from list_tags).'),
1735
1890
  },
@@ -1742,8 +1897,8 @@ export function registerTools(server, opts) {
1742
1897
  return errorResult(err);
1743
1898
  }
1744
1899
  });
1745
- // -- publish_post ---------------------------------------------------------
1746
- server.registerTool('publish_post', {
1900
+ // -- publish_card ---------------------------------------------------------
1901
+ server.registerTool('publish_card', {
1747
1902
  title: 'Publish Post',
1748
1903
  annotations: PUBLISH,
1749
1904
  description: "Publish a post NOW to its destinations (a single platform when `platform` is given, otherwise all). Each destination must have a connected account. Requires a key with the publish:write scope; holding that scope is the account owner's consent to autonomous publishing. Returns per-destination results.",
@@ -1754,7 +1909,7 @@ export function registerTools(server, opts) {
1754
1909
  }, async (args, extra) => {
1755
1910
  try {
1756
1911
  const client = await getClient(extra);
1757
- return publishResult(await client.publishPost(args.postId, { platform: args.platform }));
1912
+ return publishResult(await client.publishCard(args.postId, { platform: args.platform }));
1758
1913
  }
1759
1914
  catch (err) {
1760
1915
  return errorResult(err);
@@ -1876,7 +2031,7 @@ export function registerTools(server, opts) {
1876
2031
  server.registerTool('list_connected_accounts', {
1877
2032
  title: 'List Connected Accounts',
1878
2033
  annotations: READ,
1879
- description: "List the social accounts the owner has connected (the publish targets), default first. Use an account's id as connectedAccountId on add_post_destination, then publish_post. Read-only: connecting an account is done in the ContentHero app.",
2034
+ description: "List the social accounts the owner has connected (the publish targets), default first. Use an account's id as connectedAccountId on add_post_destination, then publish_card. Read-only: connecting an account is done in the ContentHero app.",
1880
2035
  }, async (extra) => {
1881
2036
  try {
1882
2037
  const client = await getClient(extra);
@@ -1924,10 +2079,10 @@ export function registerTools(server, opts) {
1924
2079
  server.registerTool('favorite', {
1925
2080
  title: 'Favorite',
1926
2081
  annotations: WRITE,
1927
- description: "Favorite or UNfavorite an asset: pass favorited:false to clear it (default true). For a top-level asset, pass assetType + id (post, voice, brand_kit, project, inspiration_content, gallery, transition). To favorite a single studio media variation (one image/video/audio slot from list_media / get_media), pass the output id + variationIndex (1-based) and omit assetType. Requires the favorites:write scope. Idempotent in both directions.",
2082
+ description: "Favorite or UNfavorite an asset: pass favorited:false to clear it (default true). For a top-level asset, pass assetType + id (card, voice, brand_kit, project, inspiration_content, gallery, transition, space). To favorite a single studio media variation (one image/video/audio slot from list_media / get_media), pass the output id + variationIndex (1-based) and omit assetType. Requires the favorites:write scope. Idempotent in both directions.",
1928
2083
  inputSchema: {
1929
2084
  assetType: z
1930
- .enum(['post', 'voice', 'brand_kit', 'project', 'inspiration_content', 'gallery', 'transition'])
2085
+ .enum(['card', 'voice', 'brand_kit', 'project', 'inspiration_content', 'gallery', 'transition', 'space'])
1931
2086
  .optional()
1932
2087
  .describe('The kind of asset. Required unless targeting a media variation via variationIndex.'),
1933
2088
  id: z.string().describe('The asset id (or studio output id when using variationIndex).'),
@@ -1955,10 +2110,10 @@ export function registerTools(server, opts) {
1955
2110
  server.registerTool('archive', {
1956
2111
  title: 'Archive',
1957
2112
  annotations: WRITE,
1958
- description: "Archive or UNarchive an asset: pass archived:false to restore it (default true). ContentHero never hard-deletes, so this is always reversible. For a top-level asset, pass assetType + id (post, brand_kit, brand_kit_section, project). To archive a single studio media variation, pass the output id + variationIndex (1-based) and omit assetType. Archiving a post sets its status to 'archived'; restoring returns it to 'draft'. Requires the favorites:write scope. Idempotent in both directions.",
2113
+ description: "Archive or UNarchive an asset: pass archived:false to restore it (default true). ContentHero never hard-deletes, so this is always reversible. For a top-level asset, pass assetType + id (card, brand_kit, brand_kit_section, project, space). To archive a single studio media variation, pass the output id + variationIndex (1-based) and omit assetType. Archiving a card sets its status to 'archived'; restoring returns it to 'draft'. Requires the favorites:write scope. Idempotent in both directions.",
1959
2114
  inputSchema: {
1960
2115
  assetType: z
1961
- .enum(['post', 'brand_kit', 'brand_kit_section', 'project'])
2116
+ .enum(['card', 'brand_kit', 'brand_kit_section', 'project', 'space'])
1962
2117
  .optional()
1963
2118
  .describe('The kind of asset. Required unless targeting a media variation via variationIndex.'),
1964
2119
  id: z.string().describe('The asset id (or studio output id when using variationIndex).'),