posterly-mcp-server 0.23.1 → 0.23.2
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 +7 -1
- package/dist/lib/api-client.d.ts +13 -0
- package/dist/lib/api-client.js +70 -0
- package/dist/tools/get-account-analytics.js +1 -1
- package/dist/tools/get-post-analytics.js +1 -1
- package/package.json +1 -1
- package/src/lib/api-client.ts +98 -0
- package/src/tools/get-account-analytics.ts +1 -1
- package/src/tools/get-post-analytics.ts +1 -1
package/README.md
CHANGED
|
@@ -182,7 +182,13 @@ Authenticated tools require `POSTERLY_API_KEY`:
|
|
|
182
182
|
- `test_webhook` (send a signed test delivery after explicit confirmation)
|
|
183
183
|
- `get_x_posting_quota`
|
|
184
184
|
|
|
185
|
-
Analytics tools currently support Instagram, LinkedIn, Google Business Profile, Pinterest, and
|
|
185
|
+
Analytics tools currently support Instagram, Facebook Pages, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads.
|
|
186
|
+
|
|
187
|
+
## Media uploads
|
|
188
|
+
|
|
189
|
+
This npm/stdio server can read local file paths. When `upload_media` receives a larger local file, it automatically requests a signed upload URL and uploads the raw bytes before returning the public media URL.
|
|
190
|
+
|
|
191
|
+
The hosted HTTP MCP endpoint cannot do that for local files by itself because MCP tool calls are JSON-only. On hosted HTTP MCP, `upload_media` is for small base64 uploads up to 5MB decoded. For larger media there, use `upload_media_from_url` for public direct media URLs, or call `create_signed_upload` only from clients that can also `PUT` the raw file bytes to the returned `upload_url`.
|
|
186
192
|
|
|
187
193
|
## What the brand tools are for
|
|
188
194
|
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -967,3 +967,16 @@ export declare class PosterlyClient {
|
|
|
967
967
|
}>;
|
|
968
968
|
}
|
|
969
969
|
export declare function resolvePosterlyBaseUrl(baseUrl?: string): string;
|
|
970
|
+
type PosterlyBaseUrlInspection = {
|
|
971
|
+
valid: boolean;
|
|
972
|
+
secure: boolean;
|
|
973
|
+
protocol?: string;
|
|
974
|
+
host?: string;
|
|
975
|
+
trustedPosterlyHost: boolean;
|
|
976
|
+
localDevelopmentHost: boolean;
|
|
977
|
+
warnings: string[];
|
|
978
|
+
message: string;
|
|
979
|
+
};
|
|
980
|
+
export declare function inspectPosterlyBaseUrl(baseUrl: string): PosterlyBaseUrlInspection;
|
|
981
|
+
export declare function validatePosterlyBaseUrlForKeyUse(baseUrl: string): void;
|
|
982
|
+
export {};
|
package/dist/lib/api-client.js
CHANGED
|
@@ -6,6 +6,9 @@ export class PosterlyClient {
|
|
|
6
6
|
constructor(apiKey, baseUrl) {
|
|
7
7
|
this.apiKey = apiKey || process.env.POSTERLY_API_KEY || '';
|
|
8
8
|
this.baseUrl = resolvePosterlyBaseUrl(baseUrl);
|
|
9
|
+
if (this.apiKey) {
|
|
10
|
+
validatePosterlyBaseUrlForKeyUse(this.baseUrl);
|
|
11
|
+
}
|
|
9
12
|
}
|
|
10
13
|
hasApiKey() {
|
|
11
14
|
return Boolean(this.apiKey);
|
|
@@ -485,6 +488,73 @@ export function resolvePosterlyBaseUrl(baseUrl) {
|
|
|
485
488
|
return configured.replace(/\/api\/v1\/?$/, '').replace(/\/$/, '');
|
|
486
489
|
}
|
|
487
490
|
}
|
|
491
|
+
const TRUSTED_POSTERLY_HOSTS = new Set(['poster.ly', 'www.poster.ly']);
|
|
492
|
+
function flagEnabled(name) {
|
|
493
|
+
const value = (process.env[name] || '').trim().toLowerCase();
|
|
494
|
+
return value === 'true' || value === '1' || value === 'yes';
|
|
495
|
+
}
|
|
496
|
+
function normalizeHostname(hostname) {
|
|
497
|
+
return hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
498
|
+
}
|
|
499
|
+
function isLocalHost(hostname) {
|
|
500
|
+
const normalized = normalizeHostname(hostname);
|
|
501
|
+
return (normalized === 'localhost' ||
|
|
502
|
+
normalized.endsWith('.localhost') ||
|
|
503
|
+
normalized === '127.0.0.1' ||
|
|
504
|
+
normalized === '::1');
|
|
505
|
+
}
|
|
506
|
+
export function inspectPosterlyBaseUrl(baseUrl) {
|
|
507
|
+
try {
|
|
508
|
+
const url = new URL(baseUrl);
|
|
509
|
+
const hostname = normalizeHostname(url.hostname);
|
|
510
|
+
const local = isLocalHost(hostname);
|
|
511
|
+
const trustedPosterlyHost = TRUSTED_POSTERLY_HOSTS.has(hostname);
|
|
512
|
+
const secure = url.protocol === 'https:' || (url.protocol === 'http:' && local);
|
|
513
|
+
const warnings = [];
|
|
514
|
+
if (!trustedPosterlyHost) {
|
|
515
|
+
warnings.push(local ? 'local_development_host' : 'custom_host');
|
|
516
|
+
}
|
|
517
|
+
if (!secure) {
|
|
518
|
+
warnings.push('insecure_non_local_url');
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
valid: true,
|
|
522
|
+
secure,
|
|
523
|
+
protocol: url.protocol.replace(/:$/, ''),
|
|
524
|
+
host: url.host,
|
|
525
|
+
trustedPosterlyHost,
|
|
526
|
+
localDevelopmentHost: local,
|
|
527
|
+
warnings,
|
|
528
|
+
message: secure
|
|
529
|
+
? 'Base URL is secure for API-key requests.'
|
|
530
|
+
: 'Non-local HTTP URLs are blocked for API-key requests.',
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return {
|
|
535
|
+
valid: false,
|
|
536
|
+
secure: false,
|
|
537
|
+
trustedPosterlyHost: false,
|
|
538
|
+
localDevelopmentHost: false,
|
|
539
|
+
warnings: ['invalid_url'],
|
|
540
|
+
message: 'Base URL must include a scheme and host, for example https://www.poster.ly.',
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
export function validatePosterlyBaseUrlForKeyUse(baseUrl) {
|
|
545
|
+
const check = inspectPosterlyBaseUrl(baseUrl);
|
|
546
|
+
if (!check.valid) {
|
|
547
|
+
throw new Error(`${check.message} Received: ${baseUrl}`);
|
|
548
|
+
}
|
|
549
|
+
if (!check.secure && !flagEnabled('POSTERLY_ALLOW_INSECURE_URL')) {
|
|
550
|
+
throw new Error(`Refusing to send an API key to ${baseUrl}. Use https, http://localhost for local development, or set POSTERLY_ALLOW_INSECURE_URL=true only for a trusted private development endpoint.`);
|
|
551
|
+
}
|
|
552
|
+
if (!check.trustedPosterlyHost &&
|
|
553
|
+
!check.localDevelopmentHost &&
|
|
554
|
+
!flagEnabled('POSTERLY_ALLOW_CUSTOM_BASE_URL')) {
|
|
555
|
+
throw new Error(`Refusing to send an API key to custom host ${baseUrl}. Set POSTERLY_ALLOW_CUSTOM_BASE_URL=true only for a trusted Posterly deployment.`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
488
558
|
function parseJson(text) {
|
|
489
559
|
try {
|
|
490
560
|
return text ? JSON.parse(text) : {};
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
|
|
3
3
|
export const getAccountAnalyticsTool = {
|
|
4
4
|
name: 'get_account_analytics',
|
|
5
|
-
description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and
|
|
5
|
+
description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads. Uses API-provided display_metrics for platform-native dashboard labels and supports presentation: compact, table, or json.',
|
|
6
6
|
inputSchema: z.object({
|
|
7
7
|
account_id: z
|
|
8
8
|
.number()
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
|
|
3
3
|
export const getPostAnalyticsTool = {
|
|
4
4
|
name: 'get_post_analytics',
|
|
5
|
-
description: 'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and
|
|
5
|
+
description: 'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, 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
6
|
inputSchema: z.object({
|
|
7
7
|
account_id: z
|
|
8
8
|
.number()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.2",
|
|
4
4
|
"description": "MCP server for posterly: schedule and publish social media posts across 11 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://www.poster.ly/mcp",
|
package/src/lib/api-client.ts
CHANGED
|
@@ -638,6 +638,9 @@ export class PosterlyClient {
|
|
|
638
638
|
constructor(apiKey?: string, baseUrl?: string) {
|
|
639
639
|
this.apiKey = apiKey || process.env.POSTERLY_API_KEY || '';
|
|
640
640
|
this.baseUrl = resolvePosterlyBaseUrl(baseUrl);
|
|
641
|
+
if (this.apiKey) {
|
|
642
|
+
validatePosterlyBaseUrlForKeyUse(this.baseUrl);
|
|
643
|
+
}
|
|
641
644
|
}
|
|
642
645
|
|
|
643
646
|
hasApiKey(): boolean {
|
|
@@ -1383,6 +1386,101 @@ export function resolvePosterlyBaseUrl(baseUrl?: string): string {
|
|
|
1383
1386
|
}
|
|
1384
1387
|
}
|
|
1385
1388
|
|
|
1389
|
+
const TRUSTED_POSTERLY_HOSTS = new Set(['poster.ly', 'www.poster.ly']);
|
|
1390
|
+
|
|
1391
|
+
type PosterlyBaseUrlInspection = {
|
|
1392
|
+
valid: boolean;
|
|
1393
|
+
secure: boolean;
|
|
1394
|
+
protocol?: string;
|
|
1395
|
+
host?: string;
|
|
1396
|
+
trustedPosterlyHost: boolean;
|
|
1397
|
+
localDevelopmentHost: boolean;
|
|
1398
|
+
warnings: string[];
|
|
1399
|
+
message: string;
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
function flagEnabled(name: string): boolean {
|
|
1403
|
+
const value = (process.env[name] || '').trim().toLowerCase();
|
|
1404
|
+
return value === 'true' || value === '1' || value === 'yes';
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function normalizeHostname(hostname: string): string {
|
|
1408
|
+
return hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function isLocalHost(hostname: string): boolean {
|
|
1412
|
+
const normalized = normalizeHostname(hostname);
|
|
1413
|
+
return (
|
|
1414
|
+
normalized === 'localhost' ||
|
|
1415
|
+
normalized.endsWith('.localhost') ||
|
|
1416
|
+
normalized === '127.0.0.1' ||
|
|
1417
|
+
normalized === '::1'
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
export function inspectPosterlyBaseUrl(baseUrl: string): PosterlyBaseUrlInspection {
|
|
1422
|
+
try {
|
|
1423
|
+
const url = new URL(baseUrl);
|
|
1424
|
+
const hostname = normalizeHostname(url.hostname);
|
|
1425
|
+
const local = isLocalHost(hostname);
|
|
1426
|
+
const trustedPosterlyHost = TRUSTED_POSTERLY_HOSTS.has(hostname);
|
|
1427
|
+
const secure = url.protocol === 'https:' || (url.protocol === 'http:' && local);
|
|
1428
|
+
const warnings: string[] = [];
|
|
1429
|
+
|
|
1430
|
+
if (!trustedPosterlyHost) {
|
|
1431
|
+
warnings.push(local ? 'local_development_host' : 'custom_host');
|
|
1432
|
+
}
|
|
1433
|
+
if (!secure) {
|
|
1434
|
+
warnings.push('insecure_non_local_url');
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
return {
|
|
1438
|
+
valid: true,
|
|
1439
|
+
secure,
|
|
1440
|
+
protocol: url.protocol.replace(/:$/, ''),
|
|
1441
|
+
host: url.host,
|
|
1442
|
+
trustedPosterlyHost,
|
|
1443
|
+
localDevelopmentHost: local,
|
|
1444
|
+
warnings,
|
|
1445
|
+
message: secure
|
|
1446
|
+
? 'Base URL is secure for API-key requests.'
|
|
1447
|
+
: 'Non-local HTTP URLs are blocked for API-key requests.',
|
|
1448
|
+
};
|
|
1449
|
+
} catch {
|
|
1450
|
+
return {
|
|
1451
|
+
valid: false,
|
|
1452
|
+
secure: false,
|
|
1453
|
+
trustedPosterlyHost: false,
|
|
1454
|
+
localDevelopmentHost: false,
|
|
1455
|
+
warnings: ['invalid_url'],
|
|
1456
|
+
message: 'Base URL must include a scheme and host, for example https://www.poster.ly.',
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
export function validatePosterlyBaseUrlForKeyUse(baseUrl: string): void {
|
|
1462
|
+
const check = inspectPosterlyBaseUrl(baseUrl);
|
|
1463
|
+
if (!check.valid) {
|
|
1464
|
+
throw new Error(`${check.message} Received: ${baseUrl}`);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (!check.secure && !flagEnabled('POSTERLY_ALLOW_INSECURE_URL')) {
|
|
1468
|
+
throw new Error(
|
|
1469
|
+
`Refusing to send an API key to ${baseUrl}. Use https, http://localhost for local development, or set POSTERLY_ALLOW_INSECURE_URL=true only for a trusted private development endpoint.`
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
if (
|
|
1474
|
+
!check.trustedPosterlyHost &&
|
|
1475
|
+
!check.localDevelopmentHost &&
|
|
1476
|
+
!flagEnabled('POSTERLY_ALLOW_CUSTOM_BASE_URL')
|
|
1477
|
+
) {
|
|
1478
|
+
throw new Error(
|
|
1479
|
+
`Refusing to send an API key to custom host ${baseUrl}. Set POSTERLY_ALLOW_CUSTOM_BASE_URL=true only for a trusted Posterly deployment.`
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1386
1484
|
function parseJson(text: string): any {
|
|
1387
1485
|
try {
|
|
1388
1486
|
return text ? JSON.parse(text) : {};
|
|
@@ -8,7 +8,7 @@ type MetricRow = [string, string];
|
|
|
8
8
|
export const getAccountAnalyticsTool = {
|
|
9
9
|
name: 'get_account_analytics',
|
|
10
10
|
description:
|
|
11
|
-
'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and
|
|
11
|
+
'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads. Uses API-provided display_metrics for platform-native dashboard labels and supports presentation: compact, table, or json.',
|
|
12
12
|
inputSchema: z.object({
|
|
13
13
|
account_id: z
|
|
14
14
|
.number()
|
|
@@ -7,7 +7,7 @@ type AnalyticsPresentation = z.infer<typeof presentationSchema>;
|
|
|
7
7
|
export const getPostAnalyticsTool = {
|
|
8
8
|
name: 'get_post_analytics',
|
|
9
9
|
description:
|
|
10
|
-
'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and
|
|
10
|
+
'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, 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.',
|
|
11
11
|
inputSchema: z.object({
|
|
12
12
|
account_id: z
|
|
13
13
|
.number()
|