posterly-mcp-server 0.20.4 → 0.20.7

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 CHANGED
@@ -108,7 +108,7 @@ Add the same server definition to your Cursor MCP settings:
108
108
 
109
109
  ## Available tools
110
110
 
111
- `posterly-mcp-server@0.20.4` exposes 58 tools.
111
+ `posterly-mcp-server@0.20.7` exposes 58 tools.
112
112
 
113
113
  Public setup tools work before `POSTERLY_API_KEY` exists:
114
114
 
@@ -125,6 +125,7 @@ Authenticated tools require `POSTERLY_API_KEY`:
125
125
  - `get_connect_link`
126
126
  - `create_connect_session` (create a guided browser handoff for connecting a social account)
127
127
  - `get_connect_session` (poll connection progress while the user approves OAuth or enters credentials)
128
+ - `create_api_key` (create a new API key after explicit confirmation; scopes cannot exceed the calling dashboard key)
128
129
  - `list_oauth_clients`
129
130
  - `create_oauth_client` (create a public PKCE client after explicit confirmation)
130
131
  - `update_oauth_client` (update redirect URIs/scopes after explicit confirmation)
@@ -140,7 +141,6 @@ Authenticated tools require `POSTERLY_API_KEY`:
140
141
  - `get_post`
141
142
  - `get_post_missing`
142
143
  - `ask_support` (authenticated docs-backed support with read-only account/post diagnostics; human tickets require explicit confirmation)
143
- - `get_support` (alias for `ask_support` for agents that look for get-style support tools)
144
144
  - `create_post` (supports `thread_posts: string[]` for X / Threads reply chains, plus `platform_settings` for platform-specific composer controls)
145
145
  - `create_posts_batch` (create up to 25 confirmed posts in one API request)
146
146
  - `update_post` (also accepts `platform_settings`)
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import { listPostsTool } from './tools/list-posts.js';
20
20
  import { uploadMediaTool } from './tools/upload-media.js';
21
21
  import { getPostTool } from './tools/get-post.js';
22
22
  import { getPostMissingTool } from './tools/get-post-missing.js';
23
- import { askSupportTool, getSupportTool } from './tools/ask-support.js';
23
+ import { askSupportTool } from './tools/ask-support.js';
24
24
  import { updatePostTool } from './tools/update-post.js';
25
25
  import { updatePostStatusTool } from './tools/update-post-status.js';
26
26
  import { updatePostReleaseIdTool } from './tools/update-post-release-id.js';
@@ -56,6 +56,7 @@ import { uploadMediaFromUrlTool } from './tools/upload-media-from-url.js';
56
56
  import { getConnectLinkTool } from './tools/get-connect-link.js';
57
57
  import { createConnectSessionTool } from './tools/create-connect-session.js';
58
58
  import { getConnectSessionTool } from './tools/get-connect-session.js';
59
+ import { createApiKeyTool } from './tools/create-api-key.js';
59
60
  import { listOAuthClientsTool } from './tools/list-oauth-clients.js';
60
61
  import { createOAuthClientTool } from './tools/create-oauth-client.js';
61
62
  import { updateOAuthClientTool } from './tools/update-oauth-client.js';
@@ -156,6 +157,15 @@ server.tool(getConnectSessionTool.name, getConnectSessionTool.description, getCo
156
157
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
157
158
  }
158
159
  });
160
+ server.tool(createApiKeyTool.name, createApiKeyTool.description, createApiKeyTool.inputSchema.shape, async (input) => {
161
+ try {
162
+ const text = await createApiKeyTool.execute(client, input);
163
+ return { content: [{ type: 'text', text }] };
164
+ }
165
+ catch (err) {
166
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
167
+ }
168
+ });
159
169
  server.tool(listOAuthClientsTool.name, listOAuthClientsTool.description, listOAuthClientsTool.inputSchema.shape, async () => {
160
170
  try {
161
171
  const text = await listOAuthClientsTool.execute(client);
@@ -399,15 +409,6 @@ server.tool(askSupportTool.name, askSupportTool.description, askSupportTool.inpu
399
409
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
400
410
  }
401
411
  });
402
- server.tool(getSupportTool.name, getSupportTool.description, getSupportTool.inputSchema.shape, async (input) => {
403
- try {
404
- const text = await getSupportTool.execute(client, input);
405
- return { content: [{ type: 'text', text }] };
406
- }
407
- catch (err) {
408
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
409
- }
410
- });
411
412
  server.tool(updatePostTool.name, updatePostTool.description, updatePostTool.inputSchema.shape, async (input) => {
412
413
  try {
413
414
  const text = await updatePostTool.execute(client, input);
@@ -159,6 +159,34 @@ export type AskSupportPayload = {
159
159
  request_human?: boolean;
160
160
  confirm_escalation?: boolean;
161
161
  };
162
+ export type ApiKeyScope = 'accounts:read' | 'accounts:write' | 'posts:read' | 'posts:write' | 'media:write' | 'analytics:read';
163
+ export type CreateApiKeyPayload = {
164
+ name?: string;
165
+ scopes?: ApiKeyScope[];
166
+ workspace_id?: string;
167
+ expires_in_days?: number;
168
+ confirm: true;
169
+ };
170
+ export type CreateApiKeyResponse = {
171
+ key: string;
172
+ api_key: {
173
+ id: string;
174
+ name: string;
175
+ key_prefix: string;
176
+ scopes: ApiKeyScope[];
177
+ workspace_id?: string | null;
178
+ expires_at?: string | null;
179
+ created_at: string;
180
+ };
181
+ warning?: string;
182
+ limits?: {
183
+ tier?: string;
184
+ max_user_keys?: number;
185
+ active_user_keys?: number;
186
+ remaining_user_keys?: number;
187
+ uses_grandfathered_keys?: boolean;
188
+ };
189
+ };
162
190
  export type AskSupportResponse = {
163
191
  success: boolean;
164
192
  workspace_id: string;
@@ -552,6 +580,7 @@ export declare class PosterlyClient {
552
580
  startSignup(data: PublicSignupPayload): Promise<PublicSignupResponse>;
553
581
  getSignupSession(sessionId: string): Promise<PublicSignupSessionResponse>;
554
582
  whoami(): Promise<Whoami>;
583
+ createApiKey(data: CreateApiKeyPayload): Promise<CreateApiKeyResponse>;
555
584
  listAccounts(params?: {
556
585
  workspace_id?: string;
557
586
  }): Promise<Account[]>;
@@ -113,6 +113,9 @@ export class PosterlyClient {
113
113
  async whoami() {
114
114
  return this.request('GET', '/whoami');
115
115
  }
116
+ async createApiKey(data) {
117
+ return this.request('POST', '/api-keys', data);
118
+ }
116
119
  async listAccounts(params) {
117
120
  const searchParams = new URLSearchParams();
118
121
  if (params?.workspace_id)
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.20.4";
1
+ export declare const POSTERLY_MCP_VERSION = "0.20.7";
@@ -1 +1 @@
1
- export const POSTERLY_MCP_VERSION = '0.20.4';
1
+ export const POSTERLY_MCP_VERSION = '0.20.7';
@@ -34,37 +34,4 @@ export declare const askSupportTool: {
34
34
  }>;
35
35
  execute: typeof executeSupport;
36
36
  };
37
- export declare const getSupportTool: {
38
- name: string;
39
- description: string;
40
- inputSchema: z.ZodObject<{
41
- question: z.ZodString;
42
- workspace_id: z.ZodOptional<z.ZodString>;
43
- conversation_id: z.ZodOptional<z.ZodString>;
44
- page_url: z.ZodOptional<z.ZodString>;
45
- post_id: z.ZodOptional<z.ZodNumber>;
46
- referenced_post_ids: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
47
- request_human: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
48
- confirm_escalation: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
49
- }, "strip", z.ZodTypeAny, {
50
- question: string;
51
- workspace_id?: string | undefined;
52
- post_id?: number | undefined;
53
- conversation_id?: string | undefined;
54
- page_url?: string | undefined;
55
- referenced_post_ids?: number[] | undefined;
56
- request_human?: boolean | undefined;
57
- confirm_escalation?: boolean | undefined;
58
- }, {
59
- question: string;
60
- workspace_id?: string | undefined;
61
- post_id?: number | undefined;
62
- conversation_id?: string | undefined;
63
- page_url?: string | undefined;
64
- referenced_post_ids?: number[] | undefined;
65
- request_human?: boolean | undefined;
66
- confirm_escalation?: boolean | undefined;
67
- }>;
68
- execute: typeof executeSupport;
69
- };
70
37
  export {};
@@ -46,9 +46,3 @@ export const askSupportTool = {
46
46
  inputSchema: supportInputSchema,
47
47
  execute: executeSupport,
48
48
  };
49
- export const getSupportTool = {
50
- name: 'get_support',
51
- description: 'Alias for ask_support. Ask Posterly Support AI an authenticated question using Posterly docs plus read-only account/post diagnostics. Human tickets require request_human=true and confirm_escalation=true after explicit user confirmation.',
52
- inputSchema: supportInputSchema,
53
- execute: executeSupport,
54
- };
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ import type { CreateApiKeyPayload, PosterlyClient } from '../lib/api-client.js';
3
+ export declare const createApiKeyTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ name: z.ZodOptional<z.ZodString>;
8
+ scopes: z.ZodOptional<z.ZodArray<z.ZodEnum<["accounts:read", "accounts:write", "posts:read", "posts:write", "media:write", "analytics:read"]>, "many">>;
9
+ workspace_id: z.ZodOptional<z.ZodString>;
10
+ expires_in_days: z.ZodOptional<z.ZodNumber>;
11
+ confirm: z.ZodLiteral<true>;
12
+ }, "strip", z.ZodTypeAny, {
13
+ confirm: true;
14
+ workspace_id?: string | undefined;
15
+ name?: string | undefined;
16
+ scopes?: ("accounts:read" | "accounts:write" | "posts:read" | "posts:write" | "media:write" | "analytics:read")[] | undefined;
17
+ expires_in_days?: number | undefined;
18
+ }, {
19
+ confirm: true;
20
+ workspace_id?: string | undefined;
21
+ name?: string | undefined;
22
+ scopes?: ("accounts:read" | "accounts:write" | "posts:read" | "posts:write" | "media:write" | "analytics:read")[] | undefined;
23
+ expires_in_days?: number | undefined;
24
+ }>;
25
+ execute(client: PosterlyClient, input: CreateApiKeyPayload): Promise<string>;
26
+ };
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { code, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
3
+ const scopeSchema = z.enum(['accounts:read', 'accounts:write', 'posts:read', 'posts:write', 'media:write', 'analytics:read']);
4
+ function formatCreatedApiKey(result) {
5
+ const key = result.api_key;
6
+ return [
7
+ mdTitle('API key created'),
8
+ `Full key (shown once): ${code(result.key)}`,
9
+ mdSection('Details', mdKeyValue([
10
+ ['Name', key.name],
11
+ ['Key ID', code(key.id)],
12
+ ['Prefix', code(key.key_prefix)],
13
+ ['Scopes', key.scopes.join(', ')],
14
+ ['Workspace', key.workspace_id ? code(key.workspace_id) : 'all accessible workspaces'],
15
+ ['Expires', key.expires_at || 'no expiry'],
16
+ ['Remaining key slots', result.limits?.remaining_user_keys ?? 'unknown'],
17
+ ])),
18
+ mdSection('Warning', result.warning || 'Store this securely. It will not be shown again.'),
19
+ ].filter(Boolean).join('\n\n');
20
+ }
21
+ export const createApiKeyTool = {
22
+ name: 'create_api_key',
23
+ description: 'Create a new Posterly API key for the authenticated user. SECRET-CREATING WRITE: only use after explicit user confirmation. The new key can only request scopes already present on the calling dashboard-created API key; OAuth and managed assistant tokens cannot mint keys.',
24
+ inputSchema: z.object({
25
+ name: z.string().trim().min(1).max(120).optional().describe('Human-readable key name.'),
26
+ scopes: z.array(scopeSchema).min(1).max(6).optional().describe('Scopes for the new key. Omit to copy the calling key scopes. Cannot exceed the calling key scopes.'),
27
+ workspace_id: z.string().trim().min(1).optional().describe('Optional workspace restriction. Workspace-scoped calling keys cannot create keys outside their workspace.'),
28
+ expires_in_days: z.number().int().min(1).max(365).optional().describe('Optional expiry from now, up to 365 days. Omit for no expiry.'),
29
+ confirm: z.literal(true).describe('Must be true after the user explicitly confirms that a new secret API key should be created.'),
30
+ }),
31
+ async execute(client, input) {
32
+ const result = await client.createApiKey(input);
33
+ return formatCreatedApiKey(result);
34
+ },
35
+ };
@@ -82,11 +82,11 @@ export const createPostPayloadInputSchema = z.object({
82
82
  .string()
83
83
  .optional()
84
84
  .describe('ISO 8601 datetime for scheduling (e.g. 2026-03-05T09:00:00Z). Omit for immediate publish.'),
85
- media_url: z.string().optional().describe('URL of media to attach (image or video). For threads, attaches to the lead post only.'),
85
+ media_url: z.string().optional().describe('Media URL to attach. Posterly storage URLs are used as-is; third-party HTTP(S) URLs are copied into Posterly storage before the post is saved. Short-lived signed URLs must still be live when this tool runs. For threads, attaches to the lead post only.'),
86
86
  media_urls: z
87
87
  .array(z.string())
88
88
  .optional()
89
- .describe('Multiple media URLs for carousel or multi-image posts. For TikTok, 2 or more images auto-create a photo slideshow (up to 35 images) and every image is posted.'),
89
+ .describe('Multiple media URLs for carousel or multi-image posts. Posterly storage URLs are used as-is; third-party HTTP(S) URLs are copied into Posterly storage before the post is saved. For TikTok, 2 or more images auto-create a photo slideshow (up to 35 images) and every image is posted.'),
90
90
  post_type: z
91
91
  .string()
92
92
  .optional()
@@ -156,6 +156,7 @@ export const createPostTool = {
156
156
  '4. If scheduling multiple posts in one turn, list every post first and confirm the batch as a whole, then prefer `create_posts_batch` so they are created in one API request.\n\n' +
157
157
  'AFTER CALLING: Tell the user the post was created, include the Posterly dashboard link returned by the tool, and offer the next natural action. Do not narrate raw HTTP, curl, or API plumbing.\n\n' +
158
158
  '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.\n\n' +
159
+ 'MEDIA URLS: Posterly storage URLs are used as-is. Third-party HTTP(S) media URLs are copied into Posterly storage before the post is saved, so API-created posts do not later fail on expired signed URLs. Short-lived signed URLs must still be live when this tool runs. You can also call `upload_media_from_url` first if you want to upload once and reuse the returned Posterly URL across posts.\n\n' +
159
160
  'THREADS: Pass `thread_posts` (an array of 2+ strings) to schedule a multi-post thread on X (Twitter) or Threads (Meta). The first entry is the lead post; the rest are published as replies in the same chain. X entries are capped at 280 characters each (4000 for verified, 25000 for organization accounts); Threads entries are capped at 500 characters each. When `thread_posts` is set, `caption` is ignored.\n\n' +
160
161
  'PLATFORM SETTINGS: Pass `platform_settings` for composer-equivalent controls: Facebook story/reel/backgrounds, YouTube title/privacy/thumbnail/playlist, LinkedIn document titles/mentions/video thumbnails, TikTok direct-post privacy/toggles/commercial disclosure, Pinterest post_type (image/video/carousel), board (from pinterest.boards helper), title, link, and cover_image_url (video Pins), GBP event/offer/CTA, X reply settings/polls, Threads reply controls/text attachments, Bluesky languages/alt text, and Instagram feed/story/reel/carousel/collaborators/first comments/trial Reels. `instagram_settings` remains as a backwards-compatible alias.\n\n' +
161
162
  'TIKTOK MEDIA: TikTok supports a single video or a photo slideshow of 1 to 35 images, never multiple videos. Image media auto-detects as a slideshow, so you usually do not set post_type or media_type for photos and every image is posted. Pass `platform_settings.media_type: "PHOTO"` to force a slideshow explicitly.',
@@ -67,8 +67,8 @@ export const updatePostTool = {
67
67
  .string()
68
68
  .optional()
69
69
  .describe('New scheduled time (ISO 8601, e.g. 2026-03-10T09:00:00Z)'),
70
- media_url: z.string().optional().describe('New media URL'),
71
- media_urls: z.array(z.string()).optional().describe('New media URLs for carousel'),
70
+ media_url: z.string().optional().describe('New media URL. Posterly storage URLs are used as-is; third-party HTTP(S) URLs are copied into Posterly storage before the post is saved. Short-lived signed URLs must still be live when this tool runs.'),
71
+ media_urls: z.array(z.string()).optional().describe('New media URLs for carousel. Posterly storage URLs are used as-is; third-party HTTP(S) URLs are copied into Posterly storage before the post is saved.'),
72
72
  post_type: z
73
73
  .string()
74
74
  .optional()
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export const uploadMediaFromUrlTool = {
3
3
  name: 'upload_media_from_url',
4
- description: 'Fetch an image or video from a public HTTPS URL into posterly storage. Returns a URL that can be used with create_post.',
4
+ description: 'Fetch an image or video from a public HTTPS URL into Posterly storage. Returns a URL that can be used with create_post. You can also pass public HTTP(S) media URLs directly to create_post/update_post; use this tool when you want to upload once and reuse the returned Posterly URL.',
5
5
  inputSchema: z.object({
6
6
  url: z.string().url().describe('Public HTTPS URL for the image or video. Local/private network URLs are blocked.'),
7
7
  filename: z.string().optional().describe('Optional filename override.'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.20.4",
3
+ "version": "0.20.7",
4
4
  "description": "MCP server for posterly: schedule social media posts from Claude Desktop",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.poster.ly/mcp",
package/src/index.ts CHANGED
@@ -21,7 +21,7 @@ import { listPostsTool } from './tools/list-posts.js';
21
21
  import { uploadMediaTool } from './tools/upload-media.js';
22
22
  import { getPostTool } from './tools/get-post.js';
23
23
  import { getPostMissingTool } from './tools/get-post-missing.js';
24
- import { askSupportTool, getSupportTool } from './tools/ask-support.js';
24
+ import { askSupportTool } from './tools/ask-support.js';
25
25
  import { updatePostTool } from './tools/update-post.js';
26
26
  import { updatePostStatusTool } from './tools/update-post-status.js';
27
27
  import { updatePostReleaseIdTool } from './tools/update-post-release-id.js';
@@ -57,6 +57,7 @@ import { uploadMediaFromUrlTool } from './tools/upload-media-from-url.js';
57
57
  import { getConnectLinkTool } from './tools/get-connect-link.js';
58
58
  import { createConnectSessionTool } from './tools/create-connect-session.js';
59
59
  import { getConnectSessionTool } from './tools/get-connect-session.js';
60
+ import { createApiKeyTool } from './tools/create-api-key.js';
60
61
  import { listOAuthClientsTool } from './tools/list-oauth-clients.js';
61
62
  import { createOAuthClientTool } from './tools/create-oauth-client.js';
62
63
  import { updateOAuthClientTool } from './tools/update-oauth-client.js';
@@ -210,6 +211,20 @@ server.tool(
210
211
  }
211
212
  );
212
213
 
214
+ server.tool(
215
+ createApiKeyTool.name,
216
+ createApiKeyTool.description,
217
+ createApiKeyTool.inputSchema.shape,
218
+ async (input) => {
219
+ try {
220
+ const text = await createApiKeyTool.execute(client, input as any);
221
+ return { content: [{ type: 'text' as const, text }] };
222
+ } catch (err: any) {
223
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
224
+ }
225
+ }
226
+ );
227
+
213
228
  server.tool(
214
229
  listOAuthClientsTool.name,
215
230
  listOAuthClientsTool.description,
@@ -588,20 +603,6 @@ server.tool(
588
603
  }
589
604
  );
590
605
 
591
- server.tool(
592
- getSupportTool.name,
593
- getSupportTool.description,
594
- getSupportTool.inputSchema.shape,
595
- async (input) => {
596
- try {
597
- const text = await getSupportTool.execute(client, input as any);
598
- return { content: [{ type: 'text' as const, text }] };
599
- } catch (err: any) {
600
- return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
601
- }
602
- }
603
- );
604
-
605
606
  server.tool(
606
607
  updatePostTool.name,
607
608
  updatePostTool.description,
@@ -163,6 +163,43 @@ export type AskSupportPayload = {
163
163
  confirm_escalation?: boolean;
164
164
  };
165
165
 
166
+ export type ApiKeyScope =
167
+ | 'accounts:read'
168
+ | 'accounts:write'
169
+ | 'posts:read'
170
+ | 'posts:write'
171
+ | 'media:write'
172
+ | 'analytics:read';
173
+
174
+ export type CreateApiKeyPayload = {
175
+ name?: string;
176
+ scopes?: ApiKeyScope[];
177
+ workspace_id?: string;
178
+ expires_in_days?: number;
179
+ confirm: true;
180
+ };
181
+
182
+ export type CreateApiKeyResponse = {
183
+ key: string;
184
+ api_key: {
185
+ id: string;
186
+ name: string;
187
+ key_prefix: string;
188
+ scopes: ApiKeyScope[];
189
+ workspace_id?: string | null;
190
+ expires_at?: string | null;
191
+ created_at: string;
192
+ };
193
+ warning?: string;
194
+ limits?: {
195
+ tier?: string;
196
+ max_user_keys?: number;
197
+ active_user_keys?: number;
198
+ remaining_user_keys?: number;
199
+ uses_grandfathered_keys?: boolean;
200
+ };
201
+ };
202
+
166
203
  export type AskSupportResponse = {
167
204
  success: boolean;
168
205
  workspace_id: string;
@@ -693,6 +730,10 @@ export class PosterlyClient {
693
730
  return this.request<Whoami>('GET', '/whoami');
694
731
  }
695
732
 
733
+ async createApiKey(data: CreateApiKeyPayload): Promise<CreateApiKeyResponse> {
734
+ return this.request('POST', '/api-keys', data);
735
+ }
736
+
696
737
  async listAccounts(params?: { workspace_id?: string }): Promise<Account[]> {
697
738
  const searchParams = new URLSearchParams();
698
739
  if (params?.workspace_id) searchParams.set('workspace_id', params.workspace_id);
@@ -1 +1 @@
1
- export const POSTERLY_MCP_VERSION = '0.20.4';
1
+ export const POSTERLY_MCP_VERSION = '0.20.7';
@@ -53,11 +53,3 @@ export const askSupportTool = {
53
53
  inputSchema: supportInputSchema,
54
54
  execute: executeSupport,
55
55
  };
56
-
57
- export const getSupportTool = {
58
- name: 'get_support',
59
- description:
60
- 'Alias for ask_support. Ask Posterly Support AI an authenticated question using Posterly docs plus read-only account/post diagnostics. Human tickets require request_human=true and confirm_escalation=true after explicit user confirmation.',
61
- inputSchema: supportInputSchema,
62
- execute: executeSupport,
63
- };
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ import type { CreateApiKeyPayload, CreateApiKeyResponse, PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
4
+
5
+ const scopeSchema = z.enum(['accounts:read', 'accounts:write', 'posts:read', 'posts:write', 'media:write', 'analytics:read']);
6
+
7
+ function formatCreatedApiKey(result: CreateApiKeyResponse): string {
8
+ const key = result.api_key;
9
+ return [
10
+ mdTitle('API key created'),
11
+ `Full key (shown once): ${code(result.key)}`,
12
+ mdSection('Details', mdKeyValue([
13
+ ['Name', key.name],
14
+ ['Key ID', code(key.id)],
15
+ ['Prefix', code(key.key_prefix)],
16
+ ['Scopes', key.scopes.join(', ')],
17
+ ['Workspace', key.workspace_id ? code(key.workspace_id) : 'all accessible workspaces'],
18
+ ['Expires', key.expires_at || 'no expiry'],
19
+ ['Remaining key slots', result.limits?.remaining_user_keys ?? 'unknown'],
20
+ ])),
21
+ mdSection('Warning', result.warning || 'Store this securely. It will not be shown again.'),
22
+ ].filter(Boolean).join('\n\n');
23
+ }
24
+
25
+ export const createApiKeyTool = {
26
+ name: 'create_api_key',
27
+ description:
28
+ 'Create a new Posterly API key for the authenticated user. SECRET-CREATING WRITE: only use after explicit user confirmation. The new key can only request scopes already present on the calling dashboard-created API key; OAuth and managed assistant tokens cannot mint keys.',
29
+ inputSchema: z.object({
30
+ name: z.string().trim().min(1).max(120).optional().describe('Human-readable key name.'),
31
+ scopes: z.array(scopeSchema).min(1).max(6).optional().describe('Scopes for the new key. Omit to copy the calling key scopes. Cannot exceed the calling key scopes.'),
32
+ workspace_id: z.string().trim().min(1).optional().describe('Optional workspace restriction. Workspace-scoped calling keys cannot create keys outside their workspace.'),
33
+ expires_in_days: z.number().int().min(1).max(365).optional().describe('Optional expiry from now, up to 365 days. Omit for no expiry.'),
34
+ confirm: z.literal(true).describe('Must be true after the user explicitly confirms that a new secret API key should be created.'),
35
+ }),
36
+
37
+ async execute(client: PosterlyClient, input: CreateApiKeyPayload) {
38
+ const result = await client.createApiKey(input);
39
+ return formatCreatedApiKey(result);
40
+ },
41
+ };
@@ -86,11 +86,11 @@ export const createPostPayloadInputSchema = z.object({
86
86
  .string()
87
87
  .optional()
88
88
  .describe('ISO 8601 datetime for scheduling (e.g. 2026-03-05T09:00:00Z). Omit for immediate publish.'),
89
- media_url: z.string().optional().describe('URL of media to attach (image or video). For threads, attaches to the lead post only.'),
89
+ media_url: z.string().optional().describe('Media URL to attach. Posterly storage URLs are used as-is; third-party HTTP(S) URLs are copied into Posterly storage before the post is saved. Short-lived signed URLs must still be live when this tool runs. For threads, attaches to the lead post only.'),
90
90
  media_urls: z
91
91
  .array(z.string())
92
92
  .optional()
93
- .describe('Multiple media URLs for carousel or multi-image posts. For TikTok, 2 or more images auto-create a photo slideshow (up to 35 images) and every image is posted.'),
93
+ .describe('Multiple media URLs for carousel or multi-image posts. Posterly storage URLs are used as-is; third-party HTTP(S) URLs are copied into Posterly storage before the post is saved. For TikTok, 2 or more images auto-create a photo slideshow (up to 35 images) and every image is posted.'),
94
94
  post_type: z
95
95
  .string()
96
96
  .optional()
@@ -172,6 +172,7 @@ export const createPostTool = {
172
172
  '4. If scheduling multiple posts in one turn, list every post first and confirm the batch as a whole, then prefer `create_posts_batch` so they are created in one API request.\n\n' +
173
173
  'AFTER CALLING: Tell the user the post was created, include the Posterly dashboard link returned by the tool, and offer the next natural action. Do not narrate raw HTTP, curl, or API plumbing.\n\n' +
174
174
  '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.\n\n' +
175
+ 'MEDIA URLS: Posterly storage URLs are used as-is. Third-party HTTP(S) media URLs are copied into Posterly storage before the post is saved, so API-created posts do not later fail on expired signed URLs. Short-lived signed URLs must still be live when this tool runs. You can also call `upload_media_from_url` first if you want to upload once and reuse the returned Posterly URL across posts.\n\n' +
175
176
  'THREADS: Pass `thread_posts` (an array of 2+ strings) to schedule a multi-post thread on X (Twitter) or Threads (Meta). The first entry is the lead post; the rest are published as replies in the same chain. X entries are capped at 280 characters each (4000 for verified, 25000 for organization accounts); Threads entries are capped at 500 characters each. When `thread_posts` is set, `caption` is ignored.\n\n' +
176
177
  'PLATFORM SETTINGS: Pass `platform_settings` for composer-equivalent controls: Facebook story/reel/backgrounds, YouTube title/privacy/thumbnail/playlist, LinkedIn document titles/mentions/video thumbnails, TikTok direct-post privacy/toggles/commercial disclosure, Pinterest post_type (image/video/carousel), board (from pinterest.boards helper), title, link, and cover_image_url (video Pins), GBP event/offer/CTA, X reply settings/polls, Threads reply controls/text attachments, Bluesky languages/alt text, and Instagram feed/story/reel/carousel/collaborators/first comments/trial Reels. `instagram_settings` remains as a backwards-compatible alias.\n\n' +
177
178
  'TIKTOK MEDIA: TikTok supports a single video or a photo slideshow of 1 to 35 images, never multiple videos. Image media auto-detects as a slideshow, so you usually do not set post_type or media_type for photos and every image is posted. Pass `platform_settings.media_type: "PHOTO"` to force a slideshow explicitly.',
@@ -72,8 +72,8 @@ export const updatePostTool = {
72
72
  .string()
73
73
  .optional()
74
74
  .describe('New scheduled time (ISO 8601, e.g. 2026-03-10T09:00:00Z)'),
75
- media_url: z.string().optional().describe('New media URL'),
76
- media_urls: z.array(z.string()).optional().describe('New media URLs for carousel'),
75
+ media_url: z.string().optional().describe('New media URL. Posterly storage URLs are used as-is; third-party HTTP(S) URLs are copied into Posterly storage before the post is saved. Short-lived signed URLs must still be live when this tool runs.'),
76
+ media_urls: z.array(z.string()).optional().describe('New media URLs for carousel. Posterly storage URLs are used as-is; third-party HTTP(S) URLs are copied into Posterly storage before the post is saved.'),
77
77
  post_type: z
78
78
  .string()
79
79
  .optional()
@@ -4,7 +4,7 @@ import type { PosterlyClient } from '../lib/api-client.js';
4
4
  export const uploadMediaFromUrlTool = {
5
5
  name: 'upload_media_from_url',
6
6
  description:
7
- 'Fetch an image or video from a public HTTPS URL into posterly storage. Returns a URL that can be used with create_post.',
7
+ 'Fetch an image or video from a public HTTPS URL into Posterly storage. Returns a URL that can be used with create_post. You can also pass public HTTP(S) media URLs directly to create_post/update_post; use this tool when you want to upload once and reuse the returned Posterly URL.',
8
8
  inputSchema: z.object({
9
9
  url: z.string().url().describe('Public HTTPS URL for the image or video. Local/private network URLs are blocked.'),
10
10
  filename: z.string().optional().describe('Optional filename override.'),