posterly-mcp-server 0.36.0 → 0.37.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.
Files changed (37) hide show
  1. package/README.md +21 -1
  2. package/SUBMISSIONS.md +73 -0
  3. package/dist/index.js +80 -0
  4. package/dist/lib/api-client.d.ts +73 -0
  5. package/dist/lib/api-client.js +73 -0
  6. package/dist/lib/version.d.ts +1 -1
  7. package/dist/lib/version.js +1 -1
  8. package/dist/tools/get-comment.d.ts +20 -0
  9. package/dist/tools/get-comment.js +36 -0
  10. package/dist/tools/get-conversation.d.ts +24 -0
  11. package/dist/tools/get-conversation.js +31 -0
  12. package/dist/tools/list-comments.d.ts +40 -0
  13. package/dist/tools/list-comments.js +43 -0
  14. package/dist/tools/list-conversations.d.ts +40 -0
  15. package/dist/tools/list-conversations.js +35 -0
  16. package/dist/tools/reply-to-comment.d.ts +32 -0
  17. package/dist/tools/reply-to-comment.js +17 -0
  18. package/dist/tools/send-message.d.ts +28 -0
  19. package/dist/tools/send-message.js +16 -0
  20. package/dist/tools/sync-inbox.d.ts +24 -0
  21. package/dist/tools/sync-inbox.js +18 -0
  22. package/dist/tools/update-comment.d.ts +28 -0
  23. package/dist/tools/update-comment.js +21 -0
  24. package/package.json +1 -1
  25. package/server.json +2 -2
  26. package/src/index.ts +128 -0
  27. package/src/lib/api-client.ts +144 -0
  28. package/src/lib/version.ts +1 -1
  29. package/src/tools/get-comment.ts +47 -0
  30. package/src/tools/get-conversation.ts +44 -0
  31. package/src/tools/list-comments.ts +59 -0
  32. package/src/tools/list-conversations.ts +54 -0
  33. package/src/tools/reply-to-comment.ts +30 -0
  34. package/src/tools/send-message.ts +28 -0
  35. package/src/tools/sync-inbox.ts +28 -0
  36. package/src/tools/update-comment.ts +31 -0
  37. package/tool-annotations.json +21 -4
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.36.0` exposes 78 tools.
125
+ `posterly-mcp-server@0.37.0` exposes 86 tools.
126
126
 
127
127
  Public setup tools work before `POSTERLY_API_KEY` exists:
128
128
 
@@ -190,6 +190,14 @@ Authenticated tools require `POSTERLY_API_KEY`:
190
190
  - `get_post_insights` (list per-post feedback-loop insights: performance tier, diagnosis, next action, metrics, baseline)
191
191
  - `list_post_suggestions` (list evidence-based weekly post drafts in the account's learned voice, each with a rationale)
192
192
  - `dismiss_suggestion` (dismiss a post suggestion after confirmation; never touches one already scheduled)
193
+ - `list_conversations` (Instagram and Facebook Page inbox DMs; Threads has no DMs)
194
+ - `get_conversation`
195
+ - `send_message` (DM reply after explicit confirmation; honors Meta 24-hour windows)
196
+ - `list_comments` (Instagram, Facebook Page, and Threads inbox comments)
197
+ - `get_comment`
198
+ - `reply_to_comment` (comment reply after explicit confirmation)
199
+ - `update_comment` (hide/unhide or mark read; hide is reversible)
200
+ - `sync_inbox` (pull latest inbox data for one account; 30-second cooldown)
193
201
  - `list_google_business_reviews`
194
202
  - `get_google_business_review_link`
195
203
  - `audit_google_business_profile`
@@ -267,6 +275,18 @@ Each API call counts as one request, so use `create_posts_batch` when you need t
267
275
 
268
276
  Details: [poster.ly/dashboard/api](https://www.poster.ly/dashboard/api)
269
277
 
278
+ ## Hosted MCP clients (Claude.ai, ChatGPT)
279
+
280
+ Browser-hosted MCP hosts use the HTTP endpoint at [poster.ly/mcp](https://www.poster.ly/mcp) with OAuth instead of an API key. The HTTP endpoint supports RFC 7591 Dynamic Client Registration, so a host can self-register and complete a full connect flow without manual allowlisting:
281
+
282
+ - `POST /api/oauth/register`, send `{ client_name, redirect_uris, scope }`, receive a `client_id`.
283
+ - Standard authorization-code + PKCE at `/oauth/authorize`.
284
+ - 1-hour access tokens with 30-day rotating refresh tokens via `/api/oauth/token`.
285
+
286
+ Discovery document: [/.well-known/oauth-authorization-server](https://www.poster.ly/.well-known/oauth-authorization-server).
287
+
288
+ Local stdio still uses an API key in `POSTERLY_API_KEY`.
289
+
270
290
  ## Links
271
291
 
272
292
  - Docs: [poster.ly/mcp](https://www.poster.ly/mcp)
package/SUBMISSIONS.md ADDED
@@ -0,0 +1,73 @@
1
+ # MCP connector submissions
2
+
3
+ Notes for listing the hosted MCP server as a one-click connector. The pipework
4
+ (hosted HTTP MCP + stdio MCP) already works with API keys. Dynamic Client
5
+ Registration and refresh-token rotation are what Claude.ai / ChatGPT-style hosts
6
+ need for a connect flow without a human-allowlisted `client_id`.
7
+
8
+ Do not publish these notes as user-facing marketing copy. They are for the
9
+ person who applies the production migration and files the directory forms.
10
+
11
+ ## Preconditions
12
+
13
+ 1. Apply the DCR / refresh-token migration
14
+ (`supabase/migrations/20260906140000_oauth_dynamic_registration_and_refresh.sql`)
15
+ **before or with** the production deploy, not after. Shipping the app first
16
+ leaves `oauth_refresh_tokens` missing: the token endpoint then falls back to
17
+ a 365-day access-only grant for dynamic clients, and DCR verification that
18
+ expects refresh tokens will fail. Do not apply the SQL from an agent session
19
+ that can write straight to prod.
20
+ 2. Confirm `NEXT_PUBLIC_SITE_URL` is `https://www.poster.ly` in the production
21
+ deploy so discovery documents advertise the right issuer.
22
+ 3. Create a reviewer test account with the API add-on enabled, plus a short
23
+ setup note they can follow.
24
+ 4. Confirm discovery before submitting:
25
+ - `GET /.well-known/oauth-authorization-server` includes
26
+ `registration_endpoint` and `refresh_token` in `grant_types_supported`
27
+ - `POST /api/oauth/register` returns `201` with a `dyn_` `client_id`
28
+ - consent shows the self-registered warning, callback host, and `client_id`
29
+ for that client
30
+ - refresh-token reuse within 30 seconds returns `invalid_grant` without
31
+ revoking the chain; reuse older than 30 seconds revokes the chain
32
+ - `/api/mcp` returns `403` `origin_not_allowed` for an untrusted Origin
33
+ - `tools/list` includes annotations on every tool
34
+
35
+ ## Anthropic Connectors Directory
36
+
37
+ Submit the remote MCP server at https://clau.de/mcp-directory-submission.
38
+
39
+ Fallback contact: mcp-review@anthropic.com.
40
+
41
+ Use:
42
+
43
+ - Server URL: `https://www.poster.ly/api/mcp`
44
+ - Server card: `https://www.poster.ly/.well-known/mcp/server-card.json`
45
+ - Docs: `https://www.poster.ly/mcp` and `https://www.poster.ly/agents`
46
+ (public pages only; do not submit auth-gated `https://www.poster.ly/docs/...`
47
+ URLs)
48
+ - Auth: OAuth 2.1 + PKCE, RFC 7591 Dynamic Client Registration, rotating
49
+ refresh tokens (1-hour access, 30-day refresh for DCR clients)
50
+ - Platforms: all 18 production platforms
51
+
52
+ Do not claim Autopilot ships. Do not claim the connector is already listed.
53
+
54
+ Optional after deploy: bump and publish `posterly-mcp-server` if you want the
55
+ stdio README changes on npm. Hosted `/api/mcp` picks up this repo deploy
56
+ without an npm publish.
57
+
58
+ ## Other hosted hosts
59
+
60
+ ChatGPT-style hosts follow the same DCR + authorize + token flow. HTTPS
61
+ callbacks such as `https://chatgpt.com/oauth/callback` are already accepted by
62
+ the redirect URI validator. Extra browser origins can be added with
63
+ `MCP_TRUSTED_ORIGINS` (comma-separated hostnames or full URLs).
64
+
65
+ The ChatGPT plugin / app directory is a separate OpenAI form. File it after
66
+ this OAuth DCR deploy is live; it is not covered by the Anthropic connector
67
+ submission.
68
+
69
+ ## Registry metadata
70
+
71
+ `server.json` is the official MCP Registry metadata for the stdio package
72
+ (`io.github.awpthorp/posterly`). Publishing that package is a separate release
73
+ step. See `RELEASING.md`.
package/dist/index.js CHANGED
@@ -53,6 +53,14 @@ import { deleteGoogleBusinessReviewReplyTool } from './tools/delete-google-busin
53
53
  import { listGoogleBusinessMediaTool } from './tools/list-google-business-media.js';
54
54
  import { addGoogleBusinessMediaTool } from './tools/add-google-business-media.js';
55
55
  import { deleteGoogleBusinessMediaTool } from './tools/delete-google-business-media.js';
56
+ import { listConversationsTool } from './tools/list-conversations.js';
57
+ import { getConversationTool } from './tools/get-conversation.js';
58
+ import { sendMessageTool } from './tools/send-message.js';
59
+ import { listCommentsTool } from './tools/list-comments.js';
60
+ import { getCommentTool } from './tools/get-comment.js';
61
+ import { replyToCommentTool } from './tools/reply-to-comment.js';
62
+ import { updateCommentTool } from './tools/update-comment.js';
63
+ import { syncInboxTool } from './tools/sync-inbox.js';
56
64
  import { listActivityTool } from './tools/list-activity.js';
57
65
  import { getUpdatesTool } from './tools/get-updates.js';
58
66
  import { listWebhooksTool } from './tools/list-webhooks.js';
@@ -718,6 +726,78 @@ server.tool(deleteGoogleBusinessMediaTool.name, deleteGoogleBusinessMediaTool.de
718
726
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
719
727
  }
720
728
  });
729
+ server.tool(listConversationsTool.name, listConversationsTool.description, listConversationsTool.inputSchema.shape, getToolAnnotations(listConversationsTool.name), async (input) => {
730
+ try {
731
+ const text = await listConversationsTool.execute(client, input);
732
+ return { content: [{ type: 'text', text }] };
733
+ }
734
+ catch (err) {
735
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
736
+ }
737
+ });
738
+ server.tool(getConversationTool.name, getConversationTool.description, getConversationTool.inputSchema.shape, getToolAnnotations(getConversationTool.name), async (input) => {
739
+ try {
740
+ const text = await getConversationTool.execute(client, input);
741
+ return { content: [{ type: 'text', text }] };
742
+ }
743
+ catch (err) {
744
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
745
+ }
746
+ });
747
+ server.tool(sendMessageTool.name, sendMessageTool.description, sendMessageTool.inputSchema.shape, getToolAnnotations(sendMessageTool.name), async (input) => {
748
+ try {
749
+ const text = await sendMessageTool.execute(client, input);
750
+ return { content: [{ type: 'text', text }] };
751
+ }
752
+ catch (err) {
753
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
754
+ }
755
+ });
756
+ server.tool(listCommentsTool.name, listCommentsTool.description, listCommentsTool.inputSchema.shape, getToolAnnotations(listCommentsTool.name), async (input) => {
757
+ try {
758
+ const text = await listCommentsTool.execute(client, input);
759
+ return { content: [{ type: 'text', text }] };
760
+ }
761
+ catch (err) {
762
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
763
+ }
764
+ });
765
+ server.tool(getCommentTool.name, getCommentTool.description, getCommentTool.inputSchema.shape, getToolAnnotations(getCommentTool.name), async (input) => {
766
+ try {
767
+ const text = await getCommentTool.execute(client, input);
768
+ return { content: [{ type: 'text', text }] };
769
+ }
770
+ catch (err) {
771
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
772
+ }
773
+ });
774
+ server.tool(replyToCommentTool.name, replyToCommentTool.description, replyToCommentTool.inputSchema.shape, getToolAnnotations(replyToCommentTool.name), async (input) => {
775
+ try {
776
+ const text = await replyToCommentTool.execute(client, input);
777
+ return { content: [{ type: 'text', text }] };
778
+ }
779
+ catch (err) {
780
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
781
+ }
782
+ });
783
+ server.tool(updateCommentTool.name, updateCommentTool.description, updateCommentTool.inputSchema.shape, getToolAnnotations(updateCommentTool.name), async (input) => {
784
+ try {
785
+ const text = await updateCommentTool.execute(client, input);
786
+ return { content: [{ type: 'text', text }] };
787
+ }
788
+ catch (err) {
789
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
790
+ }
791
+ });
792
+ server.tool(syncInboxTool.name, syncInboxTool.description, syncInboxTool.inputSchema.shape, getToolAnnotations(syncInboxTool.name), async (input) => {
793
+ try {
794
+ const text = await syncInboxTool.execute(client, input);
795
+ return { content: [{ type: 'text', text }] };
796
+ }
797
+ catch (err) {
798
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
799
+ }
800
+ });
721
801
  server.tool(listActivityTool.name, listActivityTool.description, listActivityTool.inputSchema.shape, getToolAnnotations(listActivityTool.name), async (input) => {
722
802
  try {
723
803
  const text = await listActivityTool.execute(client, input);
@@ -624,6 +624,45 @@ export interface GoogleBusinessReviewLinkResponse {
624
624
  locationId: string | null;
625
625
  account_id: number;
626
626
  }
627
+ export interface InboxListParams {
628
+ workspace_id?: string;
629
+ account_id?: number | number[];
630
+ platform?: string | string[];
631
+ filter?: string;
632
+ search?: string;
633
+ limit?: number;
634
+ offset?: number;
635
+ }
636
+ export interface InboxConversationsResponse {
637
+ conversations: Array<Record<string, unknown>>;
638
+ total: number;
639
+ hasMore: boolean;
640
+ }
641
+ export interface InboxConversationResponse {
642
+ conversation: Record<string, unknown>;
643
+ messages: Array<Record<string, unknown>>;
644
+ }
645
+ export interface InboxCommentsResponse {
646
+ comments: Array<Record<string, unknown>>;
647
+ total: number;
648
+ hasMore: boolean;
649
+ }
650
+ export interface InboxCommentResponse {
651
+ comment: Record<string, unknown>;
652
+ }
653
+ export interface InboxSendMessageResponse {
654
+ success: true;
655
+ message_id: string;
656
+ }
657
+ export interface InboxCommentReplyResponse {
658
+ success: true;
659
+ reply_type: string;
660
+ reply_id: string;
661
+ }
662
+ export interface InboxSyncResponse {
663
+ success: true;
664
+ results: Record<string, unknown>;
665
+ }
627
666
  export interface ActivityItem {
628
667
  id: string;
629
668
  type: 'activity' | 'publish';
@@ -1243,6 +1282,40 @@ export declare class PosterlyClient {
1243
1282
  content_type: string;
1244
1283
  size: number;
1245
1284
  }>;
1285
+ private appendInboxListParams;
1286
+ listConversations(params?: InboxListParams): Promise<InboxConversationsResponse>;
1287
+ getConversation(params: {
1288
+ conversation_id: string;
1289
+ workspace_id?: string;
1290
+ limit?: number;
1291
+ }): Promise<InboxConversationResponse>;
1292
+ sendMessage(data: {
1293
+ conversation_id: string;
1294
+ content: string;
1295
+ workspace_id?: string;
1296
+ }): Promise<InboxSendMessageResponse>;
1297
+ listComments(params?: InboxListParams): Promise<InboxCommentsResponse>;
1298
+ getComment(params: {
1299
+ comment_id: string;
1300
+ workspace_id?: string;
1301
+ }): Promise<InboxCommentResponse>;
1302
+ replyToComment(data: {
1303
+ comment_id: string;
1304
+ content: string;
1305
+ type?: 'public' | 'private';
1306
+ workspace_id?: string;
1307
+ }): Promise<InboxCommentReplyResponse>;
1308
+ updateComment(data: {
1309
+ comment_id: string;
1310
+ is_hidden?: boolean;
1311
+ is_read?: boolean;
1312
+ workspace_id?: string;
1313
+ }): Promise<InboxCommentResponse>;
1314
+ syncInbox(data: {
1315
+ account_id: number;
1316
+ sync_type?: 'all' | 'dm' | 'comments';
1317
+ workspace_id?: string;
1318
+ }): Promise<InboxSyncResponse>;
1246
1319
  }
1247
1320
  export declare function resolvePosterlyBaseUrl(baseUrl?: string): string;
1248
1321
  type PosterlyBaseUrlInspection = {
@@ -553,6 +553,79 @@ export class PosterlyClient {
553
553
  content_type: input.contentType,
554
554
  });
555
555
  }
556
+ appendInboxListParams(searchParams, params) {
557
+ if (params?.workspace_id)
558
+ searchParams.set('workspace_id', params.workspace_id);
559
+ const accountIds = params?.account_id == null ? [] : Array.isArray(params.account_id) ? params.account_id : [params.account_id];
560
+ for (const accountId of accountIds)
561
+ searchParams.append('account_id', String(accountId));
562
+ const platforms = params?.platform == null ? [] : Array.isArray(params.platform) ? params.platform : [params.platform];
563
+ for (const platform of platforms)
564
+ searchParams.append('platform', platform);
565
+ if (params?.filter)
566
+ searchParams.set('filter', params.filter);
567
+ if (params?.search)
568
+ searchParams.set('search', params.search);
569
+ if (params?.limit)
570
+ searchParams.set('limit', String(params.limit));
571
+ if (params?.offset != null)
572
+ searchParams.set('offset', String(params.offset));
573
+ }
574
+ async listConversations(params) {
575
+ const searchParams = new URLSearchParams();
576
+ this.appendInboxListParams(searchParams, params);
577
+ const qs = searchParams.toString();
578
+ return this.request('GET', `/inbox/conversations${qs ? `?${qs}` : ''}`);
579
+ }
580
+ async getConversation(params) {
581
+ const searchParams = new URLSearchParams();
582
+ if (params.workspace_id)
583
+ searchParams.set('workspace_id', params.workspace_id);
584
+ if (params.limit)
585
+ searchParams.set('limit', String(params.limit));
586
+ const qs = searchParams.toString();
587
+ return this.request('GET', `/inbox/conversations/${encodeURIComponent(params.conversation_id)}${qs ? `?${qs}` : ''}`);
588
+ }
589
+ async sendMessage(data) {
590
+ const body = { content: data.content };
591
+ if (data.workspace_id)
592
+ body.workspace_id = data.workspace_id;
593
+ return this.request('POST', `/inbox/conversations/${encodeURIComponent(data.conversation_id)}/reply`, body);
594
+ }
595
+ async listComments(params) {
596
+ const searchParams = new URLSearchParams();
597
+ this.appendInboxListParams(searchParams, params);
598
+ const qs = searchParams.toString();
599
+ return this.request('GET', `/inbox/comments${qs ? `?${qs}` : ''}`);
600
+ }
601
+ async getComment(params) {
602
+ const searchParams = new URLSearchParams();
603
+ if (params.workspace_id)
604
+ searchParams.set('workspace_id', params.workspace_id);
605
+ const qs = searchParams.toString();
606
+ return this.request('GET', `/inbox/comments/${encodeURIComponent(params.comment_id)}${qs ? `?${qs}` : ''}`);
607
+ }
608
+ async replyToComment(data) {
609
+ const body = { content: data.content };
610
+ if (data.type)
611
+ body.type = data.type;
612
+ if (data.workspace_id)
613
+ body.workspace_id = data.workspace_id;
614
+ return this.request('POST', `/inbox/comments/${encodeURIComponent(data.comment_id)}/reply`, body);
615
+ }
616
+ async updateComment(data) {
617
+ const body = {};
618
+ if (typeof data.is_hidden === 'boolean')
619
+ body.is_hidden = data.is_hidden;
620
+ if (typeof data.is_read === 'boolean')
621
+ body.is_read = data.is_read;
622
+ if (data.workspace_id)
623
+ body.workspace_id = data.workspace_id;
624
+ return this.request('PATCH', `/inbox/comments/${encodeURIComponent(data.comment_id)}`, body);
625
+ }
626
+ async syncInbox(data) {
627
+ return this.request('POST', '/inbox/sync', data);
628
+ }
556
629
  }
557
630
  export function resolvePosterlyBaseUrl(baseUrl) {
558
631
  const configured = (baseUrl ||
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.36.0";
1
+ export declare const POSTERLY_MCP_VERSION = "0.37.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.36.0';
8
+ export const POSTERLY_MCP_VERSION = '0.37.0';
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const getCommentTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ comment_id: z.ZodString;
8
+ workspace_id: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ comment_id: string;
11
+ workspace_id?: string | undefined;
12
+ }, {
13
+ comment_id: string;
14
+ workspace_id?: string | undefined;
15
+ }>;
16
+ execute(client: PosterlyClient, input: {
17
+ comment_id: string;
18
+ workspace_id?: string;
19
+ }): Promise<string>;
20
+ };
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import { formatLocalDateTime } from '../lib/format.js';
3
+ export const getCommentTool = {
4
+ name: 'get_comment',
5
+ description: 'Get one social inbox comment and its replies. Instagram, Facebook Page, and Threads comments. Requires a Pro plan or higher plus posts:read.',
6
+ inputSchema: z.object({
7
+ comment_id: z.string().min(1).describe('Comment ID from list_comments.'),
8
+ workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
9
+ }),
10
+ async execute(client, input) {
11
+ const result = await client.getComment(input);
12
+ const comment = result.comment;
13
+ const author = typeof comment.author_username === 'string' ? `@${comment.author_username}` : 'unknown';
14
+ const lines = [
15
+ `Comment ${String(comment.id)}`,
16
+ `• Platform: ${typeof comment.platform === 'string' ? comment.platform : 'unknown'}`,
17
+ `• Author: ${author}`,
18
+ `• Hidden: ${comment.is_hidden ? 'yes' : 'no'}`,
19
+ `• Read: ${comment.is_read === false ? 'no' : 'yes'}`,
20
+ `• Posted: ${formatLocalDateTime(typeof comment.posted_at === 'string' ? comment.posted_at : null)}`,
21
+ `• Content: ${typeof comment.content === 'string' ? comment.content : '(empty)'}`,
22
+ ];
23
+ if (typeof comment.media_permalink === 'string')
24
+ lines.push(`• Media: ${comment.media_permalink}`);
25
+ const replies = Array.isArray(comment.replies) ? comment.replies : [];
26
+ if (replies.length > 0) {
27
+ lines.push('', `Replies (${replies.length}):`);
28
+ for (const reply of replies) {
29
+ const row = reply;
30
+ const replyAuthor = typeof row.author_username === 'string' ? `@${row.author_username}` : 'unknown';
31
+ lines.push(`- ${String(row.id)} ${replyAuthor}: ${typeof row.content === 'string' ? row.content : '(empty)'}`);
32
+ }
33
+ }
34
+ return lines.join('\n');
35
+ },
36
+ };
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const getConversationTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ conversation_id: z.ZodString;
8
+ workspace_id: z.ZodOptional<z.ZodString>;
9
+ limit: z.ZodOptional<z.ZodNumber>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ conversation_id: string;
12
+ workspace_id?: string | undefined;
13
+ limit?: number | undefined;
14
+ }, {
15
+ conversation_id: string;
16
+ workspace_id?: string | undefined;
17
+ limit?: number | undefined;
18
+ }>;
19
+ execute(client: PosterlyClient, input: {
20
+ conversation_id: string;
21
+ workspace_id?: string;
22
+ limit?: number;
23
+ }): Promise<string>;
24
+ };
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { formatLocalDateTime, truncateText } from '../lib/format.js';
3
+ export const getConversationTool = {
4
+ name: 'get_conversation',
5
+ description: 'Get one social inbox DM conversation and its messages. Instagram and Facebook Page DMs only. Requires a Pro plan or higher plus posts:read.',
6
+ inputSchema: z.object({
7
+ conversation_id: z.string().min(1).describe('Conversation ID from list_conversations.'),
8
+ workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
9
+ limit: z.number().int().min(1).max(100).optional().describe('Maximum messages to return. Default 50, max 100.'),
10
+ }),
11
+ async execute(client, input) {
12
+ const result = await client.getConversation(input);
13
+ const conversation = result.conversation;
14
+ const username = typeof conversation.participant_username === 'string' ? `@${conversation.participant_username}` : 'unknown';
15
+ const lines = [
16
+ `Conversation ${String(conversation.id)}`,
17
+ `• Platform: ${typeof conversation.platform === 'string' ? conversation.platform : 'unknown'}`,
18
+ `• Participant: ${username}`,
19
+ `• Read: ${conversation.is_read === false ? 'no' : 'yes'}`,
20
+ `• Messages: ${result.messages.length}`,
21
+ '',
22
+ ];
23
+ for (const message of result.messages) {
24
+ const who = message.sender_is_business ? 'you' : (typeof message.sender_username === 'string' ? message.sender_username : 'them');
25
+ const when = formatLocalDateTime(typeof message.sent_at === 'string' ? message.sent_at : null);
26
+ const text = truncateText(typeof message.content === 'string' ? message.content : '', 240);
27
+ lines.push(`- [${when}] ${who}: ${text}`);
28
+ }
29
+ return lines.join('\n');
30
+ },
31
+ };
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const listCommentsTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ workspace_id: z.ZodOptional<z.ZodString>;
8
+ account_id: z.ZodOptional<z.ZodNumber>;
9
+ platform: z.ZodOptional<z.ZodEnum<["instagram", "facebook", "threads"]>>;
10
+ filter: z.ZodOptional<z.ZodEnum<["all", "unread", "flagged"]>>;
11
+ search: z.ZodOptional<z.ZodString>;
12
+ limit: z.ZodOptional<z.ZodNumber>;
13
+ offset: z.ZodOptional<z.ZodNumber>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ platform?: "instagram" | "facebook" | "threads" | undefined;
16
+ workspace_id?: string | undefined;
17
+ account_id?: number | undefined;
18
+ limit?: number | undefined;
19
+ offset?: number | undefined;
20
+ filter?: "all" | "unread" | "flagged" | undefined;
21
+ search?: string | undefined;
22
+ }, {
23
+ platform?: "instagram" | "facebook" | "threads" | undefined;
24
+ workspace_id?: string | undefined;
25
+ account_id?: number | undefined;
26
+ limit?: number | undefined;
27
+ offset?: number | undefined;
28
+ filter?: "all" | "unread" | "flagged" | undefined;
29
+ search?: string | undefined;
30
+ }>;
31
+ execute(client: PosterlyClient, input: {
32
+ workspace_id?: string;
33
+ account_id?: number;
34
+ platform?: "instagram" | "facebook" | "threads";
35
+ filter?: "all" | "unread" | "flagged";
36
+ search?: string;
37
+ limit?: number;
38
+ offset?: number;
39
+ }): Promise<string>;
40
+ };
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ import { truncateText } from '../lib/format.js';
3
+ const INBOX_PLATFORMS = ['instagram', 'facebook', 'threads'];
4
+ export const listCommentsTool = {
5
+ name: 'list_comments',
6
+ description: 'List social inbox comments and replies for Instagram, Facebook Pages, and Threads. Filter by account, platform, unread/flagged, or search. Requires a Pro plan or higher plus posts:read.',
7
+ inputSchema: z.object({
8
+ workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
9
+ account_id: z.number().int().positive().optional().describe('Social account ID from list_accounts.'),
10
+ platform: z.enum(INBOX_PLATFORMS).optional().describe('instagram, facebook, or threads.'),
11
+ filter: z.enum(['all', 'unread', 'flagged']).optional().describe('Comment filter. Defaults to all.'),
12
+ search: z.string().optional().describe('Search comment content or author username.'),
13
+ limit: z.number().int().min(1).max(100).optional().describe('Number of comments to return. Default 50, max 100.'),
14
+ offset: z.number().int().min(0).optional().describe('Skip this many comments before returning results.'),
15
+ }),
16
+ async execute(client, input) {
17
+ const result = await client.listComments(input);
18
+ if (result.comments.length === 0) {
19
+ return 'No inbox comments found matching your filters.';
20
+ }
21
+ const lines = [`Inbox comments (showing ${result.comments.length} of ${result.total}):`];
22
+ for (const comment of result.comments) {
23
+ const author = typeof comment.author_username === 'string' ? `@${comment.author_username}` : 'unknown';
24
+ const platform = typeof comment.platform === 'string' ? comment.platform : 'unknown';
25
+ const flags = [];
26
+ if (comment.is_read === false)
27
+ flags.push('unread');
28
+ if (comment.is_hidden)
29
+ flags.push('hidden');
30
+ if (comment.is_flagged)
31
+ flags.push('flagged');
32
+ const flagLabel = flags.length > 0 ? `, ${flags.join(', ')}` : '';
33
+ const text = truncateText(typeof comment.content === 'string' ? comment.content : '', 80);
34
+ const replies = Array.isArray(comment.replies) ? comment.replies.length : 0;
35
+ lines.push(`- ${String(comment.id)} (${platform}, ${author}${flagLabel}${replies ? `, ${replies} replies` : ''}): ${text}`);
36
+ }
37
+ if (result.hasMore) {
38
+ const nextOffset = (input.offset ?? 0) + result.comments.length;
39
+ lines.push('', `More comments available. Call again with offset: ${nextOffset}.`);
40
+ }
41
+ return lines.join('\n');
42
+ },
43
+ };
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const listConversationsTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ workspace_id: z.ZodOptional<z.ZodString>;
8
+ account_id: z.ZodOptional<z.ZodNumber>;
9
+ platform: z.ZodOptional<z.ZodEnum<["instagram", "facebook", "threads"]>>;
10
+ filter: z.ZodOptional<z.ZodEnum<["all", "unread", "starred", "archived"]>>;
11
+ search: z.ZodOptional<z.ZodString>;
12
+ limit: z.ZodOptional<z.ZodNumber>;
13
+ offset: z.ZodOptional<z.ZodNumber>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ platform?: "instagram" | "facebook" | "threads" | undefined;
16
+ workspace_id?: string | undefined;
17
+ account_id?: number | undefined;
18
+ limit?: number | undefined;
19
+ offset?: number | undefined;
20
+ filter?: "all" | "unread" | "starred" | "archived" | undefined;
21
+ search?: string | undefined;
22
+ }, {
23
+ platform?: "instagram" | "facebook" | "threads" | undefined;
24
+ workspace_id?: string | undefined;
25
+ account_id?: number | undefined;
26
+ limit?: number | undefined;
27
+ offset?: number | undefined;
28
+ filter?: "all" | "unread" | "starred" | "archived" | undefined;
29
+ search?: string | undefined;
30
+ }>;
31
+ execute(client: PosterlyClient, input: {
32
+ workspace_id?: string;
33
+ account_id?: number;
34
+ platform?: "instagram" | "facebook" | "threads";
35
+ filter?: "all" | "unread" | "starred" | "archived";
36
+ search?: string;
37
+ limit?: number;
38
+ offset?: number;
39
+ }): Promise<string>;
40
+ };