posterly-mcp-server 0.23.2 → 0.24.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 (41) hide show
  1. package/README.md +5 -1
  2. package/dist/generated/platform-manifest.d.ts +82 -66
  3. package/dist/generated/platform-manifest.js +126 -77
  4. package/dist/index.js +40 -0
  5. package/dist/lib/api-client.d.ts +77 -0
  6. package/dist/lib/api-client.js +30 -0
  7. package/dist/lib/version.d.ts +1 -1
  8. package/dist/lib/version.js +1 -1
  9. package/dist/tools/create-connect-session.d.ts +3 -3
  10. package/dist/tools/create-connect-session.js +1 -1
  11. package/dist/tools/create-post.d.ts +27 -9
  12. package/dist/tools/create-post.js +1 -0
  13. package/dist/tools/create-posts-batch.d.ts +12 -5
  14. package/dist/tools/dismiss-suggestion.d.ts +16 -0
  15. package/dist/tools/dismiss-suggestion.js +18 -0
  16. package/dist/tools/get-connect-link.d.ts +3 -3
  17. package/dist/tools/get-performance-profile.d.ts +16 -0
  18. package/dist/tools/get-performance-profile.js +39 -0
  19. package/dist/tools/get-platform-schema.d.ts +3 -3
  20. package/dist/tools/get-post-insights.d.ts +28 -0
  21. package/dist/tools/get-post-insights.js +39 -0
  22. package/dist/tools/get-video-job.d.ts +2 -2
  23. package/dist/tools/list-activity.d.ts +2 -2
  24. package/dist/tools/list-post-suggestions.d.ts +24 -0
  25. package/dist/tools/list-post-suggestions.js +36 -0
  26. package/dist/tools/list-posts.d.ts +3 -3
  27. package/dist/tools/trigger-platform-helper.d.ts +6 -6
  28. package/dist/tools/update-post.d.ts +8 -0
  29. package/dist/tools/update-post.js +1 -0
  30. package/package.json +1 -1
  31. package/src/generated/platform-manifest.ts +126 -77
  32. package/src/index.ts +60 -0
  33. package/src/lib/api-client.ts +107 -0
  34. package/src/lib/version.ts +1 -1
  35. package/src/tools/create-connect-session.ts +1 -1
  36. package/src/tools/create-post.ts +1 -0
  37. package/src/tools/dismiss-suggestion.ts +22 -0
  38. package/src/tools/get-performance-profile.ts +47 -0
  39. package/src/tools/get-post-insights.ts +60 -0
  40. package/src/tools/list-post-suggestions.ts +56 -0
  41. package/src/tools/update-post.ts +1 -0
@@ -62,6 +62,77 @@ export interface LearnedVoice {
62
62
  derived_at: string | null;
63
63
  }
64
64
 
65
+ export interface PerformanceProfile {
66
+ social_account_id: number;
67
+ platform: string;
68
+ username: string | null;
69
+ sample_size: number;
70
+ window_days: number;
71
+ engagement_rate_avg: number | null;
72
+ engagement_rate_trend: 'up' | 'down' | 'flat' | 'unknown';
73
+ bullets: string[];
74
+ narrative_summary: string | null;
75
+ stats: Record<string, unknown>;
76
+ derived_at: string | null;
77
+ }
78
+
79
+ export type PerformanceProfileNullReason =
80
+ | 'insufficient_data'
81
+ | 'feature_not_available'
82
+ | 'platform_not_measurable';
83
+
84
+ export interface PerformanceProfileResponse {
85
+ performance_profile: PerformanceProfile | null;
86
+ reason: PerformanceProfileNullReason | null;
87
+ }
88
+
89
+ export interface FeedbackInsight {
90
+ id: string;
91
+ post_id: number;
92
+ social_account_id: number;
93
+ username: string | null;
94
+ platform: string;
95
+ checkpoint: '1h' | '6h' | '24h' | '72h' | '7d';
96
+ performance_tier: string;
97
+ action_type: string;
98
+ title: string;
99
+ diagnosis: string;
100
+ next_action: string;
101
+ impact_score: number;
102
+ confidence: number;
103
+ metrics: Record<string, unknown>;
104
+ baseline: Record<string, unknown>;
105
+ caption: string | null;
106
+ permalink: string | null;
107
+ published_at: string | null;
108
+ created_at: string;
109
+ }
110
+
111
+ export interface FeedbackInsightsResponse {
112
+ insights: FeedbackInsight[];
113
+ }
114
+
115
+ export interface PostSuggestion {
116
+ id: string;
117
+ social_account_id: number;
118
+ platform: string;
119
+ account: { id: number; username: string | null; platform: string };
120
+ caption: string;
121
+ rationale: string | null;
122
+ period_key: string | null;
123
+ status: string;
124
+ created_at: string;
125
+ }
126
+
127
+ export interface PostSuggestionsResponse {
128
+ suggestions: PostSuggestion[];
129
+ }
130
+
131
+ export interface DismissSuggestionResponse {
132
+ ok: boolean;
133
+ status: string;
134
+ }
135
+
65
136
  export interface Post {
66
137
  id: number;
67
138
  content: string;
@@ -889,6 +960,42 @@ export class PosterlyClient {
889
960
  return this.request('GET', `/accounts/${encodeURIComponent(String(id))}/learned-voice`);
890
961
  }
891
962
 
963
+ async getAccountPerformanceProfile(id: string | number): Promise<PerformanceProfileResponse> {
964
+ return this.request('GET', `/accounts/${encodeURIComponent(String(id))}/performance-profile`);
965
+ }
966
+
967
+ async getPostInsights(params?: {
968
+ account_id?: string | number;
969
+ post_id?: number;
970
+ checkpoint?: '1h' | '6h' | '24h' | '72h' | '7d';
971
+ limit?: number;
972
+ }): Promise<FeedbackInsightsResponse> {
973
+ const searchParams = new URLSearchParams();
974
+ if (params?.account_id) searchParams.set('account_id', String(params.account_id));
975
+ if (params?.post_id) searchParams.set('post_id', String(params.post_id));
976
+ if (params?.checkpoint) searchParams.set('checkpoint', params.checkpoint);
977
+ if (params?.limit) searchParams.set('limit', String(params.limit));
978
+ const qs = searchParams.toString();
979
+ return this.request('GET', `/analytics/insights${qs ? `?${qs}` : ''}`);
980
+ }
981
+
982
+ async listPostSuggestions(params?: {
983
+ account_id?: string | number;
984
+ status?: 'pending' | 'scheduled' | 'dismissed';
985
+ limit?: number;
986
+ }): Promise<PostSuggestionsResponse> {
987
+ const searchParams = new URLSearchParams();
988
+ if (params?.account_id) searchParams.set('account_id', String(params.account_id));
989
+ if (params?.status) searchParams.set('status', params.status);
990
+ if (params?.limit) searchParams.set('limit', String(params.limit));
991
+ const qs = searchParams.toString();
992
+ return this.request('GET', `/suggestions${qs ? `?${qs}` : ''}`);
993
+ }
994
+
995
+ async dismissSuggestion(id: string): Promise<DismissSuggestionResponse> {
996
+ return this.request('POST', `/suggestions/${encodeURIComponent(id)}/dismiss`, {});
997
+ }
998
+
892
999
  async listPosts(params?: {
893
1000
  status?: string;
894
1001
  platform?: 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.23.1';
8
+ export const POSTERLY_MCP_VERSION = '0.24.0';
@@ -10,7 +10,7 @@ export const createConnectSessionTool = {
10
10
  inputSchema: z.object({
11
11
  platform: z
12
12
  .enum(CONNECT_INPUTS)
13
- .describe('Connection target such as instagram, meta, linkedin_page, twitter, google_business, telegram, or bluesky.'),
13
+ .describe('Connection target such as instagram, meta, linkedin_page, twitter, google_business, telegram, bluesky, discord, mastodon, devto, hashnode, wordpress, or lemmy.'),
14
14
  workspace_id: z
15
15
  .string()
16
16
  .optional()
@@ -39,6 +39,7 @@ export const platformSettingsSchema = z.object({
39
39
  // Threads
40
40
  reply_control: z.enum(['everyone', 'accounts_you_follow', 'mentioned_only', 'followers_only', 'parent_post_author_only']).optional(),
41
41
  topic_tag: z.string().optional(), // Threads single discovery topic tag
42
+ share_to_instagram: z.boolean().optional(), // Threads: also share the published post to the linked Instagram account as a Story
42
43
  text_attachment: z.record(z.unknown()).optional(),
43
44
  // Facebook
44
45
  text_format_preset_id: z.string().optional(),
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { mdSuccess } from '../lib/format.js';
4
+
5
+ export const dismissSuggestionTool = {
6
+ name: 'dismiss_suggestion',
7
+ description:
8
+ 'Dismiss a proactive post suggestion so it stops appearing. WRITE: confirm the exact suggestion with the user first. Never dismisses a suggestion that was already turned into a scheduled post (that would break adoption tracking).',
9
+ inputSchema: z.object({
10
+ suggestion_id: z
11
+ .string()
12
+ .describe('Suggestion ID from list_post_suggestions.'),
13
+ }),
14
+
15
+ async execute(client: PosterlyClient, input: { suggestion_id: string }) {
16
+ const result = await client.dismissSuggestion(input.suggestion_id);
17
+ return mdSuccess('Suggestion dismissed', [
18
+ ['Suggestion ID', input.suggestion_id],
19
+ ['Status', result.status || 'dismissed'],
20
+ ]);
21
+ },
22
+ };
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { mdTitle, mdSection, mdKeyValue, mdBullets, mdQuote, mdEmpty, formatPercent, formatDateTime } from '../lib/format.js';
4
+
5
+ export const getPerformanceProfileTool = {
6
+ name: 'get_performance_profile',
7
+ description:
8
+ "Get the performance profile posterly has derived for a connected account from the last 90 days of per-post analytics. Returns coaching stats (format, timing, caption-length patterns), an engagement-rate trend, and a narrative summary. Keyed by social account ID (from list_accounts or list_brand_accounts). Returns nothing until the account has a derived profile; Google Business Profile and other platforms without per-post analytics never get one. Read-only; requires a Pro plan or higher.",
9
+ inputSchema: z.object({
10
+ account_id: z
11
+ .union([z.string(), z.number()])
12
+ .describe('The social account ID to inspect (from list_accounts or list_brand_accounts).'),
13
+ }),
14
+
15
+ async execute(client: PosterlyClient, input: { account_id: string | number }) {
16
+ const { performance_profile, reason } = await client.getAccountPerformanceProfile(input.account_id);
17
+
18
+ if (!performance_profile) {
19
+ const detail = reason === 'platform_not_measurable'
20
+ ? 'This platform does not produce per-post analytics, so posterly cannot build a performance profile for it.'
21
+ : reason === 'feature_not_available'
22
+ ? 'Performance profiles require a Pro plan or higher.'
23
+ : 'posterly has not built a performance profile for this account yet. It needs about 90 days of tracked posts to learn from.';
24
+ return mdEmpty('performance profile yet', detail);
25
+ }
26
+
27
+ const label = performance_profile.username
28
+ ? `@${performance_profile.username}${performance_profile.platform ? ` (${performance_profile.platform})` : ''}`
29
+ : `account ${performance_profile.social_account_id}`;
30
+
31
+ const rate = performance_profile.engagement_rate_avg != null
32
+ ? formatPercent(performance_profile.engagement_rate_avg * 100)
33
+ : 'n/a';
34
+
35
+ return [
36
+ mdTitle('Account performance profile', `${label} · last ${performance_profile.window_days ?? 90} days`),
37
+ performance_profile.narrative_summary ? mdSection('Summary', mdQuote(performance_profile.narrative_summary)) : '',
38
+ performance_profile.bullets.length ? mdSection('What is working', mdBullets(performance_profile.bullets)) : '',
39
+ mdSection('Stats', mdKeyValue([
40
+ ['Posts sampled', performance_profile.sample_size],
41
+ ['Avg engagement rate', rate],
42
+ ['Engagement trend', performance_profile.engagement_rate_trend],
43
+ ['Derived at', formatDateTime(performance_profile.derived_at)],
44
+ ])),
45
+ ].filter(Boolean).join('\n\n');
46
+ },
47
+ };
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { mdTitle, mdSection, mdKeyValue, mdEmpty, truncateText, formatDateTime } from '../lib/format.js';
4
+
5
+ export const getPostInsightsTool = {
6
+ name: 'get_post_insights',
7
+ description:
8
+ 'List per-post feedback insights from the posterly performance feedback loop: for recently published posts, the performance tier (great/good/mixed/poor), a diagnosis, a suggested next action, metrics, and the baseline it was compared against. Filter by account_id, post_id, and checkpoint (1h, 6h, 24h, 72h, 7d). Read-only; requires a Pro plan or higher.',
9
+ inputSchema: z.object({
10
+ account_id: z
11
+ .union([z.string(), z.number()])
12
+ .optional()
13
+ .describe('Filter to one social account ID (from list_accounts).'),
14
+ post_id: z.number().int().positive().optional().describe('Filter to one post ID.'),
15
+ checkpoint: z
16
+ .enum(['1h', '6h', '24h', '72h', '7d'])
17
+ .optional()
18
+ .describe('Filter to one checkpoint after publish.'),
19
+ limit: z.number().int().min(1).max(100).optional().describe('Max insights to return (default 20).'),
20
+ }),
21
+
22
+ async execute(
23
+ client: PosterlyClient,
24
+ input: {
25
+ account_id?: string | number;
26
+ post_id?: number;
27
+ checkpoint?: '1h' | '6h' | '24h' | '72h' | '7d';
28
+ limit?: number;
29
+ },
30
+ ) {
31
+ const { insights } = await client.getPostInsights(input);
32
+
33
+ if (!insights.length) {
34
+ return mdEmpty(
35
+ 'feedback insights',
36
+ 'No per-post feedback insights are available for these filters yet. Insights appear as posts hit their 1h, 6h, 24h, 72h, and 7d checkpoints.',
37
+ );
38
+ }
39
+
40
+ const blocks = insights.map((insight) => {
41
+ const heading = `${insight.title || 'Insight'} (${insight.performance_tier}, ${insight.checkpoint})`;
42
+ return mdSection(
43
+ heading,
44
+ mdKeyValue([
45
+ ['Post ID', insight.post_id],
46
+ ['Platform', insight.platform],
47
+ ['Account', insight.username ? `@${insight.username}` : String(insight.social_account_id)],
48
+ ['Diagnosis', truncateText(insight.diagnosis, 220)],
49
+ ['Next action', truncateText(insight.next_action, 220)],
50
+ ['Impact score', insight.impact_score],
51
+ ['Confidence', insight.confidence],
52
+ ['Published', formatDateTime(insight.published_at)],
53
+ ['Permalink', insight.permalink || undefined],
54
+ ]),
55
+ );
56
+ });
57
+
58
+ return [mdTitle(`Post feedback insights (${insights.length})`), ...blocks].join('\n\n');
59
+ },
60
+ };
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { mdTitle, mdSection, mdKeyValue, mdEmpty, truncateText } from '../lib/format.js';
4
+
5
+ export const listPostSuggestionsTool = {
6
+ name: 'list_post_suggestions',
7
+ description:
8
+ "List proactive post suggestions: evidence-based weekly drafts posterly writes in each account's learned voice, each with a rationale tying it back to what has performed well for that account. Filter by account_id and status (pending, scheduled, dismissed; default pending). Read-only; requires a Pro plan or higher. Use dismiss_suggestion to hide one.",
9
+ inputSchema: z.object({
10
+ account_id: z
11
+ .union([z.string(), z.number()])
12
+ .optional()
13
+ .describe('Filter to one social account ID (from list_accounts).'),
14
+ status: z
15
+ .enum(['pending', 'scheduled', 'dismissed'])
16
+ .optional()
17
+ .describe('Suggestion status to list (default pending).'),
18
+ limit: z.number().int().min(1).max(100).optional().describe('Max suggestions to return (default 20).'),
19
+ }),
20
+
21
+ async execute(
22
+ client: PosterlyClient,
23
+ input: {
24
+ account_id?: string | number;
25
+ status?: 'pending' | 'scheduled' | 'dismissed';
26
+ limit?: number;
27
+ },
28
+ ) {
29
+ const { suggestions } = await client.listPostSuggestions(input);
30
+
31
+ if (!suggestions.length) {
32
+ return mdEmpty(
33
+ 'post suggestions',
34
+ 'No proactive post suggestions match these filters. posterly generates fresh drafts weekly for accounts with a learned voice.',
35
+ );
36
+ }
37
+
38
+ const blocks = suggestions.map((suggestion) => {
39
+ const label = suggestion.account?.username
40
+ ? `@${suggestion.account.username} (${suggestion.platform || suggestion.account?.platform})`
41
+ : `account ${suggestion.social_account_id}`;
42
+ return mdSection(
43
+ label,
44
+ mdKeyValue([
45
+ ['Suggestion ID', suggestion.id],
46
+ ['Status', suggestion.status],
47
+ ['Period', suggestion.period_key || undefined],
48
+ ['Caption', truncateText(suggestion.caption, 240)],
49
+ ['Rationale', suggestion.rationale ? truncateText(suggestion.rationale, 240) : undefined],
50
+ ]),
51
+ );
52
+ });
53
+
54
+ return [mdTitle(`Post suggestions (${suggestions.length})`), ...blocks].join('\n\n');
55
+ },
56
+ };
@@ -30,6 +30,7 @@ const platformSettingsSchema = z.object({
30
30
  poll: z.object({ options: z.array(z.string()), duration_minutes: z.number().positive().optional(), question: z.string().optional(), duration: z.enum(['ONE_DAY', 'THREE_DAYS', 'SEVEN_DAYS', 'FOURTEEN_DAYS']).optional(), allows_multiple_answers: z.boolean().optional() }).optional(),
31
31
  reply_control: z.enum(['everyone', 'accounts_you_follow', 'mentioned_only', 'followers_only', 'parent_post_author_only']).optional(),
32
32
  topic_tag: z.string().optional(),
33
+ share_to_instagram: z.boolean().optional(), // Threads: also share the published post to the linked Instagram account as a Story
33
34
  text_attachment: z.record(z.unknown()).optional(),
34
35
  text_format_preset_id: z.string().optional(),
35
36
  document_title: z.string().optional(),