posterly-mcp-server 0.38.0 → 0.40.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 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.38.0` exposes 87 tools.
125
+ `posterly-mcp-server@0.40.0` exposes 89 tools.
126
126
 
127
127
  Public setup tools work before `POSTERLY_API_KEY` exists:
128
128
 
@@ -176,6 +176,8 @@ Authenticated tools require `POSTERLY_API_KEY`:
176
176
  - `upload_media`
177
177
  - `upload_media_from_url`
178
178
  - `create_signed_upload`
179
+ - `create_media_drop` (human drop page at https://www.poster.ly/drop/<token> for ChatGPT/Claude laptop files)
180
+ - `list_media`
179
181
  - `find_available_slot`
180
182
  - `generate_captions`
181
183
  - `generate_image`
package/dist/index.js CHANGED
@@ -74,6 +74,8 @@ import { getPlatformSchemaTool } from './tools/get-platform-schema.js';
74
74
  import { triggerPlatformHelperTool } from './tools/trigger-platform-helper.js';
75
75
  import { getXPostingQuotaTool } from './tools/get-x-posting-quota.js';
76
76
  import { createSignedUploadTool } from './tools/create-signed-upload.js';
77
+ import { createMediaDropTool } from './tools/create-media-drop.js';
78
+ import { listMediaTool } from './tools/list-media.js';
77
79
  import { uploadMediaFromUrlTool } from './tools/upload-media-from-url.js';
78
80
  import { getConnectLinkTool } from './tools/get-connect-link.js';
79
81
  import { connectAccountTool } from './tools/connect-account.js';
@@ -502,6 +504,24 @@ server.tool(createSignedUploadTool.name, createSignedUploadTool.description, cre
502
504
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
503
505
  }
504
506
  });
507
+ server.tool(createMediaDropTool.name, createMediaDropTool.description, createMediaDropTool.inputSchema.shape, getToolAnnotations(createMediaDropTool.name), async (input) => {
508
+ try {
509
+ const text = await createMediaDropTool.execute(client, input);
510
+ return { content: [{ type: 'text', text }] };
511
+ }
512
+ catch (err) {
513
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
514
+ }
515
+ });
516
+ server.tool(listMediaTool.name, listMediaTool.description, listMediaTool.inputSchema.shape, getToolAnnotations(listMediaTool.name), async (input) => {
517
+ try {
518
+ const text = await listMediaTool.execute(client, input);
519
+ return { content: [{ type: 'text', text }] };
520
+ }
521
+ catch (err) {
522
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
523
+ }
524
+ });
505
525
  server.tool(getVideoOptionsTool.name, getVideoOptionsTool.description, getVideoOptionsTool.inputSchema.shape, getToolAnnotations(getVideoOptionsTool.name), async () => {
506
526
  try {
507
527
  const text = await getVideoOptionsTool.execute(client);
@@ -440,6 +440,12 @@ export interface Workspace {
440
440
  timezone?: string | null;
441
441
  role?: string;
442
442
  }
443
+ export interface WhoamiOnboarding {
444
+ completed: boolean;
445
+ next_step: string;
446
+ next_step_label: string;
447
+ instruction: string;
448
+ }
443
449
  export interface Whoami {
444
450
  user: {
445
451
  id: string;
@@ -451,6 +457,7 @@ export interface Whoami {
451
457
  };
452
458
  default_workspace: Workspace;
453
459
  workspaces: Workspace[];
460
+ onboarding?: WhoamiOnboarding;
454
461
  }
455
462
  export interface AccountAnalyticsSummary {
456
463
  current_followers: number;
@@ -1262,6 +1269,33 @@ export declare class PosterlyClient {
1262
1269
  public_url: string;
1263
1270
  headers?: Record<string, string>;
1264
1271
  }>;
1272
+ createMediaDrop(input?: {
1273
+ filename?: string;
1274
+ maxFiles?: number;
1275
+ }): Promise<{
1276
+ session_id: string;
1277
+ drop_url: string;
1278
+ expires_at: string;
1279
+ max_bytes: number;
1280
+ max_files: number;
1281
+ instructions: string;
1282
+ }>;
1283
+ listMedia(params?: {
1284
+ limit?: number;
1285
+ dropSessionId?: string;
1286
+ }): Promise<{
1287
+ media: Array<{
1288
+ id: string;
1289
+ public_url: string;
1290
+ filename: string;
1291
+ path: string;
1292
+ mime: string | null;
1293
+ bytes: number | null;
1294
+ created_at: string;
1295
+ drop_session_id: string | null;
1296
+ }>;
1297
+ total: number;
1298
+ }>;
1265
1299
  uploadMedia(input: {
1266
1300
  filePath?: string;
1267
1301
  base64Data?: string;
@@ -506,6 +506,21 @@ export class PosterlyClient {
506
506
  size,
507
507
  });
508
508
  }
509
+ async createMediaDrop(input) {
510
+ return this.request('POST', '/media/drop-sessions', {
511
+ filename: input?.filename,
512
+ max_files: input?.maxFiles,
513
+ });
514
+ }
515
+ async listMedia(params) {
516
+ const searchParams = new URLSearchParams();
517
+ if (params?.limit)
518
+ searchParams.set('limit', String(params.limit));
519
+ if (params?.dropSessionId)
520
+ searchParams.set('drop_session_id', params.dropSessionId);
521
+ const qs = searchParams.toString();
522
+ return this.request('GET', `/media${qs ? `?${qs}` : ''}`);
523
+ }
509
524
  async uploadMedia(input) {
510
525
  this.requireApiKey();
511
526
  let fileBuffer;
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.38.0";
1
+ export declare const POSTERLY_MCP_VERSION = "0.40.0";
@@ -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.38.0';
8
+ export const POSTERLY_MCP_VERSION = '0.40.0';
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const createMediaDropTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ filename: z.ZodOptional<z.ZodString>;
8
+ max_files: z.ZodOptional<z.ZodNumber>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ filename?: string | undefined;
11
+ max_files?: number | undefined;
12
+ }, {
13
+ filename?: string | undefined;
14
+ max_files?: number | undefined;
15
+ }>;
16
+ execute(client: PosterlyClient, input: {
17
+ filename?: string;
18
+ max_files?: number;
19
+ }): Promise<string>;
20
+ };
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ export const createMediaDropTool = {
3
+ name: 'create_media_drop',
4
+ description: 'Create a human drop page so ChatGPT or Claude users can upload a laptop file without dashboard login. Returns drop_url (https://www.poster.ly/drop/<token>). Send that URL to the user, wait, then call list_media. This is not the chat paperclip and not create_signed_upload.',
5
+ inputSchema: z.object({
6
+ filename: z.string().optional().describe('Optional filename hint for the user, e.g. launch-video.mp4'),
7
+ max_files: z
8
+ .number()
9
+ .int()
10
+ .min(1)
11
+ .max(10)
12
+ .optional()
13
+ .describe('How many files the drop can accept. Default 10, max 10.'),
14
+ }),
15
+ async execute(client, input) {
16
+ const drop = await client.createMediaDrop({
17
+ filename: input.filename,
18
+ maxFiles: input.max_files,
19
+ });
20
+ return [
21
+ 'Media drop created. Send this URL to the user. They open it with no posterly login and drop the file.',
22
+ `• Drop URL: ${drop.drop_url}`,
23
+ `• Session ID: ${drop.session_id}`,
24
+ `• Expires: ${drop.expires_at}`,
25
+ `• Max bytes: ${drop.max_bytes}`,
26
+ `• Max files: ${drop.max_files}`,
27
+ drop.instructions,
28
+ 'After they finish, call list_media with this drop_session_id, then validate_post / create_post using public_url.',
29
+ 'Chat attachments never reach MCP. HEIC and PDF are rejected.',
30
+ ].join('\n');
31
+ },
32
+ };
@@ -8,12 +8,12 @@ export declare const createSignedUploadTool: {
8
8
  content_type: z.ZodString;
9
9
  size: z.ZodNumber;
10
10
  }, "strip", z.ZodTypeAny, {
11
- content_type: string;
12
11
  filename: string;
12
+ content_type: string;
13
13
  size: number;
14
14
  }, {
15
- content_type: string;
16
15
  filename: string;
16
+ content_type: string;
17
17
  size: number;
18
18
  }>;
19
19
  execute(client: PosterlyClient, input: {
@@ -17,11 +17,12 @@ export const getAgentSignupInfoTool = {
17
17
  '- start_signup: starts paid signup and returns a Posterly checkout handoff URL plus a signup poll URL.',
18
18
  '- get_signup_session: polls checkout, payment, password, and agent-access status.',
19
19
  '',
20
- 'After Posterly access is installed:',
21
- '- whoami: confirm the paid Posterly account/workspace.',
22
- '- create_connect_session: create a secure browser OAuth handoff for the social platform the user chooses.',
23
- '- get_connect_session: poll the OAuth handoff until connected.',
24
- '- create_post: schedule posts after the user confirms the account and content.',
20
+ 'After Posterly access is installed, onboard the user IN THIS chat (do not send them to /onboarding-v2):',
21
+ '- whoami: confirm the paid account and read onboarding.next_step.',
22
+ '- About you: confirm name and IANA timezone.',
23
+ '- create_connect_session / get_connect_session: first social account in the browser.',
24
+ '- Brand voice: ask how they want to sound, then use that on captions.',
25
+ '- create_post: first scheduled post after validate_post and explicit confirm.',
25
26
  '',
26
27
  'Conversation style:',
27
28
  '- Keep the user updated in plain language: checkout pending, payment confirmed, password setup needed, account connected, post scheduled.',
@@ -59,9 +59,9 @@ function guidanceForStatus(status) {
59
59
  case 'authorize_agent':
60
60
  return 'Tell the user Posterly is ready for agent authorization. Send them the provided authorization or completion link if one is present.';
61
61
  case 'agent_access_required':
62
- return 'Tell the user API access is active, but this AI still needs Posterly access. Ask them to paste the Posterly MCP/API setup instructions here or install the Posterly MCP server, then continue with social account connection.';
62
+ return 'Tell the user API access is active, but this AI still needs Posterly access. Ask them to paste the Posterly MCP/API setup instructions here or install the Posterly MCP server. Then onboard them IN THIS chat: about you, connect, brand voice, first post. Do not send them to /onboarding-v2.';
63
63
  case 'api_access_active':
64
- return 'Posterly API access is active. Ask which social account the user wants to connect first, then create a connect session.';
64
+ return 'Posterly API access is active. Call whoami, then onboard them IN THIS chat: about you (name and timezone), connect the first account, brand voice, first post. Do not send them to /onboarding-v2.';
65
65
  default:
66
66
  return 'Explain the current status to the user and keep polling unless the session is terminal.';
67
67
  }
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const listMediaTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ limit: z.ZodOptional<z.ZodNumber>;
8
+ drop_session_id: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ limit?: number | undefined;
11
+ drop_session_id?: string | undefined;
12
+ }, {
13
+ limit?: number | undefined;
14
+ drop_session_id?: string | undefined;
15
+ }>;
16
+ execute(client: PosterlyClient, input: {
17
+ limit?: number;
18
+ drop_session_id?: string;
19
+ }): Promise<string>;
20
+ };
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ export const listMediaTool = {
3
+ name: 'list_media',
4
+ description: 'List the newest media assets for the authenticated user. After create_media_drop, pass drop_session_id to see files the human uploaded on /drop/<token>. Use public_url with validate_post / create_post.',
5
+ inputSchema: z.object({
6
+ limit: z
7
+ .number()
8
+ .int()
9
+ .min(1)
10
+ .max(50)
11
+ .optional()
12
+ .describe('Number of assets to return (default 10, max 50)'),
13
+ drop_session_id: z
14
+ .string()
15
+ .optional()
16
+ .describe('Filter to files uploaded through one create_media_drop session'),
17
+ }),
18
+ async execute(client, input) {
19
+ const result = await client.listMedia({
20
+ limit: input.limit,
21
+ dropSessionId: input.drop_session_id,
22
+ });
23
+ if (result.media.length === 0) {
24
+ return 'No media found. If you just sent a drop URL, wait for the user to finish uploading, then call list_media again.';
25
+ }
26
+ const lines = result.media.map((item) => {
27
+ const size = typeof item.bytes === 'number' ? `${item.bytes} bytes` : 'unknown size';
28
+ const drop = item.drop_session_id ? ` drop=${item.drop_session_id}` : '';
29
+ return `• ${item.filename} (${item.mime || 'unknown'}, ${size})${drop}\n public_url: ${item.public_url}`;
30
+ });
31
+ return [
32
+ `Media (${result.media.length} of ${result.total}):`,
33
+ ...lines,
34
+ '',
35
+ 'Pass public_url to validate_post / create_post. Do not invent URLs.',
36
+ ].join('\n');
37
+ },
38
+ };
@@ -78,7 +78,7 @@ function formatSignupStart(response, includeRaw) {
78
78
  '1. Send the Checkout handoff URL to the user so they can pay securely in their browser. Use the raw Stripe checkout URL only if you preserve the full URL including any fragment. posterly does not email this link, so do not tell the user to check their inbox at this stage.',
79
79
  '2. Call get_signup_session with the signup session ID until the status changes.',
80
80
  '3. Tell the user when Posterly sends the password setup email.',
81
- '4. If status becomes agent_access_required, ask the user to install or paste the Posterly MCP/API setup instructions into this trusted AI chat.',
81
+ '4. If status becomes agent_access_required, ask the user to install or paste the Posterly MCP/API setup instructions into this trusted AI chat, then onboard them in this conversation (about you, connect, brand voice, first post). Do not send them to /onboarding-v2.',
82
82
  '5. Do not ask for card details, the Posterly password, social passwords, or OAuth codes.',
83
83
  includeRaw ? `\nRaw signup response:\n${JSON.stringify(response, null, 2)}` : '',
84
84
  ];
@@ -9,12 +9,12 @@ export declare const uploadMediaFromUrlTool: {
9
9
  content_type: z.ZodOptional<z.ZodString>;
10
10
  }, "strip", z.ZodTypeAny, {
11
11
  url: string;
12
- content_type?: string | undefined;
13
12
  filename?: string | undefined;
13
+ content_type?: string | undefined;
14
14
  }, {
15
15
  url: string;
16
- content_type?: string | undefined;
17
16
  filename?: string | undefined;
17
+ content_type?: string | undefined;
18
18
  }>;
19
19
  execute(client: PosterlyClient, input: {
20
20
  url: string;
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { code, mdKeyValue, mdTable, mdTitle } from '../lib/format.js';
3
3
  export const whoamiTool = {
4
4
  name: 'whoami',
5
- description: 'Return the authenticated user, API key scopes, the default (personal) workspace, and every workspace the caller can post in. ALWAYS call this at the start of a session before creating, listing, or scheduling posts - posts created without an explicit workspace_id land in the default workspace shown here, and confirming with the user first prevents posts from appearing in the wrong workspace.',
5
+ description: 'Return the authenticated user, API key scopes, workspaces, and onboarding next step. ALWAYS call this at the start of a session. If onboarding.completed is false, walk the remaining onboarding steps IN THIS conversation (about you, connect, brand voice, first post). Do not send the user to /onboarding-v2 or the dashboard composer wizard.',
6
6
  inputSchema: z.object({}),
7
7
  async execute(client) {
8
8
  const info = await client.whoami();
@@ -13,7 +13,11 @@ export const whoamiTool = {
13
13
  ['API scopes', info.api_key.scopes.join(', ') || 'none'],
14
14
  ['Default workspace', `${info.default_workspace.name} (${code(info.default_workspace.id)})`],
15
15
  ['Default timezone', info.default_workspace.timezone || 'n/a'],
16
+ ['Onboarding', info.onboarding?.completed ? 'complete' : info.onboarding?.next_step_label || 'unknown'],
16
17
  ]),
18
+ info.onboarding && !info.onboarding.completed
19
+ ? `**Onboarding next:** ${info.onboarding.next_step_label}. ${info.onboarding.instruction}`
20
+ : '',
17
21
  mdTable(['Workspace', 'Workspace ID', 'Role', 'Timezone', 'Default'], info.workspaces.map((workspace) => [
18
22
  `${workspace.name}${workspace.is_personal ? ' (personal)' : ''}`,
19
23
  code(workspace.id),
@@ -22,6 +26,6 @@ export const whoamiTool = {
22
26
  workspace.id === info.default_workspace.id,
23
27
  ])),
24
28
  '**Tip:** Pass `workspace_id` to `create_post`, `list_posts`, `list_accounts`, and `find_available_slot` when you want the assistant to stay inside one workspace.',
25
- ].join('\n\n');
29
+ ].filter(Boolean).join('\n\n');
26
30
  },
27
31
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.38.0",
3
+ "version": "0.40.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",
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.38.0",
6
+ "version": "0.40.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.38.0",
17
+ "version": "0.40.0",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
package/src/index.ts CHANGED
@@ -75,6 +75,8 @@ import { getPlatformSchemaTool } from './tools/get-platform-schema.js';
75
75
  import { triggerPlatformHelperTool } from './tools/trigger-platform-helper.js';
76
76
  import { getXPostingQuotaTool } from './tools/get-x-posting-quota.js';
77
77
  import { createSignedUploadTool } from './tools/create-signed-upload.js';
78
+ import { createMediaDropTool } from './tools/create-media-drop.js';
79
+ import { listMediaTool } from './tools/list-media.js';
78
80
  import { uploadMediaFromUrlTool } from './tools/upload-media-from-url.js';
79
81
  import { getConnectLinkTool } from './tools/get-connect-link.js';
80
82
  import { connectAccountTool } from './tools/connect-account.js';
@@ -776,6 +778,36 @@ server.tool(
776
778
  }
777
779
  );
778
780
 
781
+ server.tool(
782
+ createMediaDropTool.name,
783
+ createMediaDropTool.description,
784
+ createMediaDropTool.inputSchema.shape,
785
+ getToolAnnotations(createMediaDropTool.name),
786
+ async (input) => {
787
+ try {
788
+ const text = await createMediaDropTool.execute(client, input as any);
789
+ return { content: [{ type: 'text' as const, text }] };
790
+ } catch (err: any) {
791
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
792
+ }
793
+ }
794
+ );
795
+
796
+ server.tool(
797
+ listMediaTool.name,
798
+ listMediaTool.description,
799
+ listMediaTool.inputSchema.shape,
800
+ getToolAnnotations(listMediaTool.name),
801
+ async (input) => {
802
+ try {
803
+ const text = await listMediaTool.execute(client, input as any);
804
+ return { content: [{ type: 'text' as const, text }] };
805
+ } catch (err: any) {
806
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
807
+ }
808
+ }
809
+ );
810
+
779
811
  server.tool(
780
812
  getVideoOptionsTool.name,
781
813
  getVideoOptionsTool.description,
@@ -465,11 +465,19 @@ export interface Workspace {
465
465
  role?: string;
466
466
  }
467
467
 
468
+ export interface WhoamiOnboarding {
469
+ completed: boolean;
470
+ next_step: string;
471
+ next_step_label: string;
472
+ instruction: string;
473
+ }
474
+
468
475
  export interface Whoami {
469
476
  user: { id: string; email: string | null };
470
477
  api_key: { id: string; scopes: string[] };
471
478
  default_workspace: Workspace;
472
479
  workspaces: Workspace[];
480
+ onboarding?: WhoamiOnboarding;
473
481
  }
474
482
 
475
483
  export interface AccountAnalyticsSummary {
@@ -1702,6 +1710,46 @@ export class PosterlyClient {
1702
1710
  });
1703
1711
  }
1704
1712
 
1713
+ async createMediaDrop(input?: {
1714
+ filename?: string;
1715
+ maxFiles?: number;
1716
+ }): Promise<{
1717
+ session_id: string;
1718
+ drop_url: string;
1719
+ expires_at: string;
1720
+ max_bytes: number;
1721
+ max_files: number;
1722
+ instructions: string;
1723
+ }> {
1724
+ return this.request('POST', '/media/drop-sessions', {
1725
+ filename: input?.filename,
1726
+ max_files: input?.maxFiles,
1727
+ });
1728
+ }
1729
+
1730
+ async listMedia(params?: {
1731
+ limit?: number;
1732
+ dropSessionId?: string;
1733
+ }): Promise<{
1734
+ media: Array<{
1735
+ id: string;
1736
+ public_url: string;
1737
+ filename: string;
1738
+ path: string;
1739
+ mime: string | null;
1740
+ bytes: number | null;
1741
+ created_at: string;
1742
+ drop_session_id: string | null;
1743
+ }>;
1744
+ total: number;
1745
+ }> {
1746
+ const searchParams = new URLSearchParams();
1747
+ if (params?.limit) searchParams.set('limit', String(params.limit));
1748
+ if (params?.dropSessionId) searchParams.set('drop_session_id', params.dropSessionId);
1749
+ const qs = searchParams.toString();
1750
+ return this.request('GET', `/media${qs ? `?${qs}` : ''}`);
1751
+ }
1752
+
1705
1753
  async uploadMedia(input: {
1706
1754
  filePath?: string;
1707
1755
  base64Data?: string;
@@ -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.38.0';
8
+ export const POSTERLY_MCP_VERSION = '0.40.0';
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ export const createMediaDropTool = {
5
+ name: 'create_media_drop',
6
+ description:
7
+ 'Create a human drop page so ChatGPT or Claude users can upload a laptop file without dashboard login. Returns drop_url (https://www.poster.ly/drop/<token>). Send that URL to the user, wait, then call list_media. This is not the chat paperclip and not create_signed_upload.',
8
+ inputSchema: z.object({
9
+ filename: z.string().optional().describe('Optional filename hint for the user, e.g. launch-video.mp4'),
10
+ max_files: z
11
+ .number()
12
+ .int()
13
+ .min(1)
14
+ .max(10)
15
+ .optional()
16
+ .describe('How many files the drop can accept. Default 10, max 10.'),
17
+ }),
18
+
19
+ async execute(
20
+ client: PosterlyClient,
21
+ input: { filename?: string; max_files?: number },
22
+ ) {
23
+ const drop = await client.createMediaDrop({
24
+ filename: input.filename,
25
+ maxFiles: input.max_files,
26
+ });
27
+ return [
28
+ 'Media drop created. Send this URL to the user. They open it with no posterly login and drop the file.',
29
+ `• Drop URL: ${drop.drop_url}`,
30
+ `• Session ID: ${drop.session_id}`,
31
+ `• Expires: ${drop.expires_at}`,
32
+ `• Max bytes: ${drop.max_bytes}`,
33
+ `• Max files: ${drop.max_files}`,
34
+ drop.instructions,
35
+ 'After they finish, call list_media with this drop_session_id, then validate_post / create_post using public_url.',
36
+ 'Chat attachments never reach MCP. HEIC and PDF are rejected.',
37
+ ].join('\n');
38
+ },
39
+ };
@@ -22,11 +22,12 @@ export const getAgentSignupInfoTool = {
22
22
  '- start_signup: starts paid signup and returns a Posterly checkout handoff URL plus a signup poll URL.',
23
23
  '- get_signup_session: polls checkout, payment, password, and agent-access status.',
24
24
  '',
25
- 'After Posterly access is installed:',
26
- '- whoami: confirm the paid Posterly account/workspace.',
27
- '- create_connect_session: create a secure browser OAuth handoff for the social platform the user chooses.',
28
- '- get_connect_session: poll the OAuth handoff until connected.',
29
- '- create_post: schedule posts after the user confirms the account and content.',
25
+ 'After Posterly access is installed, onboard the user IN THIS chat (do not send them to /onboarding-v2):',
26
+ '- whoami: confirm the paid account and read onboarding.next_step.',
27
+ '- About you: confirm name and IANA timezone.',
28
+ '- create_connect_session / get_connect_session: first social account in the browser.',
29
+ '- Brand voice: ask how they want to sound, then use that on captions.',
30
+ '- create_post: first scheduled post after validate_post and explicit confirm.',
30
31
  '',
31
32
  'Conversation style:',
32
33
  '- Keep the user updated in plain language: checkout pending, payment confirmed, password setup needed, account connected, post scheduled.',
@@ -68,9 +68,9 @@ function guidanceForStatus(status: string): string {
68
68
  case 'authorize_agent':
69
69
  return 'Tell the user Posterly is ready for agent authorization. Send them the provided authorization or completion link if one is present.';
70
70
  case 'agent_access_required':
71
- return 'Tell the user API access is active, but this AI still needs Posterly access. Ask them to paste the Posterly MCP/API setup instructions here or install the Posterly MCP server, then continue with social account connection.';
71
+ return 'Tell the user API access is active, but this AI still needs Posterly access. Ask them to paste the Posterly MCP/API setup instructions here or install the Posterly MCP server. Then onboard them IN THIS chat: about you, connect, brand voice, first post. Do not send them to /onboarding-v2.';
72
72
  case 'api_access_active':
73
- return 'Posterly API access is active. Ask which social account the user wants to connect first, then create a connect session.';
73
+ return 'Posterly API access is active. Call whoami, then onboard them IN THIS chat: about you (name and timezone), connect the first account, brand voice, first post. Do not send them to /onboarding-v2.';
74
74
  default:
75
75
  return 'Explain the current status to the user and keep polling unless the session is terminal.';
76
76
  }
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ export const listMediaTool = {
5
+ name: 'list_media',
6
+ description:
7
+ 'List the newest media assets for the authenticated user. After create_media_drop, pass drop_session_id to see files the human uploaded on /drop/<token>. Use public_url with validate_post / create_post.',
8
+ inputSchema: z.object({
9
+ limit: z
10
+ .number()
11
+ .int()
12
+ .min(1)
13
+ .max(50)
14
+ .optional()
15
+ .describe('Number of assets to return (default 10, max 50)'),
16
+ drop_session_id: z
17
+ .string()
18
+ .optional()
19
+ .describe('Filter to files uploaded through one create_media_drop session'),
20
+ }),
21
+
22
+ async execute(
23
+ client: PosterlyClient,
24
+ input: { limit?: number; drop_session_id?: string },
25
+ ) {
26
+ const result = await client.listMedia({
27
+ limit: input.limit,
28
+ dropSessionId: input.drop_session_id,
29
+ });
30
+
31
+ if (result.media.length === 0) {
32
+ return 'No media found. If you just sent a drop URL, wait for the user to finish uploading, then call list_media again.';
33
+ }
34
+
35
+ const lines = result.media.map((item) => {
36
+ const size = typeof item.bytes === 'number' ? `${item.bytes} bytes` : 'unknown size';
37
+ const drop = item.drop_session_id ? ` drop=${item.drop_session_id}` : '';
38
+ return `• ${item.filename} (${item.mime || 'unknown'}, ${size})${drop}\n public_url: ${item.public_url}`;
39
+ });
40
+
41
+ return [
42
+ `Media (${result.media.length} of ${result.total}):`,
43
+ ...lines,
44
+ '',
45
+ 'Pass public_url to validate_post / create_post. Do not invent URLs.',
46
+ ].join('\n');
47
+ },
48
+ };
@@ -99,7 +99,7 @@ function formatSignupStart(response: PublicSignupResponse, includeRaw: boolean):
99
99
  '1. Send the Checkout handoff URL to the user so they can pay securely in their browser. Use the raw Stripe checkout URL only if you preserve the full URL including any fragment. posterly does not email this link, so do not tell the user to check their inbox at this stage.',
100
100
  '2. Call get_signup_session with the signup session ID until the status changes.',
101
101
  '3. Tell the user when Posterly sends the password setup email.',
102
- '4. If status becomes agent_access_required, ask the user to install or paste the Posterly MCP/API setup instructions into this trusted AI chat.',
102
+ '4. If status becomes agent_access_required, ask the user to install or paste the Posterly MCP/API setup instructions into this trusted AI chat, then onboard them in this conversation (about you, connect, brand voice, first post). Do not send them to /onboarding-v2.',
103
103
  '5. Do not ask for card details, the Posterly password, social passwords, or OAuth codes.',
104
104
  includeRaw ? `\nRaw signup response:\n${JSON.stringify(response, null, 2)}` : '',
105
105
  ];
@@ -5,7 +5,7 @@ import { code, mdKeyValue, mdTable, mdTitle } from '../lib/format.js';
5
5
  export const whoamiTool = {
6
6
  name: 'whoami',
7
7
  description:
8
- 'Return the authenticated user, API key scopes, the default (personal) workspace, and every workspace the caller can post in. ALWAYS call this at the start of a session before creating, listing, or scheduling posts - posts created without an explicit workspace_id land in the default workspace shown here, and confirming with the user first prevents posts from appearing in the wrong workspace.',
8
+ 'Return the authenticated user, API key scopes, workspaces, and onboarding next step. ALWAYS call this at the start of a session. If onboarding.completed is false, walk the remaining onboarding steps IN THIS conversation (about you, connect, brand voice, first post). Do not send the user to /onboarding-v2 or the dashboard composer wizard.',
9
9
  inputSchema: z.object({}),
10
10
 
11
11
  async execute(client: PosterlyClient) {
@@ -18,7 +18,11 @@ export const whoamiTool = {
18
18
  ['API scopes', info.api_key.scopes.join(', ') || 'none'],
19
19
  ['Default workspace', `${info.default_workspace.name} (${code(info.default_workspace.id)})`],
20
20
  ['Default timezone', info.default_workspace.timezone || 'n/a'],
21
+ ['Onboarding', info.onboarding?.completed ? 'complete' : info.onboarding?.next_step_label || 'unknown'],
21
22
  ]),
23
+ info.onboarding && !info.onboarding.completed
24
+ ? `**Onboarding next:** ${info.onboarding.next_step_label}. ${info.onboarding.instruction}`
25
+ : '',
22
26
  mdTable(
23
27
  ['Workspace', 'Workspace ID', 'Role', 'Timezone', 'Default'],
24
28
  info.workspaces.map((workspace) => [
@@ -30,6 +34,6 @@ export const whoamiTool = {
30
34
  ]),
31
35
  ),
32
36
  '**Tip:** Pass `workspace_id` to `create_post`, `list_posts`, `list_accounts`, and `find_available_slot` when you want the assistant to stay inside one workspace.',
33
- ].join('\n\n');
37
+ ].filter(Boolean).join('\n\n');
34
38
  },
35
39
  };
@@ -45,6 +45,8 @@
45
45
  "upload_media",
46
46
  "upload_media_from_url",
47
47
  "create_signed_upload",
48
+ "create_media_drop",
49
+ "list_media",
48
50
  "get_video_options",
49
51
  "run_video_function",
50
52
  "generate_video",
@@ -113,6 +115,7 @@
113
115
  "validate_post",
114
116
  "find_available_slot",
115
117
  "list_posts",
118
+ "list_media",
116
119
  "get_video_options",
117
120
  "get_video_job",
118
121
  "get_post",
@@ -196,6 +199,7 @@
196
199
  "create_posts_batch",
197
200
  "upload_media_from_url",
198
201
  "create_signed_upload",
202
+ "create_media_drop",
199
203
  "run_video_function",
200
204
  "generate_video",
201
205
  "get_video_job",