posterly-mcp-server 0.45.0 → 0.45.1

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 (90) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/version.d.ts +1 -1
  3. package/dist/lib/version.js +1 -1
  4. package/dist/tools/create-post.d.ts +24 -1
  5. package/dist/tools/create-post.js +72 -6
  6. package/dist/tools/create-posts-batch.js +14 -2
  7. package/dist/tools/validate-post.js +2 -2
  8. package/package.json +1 -1
  9. package/scripts/test-payload-builders.mjs +143 -20
  10. package/server.json +2 -2
  11. package/src/lib/version.ts +1 -1
  12. package/src/tools/create-post.ts +84 -9
  13. package/src/tools/create-posts-batch.ts +13 -2
  14. package/src/tools/validate-post.ts +2 -1
  15. package/dist/tools/add-google-business-media.d.ts +0 -42
  16. package/dist/tools/add-google-business-media.js +0 -34
  17. package/dist/tools/create-oauth-client.d.ts +0 -40
  18. package/dist/tools/create-oauth-client.js +0 -20
  19. package/dist/tools/delete-comment.d.ts +0 -24
  20. package/dist/tools/delete-comment.js +0 -22
  21. package/dist/tools/delete-google-business-media.d.ts +0 -32
  22. package/dist/tools/delete-google-business-media.js +0 -17
  23. package/dist/tools/delete-google-business-review-reply.d.ts +0 -32
  24. package/dist/tools/delete-google-business-review-reply.js +0 -17
  25. package/dist/tools/delete-oauth-client.d.ts +0 -20
  26. package/dist/tools/delete-oauth-client.js +0 -13
  27. package/dist/tools/delete-post-group.d.ts +0 -20
  28. package/dist/tools/delete-post-group.js +0 -13
  29. package/dist/tools/delete-webhook.d.ts +0 -20
  30. package/dist/tools/delete-webhook.js +0 -13
  31. package/dist/tools/get-account-analytics.d.ts +0 -31
  32. package/dist/tools/get-account-analytics.js +0 -152
  33. package/dist/tools/get-brand-profile.d.ts +0 -16
  34. package/dist/tools/get-brand-profile.js +0 -73
  35. package/dist/tools/get-brand.d.ts +0 -16
  36. package/dist/tools/get-brand.js +0 -26
  37. package/dist/tools/get-comment.d.ts +0 -20
  38. package/dist/tools/get-comment.js +0 -36
  39. package/dist/tools/get-connect-link.d.ts +0 -28
  40. package/dist/tools/get-connect-link.js +0 -51
  41. package/dist/tools/get-connect-session.d.ts +0 -20
  42. package/dist/tools/get-connect-session.js +0 -54
  43. package/dist/tools/get-conversation.d.ts +0 -24
  44. package/dist/tools/get-conversation.js +0 -31
  45. package/dist/tools/get-google-business-review-link.d.ts +0 -24
  46. package/dist/tools/get-google-business-review-link.js +0 -19
  47. package/dist/tools/get-image-job.d.ts +0 -24
  48. package/dist/tools/get-image-job.js +0 -31
  49. package/dist/tools/get-learned-voice.d.ts +0 -16
  50. package/dist/tools/get-learned-voice.js +0 -22
  51. package/dist/tools/get-mcp-status.d.ts +0 -8
  52. package/dist/tools/get-mcp-status.js +0 -182
  53. package/dist/tools/get-performance-profile.d.ts +0 -16
  54. package/dist/tools/get-performance-profile.js +0 -39
  55. package/dist/tools/get-platform-schema.d.ts +0 -24
  56. package/dist/tools/get-platform-schema.js +0 -48
  57. package/dist/tools/get-post-analytics.d.ts +0 -39
  58. package/dist/tools/get-post-analytics.js +0 -222
  59. package/dist/tools/get-post-insights.d.ts +0 -28
  60. package/dist/tools/get-post-insights.js +0 -39
  61. package/dist/tools/get-post-missing.d.ts +0 -16
  62. package/dist/tools/get-post-missing.js +0 -12
  63. package/dist/tools/get-video-job.d.ts +0 -24
  64. package/dist/tools/get-video-job.js +0 -31
  65. package/dist/tools/list-brand-accounts.d.ts +0 -16
  66. package/dist/tools/list-brand-accounts.js +0 -24
  67. package/dist/tools/reply-google-business-review.d.ts +0 -36
  68. package/dist/tools/reply-google-business-review.js +0 -18
  69. package/dist/tools/reply-to-comment.d.ts +0 -32
  70. package/dist/tools/reply-to-comment.js +0 -17
  71. package/dist/tools/send-message.d.ts +0 -28
  72. package/dist/tools/send-message.js +0 -16
  73. package/dist/tools/suggest-google-business-review-reply.d.ts +0 -48
  74. package/dist/tools/suggest-google-business-review-reply.js +0 -27
  75. package/dist/tools/sync-inbox.d.ts +0 -24
  76. package/dist/tools/sync-inbox.js +0 -18
  77. package/dist/tools/test-webhook.d.ts +0 -20
  78. package/dist/tools/test-webhook.js +0 -14
  79. package/dist/tools/update-comment.d.ts +0 -28
  80. package/dist/tools/update-comment.js +0 -21
  81. package/dist/tools/update-oauth-client.d.ts +0 -44
  82. package/dist/tools/update-oauth-client.js +0 -21
  83. package/dist/tools/update-post-release-id.d.ts +0 -28
  84. package/dist/tools/update-post-release-id.js +0 -18
  85. package/dist/tools/update-post-status.d.ts +0 -28
  86. package/dist/tools/update-post-status.js +0 -23
  87. package/dist/tools/update-webhook.d.ts +0 -40
  88. package/dist/tools/update-webhook.js +0 -21
  89. package/dist/tools/upload-media-from-url.d.ts +0 -24
  90. package/dist/tools/upload-media-from-url.js +0 -26
@@ -1,182 +0,0 @@
1
- import { z } from 'zod';
2
- import { POSTERLY_MCP_VERSION } from '../lib/version.js';
3
- import { resolvePosterlyBaseUrl } from '../lib/api-client.js';
4
- import { code, mdKeyValue, mdTitle } from '../lib/format.js';
5
- const NPM_LATEST_URL = 'https://registry.npmjs.org/posterly-mcp-server/latest';
6
- export const getMcpStatusTool = {
7
- name: 'get_mcp_status',
8
- description: 'Report the posterly MCP server version, latest npm version, MCP endpoint health, API key health, and update guidance. Safe to call before POSTERLY_API_KEY is configured.',
9
- inputSchema: z.object({}),
10
- async execute(client) {
11
- const apiOrigin = client.getBaseUrl();
12
- const [latest, apiProbe, authProbe] = await Promise.all([
13
- fetchLatestNpmVersion(),
14
- probeHostedMcp(apiOrigin),
15
- client.hasApiKey() ? client.probeWhoami() : Promise.resolve(null),
16
- ]);
17
- const updateStatus = describeUpdateStatus(POSTERLY_MCP_VERSION, latest.version, latest.error);
18
- const versionMatch = apiProbe.version ? yesNo(apiProbe.version === POSTERLY_MCP_VERSION) : 'unknown';
19
- const rows = [
20
- ['Connection type', 'local app process (stdio)'],
21
- ['Current server version', POSTERLY_MCP_VERSION],
22
- ['Latest npm version', latest.version || 'unknown'],
23
- ['Update status', updateStatus],
24
- ['posterly API origin', code(apiOrigin)],
25
- ['MCP endpoint health', formatApiProbe(apiProbe)],
26
- ['Hosted MCP version', apiProbe.version || 'unknown'],
27
- ['Version match', versionMatch],
28
- ['API key configured', client.hasApiKey()],
29
- ['API auth check', formatAuthProbe(authProbe)],
30
- ];
31
- if (authProbe?.ok && authProbe.whoami) {
32
- rows.push(['User', authProbe.whoami.user.email || code(authProbe.whoami.user.id)], ['Default workspace', `${authProbe.whoami.default_workspace.name} (${code(authProbe.whoami.default_workspace.id)})`], ['API scopes', authProbe.whoami.api_key.scopes.join(', ') || 'none']);
33
- }
34
- return [
35
- mdTitle('posterly MCP status'),
36
- mdKeyValue(rows),
37
- latest.error ? `**Latest check:** ${latest.error}` : '',
38
- apiProbe.error ? `**MCP endpoint check:** ${apiProbe.error}` : '',
39
- authProbe && !authProbe.ok ? `**API auth check:** ${authProbe.error || 'API key validation failed'}` : '',
40
- nextStep({ hasApiKey: client.hasApiKey(), updateStatus, apiProbe, authProbe }),
41
- ].filter(Boolean).join('\n\n');
42
- },
43
- };
44
- async function probeHostedMcp(apiOrigin) {
45
- const startedAt = Date.now();
46
- const controller = new AbortController();
47
- const timeout = setTimeout(() => controller.abort(), 2500);
48
- try {
49
- const res = await fetch(`${resolvePosterlyBaseUrl(apiOrigin)}/api/mcp`, {
50
- headers: { Accept: 'application/json' },
51
- signal: controller.signal,
52
- });
53
- const text = await res.text();
54
- const data = parseJson(text);
55
- if (!res.ok) {
56
- return {
57
- ok: false,
58
- status: res.status,
59
- error: formatError(data, text, res.statusText),
60
- durationMs: Date.now() - startedAt,
61
- };
62
- }
63
- const version = typeof data?.serverInfo?.version === 'string' ? data.serverInfo.version : undefined;
64
- return {
65
- ok: Boolean(version),
66
- status: res.status,
67
- version,
68
- error: version ? undefined : 'Hosted MCP response did not include serverInfo.version',
69
- durationMs: Date.now() - startedAt,
70
- };
71
- }
72
- catch (err) {
73
- return {
74
- ok: false,
75
- error: err?.name === 'AbortError' ? 'timed out after 2500ms' : err?.message || String(err),
76
- durationMs: Date.now() - startedAt,
77
- };
78
- }
79
- finally {
80
- clearTimeout(timeout);
81
- }
82
- }
83
- async function fetchLatestNpmVersion() {
84
- const controller = new AbortController();
85
- const timeout = setTimeout(() => controller.abort(), 2500);
86
- try {
87
- const res = await fetch(NPM_LATEST_URL, {
88
- headers: { Accept: 'application/json' },
89
- signal: controller.signal,
90
- });
91
- if (!res.ok) {
92
- return { error: `npm registry returned HTTP ${res.status}` };
93
- }
94
- const data = await res.json();
95
- return typeof data.version === 'string'
96
- ? { version: data.version }
97
- : { error: 'npm registry response did not include a version' };
98
- }
99
- catch (err) {
100
- const reason = err?.name === 'AbortError' ? 'timed out' : err?.message || String(err);
101
- return { error: `could not check npm latest: ${reason}` };
102
- }
103
- finally {
104
- clearTimeout(timeout);
105
- }
106
- }
107
- function describeUpdateStatus(current, latest, error) {
108
- if (!latest)
109
- return error ? 'unknown' : 'unknown';
110
- const comparison = compareSemver(current, latest);
111
- if (comparison < 0) {
112
- return `update available: restart the MCP client or run ${code('npx -y posterly-mcp-server@latest')}`;
113
- }
114
- if (comparison > 0) {
115
- return 'running newer than npm latest; npm may not have been published yet';
116
- }
117
- return 'current';
118
- }
119
- function formatApiProbe(probe) {
120
- if (probe.ok)
121
- return `ok (HTTP ${probe.status}, ${probe.durationMs}ms)`;
122
- const status = probe.status ? `HTTP ${probe.status}` : 'network';
123
- return `failed (${status}, ${probe.durationMs}ms)`;
124
- }
125
- function formatAuthProbe(probe) {
126
- if (!probe)
127
- return 'skipped (POSTERLY_API_KEY not configured)';
128
- if (probe.ok)
129
- return `valid (HTTP ${probe.status}, ${probe.durationMs}ms)`;
130
- const status = probe.status ? `HTTP ${probe.status}` : 'network';
131
- return `failed (${status}, ${probe.durationMs}ms)`;
132
- }
133
- function nextStep(input) {
134
- const { hasApiKey, updateStatus, apiProbe, authProbe } = input;
135
- if (updateStatus.startsWith('update available')) {
136
- return '**Next step:** Restart your MCP client so it pulls `posterly-mcp-server@latest`, then call `get_mcp_status` again.';
137
- }
138
- if (!apiProbe.ok) {
139
- return '**Next step:** Check `POSTERLY_URL` / `POSTERLY_API_BASE_URL`, DNS, and network access to the hosted MCP endpoint, then call `get_mcp_status` again.';
140
- }
141
- if (!hasApiKey) {
142
- return '**Next step:** Add `POSTERLY_API_KEY` after activating API and MCP access, then call `whoami`.';
143
- }
144
- if (authProbe && !authProbe.ok) {
145
- return '**Next step:** Create or reinstall a valid posterly API key, then restart the MCP client and call `get_mcp_status` again.';
146
- }
147
- return '**Next step:** Call `whoami` to confirm the user, workspace, and scopes before scheduling posts.';
148
- }
149
- function yesNo(value) {
150
- return value ? 'yes' : 'no';
151
- }
152
- function parseJson(text) {
153
- try {
154
- return text ? JSON.parse(text) : {};
155
- }
156
- catch {
157
- return null;
158
- }
159
- }
160
- function formatError(data, fallback, statusText) {
161
- if (data && typeof data === 'object') {
162
- return data.error || data.message || data.code || statusText || 'API request failed';
163
- }
164
- return fallback || statusText || 'API request failed';
165
- }
166
- function compareSemver(a, b) {
167
- const left = parts(a);
168
- const right = parts(b);
169
- for (let i = 0; i < 3; i += 1) {
170
- if (left[i] !== right[i])
171
- return left[i] > right[i] ? 1 : -1;
172
- }
173
- return 0;
174
- }
175
- function parts(value) {
176
- const nums = value.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10));
177
- return [
178
- Number.isFinite(nums[0]) ? nums[0] : 0,
179
- Number.isFinite(nums[1]) ? nums[1] : 0,
180
- Number.isFinite(nums[2]) ? nums[2] : 0,
181
- ];
182
- }
@@ -1,16 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
- export declare const getPerformanceProfileTool: {
4
- name: string;
5
- description: string;
6
- inputSchema: z.ZodObject<{
7
- account_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
8
- }, "strip", z.ZodTypeAny, {
9
- account_id: string | number;
10
- }, {
11
- account_id: string | number;
12
- }>;
13
- execute(client: PosterlyClient, input: {
14
- account_id: string | number;
15
- }): Promise<string>;
16
- };
@@ -1,39 +0,0 @@
1
- import { z } from 'zod';
2
- import { mdTitle, mdSection, mdKeyValue, mdBullets, mdQuote, mdEmpty, formatPercent, formatDateTime } from '../lib/format.js';
3
- export const getPerformanceProfileTool = {
4
- name: 'get_performance_profile',
5
- description: "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.",
6
- inputSchema: z.object({
7
- account_id: z
8
- .union([z.string(), z.number()])
9
- .describe('The social account ID to inspect (from list_accounts or list_brand_accounts).'),
10
- }),
11
- async execute(client, input) {
12
- const { performance_profile, reason } = await client.getAccountPerformanceProfile(input.account_id);
13
- if (!performance_profile) {
14
- const detail = reason === 'platform_not_measurable'
15
- ? 'This platform does not produce per-post analytics, so posterly cannot build a performance profile for it.'
16
- : reason === 'feature_not_available'
17
- ? 'Performance profiles require a Pro plan or higher.'
18
- : 'posterly has not built a performance profile for this account yet. It needs about 90 days of tracked posts to learn from.';
19
- return mdEmpty('performance profile yet', detail);
20
- }
21
- const label = performance_profile.username
22
- ? `@${performance_profile.username}${performance_profile.platform ? ` (${performance_profile.platform})` : ''}`
23
- : `account ${performance_profile.social_account_id}`;
24
- const rate = performance_profile.engagement_rate_avg != null
25
- ? formatPercent(performance_profile.engagement_rate_avg * 100)
26
- : 'n/a';
27
- return [
28
- mdTitle('Account performance profile', `${label} · last ${performance_profile.window_days ?? 90} days`),
29
- performance_profile.narrative_summary ? mdSection('Summary', mdQuote(performance_profile.narrative_summary)) : '',
30
- performance_profile.bullets.length ? mdSection('What is working', mdBullets(performance_profile.bullets)) : '',
31
- mdSection('Stats', mdKeyValue([
32
- ['Posts sampled', performance_profile.sample_size],
33
- ['Avg engagement rate', rate],
34
- ['Engagement trend', performance_profile.engagement_rate_trend],
35
- ['Derived at', formatDateTime(performance_profile.derived_at)],
36
- ])),
37
- ].filter(Boolean).join('\n\n');
38
- },
39
- };
@@ -1,24 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
- export declare const getPlatformSchemaTool: {
4
- name: string;
5
- description: string;
6
- inputSchema: z.ZodObject<{
7
- account_id: z.ZodOptional<z.ZodString>;
8
- platform: z.ZodOptional<z.ZodEnum<["bluesky", "devto", "discord", "facebook", "gmb", "google-business", "google_business", "google_business_profile", "hashnode", "instagram", "instagram-standalone", "lemmy", "linkedin", "mastodon", "medium", "pinterest", "reddit", "skool", "slack", "telegram", "threads", "tiktok", "twitter", "whop", "wordpress", "x", "youtube"]>>;
9
- include_planned: z.ZodOptional<z.ZodBoolean>;
10
- }, "strip", z.ZodTypeAny, {
11
- platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "medium" | "x" | "telegram" | "bluesky" | "discord" | "slack" | "mastodon" | "devto" | "hashnode" | "wordpress" | "lemmy" | "reddit" | "skool" | "whop" | "gmb" | "google-business" | "google_business_profile" | undefined;
12
- include_planned?: boolean | undefined;
13
- account_id?: string | undefined;
14
- }, {
15
- platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "medium" | "x" | "telegram" | "bluesky" | "discord" | "slack" | "mastodon" | "devto" | "hashnode" | "wordpress" | "lemmy" | "reddit" | "skool" | "whop" | "gmb" | "google-business" | "google_business_profile" | undefined;
16
- include_planned?: boolean | undefined;
17
- account_id?: string | undefined;
18
- }>;
19
- execute(client: PosterlyClient, input: {
20
- account_id?: string;
21
- platform?: string;
22
- include_planned?: boolean;
23
- }): Promise<string>;
24
- };
@@ -1,48 +0,0 @@
1
- import { z } from 'zod';
2
- import { ALL_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
3
- export const getPlatformSchemaTool = {
4
- name: 'get_platform_schema',
5
- description: 'Get the platform schema for a specific connected account or platform. For Instagram, pass account_id before native tags/collaborators so connection-specific capabilities are returned.',
6
- inputSchema: z.object({
7
- account_id: z
8
- .string()
9
- .optional()
10
- .describe('Connected account ID from list_accounts. Preferred because it returns account context.'),
11
- platform: z
12
- .enum(ALL_PLATFORM_INPUTS)
13
- .optional()
14
- .describe(`Platform ID such as ${SUPPORTED_PLATFORM_IDS.join(', ')}. Set include_planned to inspect planned providers.`),
15
- include_planned: z.boolean().optional(),
16
- }),
17
- async execute(client, input) {
18
- const result = await client.getPlatformSchema(input);
19
- const schema = result.schema || result.platform || result.platforms?.[0];
20
- if (!schema)
21
- return JSON.stringify(result, null, 2);
22
- const fields = schema.settings_schema?.fields || [];
23
- const helpers = schema.helper_tools || [];
24
- const accountLine = result.account
25
- ? `Account: ${result.account.username || result.account.id} (${result.account.platform})\n`
26
- : '';
27
- const instagramTaggingLine = result.account?.instagram_tagging
28
- ? `Instagram tagging capabilities: ${JSON.stringify(result.account.instagram_tagging)}`
29
- : '';
30
- return [
31
- `${schema.label} (${schema.id}) schema`,
32
- accountLine.trim(),
33
- instagramTaggingLine,
34
- `Status: ${schema.status}`,
35
- `Post types: ${schema.post_types.join(', ') || 'n/a'}`,
36
- `Content limit: ${schema.content_limit ?? 'n/a'}`,
37
- `Media: ${JSON.stringify(schema.media)}`,
38
- `Analytics: ${schema.analytics_supported ? 'yes' : 'no'}`,
39
- fields.length
40
- ? `Settings:\n${fields.map((field) => `• ${field.key}${field.required ? ' (required)' : ''}: ${field.description}`).join('\n')}`
41
- : 'Settings: none',
42
- helpers.length
43
- ? `Helpers:\n${helpers.map((helper) => `• ${helper.name}: ${helper.description}`).join('\n')}`
44
- : 'Helpers: none',
45
- `Example settings: ${JSON.stringify(schema.settings_schema?.example || {})}`,
46
- ].filter(Boolean).join('\n');
47
- },
48
- };
@@ -1,39 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
- declare const presentationSchema: z.ZodOptional<z.ZodEnum<["compact", "table", "json"]>>;
4
- type AnalyticsPresentation = z.infer<typeof presentationSchema>;
5
- export declare const getPostAnalyticsTool: {
6
- name: string;
7
- description: string;
8
- inputSchema: z.ZodObject<{
9
- account_id: z.ZodNumber;
10
- from: z.ZodOptional<z.ZodString>;
11
- to: z.ZodOptional<z.ZodString>;
12
- limit: z.ZodOptional<z.ZodNumber>;
13
- offset: z.ZodOptional<z.ZodNumber>;
14
- presentation: z.ZodOptional<z.ZodEnum<["compact", "table", "json"]>>;
15
- }, "strip", z.ZodTypeAny, {
16
- account_id: number;
17
- limit?: number | undefined;
18
- offset?: number | undefined;
19
- from?: string | undefined;
20
- to?: string | undefined;
21
- presentation?: "compact" | "table" | "json" | undefined;
22
- }, {
23
- account_id: number;
24
- limit?: number | undefined;
25
- offset?: number | undefined;
26
- from?: string | undefined;
27
- to?: string | undefined;
28
- presentation?: "compact" | "table" | "json" | undefined;
29
- }>;
30
- execute(client: PosterlyClient, input: {
31
- account_id: number;
32
- from?: string;
33
- to?: string;
34
- limit?: number;
35
- offset?: number;
36
- presentation?: AnalyticsPresentation;
37
- }): Promise<string>;
38
- };
39
- export {};
@@ -1,222 +0,0 @@
1
- import { z } from 'zod';
2
- const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
3
- export const getPostAnalyticsTool = {
4
- name: 'get_post_analytics',
5
- description: 'Get per-post engagement metrics (likes, comments/replies, reach, impressions/views, saves, shares, reposts, quotes, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads. Returns the most recent posts first. presentation controls output: compact for Telegram/mobile, table for Markdown clients, json for custom chart/card renderers.',
6
- inputSchema: z.object({
7
- account_id: z
8
- .number()
9
- .describe('The social account ID (from list_accounts)'),
10
- from: z
11
- .string()
12
- .optional()
13
- .describe('Start date (ISO date, e.g. 2026-03-19). Defaults to 30 days ago.'),
14
- to: z
15
- .string()
16
- .optional()
17
- .describe('End date (ISO date). Defaults to today.'),
18
- limit: z
19
- .number()
20
- .min(1)
21
- .max(200)
22
- .optional()
23
- .describe('Number of posts to return (default 50, max 200)'),
24
- offset: z.number().min(0).optional().describe('Pagination offset'),
25
- presentation: presentationSchema.describe('Output style: compact bullets, Markdown table, or raw JSON for client-side chart/card rendering.'),
26
- }),
27
- async execute(client, input) {
28
- const result = await client.getPostAnalytics(input);
29
- return formatPostAnalytics(result, input.presentation || 'compact');
30
- },
31
- };
32
- function formatPostAnalytics(result, presentation) {
33
- const { account, range, posts, total } = result;
34
- if (posts.length === 0) {
35
- return `No analytics found for ${accountLabel(account.username)} (${account.platform}) between ${range.from} and ${range.to}.`;
36
- }
37
- const totals = posts.reduce((acc, post) => {
38
- acc.likes += post.likes || 0;
39
- acc.comments += post.comments || 0;
40
- acc.reach += post.reach || 0;
41
- acc.impressions += post.impressions || 0;
42
- acc.shares += post.shares || 0;
43
- acc.reposts += post.reposts || 0;
44
- acc.quotes += post.quotes || 0;
45
- acc.saves += post.saved || 0;
46
- return acc;
47
- }, { likes: 0, comments: 0, reach: 0, impressions: 0, shares: 0, reposts: 0, quotes: 0, saves: 0 });
48
- const isThreads = account.platform === 'threads';
49
- if (presentation === 'json') {
50
- return JSON.stringify({
51
- type: 'post_analytics',
52
- account,
53
- range,
54
- returned: {
55
- count: posts.length,
56
- total,
57
- },
58
- totals,
59
- posts,
60
- }, null, 2);
61
- }
62
- if (presentation === 'table') {
63
- if (isThreads) {
64
- return [
65
- `**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
66
- `_Threads · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
67
- '',
68
- '**Returned totals**',
69
- markdownTable(['Views', 'Likes', 'Replies', 'Reposts', 'Quotes', 'Shares'], [[
70
- formatNumber(totals.impressions),
71
- formatNumber(totals.likes),
72
- formatNumber(totals.comments),
73
- formatNumber(totals.reposts),
74
- formatNumber(totals.quotes),
75
- formatNumber(totals.shares),
76
- ]]),
77
- '',
78
- '**Posts**',
79
- markdownTable(['Posted', 'Post', 'Caption', 'Views', 'Likes', 'Replies', 'Reposts', 'Quotes', 'Shares'], posts.map((post) => [
80
- dateTime(post.posted_at),
81
- post.id ? String(post.id) : post.platform_media_id,
82
- compact(post.caption_snippet, 70) || '(no caption)',
83
- formatNumber(post.impressions),
84
- formatNumber(post.likes),
85
- formatNumber(post.comments),
86
- formatNumber(post.reposts),
87
- formatNumber(post.quotes),
88
- formatNumber(post.shares),
89
- ])),
90
- ].join('\n');
91
- }
92
- return [
93
- `**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
94
- `_${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
95
- '',
96
- '**Returned totals**',
97
- markdownTable(['Likes', 'Comments', 'Reach', 'Impressions', 'Shares', 'Saves'], [[
98
- formatNumber(totals.likes),
99
- formatNumber(totals.comments),
100
- formatNumber(totals.reach),
101
- formatNumber(totals.impressions),
102
- formatNumber(totals.shares),
103
- formatNumber(totals.saves),
104
- ]]),
105
- '',
106
- '**Posts**',
107
- markdownTable(['Posted', 'Post', 'Caption', 'Reach', 'Likes', 'Comments', 'Extras'], posts.map((post) => [
108
- dateTime(post.posted_at),
109
- post.id ? String(post.id) : post.platform_media_id,
110
- compact(post.caption_snippet, 70) || '(no caption)',
111
- formatNumber(post.reach),
112
- formatNumber(post.likes),
113
- formatNumber(post.comments),
114
- postExtras(account.platform, post),
115
- ])),
116
- ].join('\n');
117
- }
118
- const lines = [
119
- `**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
120
- `_${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
121
- '',
122
- '**Returned totals**',
123
- `- **Likes:** ${formatNumber(totals.likes)}`,
124
- `- **Comments:** ${formatNumber(totals.comments)}`,
125
- `- **Reach:** ${formatNumber(totals.reach)}`,
126
- `- **Impressions:** ${formatNumber(totals.impressions)}`,
127
- `- **Shares:** ${formatNumber(totals.shares)}`,
128
- `- **Saves:** ${formatNumber(totals.saves)}`,
129
- '',
130
- '**Posts**',
131
- ];
132
- if (isThreads) {
133
- const threadLines = [
134
- `**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
135
- `_Threads · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
136
- '',
137
- '**Returned totals**',
138
- `- **Views:** ${formatNumber(totals.impressions)}`,
139
- `- **Likes:** ${formatNumber(totals.likes)}`,
140
- `- **Replies:** ${formatNumber(totals.comments)}`,
141
- `- **Reposts:** ${formatNumber(totals.reposts)}`,
142
- `- **Quotes:** ${formatNumber(totals.quotes)}`,
143
- `- **Shares:** ${formatNumber(totals.shares)}`,
144
- '',
145
- '**Posts**',
146
- ];
147
- posts.forEach((post, index) => {
148
- threadLines.push(`${index + 1}. **${compact(post.caption_snippet, 70) || '(no caption)'}**`);
149
- threadLines.push(` Posted: ${dateTime(post.posted_at)}`);
150
- threadLines.push(` Post ID: ${post.id ? String(post.id) : post.platform_media_id}`);
151
- threadLines.push(` Views: ${formatNumber(post.impressions)}`);
152
- threadLines.push(` Likes: ${formatNumber(post.likes)}`);
153
- threadLines.push(` Replies: ${formatNumber(post.comments)}`);
154
- threadLines.push(` Reposts: ${formatNumber(post.reposts)}`);
155
- threadLines.push(` Quotes: ${formatNumber(post.quotes)}`);
156
- threadLines.push(` Shares: ${formatNumber(post.shares)}`);
157
- if (post.permalink)
158
- threadLines.push(` Permalink: ${post.permalink}`);
159
- });
160
- return threadLines.join('\n');
161
- }
162
- posts.forEach((post, index) => {
163
- lines.push(`${index + 1}. **${compact(post.caption_snippet, 70) || '(no caption)'}**`);
164
- lines.push(` Posted: ${dateTime(post.posted_at)}`);
165
- lines.push(` Post ID: ${post.id ? String(post.id) : post.platform_media_id}`);
166
- lines.push(` Reach: ${formatNumber(post.reach)}`);
167
- lines.push(` Likes: ${formatNumber(post.likes)}`);
168
- lines.push(` Comments: ${formatNumber(post.comments)}`);
169
- lines.push(` Extras: ${postExtras(account.platform, post)}`);
170
- });
171
- return lines.join('\n');
172
- }
173
- function postExtras(platform, post) {
174
- const metrics = [];
175
- if (post.impressions)
176
- metrics.push(`${formatNumber(post.impressions)} impressions`);
177
- if (post.saved)
178
- metrics.push(`${formatNumber(post.saved)} saved`);
179
- if (platform !== 'threads' && post.shares)
180
- metrics.push(`${formatNumber(post.shares)} shares`);
181
- if (post.plays)
182
- metrics.push(`${formatNumber(post.plays)} plays`);
183
- if (platform === 'youtube' && post.total_watch_time_ms)
184
- metrics.push(`${formatNumber(Math.round(post.total_watch_time_ms / 60000))} watch minutes`);
185
- if (platform === 'youtube' && post.avg_watch_time_ms)
186
- metrics.push(`${formatNumber(Math.round(post.avg_watch_time_ms / 1000))}s avg view`);
187
- if (post.url_link_clicks)
188
- metrics.push(platform === 'pinterest' ? `${formatNumber(post.url_link_clicks)} outbound clicks` : `${formatNumber(post.url_link_clicks)} link clicks`);
189
- if (post.user_profile_clicks)
190
- metrics.push(platform === 'pinterest' ? `${formatNumber(post.user_profile_clicks)} pin clicks` : `${formatNumber(post.user_profile_clicks)} profile clicks`);
191
- if (post.permalink)
192
- metrics.push(post.permalink);
193
- return metrics.join(' · ') || 'n/a';
194
- }
195
- function accountLabel(username) {
196
- if (!username)
197
- return 'account';
198
- return username.startsWith('@') || /\s/.test(username) ? username : `@${username}`;
199
- }
200
- function dateTime(value) {
201
- if (!value)
202
- return 'n/a';
203
- const date = new Date(value);
204
- return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
205
- }
206
- function compact(value, maxLength) {
207
- const text = String(value || '').replace(/\s+/g, ' ').trim();
208
- return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text;
209
- }
210
- function formatNumber(value) {
211
- return value == null ? 'n/a' : value.toLocaleString();
212
- }
213
- function markdownTable(headers, rows) {
214
- return [
215
- `| ${headers.map(escapeCell).join(' | ')} |`,
216
- `| ${headers.map(() => '---').join(' | ')} |`,
217
- ...rows.map((row) => `| ${row.map(escapeCell).join(' | ')} |`),
218
- ].join('\n');
219
- }
220
- function escapeCell(value) {
221
- return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
222
- }
@@ -1,28 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
- export declare const getPostInsightsTool: {
4
- name: string;
5
- description: string;
6
- inputSchema: z.ZodObject<{
7
- account_id: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
8
- post_id: z.ZodOptional<z.ZodNumber>;
9
- checkpoint: z.ZodOptional<z.ZodEnum<["1h", "6h", "24h", "72h", "7d"]>>;
10
- limit: z.ZodOptional<z.ZodNumber>;
11
- }, "strip", z.ZodTypeAny, {
12
- account_id?: string | number | undefined;
13
- post_id?: number | undefined;
14
- checkpoint?: "1h" | "6h" | "24h" | "72h" | "7d" | undefined;
15
- limit?: number | undefined;
16
- }, {
17
- account_id?: string | number | undefined;
18
- post_id?: number | undefined;
19
- checkpoint?: "1h" | "6h" | "24h" | "72h" | "7d" | undefined;
20
- limit?: number | undefined;
21
- }>;
22
- execute(client: PosterlyClient, input: {
23
- account_id?: string | number;
24
- post_id?: number;
25
- checkpoint?: "1h" | "6h" | "24h" | "72h" | "7d";
26
- limit?: number;
27
- }): Promise<string>;
28
- };
@@ -1,39 +0,0 @@
1
- import { z } from 'zod';
2
- import { mdTitle, mdSection, mdKeyValue, mdEmpty, truncateText, formatDateTime } from '../lib/format.js';
3
- export const getPostInsightsTool = {
4
- name: 'get_post_insights',
5
- description: '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.',
6
- inputSchema: z.object({
7
- account_id: z
8
- .union([z.string(), z.number()])
9
- .optional()
10
- .describe('Filter to one social account ID (from list_accounts).'),
11
- post_id: z.number().int().positive().optional().describe('Filter to one post ID.'),
12
- checkpoint: z
13
- .enum(['1h', '6h', '24h', '72h', '7d'])
14
- .optional()
15
- .describe('Filter to one checkpoint after publish.'),
16
- limit: z.number().int().min(1).max(100).optional().describe('Max insights to return (default 20).'),
17
- }),
18
- async execute(client, input) {
19
- const { insights } = await client.getPostInsights(input);
20
- if (!insights.length) {
21
- return mdEmpty('feedback insights', 'No per-post feedback insights are available for these filters yet. Insights appear as posts hit their 1h, 6h, 24h, 72h, and 7d checkpoints.');
22
- }
23
- const blocks = insights.map((insight) => {
24
- const heading = `${insight.title || 'Insight'} (${insight.performance_tier}, ${insight.checkpoint})`;
25
- return mdSection(heading, mdKeyValue([
26
- ['Post ID', insight.post_id],
27
- ['Platform', insight.platform],
28
- ['Account', insight.username ? `@${insight.username}` : String(insight.social_account_id)],
29
- ['Diagnosis', truncateText(insight.diagnosis, 220)],
30
- ['Next action', truncateText(insight.next_action, 220)],
31
- ['Impact score', insight.impact_score],
32
- ['Confidence', insight.confidence],
33
- ['Published', formatDateTime(insight.published_at)],
34
- ['Permalink', insight.permalink || undefined],
35
- ]));
36
- });
37
- return [mdTitle(`Post feedback insights (${insights.length})`), ...blocks].join('\n\n');
38
- },
39
- };
@@ -1,16 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
- export declare const getPostMissingTool: {
4
- name: string;
5
- description: string;
6
- inputSchema: z.ZodObject<{
7
- post_id: z.ZodNumber;
8
- }, "strip", z.ZodTypeAny, {
9
- post_id: number;
10
- }, {
11
- post_id: number;
12
- }>;
13
- execute(client: PosterlyClient, input: {
14
- post_id: number;
15
- }): Promise<string>;
16
- };