posterly-mcp-server 0.19.10 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ This package gives Claude Desktop, Cursor, Windsurf, Cline, and other local MCP
14
14
  - generate images
15
15
  - read account and post analytics
16
16
 
17
- Posterly also exposes the same authenticated toolset over HTTP at [poster.ly/mcp](https://www.poster.ly/mcp), but this npm package is the local `stdio` transport.
17
+ Posterly also exposes the same authenticated toolset over HTTP at [poster.ly/mcp](https://www.poster.ly/mcp), but this npm package is the local `stdio` connection for desktop AI clients.
18
18
 
19
19
  ## Requirements
20
20
 
@@ -108,11 +108,11 @@ Add the same server definition to your Cursor MCP settings:
108
108
 
109
109
  ## Available tools
110
110
 
111
- `posterly-mcp-server@0.19.10` exposes 56 tools.
111
+ `posterly-mcp-server@0.20.0` exposes 56 tools.
112
112
 
113
113
  Public setup tools work before `POSTERLY_API_KEY` exists:
114
114
 
115
- - `get_mcp_status` (show the installed server version, latest npm version, API origin, API key presence, and update guidance)
115
+ - `get_mcp_status` (show the installed server version, latest npm version, MCP endpoint health, API auth health, and update guidance)
116
116
  - `get_agent_signup_info`
117
117
  - `start_signup` (start paid signup and return a Posterly checkout handoff URL)
118
118
  - `get_signup_session` (poll checkout, payment, password, and agent-access status)
@@ -500,6 +500,14 @@ export declare class PosterlyClient {
500
500
  private apiKey;
501
501
  constructor(apiKey?: string, baseUrl?: string);
502
502
  hasApiKey(): boolean;
503
+ getBaseUrl(): string;
504
+ probeWhoami(timeoutMs?: number): Promise<{
505
+ ok: boolean;
506
+ status?: number;
507
+ error?: string;
508
+ durationMs: number;
509
+ whoami?: Whoami;
510
+ }>;
503
511
  private requireApiKey;
504
512
  private request;
505
513
  private publicRequest;
@@ -10,6 +10,59 @@ export class PosterlyClient {
10
10
  hasApiKey() {
11
11
  return Boolean(this.apiKey);
12
12
  }
13
+ getBaseUrl() {
14
+ return this.baseUrl;
15
+ }
16
+ async probeWhoami(timeoutMs = 2500) {
17
+ const startedAt = Date.now();
18
+ if (!this.apiKey) {
19
+ return {
20
+ ok: false,
21
+ error: 'POSTERLY_API_KEY is not configured',
22
+ durationMs: 0,
23
+ };
24
+ }
25
+ const controller = new AbortController();
26
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
27
+ try {
28
+ const res = await fetch(`${this.baseUrl}/api/v1/whoami`, {
29
+ method: 'GET',
30
+ headers: {
31
+ Authorization: `Bearer ${this.apiKey}`,
32
+ Accept: 'application/json',
33
+ },
34
+ signal: controller.signal,
35
+ });
36
+ const text = await res.text();
37
+ const data = parseJson(text);
38
+ if (!res.ok) {
39
+ return {
40
+ ok: false,
41
+ status: res.status,
42
+ error: formatProbeError(data, text, res.statusText),
43
+ durationMs: Date.now() - startedAt,
44
+ };
45
+ }
46
+ return {
47
+ ok: true,
48
+ status: res.status,
49
+ durationMs: Date.now() - startedAt,
50
+ whoami: data,
51
+ };
52
+ }
53
+ catch (err) {
54
+ return {
55
+ ok: false,
56
+ error: err?.name === 'AbortError'
57
+ ? `timed out after ${timeoutMs}ms`
58
+ : err?.message || String(err),
59
+ durationMs: Date.now() - startedAt,
60
+ };
61
+ }
62
+ finally {
63
+ clearTimeout(timeout);
64
+ }
65
+ }
13
66
  requireApiKey() {
14
67
  if (!this.apiKey) {
15
68
  throw new Error('Posterly access is not installed yet. Use start_signup to begin paid setup, or set POSTERLY_API_KEY after signup.');
@@ -403,6 +456,20 @@ export function resolvePosterlyBaseUrl(baseUrl) {
403
456
  return configured.replace(/\/api\/v1\/?$/, '').replace(/\/$/, '');
404
457
  }
405
458
  }
459
+ function parseJson(text) {
460
+ try {
461
+ return text ? JSON.parse(text) : {};
462
+ }
463
+ catch {
464
+ return null;
465
+ }
466
+ }
467
+ function formatProbeError(data, fallback, statusText) {
468
+ if (data && typeof data === 'object') {
469
+ return data.error || data.message || data.code || statusText || 'API request failed';
470
+ }
471
+ return fallback || statusText || 'API request failed';
472
+ }
406
473
  function guessContentType(filename) {
407
474
  const ext = filename.split('.').pop()?.toLowerCase();
408
475
  const map = {
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.19.10";
1
+ export declare const POSTERLY_MCP_VERSION = "0.20.0";
@@ -1 +1 @@
1
- export const POSTERLY_MCP_VERSION = '0.19.10';
1
+ export const POSTERLY_MCP_VERSION = '0.20.0';
@@ -13,7 +13,7 @@ export const getAgentSignupInfoTool = {
13
13
  authState,
14
14
  '',
15
15
  'Pre-auth tools:',
16
- '- get_mcp_status: checks the installed server version, latest npm version, API origin, and whether POSTERLY_API_KEY is configured.',
16
+ '- get_mcp_status: checks the installed server version, latest npm version, MCP endpoint health, API auth health, and whether POSTERLY_API_KEY is configured.',
17
17
  '- start_signup: starts paid signup and returns a Posterly checkout handoff URL plus a signup poll URL.',
18
18
  '- get_signup_session: polls checkout, payment, password, and agent-access status.',
19
19
  '',
@@ -5,26 +5,81 @@ import { code, mdKeyValue, mdTitle } from '../lib/format.js';
5
5
  const NPM_LATEST_URL = 'https://registry.npmjs.org/posterly-mcp-server/latest';
6
6
  export const getMcpStatusTool = {
7
7
  name: 'get_mcp_status',
8
- description: 'Report the Posterly MCP server version, latest npm version, API endpoint, API key presence, and update guidance. Safe to call before POSTERLY_API_KEY is configured.',
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
9
  inputSchema: z.object({}),
10
10
  async execute(client) {
11
- const latest = await fetchLatestNpmVersion();
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
+ ]);
12
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
+ }
13
34
  return [
14
35
  mdTitle('Posterly MCP status'),
15
- mdKeyValue([
16
- ['Transport', 'stdio'],
17
- ['Current server version', POSTERLY_MCP_VERSION],
18
- ['Latest npm version', latest.version || 'unknown'],
19
- ['Update status', updateStatus],
20
- ['Posterly API origin', code(resolvePosterlyBaseUrl())],
21
- ['API key configured', client.hasApiKey()],
22
- ]),
36
+ mdKeyValue(rows),
23
37
  latest.error ? `**Latest check:** ${latest.error}` : '',
24
- nextStep(client.hasApiKey(), updateStatus),
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 }),
25
41
  ].filter(Boolean).join('\n\n');
26
42
  },
27
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
+ }
28
83
  async function fetchLatestNpmVersion() {
29
84
  const controller = new AbortController();
30
85
  const timeout = setTimeout(() => controller.abort(), 2500);
@@ -61,15 +116,53 @@ function describeUpdateStatus(current, latest, error) {
61
116
  }
62
117
  return 'current';
63
118
  }
64
- function nextStep(hasApiKey, updateStatus) {
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;
65
135
  if (updateStatus.startsWith('update available')) {
66
136
  return '**Next step:** Restart your MCP client so it pulls `posterly-mcp-server@latest`, then call `get_mcp_status` again.';
67
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
+ }
68
141
  if (!hasApiKey) {
69
142
  return '**Next step:** Add `POSTERLY_API_KEY` after activating API and MCP access, then call `whoami`.';
70
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
+ }
71
147
  return '**Next step:** Call `whoami` to confirm the user, workspace, and scopes before scheduling posts.';
72
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
+ }
73
166
  function compareSemver(a, b) {
74
167
  const left = parts(a);
75
168
  const right = parts(b);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.19.10",
3
+ "version": "0.20.0",
4
4
  "description": "MCP server for posterly — schedule social media posts from Claude Desktop",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.poster.ly/mcp",
@@ -517,6 +517,67 @@ export class PosterlyClient {
517
517
  return Boolean(this.apiKey);
518
518
  }
519
519
 
520
+ getBaseUrl(): string {
521
+ return this.baseUrl;
522
+ }
523
+
524
+ async probeWhoami(timeoutMs = 2500): Promise<{
525
+ ok: boolean;
526
+ status?: number;
527
+ error?: string;
528
+ durationMs: number;
529
+ whoami?: Whoami;
530
+ }> {
531
+ const startedAt = Date.now();
532
+ if (!this.apiKey) {
533
+ return {
534
+ ok: false,
535
+ error: 'POSTERLY_API_KEY is not configured',
536
+ durationMs: 0,
537
+ };
538
+ }
539
+
540
+ const controller = new AbortController();
541
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
542
+
543
+ try {
544
+ const res = await fetch(`${this.baseUrl}/api/v1/whoami`, {
545
+ method: 'GET',
546
+ headers: {
547
+ Authorization: `Bearer ${this.apiKey}`,
548
+ Accept: 'application/json',
549
+ },
550
+ signal: controller.signal,
551
+ });
552
+ const text = await res.text();
553
+ const data = parseJson(text);
554
+ if (!res.ok) {
555
+ return {
556
+ ok: false,
557
+ status: res.status,
558
+ error: formatProbeError(data, text, res.statusText),
559
+ durationMs: Date.now() - startedAt,
560
+ };
561
+ }
562
+ return {
563
+ ok: true,
564
+ status: res.status,
565
+ durationMs: Date.now() - startedAt,
566
+ whoami: data as Whoami,
567
+ };
568
+ } catch (err: any) {
569
+ return {
570
+ ok: false,
571
+ error: err?.name === 'AbortError'
572
+ ? `timed out after ${timeoutMs}ms`
573
+ : err?.message || String(err),
574
+ durationMs: Date.now() - startedAt,
575
+ };
576
+ } finally {
577
+ clearTimeout(timeout);
578
+ }
579
+ }
580
+
520
581
  private requireApiKey(): void {
521
582
  if (!this.apiKey) {
522
583
  throw new Error(
@@ -1146,6 +1207,21 @@ export function resolvePosterlyBaseUrl(baseUrl?: string): string {
1146
1207
  }
1147
1208
  }
1148
1209
 
1210
+ function parseJson(text: string): any {
1211
+ try {
1212
+ return text ? JSON.parse(text) : {};
1213
+ } catch {
1214
+ return null;
1215
+ }
1216
+ }
1217
+
1218
+ function formatProbeError(data: any, fallback: string, statusText: string): string {
1219
+ if (data && typeof data === 'object') {
1220
+ return data.error || data.message || data.code || statusText || 'API request failed';
1221
+ }
1222
+ return fallback || statusText || 'API request failed';
1223
+ }
1224
+
1149
1225
  function guessContentType(filename: string): string {
1150
1226
  const ext = filename.split('.').pop()?.toLowerCase();
1151
1227
  const map: Record<string, string> = {
@@ -1 +1 @@
1
- export const POSTERLY_MCP_VERSION = '0.19.10';
1
+ export const POSTERLY_MCP_VERSION = '0.20.0';
@@ -18,7 +18,7 @@ export const getAgentSignupInfoTool = {
18
18
  authState,
19
19
  '',
20
20
  'Pre-auth tools:',
21
- '- get_mcp_status: checks the installed server version, latest npm version, API origin, and whether POSTERLY_API_KEY is configured.',
21
+ '- get_mcp_status: checks the installed server version, latest npm version, MCP endpoint health, API auth health, and whether POSTERLY_API_KEY is configured.',
22
22
  '- start_signup: starts paid signup and returns a Posterly checkout handoff URL plus a signup poll URL.',
23
23
  '- get_signup_session: polls checkout, payment, password, and agent-access status.',
24
24
  '',
@@ -9,29 +9,99 @@ const NPM_LATEST_URL = 'https://registry.npmjs.org/posterly-mcp-server/latest';
9
9
  export const getMcpStatusTool = {
10
10
  name: 'get_mcp_status',
11
11
  description:
12
- 'Report the Posterly MCP server version, latest npm version, API endpoint, API key presence, and update guidance. Safe to call before POSTERLY_API_KEY is configured.',
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
13
  inputSchema: z.object({}),
14
14
 
15
15
  async execute(client: PosterlyClient) {
16
- const latest = await fetchLatestNpmVersion();
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
+ ]);
17
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
+ }
18
44
 
19
45
  return [
20
46
  mdTitle('Posterly MCP status'),
21
- mdKeyValue([
22
- ['Transport', 'stdio'],
23
- ['Current server version', POSTERLY_MCP_VERSION],
24
- ['Latest npm version', latest.version || 'unknown'],
25
- ['Update status', updateStatus],
26
- ['Posterly API origin', code(resolvePosterlyBaseUrl())],
27
- ['API key configured', client.hasApiKey()],
28
- ]),
47
+ mdKeyValue(rows),
29
48
  latest.error ? `**Latest check:** ${latest.error}` : '',
30
- nextStep(client.hasApiKey(), updateStatus),
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 }),
31
52
  ].filter(Boolean).join('\n\n');
32
53
  },
33
54
  };
34
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
+
35
105
  async function fetchLatestNpmVersion(): Promise<{ version?: string; error?: string }> {
36
106
  const controller = new AbortController();
37
107
  const timeout = setTimeout(() => controller.abort(), 2500);
@@ -69,16 +139,60 @@ function describeUpdateStatus(current: string, latest?: string, error?: string):
69
139
  return 'current';
70
140
  }
71
141
 
72
- function nextStep(hasApiKey: boolean, updateStatus: string): string {
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;
73
162
  if (updateStatus.startsWith('update available')) {
74
163
  return '**Next step:** Restart your MCP client so it pulls `posterly-mcp-server@latest`, then call `get_mcp_status` again.';
75
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
+ }
76
168
  if (!hasApiKey) {
77
169
  return '**Next step:** Add `POSTERLY_API_KEY` after activating API and MCP access, then call `whoami`.';
78
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
+ }
79
174
  return '**Next step:** Call `whoami` to confirm the user, workspace, and scopes before scheduling posts.';
80
175
  }
81
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
+
82
196
  function compareSemver(a: string, b: string): number {
83
197
  const left = parts(a);
84
198
  const right = parts(b);