posterly-mcp-server 0.3.1 → 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 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.3.1',
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);
@@ -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;
@@ -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)
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for posterly — schedule social media posts from Claude Desktop",
5
5
  "license": "MIT",
6
6
  "type": "module",
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.3.1',
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,
@@ -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;
@@ -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
+ };