posterly-mcp-server 0.28.0 → 0.30.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/README.md +2 -1
- package/dist/index.js +10 -0
- package/dist/lib/api-client.d.ts +22 -1
- package/dist/lib/api-client.js +3 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/create-post.js +11 -3
- package/dist/tools/generate-image.d.ts +4 -4
- package/dist/tools/generate-image.js +3 -3
- package/dist/tools/get-credits.d.ts +8 -0
- package/dist/tools/get-credits.js +33 -0
- package/dist/tools/update-post.d.ts +25 -0
- package/dist/tools/update-post.js +9 -1
- package/package.json +3 -2
- package/scripts/test-payload-builders.mjs +87 -0
- package/scripts/test-tool-annotations.mjs +1 -1
- package/server.json +2 -2
- package/src/index.ts +16 -0
- package/src/lib/api-client.ts +19 -1
- package/src/lib/version.ts +1 -1
- package/src/tools/create-post.ts +13 -3
- package/src/tools/generate-image.ts +4 -4
- package/src/tools/get-credits.ts +38 -0
- package/src/tools/update-post.ts +9 -1
- package/tool-annotations.json +3 -0
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ Add the same server definition to your Cursor MCP settings:
|
|
|
122
122
|
|
|
123
123
|
## Available tools
|
|
124
124
|
|
|
125
|
-
`posterly-mcp-server@0.
|
|
125
|
+
`posterly-mcp-server@0.30.0` exposes 76 tools.
|
|
126
126
|
|
|
127
127
|
Public setup tools work before `POSTERLY_API_KEY` exists:
|
|
128
128
|
|
|
@@ -180,6 +180,7 @@ Authenticated tools require `POSTERLY_API_KEY`:
|
|
|
180
180
|
- `get_video_options`
|
|
181
181
|
- `run_video_function` (read-only Veo helpers for cost estimation and request validation)
|
|
182
182
|
- `generate_video` (queues a cost-guarded Veo video job)
|
|
183
|
+
- `get_credits` (read-only AI credit balance)
|
|
183
184
|
- `get_video_job` (poll one job or list recent jobs)
|
|
184
185
|
- `get_account_analytics`
|
|
185
186
|
- `get_post_analytics`
|
package/dist/index.js
CHANGED
|
@@ -70,6 +70,7 @@ import { createConnectSessionTool } from './tools/create-connect-session.js';
|
|
|
70
70
|
import { getConnectSessionTool } from './tools/get-connect-session.js';
|
|
71
71
|
import { createApiKeyTool } from './tools/create-api-key.js';
|
|
72
72
|
import { deleteApiKeyTool } from './tools/delete-api-key.js';
|
|
73
|
+
import { getCreditsTool } from './tools/get-credits.js';
|
|
73
74
|
import { getSubscriptionTool } from './tools/get-subscription.js';
|
|
74
75
|
import { cancelSubscriptionTool } from './tools/cancel-subscription.js';
|
|
75
76
|
import { pauseSubscriptionTool } from './tools/pause-subscription.js';
|
|
@@ -193,6 +194,15 @@ server.tool(deleteApiKeyTool.name, deleteApiKeyTool.description, deleteApiKeyToo
|
|
|
193
194
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
194
195
|
}
|
|
195
196
|
});
|
|
197
|
+
server.tool(getCreditsTool.name, getCreditsTool.description, getCreditsTool.inputSchema.shape, getToolAnnotations(getCreditsTool.name), async () => {
|
|
198
|
+
try {
|
|
199
|
+
const text = await getCreditsTool.execute(client);
|
|
200
|
+
return { content: [{ type: 'text', text }] };
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
204
|
+
}
|
|
205
|
+
});
|
|
196
206
|
server.tool(getSubscriptionTool.name, getSubscriptionTool.description, getSubscriptionTool.inputSchema.shape, getToolAnnotations(getSubscriptionTool.name), async () => {
|
|
197
207
|
try {
|
|
198
208
|
const text = await getSubscriptionTool.execute(client);
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -331,6 +331,26 @@ export type SubscriptionSummary = {
|
|
|
331
331
|
export type SubscriptionSummaryResponse = {
|
|
332
332
|
subscription: SubscriptionSummary;
|
|
333
333
|
};
|
|
334
|
+
export type CreditsSummaryResponse = {
|
|
335
|
+
credits: {
|
|
336
|
+
available: number;
|
|
337
|
+
included: {
|
|
338
|
+
monthly: number;
|
|
339
|
+
used: number;
|
|
340
|
+
remaining: number;
|
|
341
|
+
};
|
|
342
|
+
purchased_balance: number;
|
|
343
|
+
plan_tier: string;
|
|
344
|
+
is_trial: boolean;
|
|
345
|
+
period_start: string;
|
|
346
|
+
/** Null while trialing or on a plan with no included pool. */
|
|
347
|
+
resets_at: string | null;
|
|
348
|
+
workspace: {
|
|
349
|
+
role: string;
|
|
350
|
+
shared_wallet: boolean;
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
};
|
|
334
354
|
export type CancelSubscriptionPayload = {
|
|
335
355
|
reason: string;
|
|
336
356
|
feedback?: string;
|
|
@@ -791,6 +811,7 @@ export declare class PosterlyClient {
|
|
|
791
811
|
confirm: true;
|
|
792
812
|
}): Promise<DeleteApiKeyResponse>;
|
|
793
813
|
getSubscription(): Promise<SubscriptionSummaryResponse>;
|
|
814
|
+
getCredits(): Promise<CreditsSummaryResponse>;
|
|
794
815
|
cancelSubscription(data: CancelSubscriptionPayload): Promise<CancelSubscriptionResponse>;
|
|
795
816
|
pauseSubscription(): Promise<PauseSubscriptionResponse>;
|
|
796
817
|
resumeSubscription(): Promise<ResumeSubscriptionResponse>;
|
|
@@ -928,7 +949,7 @@ export declare class PosterlyClient {
|
|
|
928
949
|
aspect_ratio?: string;
|
|
929
950
|
style?: string;
|
|
930
951
|
variations?: number;
|
|
931
|
-
model?: 'flash' | 'pro';
|
|
952
|
+
model?: 'lite' | 'flash' | 'pro';
|
|
932
953
|
resolution?: '512' | '1K' | '2K' | '4K';
|
|
933
954
|
}): Promise<{
|
|
934
955
|
urls: string[];
|
package/dist/lib/api-client.js
CHANGED
|
@@ -125,6 +125,9 @@ export class PosterlyClient {
|
|
|
125
125
|
async getSubscription() {
|
|
126
126
|
return this.request('GET', '/subscription');
|
|
127
127
|
}
|
|
128
|
+
async getCredits() {
|
|
129
|
+
return this.request('GET', '/credits');
|
|
130
|
+
}
|
|
128
131
|
async cancelSubscription(data) {
|
|
129
132
|
return this.request('POST', '/subscription/cancel', data);
|
|
130
133
|
}
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const POSTERLY_MCP_VERSION = "0.
|
|
1
|
+
export declare const POSTERLY_MCP_VERSION = "0.30.0";
|
package/dist/lib/version.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// tool set (minus the intentional pre-auth signup tools that only this stdio
|
|
6
6
|
// package exposes). `npm run check:mcp-parity` enforces both the version match
|
|
7
7
|
// and the tool-list match, and runs in the pre-commit hook.
|
|
8
|
-
export const POSTERLY_MCP_VERSION = '0.
|
|
8
|
+
export const POSTERLY_MCP_VERSION = '0.30.0';
|
|
@@ -151,12 +151,20 @@ export function buildCreatePostPayload(input) {
|
|
|
151
151
|
},
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// Mirror the server rule (lib/api-v1-post-create.ts): a post needs a caption
|
|
155
|
+
// OR media, not a caption unconditionally. Requiring one here made every
|
|
156
|
+
// media-only post impossible from the npm client, most visibly Instagram and
|
|
157
|
+
// Facebook Stories, which carry no caption at all. The dashboard composer and
|
|
158
|
+
// the hosted MCP have always allowed it; only this package disagreed.
|
|
159
|
+
const hasMedia = Boolean(input.media_url) || (input.media_urls?.length ?? 0) > 0;
|
|
160
|
+
if (!caption && !hasMedia) {
|
|
161
|
+
throw new Error('Provide caption or media (or pass `thread_posts` for an X/Threads thread). Media-only posts are valid, for example an Instagram or Facebook Story.');
|
|
156
162
|
}
|
|
157
163
|
return {
|
|
158
164
|
...rest,
|
|
159
|
-
caption
|
|
165
|
+
// The server treats a missing caption as empty content when media carries
|
|
166
|
+
// the post, so send '' rather than dropping the key.
|
|
167
|
+
caption: caption ?? '',
|
|
160
168
|
post_type,
|
|
161
169
|
...(platform_settings || instagram_settings
|
|
162
170
|
? { settings: { ...(instagram_settings ? { __type: 'instagram', ...instagram_settings } : {}), ...(platform_settings || {}) } }
|
|
@@ -8,7 +8,7 @@ export declare const generateImageTool: {
|
|
|
8
8
|
aspect_ratio: z.ZodOptional<z.ZodEnum<["1:1", "9:16", "4:5", "3:4", "2:3", "1:4", "1:8", "21:9", "16:9", "4:3", "3:2", "4:1", "8:1", "5:4"]>>;
|
|
9
9
|
style: z.ZodOptional<z.ZodEnum<["photographic", "illustration", "minimal", "vibrant", "professional", "youtube_thumbnail", "reel_cover", "review_background"]>>;
|
|
10
10
|
variations: z.ZodOptional<z.ZodNumber>;
|
|
11
|
-
model: z.ZodOptional<z.ZodEnum<["flash", "pro"]>>;
|
|
11
|
+
model: z.ZodOptional<z.ZodEnum<["lite", "flash", "pro"]>>;
|
|
12
12
|
resolution: z.ZodOptional<z.ZodEnum<["512", "1K", "2K", "4K"]>>;
|
|
13
13
|
confirm: z.ZodLiteral<true>;
|
|
14
14
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -17,7 +17,7 @@ export declare const generateImageTool: {
|
|
|
17
17
|
aspect_ratio?: "16:9" | "9:16" | "1:1" | "4:5" | "3:4" | "2:3" | "1:4" | "1:8" | "21:9" | "4:3" | "3:2" | "4:1" | "8:1" | "5:4" | undefined;
|
|
18
18
|
style?: "minimal" | "professional" | "photographic" | "illustration" | "vibrant" | "youtube_thumbnail" | "reel_cover" | "review_background" | undefined;
|
|
19
19
|
variations?: number | undefined;
|
|
20
|
-
model?: "pro" | "flash" | undefined;
|
|
20
|
+
model?: "pro" | "lite" | "flash" | undefined;
|
|
21
21
|
resolution?: "512" | "1K" | "2K" | "4K" | undefined;
|
|
22
22
|
}, {
|
|
23
23
|
confirm: true;
|
|
@@ -25,7 +25,7 @@ export declare const generateImageTool: {
|
|
|
25
25
|
aspect_ratio?: "16:9" | "9:16" | "1:1" | "4:5" | "3:4" | "2:3" | "1:4" | "1:8" | "21:9" | "4:3" | "3:2" | "4:1" | "8:1" | "5:4" | undefined;
|
|
26
26
|
style?: "minimal" | "professional" | "photographic" | "illustration" | "vibrant" | "youtube_thumbnail" | "reel_cover" | "review_background" | undefined;
|
|
27
27
|
variations?: number | undefined;
|
|
28
|
-
model?: "pro" | "flash" | undefined;
|
|
28
|
+
model?: "pro" | "lite" | "flash" | undefined;
|
|
29
29
|
resolution?: "512" | "1K" | "2K" | "4K" | undefined;
|
|
30
30
|
}>;
|
|
31
31
|
execute(client: PosterlyClient, input: {
|
|
@@ -33,7 +33,7 @@ export declare const generateImageTool: {
|
|
|
33
33
|
aspect_ratio?: string;
|
|
34
34
|
style?: string;
|
|
35
35
|
variations?: number;
|
|
36
|
-
model?: "flash" | "pro";
|
|
36
|
+
model?: "lite" | "flash" | "pro";
|
|
37
37
|
resolution?: "512" | "1K" | "2K" | "4K";
|
|
38
38
|
confirm: true;
|
|
39
39
|
}): Promise<string>;
|
|
@@ -38,13 +38,13 @@ export const generateImageTool = {
|
|
|
38
38
|
.optional()
|
|
39
39
|
.describe('How many variations to generate. Default 1. Each variation costs credits separately - prefer 1 unless the user explicitly wants options.'),
|
|
40
40
|
model: z
|
|
41
|
-
.enum(['flash', 'pro'])
|
|
41
|
+
.enum(['lite', 'flash', 'pro'])
|
|
42
42
|
.optional()
|
|
43
|
-
.describe('flash (default,
|
|
43
|
+
.describe('lite (8 credits, 1K only) is the cheapest and suits drafts and exploration. flash (default, 15 credits at 1K) balances quality and cost. pro (30 credits at 1K) is highest quality for hero imagery. Start with lite or flash unless quality is critical.'),
|
|
44
44
|
resolution: z
|
|
45
45
|
.enum(['512', '1K', '2K', '4K'])
|
|
46
46
|
.optional()
|
|
47
|
-
.describe('Output resolution. Default 1K. Higher resolutions cost more credits. 512 is flash-only.'),
|
|
47
|
+
.describe('Output resolution. Default 1K. Higher resolutions cost more credits (flash: 10/15/23/35 for 512/1K/2K/4K; pro: 30 at 1K-2K, 55 at 4K). 512 is flash-only and lite supports 1K only.'),
|
|
48
48
|
confirm: z.literal(true).describe('Must be true after explicit user confirmation of subject, style, aspect ratio, and credit use.'),
|
|
49
49
|
}),
|
|
50
50
|
async execute(client, input) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const getCreditsTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
7
|
+
execute(client: PosterlyClient): Promise<string>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatDateTime, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
|
|
3
|
+
function formatCredits(result) {
|
|
4
|
+
const c = result.credits;
|
|
5
|
+
const inc = c.included;
|
|
6
|
+
return [
|
|
7
|
+
mdTitle('posterly AI credits'),
|
|
8
|
+
mdSection('Balance', mdKeyValue([
|
|
9
|
+
['Available now', String(c.available)],
|
|
10
|
+
['Included this period', `${inc.remaining} of ${inc.monthly} left (${inc.used} used)`],
|
|
11
|
+
['Purchased packs', String(c.purchased_balance)],
|
|
12
|
+
])),
|
|
13
|
+
mdSection('Plan', mdKeyValue([
|
|
14
|
+
['Tier', c.plan_tier || 'unknown'],
|
|
15
|
+
['Trial', c.is_trial ? 'yes' : 'no'],
|
|
16
|
+
[
|
|
17
|
+
'Included pool resets',
|
|
18
|
+
c.resets_at ? formatDateTime(c.resets_at) : 'no monthly reset (trial or no included pool)',
|
|
19
|
+
],
|
|
20
|
+
['Your workspace role', c.workspace?.role || 'unknown'],
|
|
21
|
+
])),
|
|
22
|
+
"Credits are a shared workspace wallet: every active member draws the same pool, and packs are added to the billing owner's balance.",
|
|
23
|
+
].join('\n\n');
|
|
24
|
+
}
|
|
25
|
+
export const getCreditsTool = {
|
|
26
|
+
name: 'get_credits',
|
|
27
|
+
description: "Get the authenticated user's posterly AI credit balance: what is available to spend now, how much of the monthly included allowance is left, purchased pack balance, plan tier, and when the included pool next resets. Credits are a shared WORKSPACE wallet, so this reports the balance the caller can actually spend, not a personal figure. Read-only, spends nothing. Requires the billing:read scope.",
|
|
28
|
+
inputSchema: z.object({}),
|
|
29
|
+
async execute(client) {
|
|
30
|
+
const result = await client.getCredits();
|
|
31
|
+
return formatCredits(result);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -34,10 +34,12 @@ declare const instagramSettingsSchema: z.ZodObject<{
|
|
|
34
34
|
first_comment: z.ZodOptional<z.ZodString>;
|
|
35
35
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
36
36
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
37
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
37
38
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
38
39
|
}, "strip", z.ZodTypeAny, {
|
|
39
40
|
post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
|
|
40
41
|
reel_cover_url?: string | undefined;
|
|
42
|
+
reel_cover_method?: string | undefined;
|
|
41
43
|
reel_thumb_offset?: number | undefined;
|
|
42
44
|
__type?: "instagram" | "instagram-standalone" | undefined;
|
|
43
45
|
is_trial_reel?: boolean | undefined;
|
|
@@ -57,6 +59,7 @@ declare const instagramSettingsSchema: z.ZodObject<{
|
|
|
57
59
|
}, {
|
|
58
60
|
post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
|
|
59
61
|
reel_cover_url?: string | undefined;
|
|
62
|
+
reel_cover_method?: string | undefined;
|
|
60
63
|
reel_thumb_offset?: number | undefined;
|
|
61
64
|
__type?: "instagram" | "instagram-standalone" | undefined;
|
|
62
65
|
is_trial_reel?: boolean | undefined;
|
|
@@ -80,6 +83,7 @@ declare const platformSettingsSchema: z.ZodObject<{
|
|
|
80
83
|
content_type: z.ZodOptional<z.ZodString>;
|
|
81
84
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
82
85
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
86
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
83
87
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
84
88
|
reply_settings: z.ZodOptional<z.ZodEnum<["everyone", "following", "mentionedUsers", "subscribers", "verified"]>>;
|
|
85
89
|
poll: z.ZodOptional<z.ZodObject<{
|
|
@@ -156,6 +160,7 @@ declare const platformSettingsSchema: z.ZodObject<{
|
|
|
156
160
|
content_type: z.ZodOptional<z.ZodString>;
|
|
157
161
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
158
162
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
163
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
159
164
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
160
165
|
reply_settings: z.ZodOptional<z.ZodEnum<["everyone", "following", "mentionedUsers", "subscribers", "verified"]>>;
|
|
161
166
|
poll: z.ZodOptional<z.ZodObject<{
|
|
@@ -232,6 +237,7 @@ declare const platformSettingsSchema: z.ZodObject<{
|
|
|
232
237
|
content_type: z.ZodOptional<z.ZodString>;
|
|
233
238
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
234
239
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
240
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
235
241
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
236
242
|
reply_settings: z.ZodOptional<z.ZodEnum<["everyone", "following", "mentionedUsers", "subscribers", "verified"]>>;
|
|
237
243
|
poll: z.ZodOptional<z.ZodObject<{
|
|
@@ -313,6 +319,9 @@ export declare const updatePostTool: {
|
|
|
313
319
|
media_url: z.ZodOptional<z.ZodString>;
|
|
314
320
|
media_urls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
315
321
|
post_type: z.ZodOptional<z.ZodString>;
|
|
322
|
+
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
323
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
324
|
+
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
316
325
|
instagram_settings: z.ZodOptional<z.ZodObject<{
|
|
317
326
|
__type: z.ZodOptional<z.ZodEnum<["instagram", "instagram-standalone"]>>;
|
|
318
327
|
post_type: z.ZodOptional<z.ZodEnum<["post", "feed", "story", "reel", "carousel"]>>;
|
|
@@ -347,10 +356,12 @@ export declare const updatePostTool: {
|
|
|
347
356
|
first_comment: z.ZodOptional<z.ZodString>;
|
|
348
357
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
349
358
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
359
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
350
360
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
351
361
|
}, "strip", z.ZodTypeAny, {
|
|
352
362
|
post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
|
|
353
363
|
reel_cover_url?: string | undefined;
|
|
364
|
+
reel_cover_method?: string | undefined;
|
|
354
365
|
reel_thumb_offset?: number | undefined;
|
|
355
366
|
__type?: "instagram" | "instagram-standalone" | undefined;
|
|
356
367
|
is_trial_reel?: boolean | undefined;
|
|
@@ -370,6 +381,7 @@ export declare const updatePostTool: {
|
|
|
370
381
|
}, {
|
|
371
382
|
post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
|
|
372
383
|
reel_cover_url?: string | undefined;
|
|
384
|
+
reel_cover_method?: string | undefined;
|
|
373
385
|
reel_thumb_offset?: number | undefined;
|
|
374
386
|
__type?: "instagram" | "instagram-standalone" | undefined;
|
|
375
387
|
is_trial_reel?: boolean | undefined;
|
|
@@ -393,6 +405,7 @@ export declare const updatePostTool: {
|
|
|
393
405
|
content_type: z.ZodOptional<z.ZodString>;
|
|
394
406
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
395
407
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
408
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
396
409
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
397
410
|
reply_settings: z.ZodOptional<z.ZodEnum<["everyone", "following", "mentionedUsers", "subscribers", "verified"]>>;
|
|
398
411
|
poll: z.ZodOptional<z.ZodObject<{
|
|
@@ -469,6 +482,7 @@ export declare const updatePostTool: {
|
|
|
469
482
|
content_type: z.ZodOptional<z.ZodString>;
|
|
470
483
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
471
484
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
485
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
472
486
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
473
487
|
reply_settings: z.ZodOptional<z.ZodEnum<["everyone", "following", "mentionedUsers", "subscribers", "verified"]>>;
|
|
474
488
|
poll: z.ZodOptional<z.ZodObject<{
|
|
@@ -545,6 +559,7 @@ export declare const updatePostTool: {
|
|
|
545
559
|
content_type: z.ZodOptional<z.ZodString>;
|
|
546
560
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
547
561
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
562
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
548
563
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
549
564
|
reply_settings: z.ZodOptional<z.ZodEnum<["everyone", "following", "mentionedUsers", "subscribers", "verified"]>>;
|
|
550
565
|
poll: z.ZodOptional<z.ZodObject<{
|
|
@@ -624,10 +639,14 @@ export declare const updatePostTool: {
|
|
|
624
639
|
media_url?: string | undefined;
|
|
625
640
|
media_urls?: string[] | undefined;
|
|
626
641
|
post_type?: string | undefined;
|
|
642
|
+
reel_cover_url?: string | undefined;
|
|
643
|
+
reel_cover_method?: string | undefined;
|
|
644
|
+
reel_thumb_offset?: number | undefined;
|
|
627
645
|
caption?: string | undefined;
|
|
628
646
|
instagram_settings?: {
|
|
629
647
|
post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
|
|
630
648
|
reel_cover_url?: string | undefined;
|
|
649
|
+
reel_cover_method?: string | undefined;
|
|
631
650
|
reel_thumb_offset?: number | undefined;
|
|
632
651
|
__type?: "instagram" | "instagram-standalone" | undefined;
|
|
633
652
|
is_trial_reel?: boolean | undefined;
|
|
@@ -651,6 +670,7 @@ export declare const updatePostTool: {
|
|
|
651
670
|
content_type: z.ZodOptional<z.ZodString>;
|
|
652
671
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
653
672
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
673
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
654
674
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
655
675
|
reply_settings: z.ZodOptional<z.ZodEnum<["everyone", "following", "mentionedUsers", "subscribers", "verified"]>>;
|
|
656
676
|
poll: z.ZodOptional<z.ZodObject<{
|
|
@@ -729,10 +749,14 @@ export declare const updatePostTool: {
|
|
|
729
749
|
media_url?: string | undefined;
|
|
730
750
|
media_urls?: string[] | undefined;
|
|
731
751
|
post_type?: string | undefined;
|
|
752
|
+
reel_cover_url?: string | undefined;
|
|
753
|
+
reel_cover_method?: string | undefined;
|
|
754
|
+
reel_thumb_offset?: number | undefined;
|
|
732
755
|
caption?: string | undefined;
|
|
733
756
|
instagram_settings?: {
|
|
734
757
|
post_type?: "post" | "feed" | "story" | "reel" | "carousel" | undefined;
|
|
735
758
|
reel_cover_url?: string | undefined;
|
|
759
|
+
reel_cover_method?: string | undefined;
|
|
736
760
|
reel_thumb_offset?: number | undefined;
|
|
737
761
|
__type?: "instagram" | "instagram-standalone" | undefined;
|
|
738
762
|
is_trial_reel?: boolean | undefined;
|
|
@@ -756,6 +780,7 @@ export declare const updatePostTool: {
|
|
|
756
780
|
content_type: z.ZodOptional<z.ZodString>;
|
|
757
781
|
media_alt_texts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
758
782
|
reel_cover_url: z.ZodOptional<z.ZodString>;
|
|
783
|
+
reel_cover_method: z.ZodOptional<z.ZodString>;
|
|
759
784
|
reel_thumb_offset: z.ZodOptional<z.ZodNumber>;
|
|
760
785
|
reply_settings: z.ZodOptional<z.ZodEnum<["everyone", "following", "mentionedUsers", "subscribers", "verified"]>>;
|
|
761
786
|
poll: z.ZodOptional<z.ZodObject<{
|
|
@@ -14,6 +14,7 @@ const instagramSettingsSchema = z.object({
|
|
|
14
14
|
first_comment: z.string().optional(),
|
|
15
15
|
media_alt_texts: z.record(z.string(), z.string()).optional(),
|
|
16
16
|
reel_cover_url: z.string().optional(),
|
|
17
|
+
reel_cover_method: z.string().optional(),
|
|
17
18
|
reel_thumb_offset: z.number().nonnegative().optional(),
|
|
18
19
|
});
|
|
19
20
|
const platformSettingsSchema = z.object({
|
|
@@ -22,6 +23,7 @@ const platformSettingsSchema = z.object({
|
|
|
22
23
|
content_type: z.string().optional(),
|
|
23
24
|
media_alt_texts: z.record(z.string(), z.string()).optional(),
|
|
24
25
|
reel_cover_url: z.string().optional(),
|
|
26
|
+
reel_cover_method: z.string().optional(),
|
|
25
27
|
reel_thumb_offset: z.number().nonnegative().optional(),
|
|
26
28
|
reply_settings: z.enum(['everyone', 'following', 'mentionedUsers', 'subscribers', 'verified']).optional(),
|
|
27
29
|
poll: z.object({ options: z.array(z.string()), duration_minutes: z.number().positive().optional(), question: z.string().optional(), duration: z.enum(['ONE_DAY', 'THREE_DAYS', 'SEVEN_DAYS', 'FOURTEEN_DAYS']).optional(), allows_multiple_answers: z.boolean().optional() }).optional(),
|
|
@@ -83,9 +85,15 @@ export const updatePostTool = {
|
|
|
83
85
|
.string()
|
|
84
86
|
.optional()
|
|
85
87
|
.describe('New post type: text, image, video, carousel, reel, story, story_series, document, photo, or cover_photo'),
|
|
88
|
+
// Reel cover controls are also accepted top-level, matching the hosted
|
|
89
|
+
// endpoint. An agent reading the hosted schema will reach for these names,
|
|
90
|
+
// and scripts/check-mcp-schema-parity.ts enforces that both surfaces agree.
|
|
91
|
+
reel_cover_url: z.string().optional().describe('Cover image URL for a reel.'),
|
|
92
|
+
reel_cover_method: z.string().optional().describe('How the reel cover is chosen, e.g. an uploaded image or a frame offset.'),
|
|
93
|
+
reel_thumb_offset: z.number().nonnegative().optional().describe('Frame offset in milliseconds to use as the reel cover.'),
|
|
86
94
|
instagram_settings: instagramSettingsSchema
|
|
87
95
|
.optional()
|
|
88
|
-
.describe('Instagram-specific updates: post_type, collaborators, user_tags, first_comment, media_alt_texts, is_trial_reel, graduation_strategy (defaults to MANUAL), reel_cover_url, reel_thumb_offset.'),
|
|
96
|
+
.describe('Instagram-specific updates: post_type, collaborators, user_tags, first_comment, media_alt_texts, is_trial_reel, graduation_strategy (defaults to MANUAL), reel_cover_url, reel_cover_method, reel_thumb_offset.'),
|
|
89
97
|
platform_settings: platformSettingsSchema
|
|
90
98
|
.optional()
|
|
91
99
|
.describe('Platform-specific settings to merge into the post. Prefer this over raw metadata.'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"mcpName": "io.github.awpthorp/posterly",
|
|
5
5
|
"description": "MCP server for posterly: schedule and publish social media posts across 18 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"build": "tsc",
|
|
33
33
|
"dev": "tsc --watch",
|
|
34
34
|
"start": "node dist/index.js",
|
|
35
|
-
"test:annotations": "npm run build && node scripts/test-tool-annotations.mjs"
|
|
35
|
+
"test:annotations": "npm run build && node scripts/test-tool-annotations.mjs",
|
|
36
|
+
"test:payloads": "npm run build && node scripts/test-payload-builders.mjs"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavioural tests for the payload builders.
|
|
3
|
+
*
|
|
4
|
+
* scripts/check-mcp-schema-parity.ts compares the JSON Schema each surface
|
|
5
|
+
* publishes, which catches a field going missing or an enum narrowing. It does
|
|
6
|
+
* NOT catch validation written as code rather than schema, and that is exactly
|
|
7
|
+
* how the caption bug shipped: buildCreatePostPayload threw
|
|
8
|
+
* "caption is required" for every post without caption text while the schema
|
|
9
|
+
* happily declared caption optional. Nothing compared behaviour, so the npm
|
|
10
|
+
* package rejected every media-only post (Instagram and Facebook Stories carry
|
|
11
|
+
* no caption) for as long as it did.
|
|
12
|
+
*
|
|
13
|
+
* These tests pin the rules the server actually applies, so a future edit that
|
|
14
|
+
* tightens the client past the server fails here.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { buildCreatePostPayload } from '../dist/tools/create-post.js';
|
|
19
|
+
|
|
20
|
+
let failures = 0;
|
|
21
|
+
function check(name, fn) {
|
|
22
|
+
try {
|
|
23
|
+
fn();
|
|
24
|
+
console.log(`PASS ${name}`);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
failures += 1;
|
|
27
|
+
console.error(`FAIL ${name}\n ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const IMAGE = 'https://assets.poster.ly/users/test/a.jpg';
|
|
32
|
+
|
|
33
|
+
// The server rule (lib/api-v1-post-create.ts) is "provide caption or media".
|
|
34
|
+
// Media-only posts are valid and must not be rejected by the client.
|
|
35
|
+
check('media-only post is accepted (Instagram Story shape)', () => {
|
|
36
|
+
const payload = buildCreatePostPayload({
|
|
37
|
+
platform: 'instagram',
|
|
38
|
+
media_url: IMAGE,
|
|
39
|
+
post_type: 'story',
|
|
40
|
+
instagram_settings: { post_type: 'story' },
|
|
41
|
+
});
|
|
42
|
+
assert.equal(typeof payload.caption, 'string', 'caption should be sent as a string');
|
|
43
|
+
assert.equal(payload.post_type, 'story');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
check('media-only post is accepted (Facebook Story shape)', () => {
|
|
47
|
+
const payload = buildCreatePostPayload({
|
|
48
|
+
platform: 'facebook',
|
|
49
|
+
media_url: IMAGE,
|
|
50
|
+
post_type: 'story',
|
|
51
|
+
platform_settings: { __type: 'facebook', post_type: 'story' },
|
|
52
|
+
});
|
|
53
|
+
assert.equal(typeof payload.caption, 'string');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
check('media_urls-only post is accepted', () => {
|
|
57
|
+
const payload = buildCreatePostPayload({ platform: 'instagram', media_urls: [IMAGE] });
|
|
58
|
+
assert.equal(typeof payload.caption, 'string');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
check('caption-only post is accepted', () => {
|
|
62
|
+
const payload = buildCreatePostPayload({ platform: 'linkedin', caption: 'hello' });
|
|
63
|
+
assert.equal(payload.caption, 'hello');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
check('post with neither caption nor media is rejected', () => {
|
|
67
|
+
assert.throws(
|
|
68
|
+
() => buildCreatePostPayload({ platform: 'instagram' }),
|
|
69
|
+
/caption or media/i,
|
|
70
|
+
'should reject, and the message should tell the caller media is an option',
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
check('thread_posts still bypasses the caption rule', () => {
|
|
75
|
+
const payload = buildCreatePostPayload({
|
|
76
|
+
platform: 'twitter',
|
|
77
|
+
thread_posts: ['one', 'two'],
|
|
78
|
+
});
|
|
79
|
+
assert.equal(payload.caption, 'one');
|
|
80
|
+
assert.equal(payload.post_type, 'x_thread');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (failures > 0) {
|
|
84
|
+
console.error(`\n${failures} payload-builder test(s) failed.`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log(`\nOK payload builders honour the server's "caption or media" rule.`);
|
|
@@ -12,7 +12,7 @@ try {
|
|
|
12
12
|
await client.connect(transport);
|
|
13
13
|
const { tools } = await client.listTools();
|
|
14
14
|
|
|
15
|
-
assert.equal(tools.length,
|
|
15
|
+
assert.equal(tools.length, 76);
|
|
16
16
|
for (const tool of tools) {
|
|
17
17
|
assert(tool.annotations, `${tool.name} is missing annotations`);
|
|
18
18
|
assert(tool.annotations.title, `${tool.name} is missing an annotation title`);
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.awpthorp/posterly",
|
|
4
4
|
"title": "posterly",
|
|
5
5
|
"description": "Validate, schedule, publish, and analyze social content across 18 platforms with posterly.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.30.0",
|
|
7
7
|
"websiteUrl": "https://www.poster.ly/mcp",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/awpthorp/posterly",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"identifier": "posterly-mcp-server",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.30.0",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|
package/src/index.ts
CHANGED
|
@@ -71,6 +71,7 @@ import { createConnectSessionTool } from './tools/create-connect-session.js';
|
|
|
71
71
|
import { getConnectSessionTool } from './tools/get-connect-session.js';
|
|
72
72
|
import { createApiKeyTool } from './tools/create-api-key.js';
|
|
73
73
|
import { deleteApiKeyTool } from './tools/delete-api-key.js';
|
|
74
|
+
import { getCreditsTool } from './tools/get-credits.js';
|
|
74
75
|
import { getSubscriptionTool } from './tools/get-subscription.js';
|
|
75
76
|
import { cancelSubscriptionTool } from './tools/cancel-subscription.js';
|
|
76
77
|
import { pauseSubscriptionTool } from './tools/pause-subscription.js';
|
|
@@ -269,6 +270,21 @@ server.tool(
|
|
|
269
270
|
}
|
|
270
271
|
);
|
|
271
272
|
|
|
273
|
+
server.tool(
|
|
274
|
+
getCreditsTool.name,
|
|
275
|
+
getCreditsTool.description,
|
|
276
|
+
getCreditsTool.inputSchema.shape,
|
|
277
|
+
getToolAnnotations(getCreditsTool.name),
|
|
278
|
+
async () => {
|
|
279
|
+
try {
|
|
280
|
+
const text = await getCreditsTool.execute(client);
|
|
281
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
282
|
+
} catch (err: any) {
|
|
283
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
272
288
|
server.tool(
|
|
273
289
|
getSubscriptionTool.name,
|
|
274
290
|
getSubscriptionTool.description,
|
package/src/lib/api-client.ts
CHANGED
|
@@ -356,6 +356,20 @@ export type SubscriptionSummaryResponse = {
|
|
|
356
356
|
subscription: SubscriptionSummary;
|
|
357
357
|
};
|
|
358
358
|
|
|
359
|
+
export type CreditsSummaryResponse = {
|
|
360
|
+
credits: {
|
|
361
|
+
available: number;
|
|
362
|
+
included: { monthly: number; used: number; remaining: number };
|
|
363
|
+
purchased_balance: number;
|
|
364
|
+
plan_tier: string;
|
|
365
|
+
is_trial: boolean;
|
|
366
|
+
period_start: string;
|
|
367
|
+
/** Null while trialing or on a plan with no included pool. */
|
|
368
|
+
resets_at: string | null;
|
|
369
|
+
workspace: { role: string; shared_wallet: boolean };
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
|
|
359
373
|
export type CancelSubscriptionPayload = {
|
|
360
374
|
reason: string;
|
|
361
375
|
feedback?: string;
|
|
@@ -968,6 +982,10 @@ export class PosterlyClient {
|
|
|
968
982
|
return this.request('GET', '/subscription');
|
|
969
983
|
}
|
|
970
984
|
|
|
985
|
+
async getCredits(): Promise<CreditsSummaryResponse> {
|
|
986
|
+
return this.request('GET', '/credits');
|
|
987
|
+
}
|
|
988
|
+
|
|
971
989
|
async cancelSubscription(data: CancelSubscriptionPayload): Promise<CancelSubscriptionResponse> {
|
|
972
990
|
return this.request('POST', '/subscription/cancel', data);
|
|
973
991
|
}
|
|
@@ -1215,7 +1233,7 @@ export class PosterlyClient {
|
|
|
1215
1233
|
aspect_ratio?: string;
|
|
1216
1234
|
style?: string;
|
|
1217
1235
|
variations?: number;
|
|
1218
|
-
model?: 'flash' | 'pro';
|
|
1236
|
+
model?: 'lite' | 'flash' | 'pro';
|
|
1219
1237
|
resolution?: '512' | '1K' | '2K' | '4K';
|
|
1220
1238
|
}): Promise<{
|
|
1221
1239
|
urls: string[];
|
package/src/lib/version.ts
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// tool set (minus the intentional pre-auth signup tools that only this stdio
|
|
6
6
|
// package exposes). `npm run check:mcp-parity` enforces both the version match
|
|
7
7
|
// and the tool-list match, and runs in the pre-commit hook.
|
|
8
|
-
export const POSTERLY_MCP_VERSION = '0.
|
|
8
|
+
export const POSTERLY_MCP_VERSION = '0.30.0';
|
package/src/tools/create-post.ts
CHANGED
|
@@ -164,13 +164,23 @@ export function buildCreatePostPayload(input: CreatePostPayloadInput | CreatePos
|
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
// Mirror the server rule (lib/api-v1-post-create.ts): a post needs a caption
|
|
168
|
+
// OR media, not a caption unconditionally. Requiring one here made every
|
|
169
|
+
// media-only post impossible from the npm client, most visibly Instagram and
|
|
170
|
+
// Facebook Stories, which carry no caption at all. The dashboard composer and
|
|
171
|
+
// the hosted MCP have always allowed it; only this package disagreed.
|
|
172
|
+
const hasMedia = Boolean(input.media_url) || (input.media_urls?.length ?? 0) > 0;
|
|
173
|
+
if (!caption && !hasMedia) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
'Provide caption or media (or pass `thread_posts` for an X/Threads thread). Media-only posts are valid, for example an Instagram or Facebook Story.',
|
|
176
|
+
);
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
return {
|
|
172
180
|
...rest,
|
|
173
|
-
caption
|
|
181
|
+
// The server treats a missing caption as empty content when media carries
|
|
182
|
+
// the post, so send '' rather than dropping the key.
|
|
183
|
+
caption: caption ?? '',
|
|
174
184
|
post_type,
|
|
175
185
|
...(platform_settings || instagram_settings
|
|
176
186
|
? { settings: { ...(instagram_settings ? { __type: 'instagram', ...instagram_settings } : {}), ...(platform_settings || {}) } }
|
|
@@ -41,13 +41,13 @@ export const generateImageTool = {
|
|
|
41
41
|
.optional()
|
|
42
42
|
.describe('How many variations to generate. Default 1. Each variation costs credits separately - prefer 1 unless the user explicitly wants options.'),
|
|
43
43
|
model: z
|
|
44
|
-
.enum(['flash', 'pro'])
|
|
44
|
+
.enum(['lite', 'flash', 'pro'])
|
|
45
45
|
.optional()
|
|
46
|
-
.describe('flash (default,
|
|
46
|
+
.describe('lite (8 credits, 1K only) is the cheapest and suits drafts and exploration. flash (default, 15 credits at 1K) balances quality and cost. pro (30 credits at 1K) is highest quality for hero imagery. Start with lite or flash unless quality is critical.'),
|
|
47
47
|
resolution: z
|
|
48
48
|
.enum(['512', '1K', '2K', '4K'])
|
|
49
49
|
.optional()
|
|
50
|
-
.describe('Output resolution. Default 1K. Higher resolutions cost more credits. 512 is flash-only.'),
|
|
50
|
+
.describe('Output resolution. Default 1K. Higher resolutions cost more credits (flash: 10/15/23/35 for 512/1K/2K/4K; pro: 30 at 1K-2K, 55 at 4K). 512 is flash-only and lite supports 1K only.'),
|
|
51
51
|
confirm: z.literal(true).describe('Must be true after explicit user confirmation of subject, style, aspect ratio, and credit use.'),
|
|
52
52
|
}),
|
|
53
53
|
|
|
@@ -58,7 +58,7 @@ export const generateImageTool = {
|
|
|
58
58
|
aspect_ratio?: string;
|
|
59
59
|
style?: string;
|
|
60
60
|
variations?: number;
|
|
61
|
-
model?: 'flash' | 'pro';
|
|
61
|
+
model?: 'lite' | 'flash' | 'pro';
|
|
62
62
|
resolution?: '512' | '1K' | '2K' | '4K';
|
|
63
63
|
confirm: true;
|
|
64
64
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { CreditsSummaryResponse, PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { formatDateTime, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
|
|
4
|
+
|
|
5
|
+
function formatCredits(result: CreditsSummaryResponse): string {
|
|
6
|
+
const c = result.credits;
|
|
7
|
+
const inc = c.included;
|
|
8
|
+
return [
|
|
9
|
+
mdTitle('posterly AI credits'),
|
|
10
|
+
mdSection('Balance', mdKeyValue([
|
|
11
|
+
['Available now', String(c.available)],
|
|
12
|
+
['Included this period', `${inc.remaining} of ${inc.monthly} left (${inc.used} used)`],
|
|
13
|
+
['Purchased packs', String(c.purchased_balance)],
|
|
14
|
+
])),
|
|
15
|
+
mdSection('Plan', mdKeyValue([
|
|
16
|
+
['Tier', c.plan_tier || 'unknown'],
|
|
17
|
+
['Trial', c.is_trial ? 'yes' : 'no'],
|
|
18
|
+
[
|
|
19
|
+
'Included pool resets',
|
|
20
|
+
c.resets_at ? formatDateTime(c.resets_at) : 'no monthly reset (trial or no included pool)',
|
|
21
|
+
],
|
|
22
|
+
['Your workspace role', c.workspace?.role || 'unknown'],
|
|
23
|
+
])),
|
|
24
|
+
"Credits are a shared workspace wallet: every active member draws the same pool, and packs are added to the billing owner's balance.",
|
|
25
|
+
].join('\n\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getCreditsTool = {
|
|
29
|
+
name: 'get_credits',
|
|
30
|
+
description:
|
|
31
|
+
"Get the authenticated user's posterly AI credit balance: what is available to spend now, how much of the monthly included allowance is left, purchased pack balance, plan tier, and when the included pool next resets. Credits are a shared WORKSPACE wallet, so this reports the balance the caller can actually spend, not a personal figure. Read-only, spends nothing. Requires the billing:read scope.",
|
|
32
|
+
inputSchema: z.object({}),
|
|
33
|
+
|
|
34
|
+
async execute(client: PosterlyClient) {
|
|
35
|
+
const result = await client.getCredits();
|
|
36
|
+
return formatCredits(result);
|
|
37
|
+
},
|
|
38
|
+
};
|
package/src/tools/update-post.ts
CHANGED
|
@@ -16,6 +16,7 @@ const instagramSettingsSchema = z.object({
|
|
|
16
16
|
first_comment: z.string().optional(),
|
|
17
17
|
media_alt_texts: z.record(z.string(), z.string()).optional(),
|
|
18
18
|
reel_cover_url: z.string().optional(),
|
|
19
|
+
reel_cover_method: z.string().optional(),
|
|
19
20
|
reel_thumb_offset: z.number().nonnegative().optional(),
|
|
20
21
|
});
|
|
21
22
|
|
|
@@ -25,6 +26,7 @@ const platformSettingsSchema = z.object({
|
|
|
25
26
|
content_type: z.string().optional(),
|
|
26
27
|
media_alt_texts: z.record(z.string(), z.string()).optional(),
|
|
27
28
|
reel_cover_url: z.string().optional(),
|
|
29
|
+
reel_cover_method: z.string().optional(),
|
|
28
30
|
reel_thumb_offset: z.number().nonnegative().optional(),
|
|
29
31
|
reply_settings: z.enum(['everyone', 'following', 'mentionedUsers', 'subscribers', 'verified']).optional(),
|
|
30
32
|
poll: z.object({ options: z.array(z.string()), duration_minutes: z.number().positive().optional(), question: z.string().optional(), duration: z.enum(['ONE_DAY', 'THREE_DAYS', 'SEVEN_DAYS', 'FOURTEEN_DAYS']).optional(), allows_multiple_answers: z.boolean().optional() }).optional(),
|
|
@@ -88,9 +90,15 @@ export const updatePostTool = {
|
|
|
88
90
|
.string()
|
|
89
91
|
.optional()
|
|
90
92
|
.describe('New post type: text, image, video, carousel, reel, story, story_series, document, photo, or cover_photo'),
|
|
93
|
+
// Reel cover controls are also accepted top-level, matching the hosted
|
|
94
|
+
// endpoint. An agent reading the hosted schema will reach for these names,
|
|
95
|
+
// and scripts/check-mcp-schema-parity.ts enforces that both surfaces agree.
|
|
96
|
+
reel_cover_url: z.string().optional().describe('Cover image URL for a reel.'),
|
|
97
|
+
reel_cover_method: z.string().optional().describe('How the reel cover is chosen, e.g. an uploaded image or a frame offset.'),
|
|
98
|
+
reel_thumb_offset: z.number().nonnegative().optional().describe('Frame offset in milliseconds to use as the reel cover.'),
|
|
91
99
|
instagram_settings: instagramSettingsSchema
|
|
92
100
|
.optional()
|
|
93
|
-
.describe('Instagram-specific updates: post_type, collaborators, user_tags, first_comment, media_alt_texts, is_trial_reel, graduation_strategy (defaults to MANUAL), reel_cover_url, reel_thumb_offset.'),
|
|
101
|
+
.describe('Instagram-specific updates: post_type, collaborators, user_tags, first_comment, media_alt_texts, is_trial_reel, graduation_strategy (defaults to MANUAL), reel_cover_url, reel_cover_method, reel_thumb_offset.'),
|
|
94
102
|
platform_settings: platformSettingsSchema
|
|
95
103
|
.optional()
|
|
96
104
|
.describe('Platform-specific settings to merge into the post. Prefer this over raw metadata.'),
|
package/tool-annotations.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"get_connect_session",
|
|
13
13
|
"create_api_key",
|
|
14
14
|
"delete_api_key",
|
|
15
|
+
"get_credits",
|
|
15
16
|
"get_subscription",
|
|
16
17
|
"cancel_subscription",
|
|
17
18
|
"pause_subscription",
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
"list_accounts",
|
|
85
86
|
"get_connect_link",
|
|
86
87
|
"get_connect_session",
|
|
88
|
+
"get_credits",
|
|
87
89
|
"get_subscription",
|
|
88
90
|
"list_oauth_clients",
|
|
89
91
|
"list_platforms",
|
|
@@ -162,6 +164,7 @@
|
|
|
162
164
|
"disconnect_account",
|
|
163
165
|
"create_connect_session",
|
|
164
166
|
"get_connect_session",
|
|
167
|
+
"get_credits",
|
|
165
168
|
"get_subscription",
|
|
166
169
|
"cancel_subscription",
|
|
167
170
|
"pause_subscription",
|