@post-ripple/mcp 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/dist/index.js +107 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -58,6 +58,8 @@ env = { POSTRIPPLE_API_KEY = "pr_..." }
58
58
  | `list_videos` | read | Video library (status, source) |
59
59
  | `list_video_groups` / `get_video_group` | read | Rotation groups and their posting rules |
60
60
  | `list_slideshows` | read | Photo-carousel slideshows |
61
+ | `list_slideshow_templates` | read | Slideshow layout templates, fonts, and text treatments |
62
+ | `get_slideshow` | read | One slideshow with per-slide render state and progress |
61
63
  | `list_posts` | read | Posts with status, schedule, errors |
62
64
  | `get_post_metrics` | read | Per-post engagement time series |
63
65
  | `get_account_metrics` | read | Follower growth history |
@@ -67,13 +69,19 @@ env = { POSTRIPPLE_API_KEY = "pr_..." }
67
69
  | `create_video_group` | content:write | Create a rotation group |
68
70
  | `add_videos_to_group` | content:write | Fill a group's rotation |
69
71
  | `update_group_schedule` | content:write | Attach accounts / set posts-per-day and time windows |
72
+ | `get_image_upload_url` | content:write | Upload a slide image (JPEG 1080×1350 recommended) |
73
+ | `import_image_from_url` | content:write | Import a slide image from a URL (synchronous) |
74
+ | `create_slideshow` | content:write | Create a slideshow from pre-rendered images and/or template slides (server-side rendered) |
70
75
  | `schedule_post` | publish | Queue a video to publish at a future time (idempotent via `idempotencyKey`) |
76
+ | `publish_slideshow` | publish | Publish or schedule a slideshow to Instagram/TikTok (supports TikTok drafts-inbox mode) |
71
77
  | `reschedule_post` | publish | Move a scheduled post |
72
78
  | `cancel_post` | publish | Cancel a scheduled post (refunds the credit) |
73
79
 
74
80
  Notes for agents:
75
81
 
76
- - `schedule_post` publishes to a **real** social account. Always pass `idempotencyKey`, and pass `tiktokSettings` if the post should be public on TikTok (the default is private/`SELF_ONLY`).
82
+ - `schedule_post` and `publish_slideshow` publish to **real** social accounts. Always pass `idempotencyKey`, and pass `tiktokSettings` if the post should be public on TikTok (the default is private/`SELF_ONLY`).
83
+ - The typical slideshow flow: `import_image_from_url` (or `get_image_upload_url` + PUT) per image → `create_slideshow` → poll `get_slideshow` until `ready` (template slides render server-side) → `publish_slideshow`. Pre-rendered JPEG slideshows are ready instantly.
84
+ - TikTok `settings.postMode: "inbox"` sends the photo post to the account's TikTok app drafts inbox instead of publishing directly — useful when a human finishes posts from a real device.
77
85
  - Imports and publishes are asynchronous. Poll `list_videos` / `list_posts` to observe progress.
78
86
  - Every call is audit-logged and attributed to the member who created the key. Keys are org-bound; usage requires the Pro plan or higher.
79
87
 
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ function toEpochMs(value, field) {
45
45
  }
46
46
  return parsed;
47
47
  }
48
- const server = new McpServer({ name: "postripple", version: "0.2.0" });
48
+ const server = new McpServer({ name: "postripple", version: "0.3.0" });
49
49
  function register(name, description, shape, transform) {
50
50
  server.tool(name, description, shape, async (args) => {
51
51
  try {
@@ -108,8 +108,68 @@ const tiktokSettingsSchema = z
108
108
  disableStitch: z.boolean(),
109
109
  brandOrganicToggle: z.boolean(),
110
110
  brandContentToggle: z.boolean(),
111
+ autoAddMusic: z
112
+ .boolean()
113
+ .optional()
114
+ .describe("Photo (slideshow) posts only: let TikTok auto-pick background music. Defaults to true."),
115
+ postMode: z
116
+ .enum(["direct", "inbox"])
117
+ .optional()
118
+ .describe("Photo (slideshow) posts only. 'direct' (default) publishes immediately; 'inbox' sends a draft to the account's TikTok app inbox — the creator finishes it in the app, and privacy/comment/brand fields are ignored by TikTok in that mode."),
111
119
  })
112
120
  .describe("TikTok compliance settings. If omitted, the post publishes as SELF_ONLY (private).");
121
+ /** One slide for create_slideshow: EITHER a finished image (prerenderedFileId)
122
+ * OR a background + text fields rendered server-side from the template. */
123
+ const slideTextStyleSchema = z
124
+ .object({
125
+ font: z
126
+ .enum(["classic", "impact", "serif", "typewriter", "handwriting"])
127
+ .optional(),
128
+ color: z.string().optional().describe("Text color (hex)"),
129
+ treatment: z
130
+ .enum(["none", "shadow", "outline", "highlight"])
131
+ .optional()
132
+ .describe("'highlight' = solid box behind each line (classic TikTok caption)"),
133
+ accentColor: z
134
+ .string()
135
+ .optional()
136
+ .describe("Box color for 'highlight' / stroke color for 'outline' (hex)"),
137
+ fontScale: z.number().min(0.5).max(2.5).optional(),
138
+ x: z.number().min(0).max(1).optional().describe("Anchor x (0..1 of canvas width)"),
139
+ y: z.number().min(0).max(1).optional().describe("Top y (0..1 of canvas height)"),
140
+ maxWidth: z.number().min(0.2).max(1).optional().describe("Text width (fraction of canvas width)"),
141
+ align: z.enum(["left", "center", "right"]).optional(),
142
+ rotation: z.number().min(-45).max(45).optional().describe("Degrees clockwise"),
143
+ })
144
+ .describe("Optional per-field style overrides; anything omitted uses the template slot's defaults");
145
+ const slideSchema = z.object({
146
+ prerenderedFileId: z
147
+ .string()
148
+ .optional()
149
+ .describe("A finished slide image (from get_image_upload_url or import_image_from_url) used as-is. JPEG at 1080x1350 recommended — other formats are re-encoded and cover-cropped to the canvas."),
150
+ backgroundFileId: z
151
+ .string()
152
+ .optional()
153
+ .describe("Background image for a template slide; text is rendered on top server-side."),
154
+ textFields: z
155
+ .array(z.object({
156
+ key: z
157
+ .string()
158
+ .describe("A template slot key (see list_slideshow_templates) or a 'custom-' prefixed key for a free text box"),
159
+ text: z.string(),
160
+ style: slideTextStyleSchema.optional(),
161
+ }))
162
+ .optional()
163
+ .describe("Template slides only"),
164
+ overlay: z
165
+ .object({
166
+ opacity: z.number().min(0).max(1),
167
+ direction: z.enum(["none", "bottom", "top", "full"]),
168
+ color: z.string().optional().describe("Hex, default #000000"),
169
+ })
170
+ .optional()
171
+ .describe("Darkening scrim over the background (template slides only); omit to use the template's default"),
172
+ });
113
173
  // ---- read tools ----
114
174
  register("whoami", "Get who this key acts as and which organizations it can work in, plus the key's scopes. When an organization is selected (single-org key, organizationId argument, or POSTRIPPLE_ORG), also returns that workspace's overview: connected accounts, active groups, scheduled posts. Call this first to orient yourself.", { ...orgParam });
115
175
  register("list_social_accounts", "List the workspace's connected social accounts (TikTok / Instagram / YouTube) with connection health: status (active/expired/revoked/error), token expiry, and last error. Use the returned accountId for scheduling and metrics tools.", { ...orgParam });
@@ -121,6 +181,8 @@ register("list_videos", "List videos in the workspace library, newest first. Vid
121
181
  register("list_video_groups", "List video groups (rotation playlists that auto-post on a schedule) with video/account counts and status.", { ...orgParam });
122
182
  register("get_video_group", "Get one video group in detail: its videos and the per-account posting rules (posts per day, posting windows, next scheduled post).", { ...orgParam, groupId: z.string() });
123
183
  register("list_slideshows", "List photo-carousel slideshows with status ('draft' = still being composed, 'ready' = publishable).", { ...orgParam, limit: z.number().min(1).max(200).optional() });
184
+ register("list_slideshow_templates", "List the slideshow layout templates: canvas size, each template's text slots (keys, defaults), available fonts and text treatments, and the max slide count. Call before create_slideshow when composing template-based slides.", {});
185
+ register("get_slideshow", "Get one slideshow: per-slide render state and overall status, plus server-side render progress ({state, error}). Poll this after create_slideshow returns status 'rendering' — publish once status is 'ready'.", { ...orgParam, slideshowId: z.string() });
124
186
  register("list_posts", "List posts (scheduled, publishing, published, failed, canceled), newest first. Scheduled posts include scheduledFor; published posts include platformPostId and publishedAt.", {
125
187
  ...orgParam,
126
188
  status: postStatusEnum.optional(),
@@ -132,10 +194,18 @@ register("get_post_metrics", "Get engagement for one published post: latest snap
132
194
  postId: z.string(),
133
195
  limit: z.number().min(1).max(200).optional().describe("Max series points, default 50"),
134
196
  });
135
- register("get_account_metrics", "Get follower/profile growth history for one connected social account (daily snapshots).", {
197
+ register("get_account_metrics", "Get follower/profile growth history (daily snapshots) for connected social accounts. Omit accountId to get every account in the organization at once; pass it to get a single account.", {
136
198
  ...orgParam,
137
- accountId: z.string(),
138
- limit: z.number().min(1).max(200).optional(),
199
+ accountId: z
200
+ .string()
201
+ .optional()
202
+ .describe("Omit for all accounts in the organization"),
203
+ limit: z
204
+ .number()
205
+ .min(1)
206
+ .max(200)
207
+ .optional()
208
+ .describe("Max series points per account, default 50"),
139
209
  });
140
210
  register("get_engagement_summary", "Get an org-wide engagement overview for the last N days: totals, per-platform breakdown, top 5 posts by views, and current follower counts per account. The best starting point for 'how is content performing?'", { ...orgParam, days: z.number().min(1).max(90).optional().describe("Window in days, default 30") });
141
211
  // ---- content:write tools ----
@@ -170,6 +240,17 @@ register("update_group_schedule", "Attach a social account to a group or update
170
240
  status: z.enum(["active", "paused"]).optional(),
171
241
  postingWindows: z.array(postingWindowSchema).optional(),
172
242
  });
243
+ register("get_image_upload_url", "Get a presigned upload URL for pushing a slide image into storage. PUT the raw image bytes to the returned uploadUrl with a Content-Type header, then use the returned fileId in create_slideshow. JPEG at 1080x1350 (4:5) strongly recommended.", { ...orgParam });
244
+ register("import_image_from_url", "Import an image into storage from a public URL (synchronous — the returned fileId is immediately usable in create_slideshow). Accepts JPEG/PNG/WebP up to 15MB; JPEG is recommended since other formats get re-encoded and cover-cropped to the 1080x1350 canvas.", { ...orgParam, url: z.string().url() });
245
+ register("create_slideshow", "Create a photo slideshow (1-10 slides). Each slide is EITHER {prerenderedFileId} — a finished image you composed — OR {backgroundFileId, textFields, overlay} rendered server-side using templateId's layout. Pre-rendered JPEG slides are ready instantly; template slides return status 'rendering' — poll get_slideshow until 'ready', then call publish_slideshow. Requires the slideshows plan feature.", {
246
+ ...orgParam,
247
+ title: z.string().optional(),
248
+ templateId: z
249
+ .string()
250
+ .optional()
251
+ .describe("Layout template id (see list_slideshow_templates). Default 'bold-bottom'; use 'blank' for custom-only text."),
252
+ slides: z.array(slideSchema).min(1).max(10),
253
+ });
173
254
  // ---- publish tools ----
174
255
  register("schedule_post", "Schedule a completed library video to publish on a connected account at a future time. This posts to a REAL social account when the time arrives. The platform is derived from the account. Uses one 'posts' credit (refunded if canceled). ALWAYS pass idempotencyKey (any unique string) so retries can't double-post. For TikTok, pass tiktokSettings to control privacy — omitting it publishes as private (SELF_ONLY). For YouTube, pass title.", {
175
256
  ...orgParam,
@@ -195,6 +276,28 @@ register("reschedule_post", "Move a still-scheduled post to a new future time. F
195
276
  ...args,
196
277
  scheduledFor: toEpochMs(args.scheduledFor, "scheduledFor"),
197
278
  }));
279
+ register("publish_slideshow", "Publish or schedule a READY slideshow to one or more Instagram/TikTok accounts as a native carousel / photo post. This posts to REAL social accounts. Omit scheduledFor to publish immediately; pass it to schedule (uses one 'posts' credit per account, refunded if canceled). ALWAYS pass idempotencyKey so retries can't double-post. TikTok targets without settings publish as SELF_ONLY (private); pass settings with postMode 'inbox' to send a draft to the account's TikTok app inbox instead.", {
280
+ ...orgParam,
281
+ slideshowId: z.string(),
282
+ targets: z
283
+ .array(z.object({
284
+ accountId: z.string(),
285
+ settings: tiktokSettingsSchema.optional(),
286
+ }))
287
+ .min(1)
288
+ .describe("Accounts to post to. settings applies to TikTok accounts only."),
289
+ caption: z.string().optional(),
290
+ scheduledFor: scheduledForSchema.optional(),
291
+ idempotencyKey: z
292
+ .string()
293
+ .optional()
294
+ .describe("Unique string for safe retries — strongly recommended"),
295
+ }, (args) => args.scheduledFor === undefined
296
+ ? args
297
+ : {
298
+ ...args,
299
+ scheduledFor: toEpochMs(args.scheduledFor, "scheduledFor"),
300
+ });
198
301
  register("cancel_post", "Cancel a still-scheduled post and refund its credit. Fails once publishing has started.", { ...orgParam, postId: z.string() });
199
302
  const transport = new StdioServerTransport();
200
303
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@post-ripple/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server that lets AI agents (Claude Code, Codex, opencode) manage a Post Ripple workspace: content, scheduling, publishing, and analytics.",
5
5
  "type": "module",
6
6
  "bin": {