posterly-mcp-server 0.43.8 → 0.45.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 (169) hide show
  1. package/README.md +310 -109
  2. package/dist/index.js +190 -874
  3. package/dist/lib/alias-transport.d.ts +84 -0
  4. package/dist/lib/alias-transport.js +193 -0
  5. package/dist/lib/api-client.d.ts +97 -0
  6. package/dist/lib/api-client.js +81 -0
  7. package/dist/lib/preview-token-core.d.ts +62 -0
  8. package/dist/lib/preview-token-core.js +131 -0
  9. package/dist/lib/preview-token.d.ts +23 -0
  10. package/dist/lib/preview-token.js +44 -0
  11. package/dist/lib/tool-registry.d.ts +246 -0
  12. package/dist/lib/tool-registry.js +394 -0
  13. package/dist/lib/tool-runtime.d.ts +68 -0
  14. package/dist/lib/tool-runtime.js +85 -0
  15. package/dist/lib/version.d.ts +1 -1
  16. package/dist/lib/version.js +1 -1
  17. package/dist/tools/audit-google-business-profile.js +1 -1
  18. package/dist/tools/connect-account.d.ts +4 -4
  19. package/dist/tools/connect-account.js +2 -2
  20. package/dist/tools/create-api-key.js +1 -1
  21. package/dist/tools/create-connect-session.d.ts +2 -2
  22. package/dist/tools/create-connect-session.js +2 -2
  23. package/dist/tools/create-post.d.ts +6 -6
  24. package/dist/tools/create-post.js +3 -3
  25. package/dist/tools/create-posts-batch.d.ts +4 -4
  26. package/dist/tools/create-webhook.d.ts +4 -4
  27. package/dist/tools/delete-post.d.ts +34 -10
  28. package/dist/tools/delete-post.js +52 -16
  29. package/dist/tools/disconnect-account.d.ts +2 -2
  30. package/dist/tools/generate-captions.d.ts +2 -2
  31. package/dist/tools/generate-image.d.ts +4 -4
  32. package/dist/tools/generate-image.js +2 -2
  33. package/dist/tools/generate-video.d.ts +6 -6
  34. package/dist/tools/generate-video.js +1 -1
  35. package/dist/tools/get-agent-signup-info.js +2 -2
  36. package/dist/tools/get-post.d.ts +16 -1
  37. package/dist/tools/get-post.js +35 -19
  38. package/dist/tools/list-accounts.d.ts +49 -3
  39. package/dist/tools/list-accounts.js +148 -33
  40. package/dist/tools/list-analytics.d.ts +111 -0
  41. package/dist/tools/list-analytics.js +393 -0
  42. package/dist/tools/list-brands.d.ts +38 -3
  43. package/dist/tools/list-brands.js +138 -20
  44. package/dist/tools/list-comments.d.ts +29 -9
  45. package/dist/tools/list-comments.js +75 -34
  46. package/dist/tools/list-conversations.d.ts +32 -9
  47. package/dist/tools/list-conversations.js +63 -27
  48. package/dist/tools/list-google-business-media.js +1 -1
  49. package/dist/tools/list-google-business-reviews.d.ts +32 -9
  50. package/dist/tools/list-google-business-reviews.js +77 -52
  51. package/dist/tools/list-jobs.d.ts +60 -0
  52. package/dist/tools/list-jobs.js +81 -0
  53. package/dist/tools/list-platforms.d.ts +62 -3
  54. package/dist/tools/list-platforms.js +118 -9
  55. package/dist/tools/list-posts.d.ts +2 -2
  56. package/dist/tools/list-workspace-members.d.ts +17 -0
  57. package/dist/tools/list-workspace-members.js +51 -0
  58. package/dist/tools/manage-comment.d.ts +102 -0
  59. package/dist/tools/manage-comment.js +125 -0
  60. package/dist/tools/manage-conversation.d.ts +79 -0
  61. package/dist/tools/manage-conversation.js +75 -0
  62. package/dist/tools/manage-google-business-media.d.ts +103 -0
  63. package/dist/tools/manage-google-business-media.js +126 -0
  64. package/dist/tools/manage-google-business-review.d.ts +146 -0
  65. package/dist/tools/manage-google-business-review.js +115 -0
  66. package/dist/tools/manage-oauth-client.d.ts +121 -0
  67. package/dist/tools/manage-oauth-client.js +94 -0
  68. package/dist/tools/manage-webhook.d.ts +99 -0
  69. package/dist/tools/manage-webhook.js +87 -0
  70. package/dist/tools/manage-workspace-member.d.ts +57 -0
  71. package/dist/tools/manage-workspace-member.js +131 -0
  72. package/dist/tools/reply-to-comment.d.ts +2 -2
  73. package/dist/tools/send-message.d.ts +2 -2
  74. package/dist/tools/start-signup.d.ts +2 -2
  75. package/dist/tools/submit-agent-feedback.d.ts +4 -4
  76. package/dist/tools/submit-product-feedback.d.ts +4 -4
  77. package/dist/tools/trigger-platform-helper.d.ts +2 -2
  78. package/dist/tools/update-post-release-id.d.ts +2 -2
  79. package/dist/tools/update-post-status.d.ts +2 -2
  80. package/dist/tools/update-post.d.ts +64 -13
  81. package/dist/tools/update-post.js +79 -26
  82. package/dist/tools/upload-media.d.ts +32 -9
  83. package/dist/tools/upload-media.js +54 -18
  84. package/dist/tools/validate-post.d.ts +4 -4
  85. package/dist/tools/whoami.d.ts +15 -2
  86. package/dist/tools/whoami.js +221 -25
  87. package/package.json +5 -3
  88. package/scripts/test-alias-transport.mjs +368 -0
  89. package/scripts/test-client-header-sanitization.mjs +78 -0
  90. package/scripts/test-tool-annotations.mjs +38 -17
  91. package/server.json +9 -2
  92. package/src/index.ts +227 -1411
  93. package/src/lib/alias-transport.ts +233 -0
  94. package/src/lib/api-client.ts +168 -0
  95. package/src/lib/preview-token-core.ts +157 -0
  96. package/src/lib/preview-token.ts +54 -0
  97. package/src/lib/tool-registry.ts +538 -0
  98. package/src/lib/tool-runtime.ts +117 -0
  99. package/src/lib/version.ts +1 -1
  100. package/src/tools/audit-google-business-profile.ts +1 -1
  101. package/src/tools/connect-account.ts +2 -2
  102. package/src/tools/create-api-key.ts +1 -1
  103. package/src/tools/create-connect-session.ts +2 -2
  104. package/src/tools/create-post.ts +3 -3
  105. package/src/tools/delete-post.ts +60 -19
  106. package/src/tools/generate-image.ts +2 -2
  107. package/src/tools/generate-video.ts +1 -1
  108. package/src/tools/get-agent-signup-info.ts +2 -2
  109. package/src/tools/get-post.ts +35 -21
  110. package/src/tools/list-accounts.ts +187 -39
  111. package/src/tools/list-analytics.ts +444 -0
  112. package/src/tools/list-brands.ts +162 -26
  113. package/src/tools/list-comments.ts +86 -43
  114. package/src/tools/list-conversations.ts +81 -39
  115. package/src/tools/list-google-business-media.ts +1 -1
  116. package/src/tools/list-google-business-reviews.ts +93 -64
  117. package/src/tools/list-jobs.ts +91 -0
  118. package/src/tools/list-platforms.ts +135 -11
  119. package/src/tools/list-workspace-members.ts +61 -0
  120. package/src/tools/manage-comment.ts +142 -0
  121. package/src/tools/manage-conversation.ts +89 -0
  122. package/src/tools/manage-google-business-media.ts +147 -0
  123. package/src/tools/manage-google-business-review.ts +139 -0
  124. package/src/tools/manage-oauth-client.ts +116 -0
  125. package/src/tools/manage-webhook.ts +103 -0
  126. package/src/tools/manage-workspace-member.ts +151 -0
  127. package/src/tools/update-post.ts +106 -39
  128. package/src/tools/upload-media.ts +76 -28
  129. package/src/tools/whoami.ts +253 -30
  130. package/tool-annotations.json +76 -144
  131. package/tool-registry.json +2085 -0
  132. package/src/tools/add-google-business-media.ts +0 -50
  133. package/src/tools/create-oauth-client.ts +0 -36
  134. package/src/tools/delete-comment.ts +0 -29
  135. package/src/tools/delete-google-business-media.ts +0 -30
  136. package/src/tools/delete-google-business-review-reply.ts +0 -30
  137. package/src/tools/delete-oauth-client.ts +0 -23
  138. package/src/tools/delete-post-group.ts +0 -17
  139. package/src/tools/delete-webhook.ts +0 -17
  140. package/src/tools/get-account-analytics.ts +0 -171
  141. package/src/tools/get-brand-profile.ts +0 -86
  142. package/src/tools/get-brand.ts +0 -34
  143. package/src/tools/get-comment.ts +0 -47
  144. package/src/tools/get-connect-link.ts +0 -61
  145. package/src/tools/get-connect-session.ts +0 -63
  146. package/src/tools/get-conversation.ts +0 -44
  147. package/src/tools/get-google-business-review-link.ts +0 -30
  148. package/src/tools/get-image-job.ts +0 -36
  149. package/src/tools/get-learned-voice.ts +0 -31
  150. package/src/tools/get-mcp-status.ts +0 -212
  151. package/src/tools/get-performance-profile.ts +0 -47
  152. package/src/tools/get-platform-schema.ts +0 -56
  153. package/src/tools/get-post-analytics.ts +0 -243
  154. package/src/tools/get-post-insights.ts +0 -60
  155. package/src/tools/get-post-missing.ts +0 -16
  156. package/src/tools/get-video-job.ts +0 -36
  157. package/src/tools/list-brand-accounts.ts +0 -36
  158. package/src/tools/reply-google-business-review.ts +0 -32
  159. package/src/tools/reply-to-comment.ts +0 -30
  160. package/src/tools/send-message.ts +0 -28
  161. package/src/tools/suggest-google-business-review-reply.ts +0 -44
  162. package/src/tools/sync-inbox.ts +0 -28
  163. package/src/tools/test-webhook.ts +0 -18
  164. package/src/tools/update-comment.ts +0 -31
  165. package/src/tools/update-oauth-client.ts +0 -38
  166. package/src/tools/update-post-release-id.ts +0 -25
  167. package/src/tools/update-post-status.ts +0 -31
  168. package/src/tools/update-webhook.ts +0 -36
  169. package/src/tools/upload-media-from-url.ts +0 -33
@@ -1,63 +0,0 @@
1
- import { z } from 'zod';
2
- import type { ConnectSession, PosterlyClient } from '../lib/api-client.js';
3
- import { DASHBOARD_CONNECT_URL, formatConnectedAccounts, formatLocalDateTime } from '../lib/format.js';
4
-
5
- export const getConnectSessionTool = {
6
- name: 'get_connect_session',
7
- description:
8
- 'Poll a posterly connect session. Use the returned status and next-step guidance to keep the user updated in plain language. Stop when status is connected, failed, cancelled, or expired.',
9
- inputSchema: z.object({
10
- session_id: z.string().describe('Connect session ID returned by create_connect_session.'),
11
- debug: z
12
- .boolean()
13
- .optional()
14
- .describe('When true, include the raw connect session JSON for debugging. Keep false for normal user-facing agent flows.'),
15
- }),
16
-
17
- async execute(
18
- client: PosterlyClient,
19
- input: { session_id: string; debug?: boolean },
20
- ) {
21
- const result = await client.getConnectSession(input.session_id);
22
- return formatConnectSession(result.connect_session, Boolean(input.debug));
23
- },
24
- };
25
-
26
- function formatConnectSession(session: ConnectSession, includeRaw: boolean): string {
27
- const connected = session.connected_count ?? session.connected_accounts?.length ?? 0;
28
- return [
29
- `posterly connect session: ${session.id}`,
30
- `Platform: ${session.platform}`,
31
- `Status: ${session.status}`,
32
- `Message: ${session.status_message}`,
33
- `Connected accounts: ${connected}`,
34
- `Accounts: ${formatConnectedAccounts(session.connected_accounts)}`,
35
- session.error_message ? `Error: ${session.error_message}` : '',
36
- `Expires: ${formatLocalDateTime(session.expires_at)}`,
37
- `Dashboard connect page: ${DASHBOARD_CONNECT_URL}`,
38
- '',
39
- guidanceForStatus(session),
40
- includeRaw ? `\nRaw connect session:\n${JSON.stringify(session, null, 2)}` : '',
41
- ].filter(Boolean).join('\n');
42
- }
43
-
44
- function guidanceForStatus(session: ConnectSession): string {
45
- switch (session.status) {
46
- case 'created':
47
- case 'opened':
48
- case 'awaiting_provider':
49
- return 'Next step for the AI agent: tell the user posterly is waiting for browser approval, then keep polling this session. Do not ask for social passwords or OAuth codes.';
50
- case 'awaiting_credentials':
51
- return 'Next step for the AI agent: tell the user posterly needs them to finish the browser credential step, then keep polling. Do not ask the user to paste credentials into chat.';
52
- case 'connected':
53
- return `Next step for the AI agent: tell the user ${formatConnectedAccounts(session.connected_accounts)} is connected, then ask what they want to schedule first or offer to create a test post.`;
54
- case 'failed':
55
- return 'Next step for the AI agent: tell the user the connection failed, share the error if present, and offer to create a fresh connect session.';
56
- case 'cancelled':
57
- return 'Next step for the AI agent: tell the user the connection was cancelled and offer to create a fresh connect session when they are ready.';
58
- case 'expired':
59
- return 'Next step for the AI agent: tell the user the connection link expired and create a fresh connect session if they still want to connect this account.';
60
- default:
61
- return 'Next step for the AI agent: explain the current connection status in plain language and keep polling unless the session is terminal.';
62
- }
63
- }
@@ -1,44 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
- import { formatLocalDateTime, truncateText } from '../lib/format.js';
4
-
5
- export const getConversationTool = {
6
- name: 'get_conversation',
7
- description:
8
- 'Get one social inbox DM conversation and its messages. Instagram and Facebook Page DMs only. Requires a Pro plan or higher plus posts:read.',
9
- inputSchema: z.object({
10
- conversation_id: z.string().min(1).describe('Conversation ID from list_conversations.'),
11
- workspace_id: z.string().optional().describe('Optional. Omit to search every workspace you belong to; pass a workspace ID from whoami to limit to one.'),
12
- limit: z.number().int().min(1).max(100).optional().describe('Maximum messages to return. Default 50, max 100.'),
13
- }),
14
-
15
- async execute(
16
- client: PosterlyClient,
17
- input: {
18
- conversation_id: string;
19
- workspace_id?: string;
20
- limit?: number;
21
- },
22
- ) {
23
- const result = await client.getConversation(input);
24
- const conversation = result.conversation;
25
- const username = typeof conversation.participant_username === 'string' ? `@${conversation.participant_username}` : 'unknown';
26
- const lines = [
27
- `Conversation ${String(conversation.id)}`,
28
- `• Platform: ${typeof conversation.platform === 'string' ? conversation.platform : 'unknown'}`,
29
- `• Participant: ${username}`,
30
- `• Read: ${conversation.is_read === false ? 'no' : 'yes'}`,
31
- `• Messages: ${result.messages.length}`,
32
- '',
33
- ];
34
-
35
- for (const message of result.messages) {
36
- const who = message.sender_is_business ? 'you' : (typeof message.sender_username === 'string' ? message.sender_username : 'them');
37
- const when = formatLocalDateTime(typeof message.sent_at === 'string' ? message.sent_at : null);
38
- const text = truncateText(typeof message.content === 'string' ? message.content : '', 240);
39
- lines.push(`- [${when}] ${who}: ${text}`);
40
- }
41
-
42
- return lines.join('\n');
43
- },
44
- };
@@ -1,30 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
-
4
- export const getGoogleBusinessReviewLinkTool = {
5
- name: 'get_google_business_review_link',
6
- description:
7
- 'Get the direct public Google review link for a connected Google Business Profile location. Requires account_id or location_id. Returns reviewLink plus the Google Place ID (ChIJ...); that Place ID is output only and must NOT be passed back as location_id to other tools.',
8
- inputSchema: z.object({
9
- workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
10
- account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
11
- location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID this tool returns. Prefer account_id.'),
12
- }),
13
-
14
- async execute(
15
- client: PosterlyClient,
16
- input: {
17
- workspace_id?: string;
18
- account_id?: number;
19
- location_id?: string;
20
- },
21
- ) {
22
- const result = await client.getGoogleBusinessReviewLink(input);
23
- return [
24
- `Review link for ${result.businessName}:`,
25
- result.reviewLink,
26
- `Place ID: ${result.placeId}`,
27
- `Account ID: ${result.account_id}`,
28
- ].join('\n');
29
- },
30
- };
@@ -1,36 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient, ImageJob } from '../lib/api-client.js';
3
-
4
- export const getImageJobTool = {
5
- name: 'get_image_job',
6
- description:
7
- 'Get one AI image generation job by ID, or list recent image jobs when job_id is omitted. Use after generate_image to poll for completed urls.',
8
- inputSchema: z.object({
9
- job_id: z.string().optional(),
10
- status: z.enum(['pending', 'processing', 'completed', 'failed']).optional(),
11
- limit: z.number().int().min(1).max(50).default(10).optional(),
12
- }),
13
-
14
- async execute(client: PosterlyClient, input: { job_id?: string; status?: string; limit?: number }) {
15
- const result = await client.getImageJob(input);
16
- if (result.job) return formatJob(result.job, true);
17
-
18
- const jobs = result.jobs || [];
19
- return [
20
- `Image jobs (${jobs.length})`,
21
- jobs.map((job) => formatJob(job, false)).join('\n\n'),
22
- ].filter(Boolean).join('\n');
23
- },
24
- };
25
-
26
- function formatJob(job: ImageJob, includeRaw: boolean): string {
27
- const lines = [
28
- `${job.id}: ${job.status}`,
29
- `Prompt: ${job.prompt}`,
30
- `Model: ${job.model}; resolution: ${job.resolution}; thinking: ${job.thinking_level || 'n/a'}; cost: ${job.credit_cost}`,
31
- job.urls?.length ? `URLs: ${job.urls.join(', ')}` : null,
32
- job.error_message ? `Error: ${job.error_message}` : null,
33
- includeRaw ? `\nRaw job:\n${JSON.stringify(job, null, 2)}` : null,
34
- ];
35
- return lines.filter(Boolean).join('\n');
36
- }
@@ -1,31 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
- import { mdLearnedVoice, mdTitle } from '../lib/format.js';
4
-
5
- export const getLearnedVoiceTool = {
6
- name: 'get_learned_voice',
7
- description:
8
- 'Get the voice posterly has learned from a connected account’s real published captions (the "learned captions" shown on the dashboard). Returns a narrative summary, voice traits, style guidelines, and observed posting habits. Keyed by social account ID (from list_accounts or list_brand_accounts). Returns nothing until the account has enough published posts to learn from. Read-only; this never overwrites the saved brand profile.',
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(
16
- client: PosterlyClient,
17
- input: { account_id: string | number },
18
- ) {
19
- const { learned_voice } = await client.getAccountLearnedVoice(input.account_id);
20
-
21
- const body = mdLearnedVoice(learned_voice);
22
- if (!body) {
23
- return [
24
- mdTitle('No learned voice yet'),
25
- 'posterly has not learned a voice for this account yet. It learns automatically once the account has enough published posts (the captions are analyzed; the saved brand profile is never overwritten).',
26
- ].join('\n\n');
27
- }
28
-
29
- return [mdTitle('Learned voice (from published captions)'), body].join('\n\n');
30
- },
31
- };
@@ -1,212 +0,0 @@
1
- import { z } from 'zod';
2
- import { POSTERLY_MCP_VERSION } from '../lib/version.js';
3
- import type { PosterlyClient } from '../lib/api-client.js';
4
- import { resolvePosterlyBaseUrl } from '../lib/api-client.js';
5
- import { code, mdKeyValue, mdTitle } from '../lib/format.js';
6
-
7
- const NPM_LATEST_URL = 'https://registry.npmjs.org/posterly-mcp-server/latest';
8
-
9
- export const getMcpStatusTool = {
10
- name: 'get_mcp_status',
11
- description:
12
- '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.',
13
- inputSchema: z.object({}),
14
-
15
- async execute(client: PosterlyClient) {
16
- const apiOrigin = client.getBaseUrl();
17
- const [latest, apiProbe, authProbe] = await Promise.all([
18
- fetchLatestNpmVersion(),
19
- probeHostedMcp(apiOrigin),
20
- client.hasApiKey() ? client.probeWhoami() : Promise.resolve(null),
21
- ]);
22
- const updateStatus = describeUpdateStatus(POSTERLY_MCP_VERSION, latest.version, latest.error);
23
- const versionMatch = apiProbe.version ? yesNo(apiProbe.version === POSTERLY_MCP_VERSION) : 'unknown';
24
- const rows: Array<[string, string | number | boolean]> = [
25
- ['Connection type', 'local app process (stdio)'],
26
- ['Current server version', POSTERLY_MCP_VERSION],
27
- ['Latest npm version', latest.version || 'unknown'],
28
- ['Update status', updateStatus],
29
- ['posterly API origin', code(apiOrigin)],
30
- ['MCP endpoint health', formatApiProbe(apiProbe)],
31
- ['Hosted MCP version', apiProbe.version || 'unknown'],
32
- ['Version match', versionMatch],
33
- ['API key configured', client.hasApiKey()],
34
- ['API auth check', formatAuthProbe(authProbe)],
35
- ];
36
-
37
- if (authProbe?.ok && authProbe.whoami) {
38
- rows.push(
39
- ['User', authProbe.whoami.user.email || code(authProbe.whoami.user.id)],
40
- ['Default workspace', `${authProbe.whoami.default_workspace.name} (${code(authProbe.whoami.default_workspace.id)})`],
41
- ['API scopes', authProbe.whoami.api_key.scopes.join(', ') || 'none'],
42
- );
43
- }
44
-
45
- return [
46
- mdTitle('posterly MCP status'),
47
- mdKeyValue(rows),
48
- latest.error ? `**Latest check:** ${latest.error}` : '',
49
- apiProbe.error ? `**MCP endpoint check:** ${apiProbe.error}` : '',
50
- authProbe && !authProbe.ok ? `**API auth check:** ${authProbe.error || 'API key validation failed'}` : '',
51
- nextStep({ hasApiKey: client.hasApiKey(), updateStatus, apiProbe, authProbe }),
52
- ].filter(Boolean).join('\n\n');
53
- },
54
- };
55
-
56
- type HostedMcpProbe = {
57
- ok: boolean;
58
- status?: number;
59
- version?: string;
60
- error?: string;
61
- durationMs: number;
62
- };
63
-
64
- type AuthProbe = Awaited<ReturnType<PosterlyClient['probeWhoami']>> | null;
65
-
66
- async function probeHostedMcp(apiOrigin: string): Promise<HostedMcpProbe> {
67
- const startedAt = Date.now();
68
- const controller = new AbortController();
69
- const timeout = setTimeout(() => controller.abort(), 2500);
70
-
71
- try {
72
- const res = await fetch(`${resolvePosterlyBaseUrl(apiOrigin)}/api/mcp`, {
73
- headers: { Accept: 'application/json' },
74
- signal: controller.signal,
75
- });
76
- const text = await res.text();
77
- const data = parseJson(text);
78
- if (!res.ok) {
79
- return {
80
- ok: false,
81
- status: res.status,
82
- error: formatError(data, text, res.statusText),
83
- durationMs: Date.now() - startedAt,
84
- };
85
- }
86
- const version = typeof data?.serverInfo?.version === 'string' ? data.serverInfo.version : undefined;
87
- return {
88
- ok: Boolean(version),
89
- status: res.status,
90
- version,
91
- error: version ? undefined : 'Hosted MCP response did not include serverInfo.version',
92
- durationMs: Date.now() - startedAt,
93
- };
94
- } catch (err: any) {
95
- return {
96
- ok: false,
97
- error: err?.name === 'AbortError' ? 'timed out after 2500ms' : err?.message || String(err),
98
- durationMs: Date.now() - startedAt,
99
- };
100
- } finally {
101
- clearTimeout(timeout);
102
- }
103
- }
104
-
105
- async function fetchLatestNpmVersion(): Promise<{ version?: string; error?: string }> {
106
- const controller = new AbortController();
107
- const timeout = setTimeout(() => controller.abort(), 2500);
108
-
109
- try {
110
- const res = await fetch(NPM_LATEST_URL, {
111
- headers: { Accept: 'application/json' },
112
- signal: controller.signal,
113
- });
114
- if (!res.ok) {
115
- return { error: `npm registry returned HTTP ${res.status}` };
116
- }
117
- const data = await res.json() as { version?: unknown };
118
- return typeof data.version === 'string'
119
- ? { version: data.version }
120
- : { error: 'npm registry response did not include a version' };
121
- } catch (err: any) {
122
- const reason = err?.name === 'AbortError' ? 'timed out' : err?.message || String(err);
123
- return { error: `could not check npm latest: ${reason}` };
124
- } finally {
125
- clearTimeout(timeout);
126
- }
127
- }
128
-
129
- function describeUpdateStatus(current: string, latest?: string, error?: string): string {
130
- if (!latest) return error ? 'unknown' : 'unknown';
131
-
132
- const comparison = compareSemver(current, latest);
133
- if (comparison < 0) {
134
- return `update available: restart the MCP client or run ${code('npx -y posterly-mcp-server@latest')}`;
135
- }
136
- if (comparison > 0) {
137
- return 'running newer than npm latest; npm may not have been published yet';
138
- }
139
- return 'current';
140
- }
141
-
142
- function formatApiProbe(probe: HostedMcpProbe): string {
143
- if (probe.ok) return `ok (HTTP ${probe.status}, ${probe.durationMs}ms)`;
144
- const status = probe.status ? `HTTP ${probe.status}` : 'network';
145
- return `failed (${status}, ${probe.durationMs}ms)`;
146
- }
147
-
148
- function formatAuthProbe(probe: AuthProbe): string {
149
- if (!probe) return 'skipped (POSTERLY_API_KEY not configured)';
150
- if (probe.ok) return `valid (HTTP ${probe.status}, ${probe.durationMs}ms)`;
151
- const status = probe.status ? `HTTP ${probe.status}` : 'network';
152
- return `failed (${status}, ${probe.durationMs}ms)`;
153
- }
154
-
155
- function nextStep(input: {
156
- hasApiKey: boolean;
157
- updateStatus: string;
158
- apiProbe: HostedMcpProbe;
159
- authProbe: AuthProbe;
160
- }): string {
161
- const { hasApiKey, updateStatus, apiProbe, authProbe } = input;
162
- if (updateStatus.startsWith('update available')) {
163
- return '**Next step:** Restart your MCP client so it pulls `posterly-mcp-server@latest`, then call `get_mcp_status` again.';
164
- }
165
- if (!apiProbe.ok) {
166
- 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.';
167
- }
168
- if (!hasApiKey) {
169
- return '**Next step:** Add `POSTERLY_API_KEY` after activating API and MCP access, then call `whoami`.';
170
- }
171
- if (authProbe && !authProbe.ok) {
172
- return '**Next step:** Create or reinstall a valid posterly API key, then restart the MCP client and call `get_mcp_status` again.';
173
- }
174
- return '**Next step:** Call `whoami` to confirm the user, workspace, and scopes before scheduling posts.';
175
- }
176
-
177
- function yesNo(value: boolean): string {
178
- return value ? 'yes' : 'no';
179
- }
180
-
181
- function parseJson(text: string): any {
182
- try {
183
- return text ? JSON.parse(text) : {};
184
- } catch {
185
- return null;
186
- }
187
- }
188
-
189
- function formatError(data: any, fallback: string, statusText: string): string {
190
- if (data && typeof data === 'object') {
191
- return data.error || data.message || data.code || statusText || 'API request failed';
192
- }
193
- return fallback || statusText || 'API request failed';
194
- }
195
-
196
- function compareSemver(a: string, b: string): number {
197
- const left = parts(a);
198
- const right = parts(b);
199
- for (let i = 0; i < 3; i += 1) {
200
- if (left[i] !== right[i]) return left[i] > right[i] ? 1 : -1;
201
- }
202
- return 0;
203
- }
204
-
205
- function parts(value: string): [number, number, number] {
206
- const nums = value.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10));
207
- return [
208
- Number.isFinite(nums[0]) ? nums[0] : 0,
209
- Number.isFinite(nums[1]) ? nums[1] : 0,
210
- Number.isFinite(nums[2]) ? nums[2] : 0,
211
- ];
212
- }
@@ -1,47 +0,0 @@
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
- };
@@ -1,56 +0,0 @@
1
- import { z } from 'zod';
2
- import type { PosterlyClient } from '../lib/api-client.js';
3
- import { ALL_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
4
-
5
- export const getPlatformSchemaTool = {
6
- name: 'get_platform_schema',
7
- description:
8
- '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.',
9
- inputSchema: z.object({
10
- account_id: z
11
- .string()
12
- .optional()
13
- .describe('Connected account ID from list_accounts. Preferred because it returns account context.'),
14
- platform: z
15
- .enum(ALL_PLATFORM_INPUTS)
16
- .optional()
17
- .describe(`Platform ID such as ${SUPPORTED_PLATFORM_IDS.join(', ')}. Set include_planned to inspect planned providers.`),
18
- include_planned: z.boolean().optional(),
19
- }),
20
-
21
- async execute(
22
- client: PosterlyClient,
23
- input: { account_id?: string; platform?: string; include_planned?: boolean },
24
- ) {
25
- const result = await client.getPlatformSchema(input);
26
- const schema = result.schema || result.platform || result.platforms?.[0];
27
- if (!schema) return JSON.stringify(result, null, 2);
28
-
29
- const fields = schema.settings_schema?.fields || [];
30
- const helpers = schema.helper_tools || [];
31
- const accountLine = result.account
32
- ? `Account: ${(result.account.username as string) || result.account.id} (${result.account.platform})\n`
33
- : '';
34
- const instagramTaggingLine = result.account?.instagram_tagging
35
- ? `Instagram tagging capabilities: ${JSON.stringify(result.account.instagram_tagging)}`
36
- : '';
37
-
38
- return [
39
- `${schema.label} (${schema.id}) schema`,
40
- accountLine.trim(),
41
- instagramTaggingLine,
42
- `Status: ${schema.status}`,
43
- `Post types: ${schema.post_types.join(', ') || 'n/a'}`,
44
- `Content limit: ${schema.content_limit ?? 'n/a'}`,
45
- `Media: ${JSON.stringify(schema.media)}`,
46
- `Analytics: ${schema.analytics_supported ? 'yes' : 'no'}`,
47
- fields.length
48
- ? `Settings:\n${fields.map((field: any) => `• ${field.key}${field.required ? ' (required)' : ''}: ${field.description}`).join('\n')}`
49
- : 'Settings: none',
50
- helpers.length
51
- ? `Helpers:\n${helpers.map((helper: any) => `• ${helper.name}: ${helper.description}`).join('\n')}`
52
- : 'Helpers: none',
53
- `Example settings: ${JSON.stringify(schema.settings_schema?.example || {})}`,
54
- ].filter(Boolean).join('\n');
55
- },
56
- };