posterly-mcp-server 0.3.0 → 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/index.js +11 -1
- package/dist/lib/api-client.d.ts +25 -0
- package/dist/lib/api-client.js +3 -0
- package/dist/tools/create-post.js +7 -1
- package/dist/tools/delete-post.js +2 -1
- package/dist/tools/generate-image.d.ts +36 -0
- package/dist/tools/generate-image.js +70 -0
- package/dist/tools/update-post.js +2 -1
- package/dist/tools/upload-media.js +3 -1
- package/package.json +1 -1
- package/src/index.ts +16 -1
- package/src/lib/api-client.ts +24 -0
- package/src/tools/create-post.ts +7 -1
- package/src/tools/delete-post.ts +2 -1
- package/src/tools/generate-image.ts +86 -0
- package/src/tools/update-post.ts +2 -1
- package/src/tools/upload-media.ts +3 -1
package/dist/index.js
CHANGED
|
@@ -11,9 +11,10 @@ import { getPostTool } from './tools/get-post.js';
|
|
|
11
11
|
import { updatePostTool } from './tools/update-post.js';
|
|
12
12
|
import { deletePostTool } from './tools/delete-post.js';
|
|
13
13
|
import { whoamiTool } from './tools/whoami.js';
|
|
14
|
+
import { generateImageTool } from './tools/generate-image.js';
|
|
14
15
|
const server = new McpServer({
|
|
15
16
|
name: 'posterly',
|
|
16
|
-
version: '0.
|
|
17
|
+
version: '0.4.0',
|
|
17
18
|
});
|
|
18
19
|
let client;
|
|
19
20
|
try {
|
|
@@ -78,6 +79,15 @@ server.tool(uploadMediaTool.name, uploadMediaTool.description, uploadMediaTool.i
|
|
|
78
79
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
79
80
|
}
|
|
80
81
|
});
|
|
82
|
+
server.tool(generateImageTool.name, generateImageTool.description, generateImageTool.inputSchema.shape, async (input) => {
|
|
83
|
+
try {
|
|
84
|
+
const text = await generateImageTool.execute(client, input);
|
|
85
|
+
return { content: [{ type: 'text', text }] };
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
89
|
+
}
|
|
90
|
+
});
|
|
81
91
|
server.tool(getPostTool.name, getPostTool.description, getPostTool.inputSchema.shape, async (input) => {
|
|
82
92
|
try {
|
|
83
93
|
const text = await getPostTool.execute(client, input);
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -99,6 +99,31 @@ export declare class PosterlyClient {
|
|
|
99
99
|
deleted: boolean;
|
|
100
100
|
id: number;
|
|
101
101
|
}>;
|
|
102
|
+
generateImage(data: {
|
|
103
|
+
prompt: string;
|
|
104
|
+
aspect_ratio?: string;
|
|
105
|
+
style?: string;
|
|
106
|
+
variations?: number;
|
|
107
|
+
model?: 'flash' | 'pro';
|
|
108
|
+
resolution?: '512' | '1K' | '2K' | '4K';
|
|
109
|
+
}): Promise<{
|
|
110
|
+
urls: string[];
|
|
111
|
+
images: Array<{
|
|
112
|
+
url: string;
|
|
113
|
+
filename: string;
|
|
114
|
+
path: string;
|
|
115
|
+
}>;
|
|
116
|
+
model: string;
|
|
117
|
+
credits_used: number;
|
|
118
|
+
warnings?: string[];
|
|
119
|
+
usage: {
|
|
120
|
+
used: number | null;
|
|
121
|
+
limit: number | null;
|
|
122
|
+
period: string | null;
|
|
123
|
+
tier: string | null;
|
|
124
|
+
billed_from: 'plan_quota' | 'credits';
|
|
125
|
+
};
|
|
126
|
+
}>;
|
|
102
127
|
findAvailableSlots(params?: {
|
|
103
128
|
account_ids?: string[];
|
|
104
129
|
timezone?: string;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -68,6 +68,9 @@ export class PosterlyClient {
|
|
|
68
68
|
async deletePost(id) {
|
|
69
69
|
return this.request('DELETE', `/posts/${id}`);
|
|
70
70
|
}
|
|
71
|
+
async generateImage(data) {
|
|
72
|
+
return this.request('POST', '/ai/generate-image', data);
|
|
73
|
+
}
|
|
71
74
|
async findAvailableSlots(params) {
|
|
72
75
|
const searchParams = new URLSearchParams();
|
|
73
76
|
if (params?.account_ids?.length)
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const createPostTool = {
|
|
3
3
|
name: 'create_post',
|
|
4
|
-
description: 'Schedule or immediately publish a social media post.
|
|
4
|
+
description: 'Schedule or immediately publish a social media post. This is a DESTRUCTIVE WRITE that creates content on the user\'s real social accounts — once scheduled_at passes, it will be posted publicly and cannot be un-posted.\n\n' +
|
|
5
|
+
'REQUIRED BEFORE CALLING:\n' +
|
|
6
|
+
'1. Call `whoami` at the start of any new session to confirm which workspace and user you are acting for.\n' +
|
|
7
|
+
'2. Show the user a preview containing ALL of: account(s) and platform(s), final caption text, scheduled time (in the user\'s timezone), media attached (if any), and workspace name.\n' +
|
|
8
|
+
'3. Get explicit confirmation from the user (e.g. "post it", "yes schedule that", "looks good") BEFORE calling this tool. Do NOT infer consent from earlier instructions like "post about X every Monday" — confirm each individual post or the entire batch.\n' +
|
|
9
|
+
'4. If scheduling multiple posts in one turn, list every post first and confirm the batch as a whole before calling create_post repeatedly.\n\n' +
|
|
10
|
+
'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace.',
|
|
5
11
|
inputSchema: z.object({
|
|
6
12
|
account_id: z.string().optional().describe('Social account ID (from list_accounts)'),
|
|
7
13
|
username: z.string().optional().describe('Account username (alternative to account_id)'),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const deletePostTool = {
|
|
3
3
|
name: 'delete_post',
|
|
4
|
-
description: 'Delete a scheduled or draft post. Cannot delete published or currently publishing posts
|
|
4
|
+
description: 'Delete a scheduled or draft post. DESTRUCTIVE and IRREVERSIBLE — the post and its caption cannot be recovered. Cannot delete published or currently publishing posts.\n\n' +
|
|
5
|
+
'REQUIRED BEFORE CALLING: Fetch the post with `get_post` first and show the user what will be deleted (caption, account, scheduled time). Get explicit confirmation ("yes delete it", "remove it") before calling. Never delete multiple posts in a single batch without listing each one and confirming the full list.',
|
|
5
6
|
inputSchema: z.object({
|
|
6
7
|
post_id: z.number().describe('The post ID to delete'),
|
|
7
8
|
}),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const generateImageTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
prompt: z.ZodString;
|
|
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
|
+
style: z.ZodOptional<z.ZodEnum<["photographic", "illustration", "minimal", "vibrant", "professional", "youtube_thumbnail", "reel_cover", "review_background"]>>;
|
|
10
|
+
variations: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
model: z.ZodOptional<z.ZodEnum<["flash", "pro"]>>;
|
|
12
|
+
resolution: z.ZodOptional<z.ZodEnum<["512", "1K", "2K", "4K"]>>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
prompt: string;
|
|
15
|
+
aspect_ratio?: "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" | undefined;
|
|
16
|
+
style?: "photographic" | "illustration" | "minimal" | "vibrant" | "professional" | "youtube_thumbnail" | "reel_cover" | "review_background" | undefined;
|
|
17
|
+
variations?: number | undefined;
|
|
18
|
+
model?: "flash" | "pro" | undefined;
|
|
19
|
+
resolution?: "512" | "1K" | "2K" | "4K" | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
prompt: string;
|
|
22
|
+
aspect_ratio?: "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" | undefined;
|
|
23
|
+
style?: "photographic" | "illustration" | "minimal" | "vibrant" | "professional" | "youtube_thumbnail" | "reel_cover" | "review_background" | undefined;
|
|
24
|
+
variations?: number | undefined;
|
|
25
|
+
model?: "flash" | "pro" | undefined;
|
|
26
|
+
resolution?: "512" | "1K" | "2K" | "4K" | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
execute(client: PosterlyClient, input: {
|
|
29
|
+
prompt: string;
|
|
30
|
+
aspect_ratio?: string;
|
|
31
|
+
style?: string;
|
|
32
|
+
variations?: number;
|
|
33
|
+
model?: "flash" | "pro";
|
|
34
|
+
resolution?: "512" | "1K" | "2K" | "4K";
|
|
35
|
+
}): Promise<string>;
|
|
36
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const generateImageTool = {
|
|
3
|
+
name: 'generate_image',
|
|
4
|
+
description: 'Generate an AI image via Posterly\'s Nano Banana (Gemini) integration. The image is saved to the user\'s media storage and the returned URL can be passed to `create_post` as `media_url`.\n\n' +
|
|
5
|
+
'COSTS CREDITS. Every call consumes part of the user\'s monthly AI image quota (or purchased credits once the quota is exhausted). Do NOT call speculatively — always describe the image you\'re about to generate (subject, style, aspect ratio) to the user and get confirmation before calling.\n\n' +
|
|
6
|
+
'If the user is over their plan limit and has no credits, this tool returns a 402 with upgrade info. Surface that message verbatim — do not retry.\n\n' +
|
|
7
|
+
'Common aspect ratios by platform:\n' +
|
|
8
|
+
' • Instagram feed / LinkedIn: 1:1 (square) or 4:5 (portrait)\n' +
|
|
9
|
+
' • Instagram Story/Reel, TikTok, YouTube Shorts: 9:16\n' +
|
|
10
|
+
' • YouTube thumbnail, Twitter cards, landscape feed: 16:9\n' +
|
|
11
|
+
' • Pinterest: 2:3',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
prompt: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(5)
|
|
16
|
+
.max(4000)
|
|
17
|
+
.describe('Detailed image description. 5–4000 characters. Describe subject, scene, lighting, mood, and style.'),
|
|
18
|
+
aspect_ratio: z
|
|
19
|
+
.enum([
|
|
20
|
+
'1:1', '9:16', '4:5', '3:4', '2:3', '1:4', '1:8',
|
|
21
|
+
'21:9', '16:9', '4:3', '3:2', '4:1', '8:1', '5:4',
|
|
22
|
+
])
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Aspect ratio. Default 1:1. Pick based on target platform (see tool description).'),
|
|
25
|
+
style: z
|
|
26
|
+
.enum([
|
|
27
|
+
'photographic', 'illustration', 'minimal', 'vibrant', 'professional',
|
|
28
|
+
'youtube_thumbnail', 'reel_cover', 'review_background',
|
|
29
|
+
])
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Preset style. Default photographic. Use youtube_thumbnail / reel_cover for platform-optimized covers; review_background for testimonial backgrounds.'),
|
|
32
|
+
variations: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.min(1)
|
|
36
|
+
.max(4)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('How many variations to generate. Default 1. Each variation costs credits separately — prefer 1 unless the user explicitly wants options.'),
|
|
39
|
+
model: z
|
|
40
|
+
.enum(['flash', 'pro'])
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('flash (default, 1 credit per 1K image) is fast and cost-effective. pro (2 credits per 1K) is higher quality for hero imagery. Start with flash unless quality is critical.'),
|
|
43
|
+
resolution: z
|
|
44
|
+
.enum(['512', '1K', '2K', '4K'])
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Output resolution. Default 1K. Higher resolutions cost more credits. 512 is flash-only.'),
|
|
47
|
+
}),
|
|
48
|
+
async execute(client, input) {
|
|
49
|
+
const result = await client.generateImage(input);
|
|
50
|
+
const lines = [];
|
|
51
|
+
lines.push(`Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'} via ${result.model}.`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
result.images.forEach((img, i) => {
|
|
54
|
+
lines.push(`${i + 1}. ${img.url}`);
|
|
55
|
+
});
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(`Billed from: ${result.usage.billed_from}${result.credits_used > 0 ? ` (${result.credits_used} credits)` : ''}`);
|
|
58
|
+
if (result.usage.limit != null) {
|
|
59
|
+
lines.push(`Plan usage: ${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}${result.usage.tier ? ` (${result.usage.tier})` : ''}`);
|
|
60
|
+
}
|
|
61
|
+
if (result.warnings?.length) {
|
|
62
|
+
lines.push('');
|
|
63
|
+
lines.push('Warnings:');
|
|
64
|
+
result.warnings.forEach((w) => lines.push(`• ${w}`));
|
|
65
|
+
}
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push('Pass any of these URLs to create_post as media_url (or media_urls for a carousel).');
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const updatePostTool = {
|
|
3
3
|
name: 'update_post',
|
|
4
|
-
description: 'Update a scheduled or draft post.
|
|
4
|
+
description: 'Update a scheduled or draft post. DESTRUCTIVE WRITE — overwrites the existing post\'s caption/media/schedule. Cannot edit published or currently publishing posts.\n\n' +
|
|
5
|
+
'REQUIRED BEFORE CALLING: Show the user a side-by-side of the CURRENT post (caption, scheduled time, media) and the PROPOSED changes, and get explicit confirmation ("yes update it", "apply those changes") before calling. Do not auto-edit posts based on a general instruction — each edit needs its own confirmation.',
|
|
5
6
|
inputSchema: z.object({
|
|
6
7
|
post_id: z.number().describe('The post ID to update'),
|
|
7
8
|
caption: z.string().optional().describe('New caption/text content'),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const uploadMediaTool = {
|
|
3
3
|
name: 'upload_media',
|
|
4
|
-
description: 'Upload an image or video file to posterly. Returns a URL that can be used with create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB
|
|
4
|
+
description: 'Upload an image or video file to posterly storage. Returns a URL that can be used with create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB.\n\n' +
|
|
5
|
+
'This writes to the user\'s media storage (counts against quota) but does NOT publish the file anywhere. Uploading is safe — it\'s the subsequent `create_post` call that publishes content, and that tool has its own confirmation requirements.\n\n' +
|
|
6
|
+
'IMPORTANT: If the user shares an image in the chat, use base64_data (not file_path) since chat-uploaded images are not on the local filesystem. Only use file_path when the user provides an actual local filesystem path — never guess paths.',
|
|
5
7
|
inputSchema: z.object({
|
|
6
8
|
file_path: z
|
|
7
9
|
.string()
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { getPostTool } from './tools/get-post.js';
|
|
|
12
12
|
import { updatePostTool } from './tools/update-post.js';
|
|
13
13
|
import { deletePostTool } from './tools/delete-post.js';
|
|
14
14
|
import { whoamiTool } from './tools/whoami.js';
|
|
15
|
+
import { generateImageTool } from './tools/generate-image.js';
|
|
15
16
|
|
|
16
17
|
const server = new McpServer({
|
|
17
18
|
name: 'posterly',
|
|
18
|
-
version: '0.
|
|
19
|
+
version: '0.4.0',
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
let client: PosterlyClient;
|
|
@@ -112,6 +113,20 @@ server.tool(
|
|
|
112
113
|
}
|
|
113
114
|
);
|
|
114
115
|
|
|
116
|
+
server.tool(
|
|
117
|
+
generateImageTool.name,
|
|
118
|
+
generateImageTool.description,
|
|
119
|
+
generateImageTool.inputSchema.shape,
|
|
120
|
+
async (input) => {
|
|
121
|
+
try {
|
|
122
|
+
const text = await generateImageTool.execute(client, input as any);
|
|
123
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
124
|
+
} catch (err: any) {
|
|
125
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
115
130
|
server.tool(
|
|
116
131
|
getPostTool.name,
|
|
117
132
|
getPostTool.description,
|
package/src/lib/api-client.ts
CHANGED
|
@@ -157,6 +157,30 @@ export class PosterlyClient {
|
|
|
157
157
|
return this.request('DELETE', `/posts/${id}`);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
async generateImage(data: {
|
|
161
|
+
prompt: string;
|
|
162
|
+
aspect_ratio?: string;
|
|
163
|
+
style?: string;
|
|
164
|
+
variations?: number;
|
|
165
|
+
model?: 'flash' | 'pro';
|
|
166
|
+
resolution?: '512' | '1K' | '2K' | '4K';
|
|
167
|
+
}): Promise<{
|
|
168
|
+
urls: string[];
|
|
169
|
+
images: Array<{ url: string; filename: string; path: string }>;
|
|
170
|
+
model: string;
|
|
171
|
+
credits_used: number;
|
|
172
|
+
warnings?: string[];
|
|
173
|
+
usage: {
|
|
174
|
+
used: number | null;
|
|
175
|
+
limit: number | null;
|
|
176
|
+
period: string | null;
|
|
177
|
+
tier: string | null;
|
|
178
|
+
billed_from: 'plan_quota' | 'credits';
|
|
179
|
+
};
|
|
180
|
+
}> {
|
|
181
|
+
return this.request('POST', '/ai/generate-image', data);
|
|
182
|
+
}
|
|
183
|
+
|
|
160
184
|
async findAvailableSlots(params?: {
|
|
161
185
|
account_ids?: string[];
|
|
162
186
|
timezone?: string;
|
package/src/tools/create-post.ts
CHANGED
|
@@ -4,7 +4,13 @@ import type { PosterlyClient } from '../lib/api-client.js';
|
|
|
4
4
|
export const createPostTool = {
|
|
5
5
|
name: 'create_post',
|
|
6
6
|
description:
|
|
7
|
-
'Schedule or immediately publish a social media post.
|
|
7
|
+
'Schedule or immediately publish a social media post. This is a DESTRUCTIVE WRITE that creates content on the user\'s real social accounts — once scheduled_at passes, it will be posted publicly and cannot be un-posted.\n\n' +
|
|
8
|
+
'REQUIRED BEFORE CALLING:\n' +
|
|
9
|
+
'1. Call `whoami` at the start of any new session to confirm which workspace and user you are acting for.\n' +
|
|
10
|
+
'2. Show the user a preview containing ALL of: account(s) and platform(s), final caption text, scheduled time (in the user\'s timezone), media attached (if any), and workspace name.\n' +
|
|
11
|
+
'3. Get explicit confirmation from the user (e.g. "post it", "yes schedule that", "looks good") BEFORE calling this tool. Do NOT infer consent from earlier instructions like "post about X every Monday" — confirm each individual post or the entire batch.\n' +
|
|
12
|
+
'4. If scheduling multiple posts in one turn, list every post first and confirm the batch as a whole before calling create_post repeatedly.\n\n' +
|
|
13
|
+
'Provide either account_id OR username+platform to identify the account. If scheduled_at is omitted, the post publishes immediately. If workspace_id is omitted, the server resolves one from the social account, falling back to the caller\'s default (personal) workspace — pass workspace_id explicitly if the user has more than one workspace.',
|
|
8
14
|
inputSchema: z.object({
|
|
9
15
|
account_id: z.string().optional().describe('Social account ID (from list_accounts)'),
|
|
10
16
|
username: z.string().optional().describe('Account username (alternative to account_id)'),
|
package/src/tools/delete-post.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { PosterlyClient } from '../lib/api-client.js';
|
|
|
4
4
|
export const deletePostTool = {
|
|
5
5
|
name: 'delete_post',
|
|
6
6
|
description:
|
|
7
|
-
'Delete a scheduled or draft post. Cannot delete published or currently publishing posts
|
|
7
|
+
'Delete a scheduled or draft post. DESTRUCTIVE and IRREVERSIBLE — the post and its caption cannot be recovered. Cannot delete published or currently publishing posts.\n\n' +
|
|
8
|
+
'REQUIRED BEFORE CALLING: Fetch the post with `get_post` first and show the user what will be deleted (caption, account, scheduled time). Get explicit confirmation ("yes delete it", "remove it") before calling. Never delete multiple posts in a single batch without listing each one and confirming the full list.',
|
|
8
9
|
inputSchema: z.object({
|
|
9
10
|
post_id: z.number().describe('The post ID to delete'),
|
|
10
11
|
}),
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const generateImageTool = {
|
|
5
|
+
name: 'generate_image',
|
|
6
|
+
description:
|
|
7
|
+
'Generate an AI image via Posterly\'s Nano Banana (Gemini) integration. The image is saved to the user\'s media storage and the returned URL can be passed to `create_post` as `media_url`.\n\n' +
|
|
8
|
+
'COSTS CREDITS. Every call consumes part of the user\'s monthly AI image quota (or purchased credits once the quota is exhausted). Do NOT call speculatively — always describe the image you\'re about to generate (subject, style, aspect ratio) to the user and get confirmation before calling.\n\n' +
|
|
9
|
+
'If the user is over their plan limit and has no credits, this tool returns a 402 with upgrade info. Surface that message verbatim — do not retry.\n\n' +
|
|
10
|
+
'Common aspect ratios by platform:\n' +
|
|
11
|
+
' • Instagram feed / LinkedIn: 1:1 (square) or 4:5 (portrait)\n' +
|
|
12
|
+
' • Instagram Story/Reel, TikTok, YouTube Shorts: 9:16\n' +
|
|
13
|
+
' • YouTube thumbnail, Twitter cards, landscape feed: 16:9\n' +
|
|
14
|
+
' • Pinterest: 2:3',
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
prompt: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(5)
|
|
19
|
+
.max(4000)
|
|
20
|
+
.describe('Detailed image description. 5–4000 characters. Describe subject, scene, lighting, mood, and style.'),
|
|
21
|
+
aspect_ratio: z
|
|
22
|
+
.enum([
|
|
23
|
+
'1:1', '9:16', '4:5', '3:4', '2:3', '1:4', '1:8',
|
|
24
|
+
'21:9', '16:9', '4:3', '3:2', '4:1', '8:1', '5:4',
|
|
25
|
+
])
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Aspect ratio. Default 1:1. Pick based on target platform (see tool description).'),
|
|
28
|
+
style: z
|
|
29
|
+
.enum([
|
|
30
|
+
'photographic', 'illustration', 'minimal', 'vibrant', 'professional',
|
|
31
|
+
'youtube_thumbnail', 'reel_cover', 'review_background',
|
|
32
|
+
])
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Preset style. Default photographic. Use youtube_thumbnail / reel_cover for platform-optimized covers; review_background for testimonial backgrounds.'),
|
|
35
|
+
variations: z
|
|
36
|
+
.number()
|
|
37
|
+
.int()
|
|
38
|
+
.min(1)
|
|
39
|
+
.max(4)
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('How many variations to generate. Default 1. Each variation costs credits separately — prefer 1 unless the user explicitly wants options.'),
|
|
42
|
+
model: z
|
|
43
|
+
.enum(['flash', 'pro'])
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('flash (default, 1 credit per 1K image) is fast and cost-effective. pro (2 credits per 1K) is higher quality for hero imagery. Start with flash unless quality is critical.'),
|
|
46
|
+
resolution: z
|
|
47
|
+
.enum(['512', '1K', '2K', '4K'])
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Output resolution. Default 1K. Higher resolutions cost more credits. 512 is flash-only.'),
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
async execute(
|
|
53
|
+
client: PosterlyClient,
|
|
54
|
+
input: {
|
|
55
|
+
prompt: string;
|
|
56
|
+
aspect_ratio?: string;
|
|
57
|
+
style?: string;
|
|
58
|
+
variations?: number;
|
|
59
|
+
model?: 'flash' | 'pro';
|
|
60
|
+
resolution?: '512' | '1K' | '2K' | '4K';
|
|
61
|
+
}
|
|
62
|
+
) {
|
|
63
|
+
const result = await client.generateImage(input);
|
|
64
|
+
|
|
65
|
+
const lines: string[] = [];
|
|
66
|
+
lines.push(`Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'} via ${result.model}.`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
result.images.forEach((img, i) => {
|
|
69
|
+
lines.push(`${i + 1}. ${img.url}`);
|
|
70
|
+
});
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push(`Billed from: ${result.usage.billed_from}${result.credits_used > 0 ? ` (${result.credits_used} credits)` : ''}`);
|
|
73
|
+
if (result.usage.limit != null) {
|
|
74
|
+
lines.push(`Plan usage: ${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}${result.usage.tier ? ` (${result.usage.tier})` : ''}`);
|
|
75
|
+
}
|
|
76
|
+
if (result.warnings?.length) {
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push('Warnings:');
|
|
79
|
+
result.warnings.forEach((w) => lines.push(`• ${w}`));
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push('Pass any of these URLs to create_post as media_url (or media_urls for a carousel).');
|
|
83
|
+
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
},
|
|
86
|
+
};
|
package/src/tools/update-post.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { PosterlyClient } from '../lib/api-client.js';
|
|
|
4
4
|
export const updatePostTool = {
|
|
5
5
|
name: 'update_post',
|
|
6
6
|
description:
|
|
7
|
-
'Update a scheduled or draft post.
|
|
7
|
+
'Update a scheduled or draft post. DESTRUCTIVE WRITE — overwrites the existing post\'s caption/media/schedule. Cannot edit published or currently publishing posts.\n\n' +
|
|
8
|
+
'REQUIRED BEFORE CALLING: Show the user a side-by-side of the CURRENT post (caption, scheduled time, media) and the PROPOSED changes, and get explicit confirmation ("yes update it", "apply those changes") before calling. Do not auto-edit posts based on a general instruction — each edit needs its own confirmation.',
|
|
8
9
|
inputSchema: z.object({
|
|
9
10
|
post_id: z.number().describe('The post ID to update'),
|
|
10
11
|
caption: z.string().optional().describe('New caption/text content'),
|
|
@@ -4,7 +4,9 @@ import type { PosterlyClient } from '../lib/api-client.js';
|
|
|
4
4
|
export const uploadMediaTool = {
|
|
5
5
|
name: 'upload_media',
|
|
6
6
|
description:
|
|
7
|
-
'Upload an image or video file to posterly. Returns a URL that can be used with create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB
|
|
7
|
+
'Upload an image or video file to posterly storage. Returns a URL that can be used with create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB.\n\n' +
|
|
8
|
+
'This writes to the user\'s media storage (counts against quota) but does NOT publish the file anywhere. Uploading is safe — it\'s the subsequent `create_post` call that publishes content, and that tool has its own confirmation requirements.\n\n' +
|
|
9
|
+
'IMPORTANT: If the user shares an image in the chat, use base64_data (not file_path) since chat-uploaded images are not on the local filesystem. Only use file_path when the user provides an actual local filesystem path — never guess paths.',
|
|
8
10
|
inputSchema: z.object({
|
|
9
11
|
file_path: z
|
|
10
12
|
.string()
|