posterly-mcp-server 0.51.1 → 0.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/lib/api-client.d.ts +32 -0
- package/dist/lib/api-client.js +6 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/create-media-drop.js +4 -4
- package/dist/tools/create-signed-upload.d.ts +49 -16
- package/dist/tools/create-signed-upload.js +61 -4
- package/dist/tools/list-media.js +2 -2
- package/dist/tools/upload-media.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ Add the same server definition to your Cursor MCP settings:
|
|
|
123
123
|
|
|
124
124
|
## Available tools
|
|
125
125
|
|
|
126
|
-
`posterly-mcp-server@0.
|
|
126
|
+
`posterly-mcp-server@0.52.1` exposes 64 tools covering 100 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 61 tools covering 97 actions.
|
|
127
127
|
|
|
128
128
|
Many tools pick what to do with one parameter: `view` (reads), `action` (writes), or `type`. A few pick their mode from which parameters you pass (for example `update_post` with `status` changes the status instead of editing). Every tool description names all of its views and actions.
|
|
129
129
|
|
|
@@ -446,7 +446,7 @@ In 0.45.0, the old tools below moved into the tools above. The old names still w
|
|
|
446
446
|
|
|
447
447
|
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.
|
|
448
448
|
|
|
449
|
-
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` with `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`.
|
|
449
|
+
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` with `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`. One call covers 1 to 20 files with `files`; each file gets its own slot (or its own error), and slots expire in 15 minutes.
|
|
450
450
|
|
|
451
451
|
## What the brand tools are for
|
|
452
452
|
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -1242,6 +1242,33 @@ export interface XPostingQuotaResponse {
|
|
|
1242
1242
|
addon?: Record<string, unknown>;
|
|
1243
1243
|
plans?: Array<Record<string, unknown>>;
|
|
1244
1244
|
}
|
|
1245
|
+
export type SignedUploadBatchSlot = {
|
|
1246
|
+
index: number;
|
|
1247
|
+
filename: string;
|
|
1248
|
+
ok: true;
|
|
1249
|
+
upload_url: string;
|
|
1250
|
+
token: string;
|
|
1251
|
+
path: string;
|
|
1252
|
+
public_url: string;
|
|
1253
|
+
expires_at: string;
|
|
1254
|
+
max_size: number;
|
|
1255
|
+
headers?: Record<string, string>;
|
|
1256
|
+
provider?: string;
|
|
1257
|
+
} | {
|
|
1258
|
+
index: number;
|
|
1259
|
+
filename: string;
|
|
1260
|
+
ok: false;
|
|
1261
|
+
error: string;
|
|
1262
|
+
code: string;
|
|
1263
|
+
};
|
|
1264
|
+
export interface SignedUploadBatchResponse {
|
|
1265
|
+
slots: SignedUploadBatchSlot[];
|
|
1266
|
+
summary: {
|
|
1267
|
+
requested: number;
|
|
1268
|
+
ok: number;
|
|
1269
|
+
failed: number;
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1245
1272
|
export declare class PosterlyClient {
|
|
1246
1273
|
private baseUrl;
|
|
1247
1274
|
private apiKey;
|
|
@@ -1683,6 +1710,11 @@ export declare class PosterlyClient {
|
|
|
1683
1710
|
public_url: string;
|
|
1684
1711
|
headers?: Record<string, string>;
|
|
1685
1712
|
}>;
|
|
1713
|
+
getSignedUploadUrls(files: Array<{
|
|
1714
|
+
filename: string;
|
|
1715
|
+
content_type: string;
|
|
1716
|
+
size: number;
|
|
1717
|
+
}>, workspaceId?: string): Promise<SignedUploadBatchResponse>;
|
|
1686
1718
|
createMediaDrop(input?: {
|
|
1687
1719
|
filename?: string;
|
|
1688
1720
|
maxFiles?: number;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -673,6 +673,12 @@ export class PosterlyClient {
|
|
|
673
673
|
...(workspaceId ? { workspace_id: workspaceId } : {}),
|
|
674
674
|
});
|
|
675
675
|
}
|
|
676
|
+
async getSignedUploadUrls(files, workspaceId) {
|
|
677
|
+
return this.request('POST', '/media/signed-upload', {
|
|
678
|
+
files,
|
|
679
|
+
...(workspaceId ? { workspace_id: workspaceId } : {}),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
676
682
|
async createMediaDrop(input) {
|
|
677
683
|
return this.request('POST', '/media/drop-sessions', {
|
|
678
684
|
filename: input?.filename,
|
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.52.1";
|
package/dist/lib/version.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// tool set (minus the intentional pre-auth signup tools that only this stdio
|
|
6
6
|
// package exposes). `npm run check:mcp-parity` enforces both the version match
|
|
7
7
|
// and the tool-list match, and runs in the pre-commit hook.
|
|
8
|
-
export const POSTERLY_MCP_VERSION = '0.
|
|
8
|
+
export const POSTERLY_MCP_VERSION = '0.52.1';
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const createMediaDropTool = {
|
|
3
3
|
name: 'create_media_drop',
|
|
4
|
-
description: 'Create a human drop page so ChatGPT or Claude users can upload
|
|
4
|
+
description: 'Create a human drop page so ChatGPT or Claude users can upload laptop files (up to 20 per link) without dashboard login. Returns drop_url (https://www.poster.ly/drop/<token>). Send that URL to the user, wait, then call list_media. Accepts JPEG, PNG, GIF, WebP, MP4, MOV, WebM, and PDF up to 50MB (a PDF is for LinkedIn document posts); HEIC is not accepted. This is not the chat paperclip and not create_signed_upload.',
|
|
5
5
|
inputSchema: z.object({
|
|
6
6
|
filename: z.string().optional().describe('Optional filename hint for the user, e.g. launch-video.mp4'),
|
|
7
7
|
max_files: z
|
|
8
8
|
.number()
|
|
9
9
|
.int()
|
|
10
10
|
.min(1)
|
|
11
|
-
.max(
|
|
11
|
+
.max(20)
|
|
12
12
|
.optional()
|
|
13
|
-
.describe('How many files the drop can accept. Default 10, max
|
|
13
|
+
.describe('How many files the drop can accept. Default 10, max 20.'),
|
|
14
14
|
workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
|
|
15
15
|
}),
|
|
16
16
|
async execute(client, input) {
|
|
@@ -28,7 +28,7 @@ export const createMediaDropTool = {
|
|
|
28
28
|
`• Max files: ${drop.max_files}`,
|
|
29
29
|
drop.instructions,
|
|
30
30
|
'After they finish, call list_media with this drop_session_id, then validate_post / create_post using public_url.',
|
|
31
|
-
'Chat attachments never reach MCP.
|
|
31
|
+
'Chat attachments never reach MCP. Accepted: JPEG, PNG, GIF, WebP, MP4, MOV, WebM, and PDF up to 50MB (a PDF is for LinkedIn document posts). HEIC is not accepted.',
|
|
32
32
|
].join('\n');
|
|
33
33
|
},
|
|
34
34
|
};
|
|
@@ -1,28 +1,61 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { PosterlyClient } from '../lib/api-client.js';
|
|
2
|
+
import type { PosterlyClient, SignedUploadBatchResponse } from '../lib/api-client.js';
|
|
3
|
+
type SignedUploadInput = {
|
|
4
|
+
filename?: string;
|
|
5
|
+
content_type?: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
files?: Array<{
|
|
8
|
+
filename: string;
|
|
9
|
+
content_type: string;
|
|
10
|
+
size: number;
|
|
11
|
+
}>;
|
|
12
|
+
workspace_id?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function formatSignedUploadBatch(batch: SignedUploadBatchResponse, requested: Array<{
|
|
15
|
+
content_type: string;
|
|
16
|
+
}>): string;
|
|
3
17
|
export declare const createSignedUploadTool: {
|
|
4
18
|
name: string;
|
|
5
19
|
description: string;
|
|
6
20
|
inputSchema: z.ZodObject<{
|
|
7
|
-
filename: z.ZodString
|
|
8
|
-
content_type: z.ZodString
|
|
9
|
-
size: z.ZodNumber
|
|
21
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
22
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
23
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
24
|
+
files: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
25
|
+
filename: z.ZodString;
|
|
26
|
+
content_type: z.ZodString;
|
|
27
|
+
size: z.ZodNumber;
|
|
28
|
+
}, "strip", z.ZodTypeAny, {
|
|
29
|
+
filename: string;
|
|
30
|
+
content_type: string;
|
|
31
|
+
size: number;
|
|
32
|
+
}, {
|
|
33
|
+
filename: string;
|
|
34
|
+
content_type: string;
|
|
35
|
+
size: number;
|
|
36
|
+
}>, "many">>;
|
|
10
37
|
workspace_id: z.ZodOptional<z.ZodString>;
|
|
11
38
|
}, "strip", z.ZodTypeAny, {
|
|
12
|
-
filename: string;
|
|
13
|
-
content_type: string;
|
|
14
|
-
size: number;
|
|
15
39
|
workspace_id?: string | undefined;
|
|
40
|
+
filename?: string | undefined;
|
|
41
|
+
content_type?: string | undefined;
|
|
42
|
+
size?: number | undefined;
|
|
43
|
+
files?: {
|
|
44
|
+
filename: string;
|
|
45
|
+
content_type: string;
|
|
46
|
+
size: number;
|
|
47
|
+
}[] | undefined;
|
|
16
48
|
}, {
|
|
17
|
-
filename: string;
|
|
18
|
-
content_type: string;
|
|
19
|
-
size: number;
|
|
20
49
|
workspace_id?: string | undefined;
|
|
50
|
+
filename?: string | undefined;
|
|
51
|
+
content_type?: string | undefined;
|
|
52
|
+
size?: number | undefined;
|
|
53
|
+
files?: {
|
|
54
|
+
filename: string;
|
|
55
|
+
content_type: string;
|
|
56
|
+
size: number;
|
|
57
|
+
}[] | undefined;
|
|
21
58
|
}>;
|
|
22
|
-
execute(client: PosterlyClient, input:
|
|
23
|
-
filename: string;
|
|
24
|
-
content_type: string;
|
|
25
|
-
size: number;
|
|
26
|
-
workspace_id?: string;
|
|
27
|
-
}): Promise<string>;
|
|
59
|
+
execute(client: PosterlyClient, input: SignedUploadInput): Promise<string>;
|
|
28
60
|
};
|
|
61
|
+
export {};
|
|
@@ -1,14 +1,71 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
const fileSchema = z.object({
|
|
3
|
+
filename: z.string().describe('Filename with extension, e.g. launch-video.mp4'),
|
|
4
|
+
content_type: z.string().describe('MIME type, e.g. video/mp4'),
|
|
5
|
+
size: z.number().int().positive().describe('File size in bytes'),
|
|
6
|
+
});
|
|
7
|
+
export function formatSignedUploadBatch(batch, requested) {
|
|
8
|
+
const lines = [
|
|
9
|
+
`Signed upload slots: ${batch.summary.requested} requested, ${batch.summary.ok} ready, ${batch.summary.failed} failed.`,
|
|
10
|
+
'',
|
|
11
|
+
];
|
|
12
|
+
let earliestExpiry = null;
|
|
13
|
+
batch.slots.forEach((slot, position) => {
|
|
14
|
+
const number = position + 1;
|
|
15
|
+
if (slot.ok) {
|
|
16
|
+
const headers = slot.headers || {};
|
|
17
|
+
const headerKeys = Object.keys(headers);
|
|
18
|
+
const contentTypeKey = headerKeys.find((key) => key.toLowerCase() === 'content-type');
|
|
19
|
+
const contentType = contentTypeKey ? headers[contentTypeKey] : requested[slot.index]?.content_type;
|
|
20
|
+
lines.push(`${number}. ${slot.filename}`);
|
|
21
|
+
lines.push(` PUT ${slot.upload_url}`);
|
|
22
|
+
if (contentType)
|
|
23
|
+
lines.push(` Content-Type: ${contentType}`);
|
|
24
|
+
for (const key of headerKeys) {
|
|
25
|
+
if (key === contentTypeKey)
|
|
26
|
+
continue;
|
|
27
|
+
lines.push(` ${key}: ${headers[key]}`);
|
|
28
|
+
}
|
|
29
|
+
lines.push(` Public URL after upload: ${slot.public_url}`);
|
|
30
|
+
if (slot.expires_at && (!earliestExpiry || slot.expires_at < earliestExpiry)) {
|
|
31
|
+
earliestExpiry = slot.expires_at;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
lines.push(`${number}. ${slot.filename || '(no filename)'}`);
|
|
36
|
+
lines.push(` Failed (${slot.code}): ${slot.error}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push('');
|
|
39
|
+
});
|
|
40
|
+
lines.push(`PUT each file, then pass public_url to create_post / create_posts_batch. Slots expire at ${earliestExpiry ?? 'the expires_at time returned (15 minutes)'}, call again for fresh slots.`);
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
2
43
|
export const createSignedUploadTool = {
|
|
3
44
|
name: 'create_signed_upload',
|
|
4
|
-
description: 'Create
|
|
45
|
+
description: 'Create signed upload URLs for larger images, videos or PDFs, 1 to 20 files per call. Accepts JPEG, PNG, GIF, WebP, MP4, MOV, WebM, and PDF up to 50MB (a PDF is for LinkedIn document posts); HEIC is not accepted. For one file pass filename, content_type and size. For several pass files: [{filename, content_type, size}] (up to 20) and get one slot per file, with any file that fails reported in place. PUT the raw bytes of each file to its upload_url (object storage) with the returned headers, then pass public_url to create_post / create_posts_batch. For a PDF, create the post with post_type document, media_url set to that public_url and a document_title in settings (LinkedIn only). Slots expire in 15 minutes.',
|
|
5
46
|
inputSchema: z.object({
|
|
6
|
-
filename: z.string().describe('
|
|
7
|
-
content_type: z.string().describe('MIME type, e.g. video/mp4'),
|
|
8
|
-
size: z.number().int().positive().describe('
|
|
47
|
+
filename: z.string().optional().describe('Single file: filename with extension, e.g. launch-video.mp4. Omit when using files.'),
|
|
48
|
+
content_type: z.string().optional().describe('Single file: MIME type, e.g. video/mp4. Omit when using files.'),
|
|
49
|
+
size: z.number().int().positive().optional().describe('Single file: size in bytes. Omit when using files.'),
|
|
50
|
+
files: z
|
|
51
|
+
.array(fileSchema)
|
|
52
|
+
.min(1)
|
|
53
|
+
.max(20)
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Batch: 1 to 20 files, each with filename, content_type and size. Use instead of the single-file fields.'),
|
|
9
56
|
workspace_id: z.string().optional().describe('Optional. The workspace this acts in and whose plan pays (from whoami). Needed only when your connection can act in several workspaces.'),
|
|
10
57
|
}),
|
|
11
58
|
async execute(client, input) {
|
|
59
|
+
if (input.files !== undefined) {
|
|
60
|
+
if (input.filename !== undefined || input.content_type !== undefined || input.size !== undefined) {
|
|
61
|
+
throw new Error('Pass either files or the single-file fields (filename, content_type, size), not both.');
|
|
62
|
+
}
|
|
63
|
+
const batch = await client.getSignedUploadUrls(input.files, input.workspace_id);
|
|
64
|
+
return formatSignedUploadBatch(batch, input.files);
|
|
65
|
+
}
|
|
66
|
+
if (!input.filename || !input.content_type || !input.size) {
|
|
67
|
+
throw new Error('Provide filename, content_type and size for one file, or files for a batch of up to 20.');
|
|
68
|
+
}
|
|
12
69
|
const signed = await client.getSignedUploadUrl(input.filename, input.content_type, input.size, input.workspace_id);
|
|
13
70
|
return [
|
|
14
71
|
'Signed upload created.',
|
package/dist/tools/list-media.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { formatPagingNote } from '../lib/format.js';
|
|
3
3
|
export const listMediaTool = {
|
|
4
4
|
name: 'list_media',
|
|
5
|
-
description: 'List the newest media assets for the authenticated user. After create_media_drop, pass drop_session_id to see files the human uploaded on /drop/<token>. Use public_url with validate_post / create_post. The output says whether every matching file is shown; page with offset (offset + limit up to 1000) when it is not.',
|
|
5
|
+
description: 'List the newest media assets for the authenticated user. After create_media_drop, pass drop_session_id to see files the human uploaded on /drop/<token>. Use public_url with validate_post / create_post. A drop listing returns up to 20 files per page by default so a full drop fits one page. The output says whether every matching file is shown; page with offset (offset + limit up to 1000) when it is not.',
|
|
6
6
|
inputSchema: z.object({
|
|
7
7
|
limit: z
|
|
8
8
|
.number()
|
|
@@ -10,7 +10,7 @@ export const listMediaTool = {
|
|
|
10
10
|
.min(1)
|
|
11
11
|
.max(50)
|
|
12
12
|
.optional()
|
|
13
|
-
.describe('Number of assets to return (default 10, max 50)'),
|
|
13
|
+
.describe('Number of assets to return (default 10, or 20 when drop_session_id is set; max 50)'),
|
|
14
14
|
offset: z
|
|
15
15
|
.number()
|
|
16
16
|
.int()
|
|
@@ -3,7 +3,7 @@ import { mdError, mdSuccess } from '../lib/format.js';
|
|
|
3
3
|
import { validateArgs } from '../lib/tool-runtime.js';
|
|
4
4
|
export const uploadMediaTool = {
|
|
5
5
|
name: 'upload_media',
|
|
6
|
-
description: 'Upload an image or video to posterly storage and get a URL for create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB. Three ways in: file_path (an explicit local path the user gave you), base64_data with filename (PREFERRED for images shared in chat, which are not on the local filesystem), or url (a public HTTPS image or video; local/private network URLs are blocked).\n\n' +
|
|
6
|
+
description: 'Upload an image or video to posterly storage and get a URL for create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM (PDFs are not accepted here: use create_signed_upload or create_media_drop). Images up to 10MB, videos up to 50MB. Three ways in: file_path (an explicit local path the user gave you), base64_data with filename (PREFERRED for images shared in chat, which are not on the local filesystem), or url (a public HTTPS image or video; local/private network URLs are blocked).\n\n' +
|
|
7
7
|
'This writes to the user\'s media storage (counts against quota) but does NOT publish the file anywhere. Uploading is safe - it\'s the subsequent `create_post` call that publishes content, and that tool has its own confirmation requirements. Never guess file paths. You can also pass public HTTP(S) media URLs directly to create_post/update_post; use url here when you want to upload once and reuse the returned posterly URL.',
|
|
8
8
|
inputSchema: z.object({
|
|
9
9
|
file_path: z
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.1",
|
|
4
4
|
"mcpName": "io.github.awpthorp/posterly",
|
|
5
5
|
"description": "MCP server for posterly: schedule and publish social media posts across 18 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
|
|
6
6
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.awpthorp/posterly",
|
|
4
4
|
"title": "posterly",
|
|
5
5
|
"description": "Validate, schedule, publish, and analyze social content across 18 platforms with posterly.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.52.1",
|
|
7
7
|
"websiteUrl": "https://www.poster.ly/mcp",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/awpthorp/posterly-mcp-server",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "posterly-mcp-server",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.52.1",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|