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 +3 -3
- package/dist/lib/api-client.d.ts +8 -0
- package/dist/lib/api-client.js +67 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/get-agent-signup-info.js +1 -1
- package/dist/tools/get-mcp-status.js +105 -12
- package/package.json +1 -1
- package/src/lib/api-client.ts +76 -0
- package/src/lib/version.ts +1 -1
- package/src/tools/get-agent-signup-info.ts +1 -1
- package/src/tools/get-mcp-status.ts +126 -12
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`
|
|
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.
|
|
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,
|
|
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)
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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 = {
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const POSTERLY_MCP_VERSION = "0.
|
|
1
|
+
export declare const POSTERLY_MCP_VERSION = "0.20.0";
|
package/dist/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const POSTERLY_MCP_VERSION = '0.
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
package/src/lib/api-client.ts
CHANGED
|
@@ -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> = {
|
package/src/lib/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const POSTERLY_MCP_VERSION = '0.
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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);
|