@omnisocials/mcp-server 1.35.0 → 1.46.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 +106 -8
- package/build/client.d.ts +147 -1
- package/build/client.js +51 -0
- package/build/index.js +3 -1
- package/build/metrics.d.ts +5 -3
- package/build/metrics.js +41 -4
- package/build/tools/accounts.js +35 -3
- package/build/tools/analytics.js +2 -2
- package/build/tools/automations.d.ts +25 -0
- package/build/tools/automations.js +199 -0
- package/build/tools/inbox.js +17 -8
- package/build/tools/media.js +53 -6
- package/build/tools/posts.js +486 -38
- package/build/tools/webhooks.js +3 -3
- package/build/types.d.ts +372 -6
- package/package.json +3 -3
package/build/tools/media.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { fetchImageAsBase64, formatBytes } from "../client.js";
|
|
3
3
|
export function registerMediaTools(server, getClient) {
|
|
4
|
-
server.tool("list_media", "List media files in the workspace. Returns each file's ID, name, folder, type (image/video), and size. To reuse an existing graphic, SEARCH for it by name here FIRST instead of re-uploading — re-uploading the same image creates duplicates. Media IDs from this list can be passed to create_post. Organize assets into folders with create_folder / list_folders.", {
|
|
4
|
+
server.tool("list_media", "List media files in the workspace. Returns each file's ID, name, folder, type (image/video/document — a document is a PDF kept as one item; its ID in media_ids expands into every page), and size. To reuse an existing graphic, SEARCH for it by name here FIRST instead of re-uploading — re-uploading the same image creates duplicates. Media IDs from this list can be passed to create_post. Organize assets into folders with create_folder / list_folders.", {
|
|
5
5
|
limit: z.string().optional().describe("Max results to return"),
|
|
6
6
|
offset: z.string().optional().describe("Offset for pagination"),
|
|
7
7
|
search: z.string().optional().describe('Find a file by name or filename, e.g. "play5get50". Use this before uploading to check if the graphic already exists.'),
|
|
@@ -30,7 +30,16 @@ export function registerMediaTools(server, getClient) {
|
|
|
30
30
|
// `type` may be "video"/"image" (normalized) or a raw MIME like "video/mp4".
|
|
31
31
|
const rawType = typeof m.type === "string" ? m.type : "";
|
|
32
32
|
const isVideo = rawType === "video" || rawType.startsWith("video/");
|
|
33
|
-
const
|
|
33
|
+
const isDocument = rawType === "document";
|
|
34
|
+
// A PDF document item names its page count (pass its ONE id and the
|
|
35
|
+
// post gets every page); a slide rendered from a PDF names its file
|
|
36
|
+
// and page, so the assistant can pass the whole deck in order.
|
|
37
|
+
const pdfTag = m.pdf
|
|
38
|
+
? isDocument
|
|
39
|
+
? ` (PDF, ${Array.isArray(m.pdf.pages) ? m.pdf.pages.length : m.pdf.rendered_pages} pages)`
|
|
40
|
+
: ` (PDF ${m.pdf.name ? `"${m.pdf.name}" ` : ""}page ${m.pdf.page}/${m.pdf.total_pages})`
|
|
41
|
+
: "";
|
|
42
|
+
const type = (isVideo ? "Video" : isDocument ? "Document" : "Image") + pdfTag;
|
|
34
43
|
const folder = m.folder_id ? `\`${m.folder_id}\`` : "—";
|
|
35
44
|
md += `| ${i + 1} | ${type} | ${m.name || m.filename || "—"} | ${folder} | ${(m.size || "—")} | \`${m.id}\` |\n`;
|
|
36
45
|
}
|
|
@@ -57,7 +66,7 @@ export function registerMediaTools(server, getClient) {
|
|
|
57
66
|
});
|
|
58
67
|
server.tool("upload_media", `Upload media to the library. Accepts EITHER a public URL OR base64-encoded image data (e.g. when the user pastes an image directly in chat). Supported: JPEG, PNG, GIF, WebP, MP4, MOV, AVI, and PDF.
|
|
59
68
|
|
|
60
|
-
PDF = carousel: upload a PDF (as a 'url' or as base64_data with mime_type "application/pdf") and it is split into one image slide per page (max 20 pages). The response lists a Media ID for every slide — pass ALL of them, in order, as media_ids to create_post to post the deck as a carousel. On LinkedIn the slides post as a native swipeable DOCUMENT; on Instagram, TikTok, Threads and Pinterest as an image carousel. This is the way to let a user post an existing slide deck (Canva/PowerPoint/Figma exported to PDF) as a carousel.
|
|
69
|
+
PDF = carousel: upload a PDF (as a 'url' or as base64_data with mime_type "application/pdf") and it is split into one image slide per page (max 20 pages). The response lists a Media ID for every slide — pass ALL of them, in order, as media_ids to create_post to post the deck as a carousel. Prefer pdf_mode "document" when the user wants ONE library item for the deck: the response then has a single Media ID whose media_ids entry expands into every page at post time. On LinkedIn the slides post as a native swipeable DOCUMENT made from the ORIGINAL PDF file (the file is kept: text stays sharp, in-document links work, viewers download the real file, and every page is included even past the 20-slide cap) as long as the slides are posted unchanged and in order; on Instagram, TikTok, Threads and Pinterest as an image carousel. Set linkedin.document_source to 'slides' on the post to send a document rebuilt from the slide images instead. This is the way to let a user post an existing slide deck (Canva/PowerPoint/Figma exported to PDF) as a carousel.
|
|
61
70
|
|
|
62
71
|
Size limits: base64 and direct uploads are capped at 100 MB (and base64 inflates the request body, so the practical ceiling is ~75 MB). For anything larger — up to 1 GB — use ONE of: (a) pass a public 'url' and the server fetches it (handles up to 1 GB), or (b) have the user upload the file in the OmniSocials Library UI at https://app.omnisocials.com/library . IMPORTANT: if the user has a large LOCAL file (over ~100 MB) with no public URL, do NOT tell them to compress or shrink it — instead point them to the Library UI link above, which uploads files up to 1 GB directly.
|
|
63
72
|
|
|
@@ -72,6 +81,7 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
72
81
|
filename: z.string().optional().describe("Optional filename for the uploaded media"),
|
|
73
82
|
name: z.string().optional().describe('Human-readable label so you can find this asset by name later instead of re-uploading it, e.g. "pp-play5get50". Strongly recommended on every upload.'),
|
|
74
83
|
folder: z.string().optional().describe('Optional folder name to file this asset under (created at the top level if it does not exist), e.g. "win-graphics". See list_folders.'),
|
|
84
|
+
pdf_mode: z.enum(["slides", "document"]).optional().describe('PDF uploads only. "slides" (default): one image media item per page, one Media ID each (pass ALL of media_ids to create_post). "document": ONE media item for the whole PDF (type "document"); pass its single Media ID in media_ids and the post gets every page in order. Both keep the original file, which LinkedIn receives. Prefer "document" when the user wants one library item for the deck.'),
|
|
75
85
|
}, { title: "Upload Media", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async (params) => {
|
|
76
86
|
let result;
|
|
77
87
|
if (params.base64_data && params.mime_type) {
|
|
@@ -81,10 +91,17 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
81
91
|
filename: params.filename,
|
|
82
92
|
name: params.name || params.filename,
|
|
83
93
|
folder: params.folder,
|
|
94
|
+
...(params.pdf_mode ? { pdf_mode: params.pdf_mode } : {}),
|
|
84
95
|
});
|
|
85
96
|
}
|
|
86
97
|
else if (params.url) {
|
|
87
|
-
result = await getClient().uploadMedia({
|
|
98
|
+
result = await getClient().uploadMedia({
|
|
99
|
+
url: params.url,
|
|
100
|
+
filename: params.filename,
|
|
101
|
+
name: params.name || params.filename,
|
|
102
|
+
folder: params.folder,
|
|
103
|
+
...(params.pdf_mode ? { pdf_mode: params.pdf_mode } : {}),
|
|
104
|
+
});
|
|
88
105
|
}
|
|
89
106
|
else {
|
|
90
107
|
return {
|
|
@@ -97,6 +114,23 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
97
114
|
};
|
|
98
115
|
}
|
|
99
116
|
const compatibility = result.compatibility;
|
|
117
|
+
// PDF kept as ONE document item (pdf_mode "document").
|
|
118
|
+
if (result.data?.type === "document" && result.pdf) {
|
|
119
|
+
const d = result.data;
|
|
120
|
+
const pdfInfo = result.pdf;
|
|
121
|
+
const pageCount = Array.isArray(d.pdf?.pages) ? d.pdf.pages.length : pdfInfo.rendered_pages;
|
|
122
|
+
let md = `## PDF Uploaded as One Document\n\n`;
|
|
123
|
+
md += `**${d.name || pdfInfo.name || "document.pdf"}**, ${pageCount} ${pageCount === 1 ? "page" : "pages"}`;
|
|
124
|
+
if (pdfInfo.truncated)
|
|
125
|
+
md += ` (${pdfInfo.total_pages} in the file; the first ${pdfInfo.rendered_pages} became page images)`;
|
|
126
|
+
md += `.\n\n**Media ID:** \`${d.id}\`\n\n`;
|
|
127
|
+
md += `Pass this ONE id in \`create_post\` as \`media_ids: ["${d.id}"]\`; the post gets every page, in order. `;
|
|
128
|
+
md += `On LinkedIn it posts as a swipeable **document** made from the **original PDF file** (sharp text, working links, the real download); on Instagram, TikTok, Threads and Pinterest as an **image** carousel. Platforms that do not support carousels use the first page.`;
|
|
129
|
+
if (compatibility && compatibility.compatible === false && compatibility.summary) {
|
|
130
|
+
md += `\n\n⚠️ **${compatibility.summary}** It will still post to your other connected platforms. Ask the user whether to continue before posting.`;
|
|
131
|
+
}
|
|
132
|
+
return { content: [{ type: "text", text: md }] };
|
|
133
|
+
}
|
|
100
134
|
// PDF upload: the server split it into image slides. Surface every slide
|
|
101
135
|
// id and tell the model to pass them ALL to create_post as one carousel.
|
|
102
136
|
const mediaIds = result.media_ids;
|
|
@@ -115,7 +149,7 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
115
149
|
});
|
|
116
150
|
md += `\n**Pass ALL of these to \`create_post\` as \`media_ids\`, in this order**, to post the deck as a carousel:\n\n`;
|
|
117
151
|
md += `\`\`\`\nmedia_ids: [${mediaIds.map((id) => `"${id}"`).join(", ")}]\n\`\`\`\n\n`;
|
|
118
|
-
md += `On LinkedIn this posts as a swipeable **document** carousel; on Instagram, TikTok, Threads and Pinterest as an **image** carousel. Platforms that do not support carousels use the first slide.`;
|
|
152
|
+
md += `On LinkedIn this posts as a swipeable **document** carousel made from the **original PDF file** (kept in storage: sharp text, working links, the real download${pdf?.truncated ? `, all ${pdf.total_pages} pages` : ""}) as long as the slides are posted unchanged and in order; on Instagram, TikTok, Threads and Pinterest as an **image** carousel. Platforms that do not support carousels use the first slide. To send a document rebuilt from the slide images instead, set linkedin.document_source to "slides" on the post.`;
|
|
119
153
|
if (compatibility && compatibility.compatible === false && compatibility.summary) {
|
|
120
154
|
md += `\n\n⚠️ **${compatibility.summary}** It will still post to your other connected platforms. Ask the user whether to continue before posting.`;
|
|
121
155
|
}
|
|
@@ -154,7 +188,7 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
154
188
|
});
|
|
155
189
|
server.tool("check_media_compatibility", `Check whether a video/image will be accepted by the platforms CONNECTED to this workspace, BEFORE uploading or posting. Use this when the user gives you a file (URL) so you can warn them upfront — e.g. "this 995 MB video won't post to Instagram (max 300 MB), continue?" — and confirm before uploading.
|
|
156
190
|
|
|
157
|
-
Provide ONE of: a public 'url' (its size/type is read via a HEAD request), an existing 'media_id', or explicit 'size_bytes' + 'mime'. Returns the connected platforms and any that would reject the file by size or format.`, {
|
|
191
|
+
Provide ONE of: a public 'url' (its size/type is read via a HEAD request), an existing 'media_id', or explicit 'size_bytes' + 'mime'. Returns the connected platforms and any that would reject the file by size or format, plus IMAGE FIT rows for an image whose size is known (a library file): per connected channel, whether its feed shows the image uncropped and what a post does by default (Instagram crops a 9:16 / wide image to 4:5 / 1.91:1 around the subject; Pinterest keeps a tall Pin and reports it). When a fit row says fits: false, ask the user "crop to <target> (recommended for Instagram: Meta can boost it) or keep the original (the channel cuts it itself)?" and pass the answer as media_fit on create_post.`, {
|
|
158
192
|
url: z.string().optional().describe("Public URL of the file to check. Use this OR media_id OR size_bytes+mime."),
|
|
159
193
|
media_id: z.string().optional().describe("Id of an already-uploaded library item to check."),
|
|
160
194
|
size_bytes: z.number().optional().describe("File size in bytes (use with mime when you already know them)."),
|
|
@@ -191,6 +225,19 @@ Provide ONE of: a public 'url' (its size/type is read via a HEAD request), an ex
|
|
|
191
225
|
}
|
|
192
226
|
md += `\nAsk the user whether to continue. It will still post to the platforms not listed above.`;
|
|
193
227
|
}
|
|
228
|
+
const outside = (r.fit || []).filter((f) => !f.fits);
|
|
229
|
+
if (outside.length) {
|
|
230
|
+
const dims = r.file?.width && r.file?.height ? ` (${r.file.width}x${r.file.height})` : "";
|
|
231
|
+
md += `\n\n**Image fit**${dims}: how these channels show it in the feed\n\n| Platform | Ratio | In the feed | A post does |\n|----------|-------|-------------|-------------|\n`;
|
|
232
|
+
for (const f of outside) {
|
|
233
|
+
const does = f.default_action === "crop" ? `crops it to ${f.target} (media_fit "keep" sends the original)` : `keeps it and reports it (media_fit "crop" crops to ${f.target})`;
|
|
234
|
+
md += `| ${f.display_name} | ${f.ratio} | cut to ${f.target} | ${does} |\n`;
|
|
235
|
+
}
|
|
236
|
+
md += `\nAsk the user per channel above: crop to the target (Instagram: Meta can then boost the post) or keep the original (the channel cuts it itself)? Pass the answer as \`media_fit\` on create_post. Stories, reels and the other channels are not affected.`;
|
|
237
|
+
}
|
|
238
|
+
else if ((r.fit || []).length) {
|
|
239
|
+
md += `\n\nImage fit: this image shows uncropped in every connected feed with a rule (${(r.fit || []).map((f) => f.display_name).join(", ")}).`;
|
|
240
|
+
}
|
|
194
241
|
return { content: [{ type: "text", text: md }] };
|
|
195
242
|
});
|
|
196
243
|
server.tool("update_media", "Rename an existing media file or move it into a folder (so it's findable later instead of re-uploading it). Only the fields you pass are changed.", {
|