@omnisocials/mcp-server 1.3.1 → 1.3.3
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 +11 -0
- package/build/client.d.ts +1 -1
- package/build/client.js +23 -3
- package/build/tools/media.js +9 -2
- package/build/tools/posts.js +79 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -189,6 +189,17 @@ The agent-skills package uses a CLI + SKILL.md approach that works in any AI cod
|
|
|
189
189
|
|
|
190
190
|
Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
|
|
191
191
|
|
|
192
|
+
## Changelog
|
|
193
|
+
|
|
194
|
+
### 1.3.2 / 1.3.3
|
|
195
|
+
|
|
196
|
+
- **Fixed:** `create_post` and `create_and_publish_post` no longer throw `str.replace is not a function` when the API returns per-platform `content` as an object. Response formatters now wrapped in a safe fallback so a formatting throw can never surface as a tool error (closes the duplicate-on-retry trap).
|
|
197
|
+
- **Fixed:** `list_posts` and `get_post` now render the correct **Channels** and **Scheduled** values. Previously read the wrong field names (`p.channels` / `p.scheduled_at`) and fell through to `created_at` for the date.
|
|
198
|
+
- **Fixed:** Date rendering always includes an explicit `UTC` suffix. Agents convert to local time when answering the user.
|
|
199
|
+
- **Fixed:** `list_media` now labels video files as `Video` (was showing `Image` for every `.mp4`).
|
|
200
|
+
|
|
201
|
+
(1.3.3 is a metadata republish of 1.3.2 to refresh the README on npmjs.com. No code changes between 1.3.2 and 1.3.3.)
|
|
202
|
+
|
|
192
203
|
## License
|
|
193
204
|
|
|
194
205
|
MIT
|
package/build/client.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare function formatNumber(n: number): string;
|
|
|
7
7
|
export declare function formatBytes(bytes: number): string;
|
|
8
8
|
export declare function formatDate(iso: string | null): string;
|
|
9
9
|
export declare function formatDateTime(iso: string | null): string;
|
|
10
|
-
export declare function truncate(
|
|
10
|
+
export declare function truncate(value: unknown, len: number): string;
|
|
11
11
|
export declare function capitalize(str: string): string;
|
|
12
12
|
export interface XThreadPartInput {
|
|
13
13
|
/** Tweet text — ≤ 280 chars (the API enforces 280 even for X Premium). */
|
package/build/client.js
CHANGED
|
@@ -51,16 +51,36 @@ export function formatDateTime(iso) {
|
|
|
51
51
|
if (!iso)
|
|
52
52
|
return "—";
|
|
53
53
|
const d = new Date(iso);
|
|
54
|
+
// Always render in UTC with an explicit "UTC" suffix so the timezone is
|
|
55
|
+
// unambiguous regardless of which Node runtime / ICU build the MCP runs in.
|
|
56
|
+
// Agents can convert to the user's local time when answering.
|
|
54
57
|
return d.toLocaleDateString("en-US", {
|
|
55
58
|
month: "short",
|
|
56
59
|
day: "numeric",
|
|
57
60
|
year: "numeric",
|
|
58
61
|
hour: "numeric",
|
|
59
62
|
minute: "2-digit",
|
|
60
|
-
|
|
61
|
-
});
|
|
63
|
+
timeZone: "UTC",
|
|
64
|
+
}) + " UTC";
|
|
62
65
|
}
|
|
63
|
-
|
|
66
|
+
// Render any caption-like value (string or per-platform object) to a single
|
|
67
|
+
// display string, then truncate. The backend returns `content` as an object
|
|
68
|
+
// with one key per platform plus `default`; older callers may pass a string.
|
|
69
|
+
export function truncate(value, len) {
|
|
70
|
+
let str;
|
|
71
|
+
if (typeof value === "string") {
|
|
72
|
+
str = value;
|
|
73
|
+
}
|
|
74
|
+
else if (value && typeof value === "object") {
|
|
75
|
+
const obj = value;
|
|
76
|
+
const first = (typeof obj.default === "string" && obj.default) ||
|
|
77
|
+
Object.values(obj).find((v) => typeof v === "string" && v.length > 0) ||
|
|
78
|
+
"";
|
|
79
|
+
str = first;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
64
84
|
if (!str)
|
|
65
85
|
return "";
|
|
66
86
|
const clean = str.replace(/\n/g, " ");
|
package/build/tools/media.js
CHANGED
|
@@ -22,13 +22,20 @@ export function registerMediaTools(server, getClient) {
|
|
|
22
22
|
md += `|---|------|----------|------|----------|\n`;
|
|
23
23
|
for (let i = 0; i < items.length; i++) {
|
|
24
24
|
const m = items[i];
|
|
25
|
-
|
|
25
|
+
// `type` may be "video"/"image" (normalized) or a raw MIME like "video/mp4".
|
|
26
|
+
const rawType = typeof m.type === "string" ? m.type : "";
|
|
27
|
+
const isVideo = rawType === "video" || rawType.startsWith("video/");
|
|
28
|
+
const type = isVideo ? "Video" : "Image";
|
|
26
29
|
md += `| ${i + 1} | ${type} | ${m.filename || "—"} | ${formatBytes(m.size || 0)} | \`${m.id}\` |\n`;
|
|
27
30
|
}
|
|
28
31
|
md += `\nUse the **Media ID** with \`media_ids\` when creating posts.`;
|
|
29
32
|
// Fetch thumbnails for image files (max 4)
|
|
30
33
|
const content = [];
|
|
31
|
-
const imageItems = items.filter((m) =>
|
|
34
|
+
const imageItems = items.filter((m) => {
|
|
35
|
+
const t = typeof m.type === "string" ? m.type : "";
|
|
36
|
+
const isVideo = t === "video" || t.startsWith("video/");
|
|
37
|
+
return !isVideo && m.url;
|
|
38
|
+
}).slice(0, 4);
|
|
32
39
|
const imagePromises = imageItems.map(async (m) => {
|
|
33
40
|
const img = await fetchImageAsBase64(m.url);
|
|
34
41
|
return img ? { ...img, id: m.id } : null;
|
package/build/tools/posts.js
CHANGED
|
@@ -18,6 +18,27 @@ function renderContent(content) {
|
|
|
18
18
|
.map(([key, value]) => `**${key === "default" ? "Default (fallback)" : capitalize(key.replace(/_/g, " "))}**\n\n${value}`)
|
|
19
19
|
.join("\n\n---\n\n");
|
|
20
20
|
}
|
|
21
|
+
// The API returns `accounts` as either an array of platform IDs or an object
|
|
22
|
+
// map `{platform: true|false}`. Older clients may still receive `channels`.
|
|
23
|
+
// Normalize to an array of selected platform names.
|
|
24
|
+
function selectedChannels(p) {
|
|
25
|
+
const src = p?.accounts ?? p?.channels;
|
|
26
|
+
if (!src)
|
|
27
|
+
return [];
|
|
28
|
+
if (Array.isArray(src))
|
|
29
|
+
return src.filter((x) => typeof x === "string" && !!x);
|
|
30
|
+
if (typeof src === "object") {
|
|
31
|
+
return Object.entries(src)
|
|
32
|
+
.filter(([, v]) => !!v)
|
|
33
|
+
.map(([k]) => k);
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
// Pick the most informative timestamp to display. API returns `schedule_at`;
|
|
38
|
+
// `scheduled_at` is kept as a defensive fallback for older response shapes.
|
|
39
|
+
function displayDate(p) {
|
|
40
|
+
return p?.schedule_at ?? p?.scheduled_at ?? p?.published_at ?? p?.created_at ?? null;
|
|
41
|
+
}
|
|
21
42
|
export function registerPostTools(server, getClient) {
|
|
22
43
|
server.tool("list_posts", "List all posts in the workspace. Optionally filter by status. The content column shows a preview of the default caption; if a post has per-platform overrides (e.g. a custom X version), the preview is suffixed with *(per-platform)* — call `get_post` to see every variant.", {
|
|
23
44
|
status: z.string().optional().describe("Filter by status: draft, scheduled, published, failed"),
|
|
@@ -57,13 +78,9 @@ export function registerPostTools(server, getClient) {
|
|
|
57
78
|
else {
|
|
58
79
|
content = truncate(rawContent, hasOverrides ? 35 : 50) + (hasOverrides ? " *(per-platform)*" : "");
|
|
59
80
|
}
|
|
60
|
-
const channels = (p
|
|
81
|
+
const channels = selectedChannels(p).join(", ") || "—";
|
|
61
82
|
const status = capitalize(p.status || "—");
|
|
62
|
-
const date = p
|
|
63
|
-
? formatDateTime(p.scheduled_at)
|
|
64
|
-
: p.published_at
|
|
65
|
-
? formatDateTime(p.published_at)
|
|
66
|
-
: formatDateTime(p.created_at);
|
|
83
|
+
const date = formatDateTime(displayDate(p));
|
|
67
84
|
md += `| ${i + 1} | ${content} | ${channels} | ${status} | ${date} | \`${p.id}\` |\n`;
|
|
68
85
|
}
|
|
69
86
|
return {
|
|
@@ -88,12 +105,13 @@ export function registerPostTools(server, getClient) {
|
|
|
88
105
|
let md = `## Post Details\n\n`;
|
|
89
106
|
md += `| Field | Value |\n`;
|
|
90
107
|
md += `|-------|-------|\n`;
|
|
108
|
+
const schedAt = p.schedule_at ?? p.scheduled_at;
|
|
91
109
|
md += `| **ID** | \`${p.id}\` |\n`;
|
|
92
110
|
md += `| **Status** | ${capitalize(p.status || "—")} |\n`;
|
|
93
111
|
md += `| **Type** | ${p.type || "post"} |\n`;
|
|
94
|
-
md += `| **Channels** | ${(p
|
|
95
|
-
if (
|
|
96
|
-
md += `| **Scheduled** | ${formatDateTime(
|
|
112
|
+
md += `| **Channels** | ${selectedChannels(p).join(", ") || "—"} |\n`;
|
|
113
|
+
if (schedAt)
|
|
114
|
+
md += `| **Scheduled** | ${formatDateTime(schedAt)} |\n`;
|
|
97
115
|
if (p.published_at)
|
|
98
116
|
md += `| **Published** | ${formatDateTime(p.published_at)} |\n`;
|
|
99
117
|
md += `| **Created** | ${formatDateTime(p.created_at)} |\n`;
|
|
@@ -148,7 +166,11 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
148
166
|
- Pinterest posts: ALWAYS require an image AND a board_id.
|
|
149
167
|
- Other platforms (LinkedIn, LinkedIn Page, X, Bluesky, etc.): Media is optional.
|
|
150
168
|
5. **Platform-specific options** (ask only when relevant):
|
|
151
|
-
- Pinterest
|
|
169
|
+
- **Pinterest board (auto-default to first board)**: If Pinterest is in \`channels\` and \`pinterest.board_id\` is NOT provided, do NOT block on asking the user — and do NOT skip Pinterest. Instead:
|
|
170
|
+
1. Call \`get_account\` on the Pinterest account ID — the response includes a \`Pinterest Boards\` table with each board's name and ID.
|
|
171
|
+
2. Use the FIRST board in that list as \`pinterest.board_id\` automatically.
|
|
172
|
+
3. In your reply to the user after the post is created/scheduled, explicitly mention which board you used (e.g. "Posted to your 'Marketing' board on Pinterest — let me know if you'd prefer a different one and I'll move it.") so they can correct course.
|
|
173
|
+
If the user named a board ("post to my Marketing board") or specified one in the request, use that one instead of the first — match on name (case-insensitive) against the boards list.
|
|
152
174
|
- YouTube: Title, privacy status, tags?
|
|
153
175
|
- TikTok: Privacy level?
|
|
154
176
|
- **X threads**: When the user asks for an X/Twitter "thread" (or anything that exceeds 280 chars on X), DO NOT cram it into \`content\` with "1/", "2/" prefixes. Instead pass \`x.thread_parts\` as an array of 2–25 \`{ text }\` objects (each ≤ 280 chars). The \`content\` field is then ignored for X. For a single tweet, omit \`thread_parts\` and use \`content\` normally.
|
|
@@ -213,19 +235,30 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
213
235
|
};
|
|
214
236
|
}
|
|
215
237
|
const p = result.data;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
md
|
|
223
|
-
|
|
224
|
-
md +=
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
238
|
+
// The post is already created by the time this runs. If response
|
|
239
|
+
// formatting throws, we must NOT surface it as a tool error — agents
|
|
240
|
+
// retry on errors and that produces orphan duplicate posts.
|
|
241
|
+
try {
|
|
242
|
+
const schedAt = p.schedule_at ?? p.scheduled_at;
|
|
243
|
+
const channels = selectedChannels(p);
|
|
244
|
+
let md = `## Post Created\n\n`;
|
|
245
|
+
md += `| Field | Value |\n`;
|
|
246
|
+
md += `|-------|-------|\n`;
|
|
247
|
+
md += `| **ID** | \`${p.id}\` |\n`;
|
|
248
|
+
md += `| **Status** | ${capitalize(p.status || "draft")} |\n`;
|
|
249
|
+
if (schedAt)
|
|
250
|
+
md += `| **Scheduled** | ${formatDateTime(schedAt)} |\n`;
|
|
251
|
+
if (channels.length)
|
|
252
|
+
md += `| **Channels** | ${channels.join(", ")} |\n`;
|
|
253
|
+
md += `\n**Content:** ${truncate(p.content, 100)}`;
|
|
254
|
+
return { content: [{ type: "text", text: md }] };
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Fall back to the minimum the agent needs to confirm success — never error.
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: "text", text: `## Post Created\n\n- **ID:** \`${p.id}\`\n- **Status:** ${p.status || "draft"}\n\n_Post was created successfully. Use \`get_post\` with the ID above to see full details._` }],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
229
262
|
});
|
|
230
263
|
server.tool("create_and_publish_post", `Create a new post and publish it immediately (no scheduling).
|
|
231
264
|
|
|
@@ -240,7 +273,8 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
240
273
|
- Instagram/TikTok posts: ALWAYS need at least one image or video.
|
|
241
274
|
- Pinterest: ALWAYS need an image + board_id.
|
|
242
275
|
- Ask the user which media to use. Use list_media to show options.
|
|
243
|
-
4. **
|
|
276
|
+
4. **Pinterest board (auto-default to first board)**: If Pinterest is in \`channels\` and \`pinterest.board_id\` is NOT provided, do NOT block on asking — and do NOT skip Pinterest. Call \`get_account\` on the Pinterest account, take the FIRST board from the returned boards list, and pass its \`id\` as \`pinterest.board_id\`. In your reply, mention which board you used (e.g. "Published to your 'Marketing' board on Pinterest — let me know if you'd prefer a different one.") so the user can redirect. If the user named a specific board in the request, match it (case-insensitive) against the list and use that one instead.
|
|
277
|
+
5. **X threads**: When the user asks for an X/Twitter "thread" (or anything > 280 chars on X), pass \`x.thread_parts\` as a 2–25 entry array of \`{ text }\` objects. Do NOT split into "1/", "2/" inside \`content\` — that produces a single tweet, not a thread.
|
|
244
278
|
|
|
245
279
|
Do NOT call without required media — it will fail.`, {
|
|
246
280
|
content: z.union([z.string(), z.record(z.string(), z.string())]).describe("Post caption. String for same text on all channels, or object with platform keys for per-channel captions: { \"default\": \"fallback\", \"linkedin\": \"long version\", \"threads\": \"short version\" }. The \"default\" key is used for any selected channel without its own key."),
|
|
@@ -284,17 +318,24 @@ Do NOT call without required media — it will fail.`, {
|
|
|
284
318
|
};
|
|
285
319
|
}
|
|
286
320
|
const p = result.data;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
md += `| **
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
321
|
+
try {
|
|
322
|
+
const channels = selectedChannels(p);
|
|
323
|
+
let md = `## Post Created & Publishing\n\n`;
|
|
324
|
+
md += `| Field | Value |\n`;
|
|
325
|
+
md += `|-------|-------|\n`;
|
|
326
|
+
md += `| **ID** | \`${p.id}\` |\n`;
|
|
327
|
+
md += `| **Status** | ${capitalize(p.status || "publishing")} |\n`;
|
|
328
|
+
if (channels.length)
|
|
329
|
+
md += `| **Channels** | ${channels.join(", ")} |\n`;
|
|
330
|
+
md += `\n**Content:** ${truncate(p.content, 100)}`;
|
|
331
|
+
return { content: [{ type: "text", text: md }] };
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// The post is already queued; never let a formatting throw look like an API error.
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text", text: `## Post Created & Publishing\n\n- **ID:** \`${p.id}\`\n- **Status:** ${p.status || "publishing"}\n\n_Post was queued successfully. Use \`get_post\` with the ID above for full details._` }],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
298
339
|
});
|
|
299
340
|
server.tool("update_post", `Update an existing post. Only draft and scheduled posts can be updated.
|
|
300
341
|
|
|
@@ -328,13 +369,14 @@ Do NOT call without required media — it will fail.`, {
|
|
|
328
369
|
};
|
|
329
370
|
}
|
|
330
371
|
const p = result.data;
|
|
372
|
+
const schedAt = p.schedule_at ?? p.scheduled_at;
|
|
331
373
|
let md = `## Post Updated\n\n`;
|
|
332
374
|
md += `| Field | Value |\n`;
|
|
333
375
|
md += `|-------|-------|\n`;
|
|
334
376
|
md += `| **ID** | \`${p.id}\` |\n`;
|
|
335
377
|
md += `| **Status** | ${capitalize(p.status || "—")} |\n`;
|
|
336
|
-
if (
|
|
337
|
-
md += `| **Scheduled** | ${formatDateTime(
|
|
378
|
+
if (schedAt)
|
|
379
|
+
md += `| **Scheduled** | ${formatDateTime(schedAt)} |\n`;
|
|
338
380
|
return {
|
|
339
381
|
content: [{ type: "text", text: md }],
|
|
340
382
|
};
|
package/package.json
CHANGED