@post-ripple/mcp 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.js +46 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ MCP server that lets AI agents (Claude Code, Codex, opencode, and any other MCP
|
|
|
10
10
|
- `publish` — schedule/cancel posts to real social accounts (off by default; enable deliberately)
|
|
11
11
|
2. Give your agent the key as the `POSTRIPPLE_API_KEY` environment variable. That's the only configuration needed.
|
|
12
12
|
|
|
13
|
+
Keys are personal: one key works in **every organization you belong to** (unless you restricted it at creation). Agents discover your organizations via `whoami` and pass `organizationId` per call — or set the optional `POSTRIPPLE_ORG` environment variable to pin a default organization (handy for pinning one brand per repo).
|
|
14
|
+
|
|
13
15
|
### Claude Code
|
|
14
16
|
|
|
15
17
|
```sh
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,9 @@ const DEFAULT_API_URL = "https://api.postripple.app";
|
|
|
6
6
|
const API_KEY = process.env.POSTRIPPLE_API_KEY;
|
|
7
7
|
// Override only for development against a non-production deployment.
|
|
8
8
|
const API_URL = (process.env.POSTRIPPLE_API_URL ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
9
|
+
// Optional default organization for keys that span several. A per-call
|
|
10
|
+
// organizationId argument always wins over this.
|
|
11
|
+
const DEFAULT_ORG = process.env.POSTRIPPLE_ORG;
|
|
9
12
|
if (!API_KEY) {
|
|
10
13
|
console.error("POSTRIPPLE_API_KEY is not set. Create a key in Post Ripple under Settings → API Keys (Pro plan and up) and set it in the MCP server env.");
|
|
11
14
|
process.exit(1);
|
|
@@ -42,11 +45,14 @@ function toEpochMs(value, field) {
|
|
|
42
45
|
}
|
|
43
46
|
return parsed;
|
|
44
47
|
}
|
|
45
|
-
const server = new McpServer({ name: "postripple", version: "0.1
|
|
48
|
+
const server = new McpServer({ name: "postripple", version: "0.2.1" });
|
|
46
49
|
function register(name, description, shape, transform) {
|
|
47
50
|
server.tool(name, description, shape, async (args) => {
|
|
48
51
|
try {
|
|
49
|
-
|
|
52
|
+
let body = transform ? transform(args) : args;
|
|
53
|
+
if (DEFAULT_ORG && body.organizationId === undefined) {
|
|
54
|
+
body = { ...body, organizationId: DEFAULT_ORG };
|
|
55
|
+
}
|
|
50
56
|
const data = await callApi(name, body);
|
|
51
57
|
return {
|
|
52
58
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
@@ -61,6 +67,14 @@ function register(name, description, shape, transform) {
|
|
|
61
67
|
}
|
|
62
68
|
});
|
|
63
69
|
}
|
|
70
|
+
// Keys are personal and can span several organizations. Every org-scoped
|
|
71
|
+
// tool accepts this; whoami lists the available organizations.
|
|
72
|
+
const orgParam = {
|
|
73
|
+
organizationId: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("Target organization id (see whoami's organizations list). Optional when the key has access to a single organization or POSTRIPPLE_ORG is set."),
|
|
77
|
+
};
|
|
64
78
|
const platformEnum = z.enum(["tiktok", "instagram", "youtube", "other"]);
|
|
65
79
|
const postStatusEnum = z.enum([
|
|
66
80
|
"scheduled",
|
|
@@ -97,50 +111,67 @@ const tiktokSettingsSchema = z
|
|
|
97
111
|
})
|
|
98
112
|
.describe("TikTok compliance settings. If omitted, the post publishes as SELF_ONLY (private).");
|
|
99
113
|
// ---- read tools ----
|
|
100
|
-
register("whoami", "Get
|
|
101
|
-
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.", {});
|
|
114
|
+
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
|
+
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 });
|
|
102
116
|
register("list_videos", "List videos in the workspace library, newest first. Videos with status 'completed' are publishable; 'pending'/'downloading' are still being imported.", {
|
|
117
|
+
...orgParam,
|
|
103
118
|
status: videoStatusEnum.optional(),
|
|
104
119
|
limit: z.number().min(1).max(200).optional().describe("Default 50, max 200"),
|
|
105
120
|
});
|
|
106
|
-
register("list_video_groups", "List video groups (rotation playlists that auto-post on a schedule) with video/account counts and status.", {});
|
|
107
|
-
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).", { groupId: z.string() });
|
|
108
|
-
register("list_slideshows", "List photo-carousel slideshows with status ('draft' = still being composed, 'ready' = publishable).", { limit: z.number().min(1).max(200).optional() });
|
|
121
|
+
register("list_video_groups", "List video groups (rotation playlists that auto-post on a schedule) with video/account counts and status.", { ...orgParam });
|
|
122
|
+
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
|
+
register("list_slideshows", "List photo-carousel slideshows with status ('draft' = still being composed, 'ready' = publishable).", { ...orgParam, limit: z.number().min(1).max(200).optional() });
|
|
109
124
|
register("list_posts", "List posts (scheduled, publishing, published, failed, canceled), newest first. Scheduled posts include scheduledFor; published posts include platformPostId and publishedAt.", {
|
|
125
|
+
...orgParam,
|
|
110
126
|
status: postStatusEnum.optional(),
|
|
111
127
|
platform: platformEnum.optional(),
|
|
112
128
|
limit: z.number().min(1).max(200).optional(),
|
|
113
129
|
});
|
|
114
130
|
register("get_post_metrics", "Get engagement for one published post: latest snapshot (views/likes/comments/shares) plus the time series of scraped metrics.", {
|
|
131
|
+
...orgParam,
|
|
115
132
|
postId: z.string(),
|
|
116
133
|
limit: z.number().min(1).max(200).optional().describe("Max series points, default 50"),
|
|
117
134
|
});
|
|
118
|
-
register("get_account_metrics", "Get follower/profile growth history for
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
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
|
+
...orgParam,
|
|
137
|
+
accountId: z
|
|
138
|
+
.string()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe("Omit for all accounts in the organization"),
|
|
141
|
+
limit: z
|
|
142
|
+
.number()
|
|
143
|
+
.min(1)
|
|
144
|
+
.max(200)
|
|
145
|
+
.optional()
|
|
146
|
+
.describe("Max series points per account, default 50"),
|
|
121
147
|
});
|
|
122
|
-
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?'", { days: z.number().min(1).max(90).optional().describe("Window in days, default 30") });
|
|
148
|
+
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") });
|
|
123
149
|
// ---- content:write tools ----
|
|
124
150
|
register("import_video_from_url", "Import a video into the library from a public URL (e.g. a TikTok/Instagram/YouTube link or a direct file URL). Download happens asynchronously — poll list_videos until its status is 'completed' before scheduling it.", {
|
|
151
|
+
...orgParam,
|
|
125
152
|
url: z.string().url(),
|
|
126
153
|
platform: platformEnum
|
|
127
154
|
.optional()
|
|
128
155
|
.describe("Source platform of the URL; helps the downloader. Defaults to 'other'."),
|
|
129
156
|
});
|
|
130
|
-
register("get_video_upload_url", "Get a presigned upload URL for pushing a local video file into the library. PUT the raw video bytes to the returned uploadUrl, then call register_uploaded_video with the returned fileId.", {});
|
|
157
|
+
register("get_video_upload_url", "Get a presigned upload URL for pushing a local video file into the library. PUT the raw video bytes to the returned uploadUrl, then call register_uploaded_video with the returned fileId.", { ...orgParam });
|
|
131
158
|
register("register_uploaded_video", "Register a video after uploading its bytes to a presigned URL from get_video_upload_url. The video becomes immediately publishable.", {
|
|
159
|
+
...orgParam,
|
|
132
160
|
fileId: z.string().describe("The fileId returned by get_video_upload_url"),
|
|
133
161
|
title: z.string(),
|
|
134
162
|
});
|
|
135
163
|
register("create_video_group", "Create a video group (a shuffle rotation playlist). Attach accounts with update_group_schedule and add videos with add_videos_to_group to start auto-posting. Uses one 'groups' credit from the plan.", {
|
|
164
|
+
...orgParam,
|
|
136
165
|
name: z.string(),
|
|
137
166
|
description: z.string().optional(),
|
|
138
167
|
});
|
|
139
168
|
register("add_videos_to_group", "Add up to 100 library videos to a group's rotation. Returns which were added and which were skipped (already present / not found).", {
|
|
169
|
+
...orgParam,
|
|
140
170
|
groupId: z.string(),
|
|
141
171
|
videoIds: z.array(z.string()).min(1).max(100),
|
|
142
172
|
});
|
|
143
173
|
register("update_group_schedule", "Attach a social account to a group or update its posting rules: posts per day (1-24), active/paused, and optional posting time windows (hours in the org's timezone). First call for an account must include postsPerDay.", {
|
|
174
|
+
...orgParam,
|
|
144
175
|
groupId: z.string(),
|
|
145
176
|
accountId: z.string(),
|
|
146
177
|
postsPerDay: z.number().min(1).max(24).optional(),
|
|
@@ -149,6 +180,7 @@ register("update_group_schedule", "Attach a social account to a group or update
|
|
|
149
180
|
});
|
|
150
181
|
// ---- publish tools ----
|
|
151
182
|
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.", {
|
|
183
|
+
...orgParam,
|
|
152
184
|
videoId: z.string(),
|
|
153
185
|
accountId: z.string(),
|
|
154
186
|
scheduledFor: scheduledForSchema,
|
|
@@ -164,13 +196,14 @@ register("schedule_post", "Schedule a completed library video to publish on a co
|
|
|
164
196
|
scheduledFor: toEpochMs(args.scheduledFor, "scheduledFor"),
|
|
165
197
|
}));
|
|
166
198
|
register("reschedule_post", "Move a still-scheduled post to a new future time. Fails once publishing has started.", {
|
|
199
|
+
...orgParam,
|
|
167
200
|
postId: z.string(),
|
|
168
201
|
scheduledFor: scheduledForSchema,
|
|
169
202
|
}, (args) => ({
|
|
170
203
|
...args,
|
|
171
204
|
scheduledFor: toEpochMs(args.scheduledFor, "scheduledFor"),
|
|
172
205
|
}));
|
|
173
|
-
register("cancel_post", "Cancel a still-scheduled post and refund its credit. Fails once publishing has started.", { postId: z.string() });
|
|
206
|
+
register("cancel_post", "Cancel a still-scheduled post and refund its credit. Fails once publishing has started.", { ...orgParam, postId: z.string() });
|
|
174
207
|
const transport = new StdioServerTransport();
|
|
175
208
|
await server.connect(transport);
|
|
176
209
|
console.error(`Post Ripple MCP server connected (${API_URL})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@post-ripple/mcp",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|