@omnisocials/mcp-server 1.31.0 → 1.33.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 +6 -1
- package/build/client.d.ts +23 -1
- package/build/client.js +4 -0
- package/build/index.js +1 -1
- package/build/tools/posts.js +106 -9
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
|
|
|
115
115
|
|
|
116
116
|
## Available Tools
|
|
117
117
|
|
|
118
|
-
### Posts (
|
|
118
|
+
### Posts (13 tools)
|
|
119
119
|
|
|
120
120
|
| Tool | Description |
|
|
121
121
|
|------|-------------|
|
|
@@ -127,6 +127,7 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
|
|
|
127
127
|
| `delete_post` | Delete a post |
|
|
128
128
|
| `publish_post` | Publish a draft or scheduled post now |
|
|
129
129
|
| `retry_post` | Retry the failed platforms of a failed or partially failed post |
|
|
130
|
+
| `list_approval_workflows` | List the workspace's approval workflows (steps + approvers); pass an id as `approval_workflow_id` on `create_post` to hold the post for review |
|
|
130
131
|
| `approve_post` | Approve the current step of a post's approval workflow (the connected user must be a listed approver for that step) |
|
|
131
132
|
| `reject_post` | Reject a post's approval workflow, stopping it immediately |
|
|
132
133
|
| `search_locations` | Find an Instagram or Threads location to tag (Instagram place IDs for `location_id`, Threads location ids for `threads.location_id`; Threads also searches by latitude/longitude) |
|
|
@@ -223,6 +224,10 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
|
|
|
223
224
|
|
|
224
225
|
## Changelog
|
|
225
226
|
|
|
227
|
+
### 1.32.0 (2026-09-02)
|
|
228
|
+
|
|
229
|
+
- **Added: TikTok Photo Mode title.** The `tiktok` option object on `create_post`, `create_and_publish_post`, and `update_post` accepts `title` (max 90 characters): the title TikTok shows above the caption on photo carousels. TikTok's photo endpoint takes the title and the caption as two separate fields, and until now only the caption (`content`) could be set here, so photo posts created through the MCP always published with an empty title. Video posts have a single caption field and ignore `title`. `get_post` returns it as `tiktok.title`. Same on the companion server (`mcp.omnisocials.com`).
|
|
230
|
+
|
|
226
231
|
### 1.31.0 (2026-08-30)
|
|
227
232
|
|
|
228
233
|
- **Added: write access to approvals.** New tools `approve_post` and `reject_post`. Both act on a post's approval workflow (`approval_status: "pending"`, post status `in_approval`) and only succeed if the connected user is a listed approver for the workflow's CURRENT step — steps approve in order. Approving the final step finalizes the post (`scheduled` or `posting`); rejecting stops the whole workflow immediately (`rejected`), with an optional `comment` shown to the requester. Previously `approval_status` was read-only via `get_post` / `list_posts`. Same on the companion server (`mcp.omnisocials.com`).
|
package/build/client.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type MediaIdEntry = string | {
|
|
|
34
34
|
alt?: string;
|
|
35
35
|
};
|
|
36
36
|
export interface XThreadPartInput {
|
|
37
|
-
/** Tweet text — ≤ 280 chars
|
|
37
|
+
/** Tweet text — ≤ 280 chars standard, 25,000 for X Premium/Premium+ accounts. */
|
|
38
38
|
text: string;
|
|
39
39
|
/** Optional per-part media as Library IDs from upload_media (max 4 combined
|
|
40
40
|
* with media_urls). Entries may be `{ id, alt }` to attach alt text (X
|
|
@@ -184,6 +184,27 @@ export interface CreatePostInput {
|
|
|
184
184
|
hashtag_set?: string;
|
|
185
185
|
hashtag_placement?: "caption_append" | "first_comment";
|
|
186
186
|
hashtag_platforms?: string[];
|
|
187
|
+
/** Route the post through a saved approval workflow (GET /approval-workflows). */
|
|
188
|
+
approval_workflow_id?: string;
|
|
189
|
+
}
|
|
190
|
+
export interface ApprovalWorkflowApprover {
|
|
191
|
+
id: string;
|
|
192
|
+
name: string | null;
|
|
193
|
+
email: string | null;
|
|
194
|
+
}
|
|
195
|
+
export interface ApprovalWorkflowStep {
|
|
196
|
+
order: number;
|
|
197
|
+
name: string;
|
|
198
|
+
require_mode: "any" | "all";
|
|
199
|
+
approvers: ApprovalWorkflowApprover[];
|
|
200
|
+
}
|
|
201
|
+
export interface ApprovalWorkflow {
|
|
202
|
+
id: string;
|
|
203
|
+
name: string;
|
|
204
|
+
workspace_id: number | null;
|
|
205
|
+
steps: ApprovalWorkflowStep[];
|
|
206
|
+
created_at: string;
|
|
207
|
+
updated_at: string;
|
|
187
208
|
}
|
|
188
209
|
export declare class OmniSocialsClient {
|
|
189
210
|
private baseUrl;
|
|
@@ -298,6 +319,7 @@ export declare class OmniSocialsClient {
|
|
|
298
319
|
name: string;
|
|
299
320
|
parent_id?: string;
|
|
300
321
|
}): Promise<ApiResponse<MediaFolder>>;
|
|
322
|
+
listApprovalWorkflows(): Promise<ApiResponse<ApprovalWorkflow[]>>;
|
|
301
323
|
listHashtagSets(): Promise<ApiResponse<HashtagSet[]>>;
|
|
302
324
|
createHashtagSet(data: {
|
|
303
325
|
name: string;
|
package/build/client.js
CHANGED
|
@@ -350,6 +350,10 @@ export class OmniSocialsClient {
|
|
|
350
350
|
async createFolder(data) {
|
|
351
351
|
return this.request("POST", "/folders", data);
|
|
352
352
|
}
|
|
353
|
+
// Approval workflows (read-only; configured in the dashboard)
|
|
354
|
+
async listApprovalWorkflows() {
|
|
355
|
+
return this.request("GET", "/approval-workflows");
|
|
356
|
+
}
|
|
353
357
|
// Hashtag Sets
|
|
354
358
|
async listHashtagSets() {
|
|
355
359
|
return this.request("GET", "/hashtag-sets");
|
package/build/index.js
CHANGED
|
@@ -29,7 +29,7 @@ const sessionState = { activeIndex: 0 };
|
|
|
29
29
|
const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
|
|
30
30
|
const server = new McpServer({
|
|
31
31
|
name: "OmniSocials",
|
|
32
|
-
version: "1.
|
|
32
|
+
version: "1.32.0",
|
|
33
33
|
});
|
|
34
34
|
// Register all tools - pass getter function so tools always use the active workspace's client
|
|
35
35
|
registerPostTools(server, getActiveClient);
|
package/build/tools/posts.js
CHANGED
|
@@ -122,6 +122,11 @@ const LINKEDIN_POLL_OPTIONS = z
|
|
|
122
122
|
// google_business, and the thread_parts wrappers with their per-tool
|
|
123
123
|
// nullability/descriptions) stay inline in each tool.
|
|
124
124
|
const TIKTOK_OPTIONS = z.object({
|
|
125
|
+
title: z
|
|
126
|
+
.string()
|
|
127
|
+
.max(90)
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("Photo carousels only. TikTok post title (max 90 characters), shown above the caption on Photo Mode posts. TikTok's photo endpoint takes the title and the caption as two separate fields; the caption (content) is the description. Ignored on video posts, which have one caption field. Read back as tiktok.title in get_post. The tiktok block replaces wholesale on update_post, so resend it together with the other TikTok options."),
|
|
125
130
|
privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
|
|
126
131
|
disable_comment: z.boolean().optional(),
|
|
127
132
|
disable_duet: z.boolean().optional().describe("Reels only. Disable Duet on the video."),
|
|
@@ -156,7 +161,7 @@ const USER_TAG_ENTRY = z.object({
|
|
|
156
161
|
// z.array(...).min(2).max(25)[.nullable()].optional().describe(...) stays
|
|
157
162
|
// per-tool (update_post allows null to clear the thread).
|
|
158
163
|
const X_THREAD_PART = z.object({
|
|
159
|
-
text: z.string().describe("Tweet text (≤ 280 chars by X's weighted count). X counts every link as 23 characters (its t.co length) and every emoji or non-Latin symbol (🚀, …, ₹, CJK) as 2 — so text that looks under 280 can still be over. When trimming generated copy to fit, leave headroom instead of cutting to exactly 280 raw characters."),
|
|
164
|
+
text: z.string().describe("Tweet text (≤ 280 chars by X's weighted count; 25,000 for X Premium/Premium+ accounts). X counts every link as 23 characters (its t.co length) and every emoji or non-Latin symbol (🚀, …, ₹, CJK) as 2 — so text that looks under 280 can still be over. When trimming generated copy to fit, leave headroom instead of cutting to exactly 280 raw characters."),
|
|
160
165
|
media_ids: z.array(mediaIdEntry).max(4).optional().describe("Per-tweet media as Library IDs from upload_media (max 4 combined with media_urls). Each entry is a plain ID string or { id, alt } to attach alt text (X applies it to photos/GIFs). Attach your uploaded graphics to any tweet in the thread."),
|
|
161
166
|
media_urls: z.array(mediaUrlEntry).max(4).optional().describe("Per-tweet media as external URLs (max 4 combined with media_ids). Each entry is a plain URL string or { url, alt } to attach alt text (X applies it to photos/GIFs)."),
|
|
162
167
|
});
|
|
@@ -550,6 +555,57 @@ export function registerPostTools(server, getClient) {
|
|
|
550
555
|
if (firstCommentRows.length) {
|
|
551
556
|
md += `\n\n### First Comments\n\n${firstCommentRows.join("\n")}\n`;
|
|
552
557
|
}
|
|
558
|
+
// Per-platform publish options read-back (TikTok cover frame, privacy,
|
|
559
|
+
// disclosures; YouTube title/privacy/tags; Pinterest board/title/link),
|
|
560
|
+
// so a caller can verify what was set without opening the dashboard.
|
|
561
|
+
// Only values that were actually chosen are listed.
|
|
562
|
+
const optionRows = [];
|
|
563
|
+
const tk = p.tiktok && typeof p.tiktok === "object" ? p.tiktok : {};
|
|
564
|
+
if (typeof tk.title === "string" && tk.title)
|
|
565
|
+
optionRows.push(["TikTok title", tk.title]);
|
|
566
|
+
if (typeof tk.privacy_level === "string" && tk.privacy_level)
|
|
567
|
+
optionRows.push(["TikTok privacy", tk.privacy_level]);
|
|
568
|
+
const coverMs = Number(tk.video_cover_timestamp_ms) || 0;
|
|
569
|
+
if (coverMs > 0)
|
|
570
|
+
optionRows.push(["TikTok cover frame", `${coverMs} ms (${(coverMs / 1000).toFixed(1)}s into the video)`]);
|
|
571
|
+
for (const [key, label] of [
|
|
572
|
+
["disable_comment", "TikTok comments"],
|
|
573
|
+
["disable_duet", "TikTok duet"],
|
|
574
|
+
["disable_stitch", "TikTok stitch"],
|
|
575
|
+
]) {
|
|
576
|
+
if (tk[key] === true)
|
|
577
|
+
optionRows.push([label, "disabled"]);
|
|
578
|
+
}
|
|
579
|
+
for (const [key, label] of [
|
|
580
|
+
["is_aigc", "TikTok AI-generated label"],
|
|
581
|
+
["brand_content_toggle", "TikTok paid partnership"],
|
|
582
|
+
["brand_organic_toggle", "TikTok own-brand disclosure"],
|
|
583
|
+
["auto_add_music", "TikTok auto-add music"],
|
|
584
|
+
]) {
|
|
585
|
+
if (tk[key] === true)
|
|
586
|
+
optionRows.push([label, "on"]);
|
|
587
|
+
}
|
|
588
|
+
const yt = p.youtube && typeof p.youtube === "object" ? p.youtube : {};
|
|
589
|
+
if (typeof yt.title === "string" && yt.title)
|
|
590
|
+
optionRows.push(["YouTube title", yt.title]);
|
|
591
|
+
if (typeof yt.privacy_status === "string" && yt.privacy_status)
|
|
592
|
+
optionRows.push(["YouTube privacy", yt.privacy_status]);
|
|
593
|
+
if (Array.isArray(yt.tags) && yt.tags.length)
|
|
594
|
+
optionRows.push(["YouTube tags", yt.tags.join(", ")]);
|
|
595
|
+
if (yt.made_for_kids === true)
|
|
596
|
+
optionRows.push(["YouTube made for kids", "yes"]);
|
|
597
|
+
const pin = p.pinterest && typeof p.pinterest === "object" ? p.pinterest : {};
|
|
598
|
+
if (typeof pin.board_id === "string" && pin.board_id)
|
|
599
|
+
optionRows.push(["Pinterest board", pin.board_id]);
|
|
600
|
+
if (typeof pin.title === "string" && pin.title)
|
|
601
|
+
optionRows.push(["Pinterest title", pin.title]);
|
|
602
|
+
if (typeof pin.link === "string" && pin.link)
|
|
603
|
+
optionRows.push(["Pinterest link", pin.link]);
|
|
604
|
+
if (optionRows.length) {
|
|
605
|
+
md += `\n\n### Platform Options\n\n| Option | Value |\n|--------|-------|\n`;
|
|
606
|
+
for (const [k, v] of optionRows)
|
|
607
|
+
md += `| ${k} | ${v.replace(/\|/g, "\\|")} |\n`;
|
|
608
|
+
}
|
|
553
609
|
// LinkedIn multi-image style: nested under each channel's options
|
|
554
610
|
// object by the API. Only rendered when the post opts out of the
|
|
555
611
|
// default PDF document carousel.
|
|
@@ -615,8 +671,8 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
615
671
|
- Pinterest carousel — 2–5 images, all same aspect ratio (a video splits off into its own video pin)
|
|
616
672
|
- Facebook, LinkedIn, LinkedIn Page — cannot mix images and video in one post (images only, or a single video)
|
|
617
673
|
|
|
618
|
-
**Per-platform video caps (duration + size, checked via ffprobe at submit
|
|
619
|
-
- X — **140s (2min 20s)** · **512MB** (X also can't mix a video with images in one tweet: a tweet is either 1 video OR up to 4 images
|
|
674
|
+
**Per-platform video caps (duration + size, checked via ffprobe at submit; exceeding either returns a 400 validation_error stating the exact cap and the file's value, e.g. "X only allows videos up to 2min 20s; yours is 2min 35s. Trim the video or deselect X."):**
|
|
675
|
+
- X — **140s (2min 20s)** · **512MB** on free/Basic accounts; when the connected X account has **Premium or Premium+** the caps rise to **125min / 16GB** automatically (tier is read from the connection; reconnect X after upgrading). X also can't mix a video with images in one tweet: a tweet is either 1 video OR up to 4 images
|
|
620
676
|
- Bluesky 180s · Threads 5min · Instagram 15min · TikTok 10min · YouTube Short 3min · LinkedIn 10min · Pinterest 15min · Facebook Reel 90s
|
|
621
677
|
Pick a file within the cap up front; if the user's video is over, tell them the limit so they can trim it rather than retrying blindly.
|
|
622
678
|
|
|
@@ -629,7 +685,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
629
685
|
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.
|
|
630
686
|
- YouTube: Title, privacy status, tags?
|
|
631
687
|
- TikTok: Privacy level?
|
|
632
|
-
- **X threads vs long-form**: A chained "thread" (the user explicitly asks for one) → pass \`x.thread_parts\` as an array of 2–25 \`{ text }\` objects (each ≤ 280 chars); the \`content\` field is then ignored for X. A single **long-form** post on a Premium / Premium+ account → just put the full text (up to 25,000 chars) in \`content\` — no threading needed (check \`platform_details.subscription_type\` via list_accounts). On free / Basic, X caps a single post at 280 chars, so either split into a thread or shorten. Never cram "1/", "2/" prefixes into \`content\` — that posts one tweet, not a thread.
|
|
688
|
+
- **X threads vs long-form**: A chained "thread" (the user explicitly asks for one) → pass \`x.thread_parts\` as an array of 2–25 \`{ text }\` objects (each ≤ 280 chars on free/Basic, 25,000 on Premium/Premium+); the \`content\` field is then ignored for X. A single **long-form** post on a Premium / Premium+ account → just put the full text (up to 25,000 chars) in \`content\` — no threading needed (check \`platform_details.subscription_type\` via list_accounts). On free / Basic, X caps a single post at 280 chars, so either split into a thread or shorten. Never cram "1/", "2/" prefixes into \`content\` — that posts one tweet, not a thread.
|
|
633
689
|
- **Bluesky, Mastodon and Threads chains**: the same \`thread_parts\` shape works under \`bluesky\`, \`mastodon\` and \`threads\` (each an array of 2 to 25 \`{ text, media_ids?, media_urls? }\` objects); parts after the first publish as replies to the previous part and \`content\` is then ignored for that platform. Threads: 2 to 25 parts, 500 characters per part, up to 10 media per part (images and videos can be mixed). Bluesky: 300 characters per part, 4 media. Mastodon: 500 characters per part (instance-dependent), 4 media.
|
|
634
690
|
- **X posts containing a link cost credits**: X's API bills posts whose text contains a URL at a premium, and OmniSocials passes that through as prepaid credits at X's exact rate (20 credits ≈ $0.20 per URL-containing tweet; threads are charged per part that contains a link). The create response includes a \`warnings\` entry (\`x_url_post_credits\`) with the cost and current balance — relay it to the user. Credits are only deducted after the post successfully publishes; a failed publish is never charged. If the balance can't cover it at publish time, only the X target fails (message says to top up at https://app.omnisocials.com/credits) and the post can be retried after topping up. Posts without links stay free — never remove a user's link to dodge the fee without asking them. Scheduling is also gated up front: every scheduled X link post reserves its cost, and a create/schedule that would push the reserved total past the balance is refused with a 402 \`x_credits_insufficient\` error (details carry credits_required / credits_balance / credits_reserved) — tell the user to top up or remove the link, don't silently retry.
|
|
635
691
|
|
|
@@ -704,6 +760,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
704
760
|
thread_parts: z.array(THREADS_THREAD_PART).min(2).max(25).optional().describe("Publish as a chained Threads (Meta) thread instead of a single post. Provide 2 to 25 parts; parts after the first are posted as replies to the previous part. Each part holds up to 500 characters and up to 10 media (images and videos can be mixed) via media_ids (from upload_media) or media_urls. When set, the Threads caption is taken from part 1 and content is ignored for Threads. For a single post, omit thread_parts and use content."),
|
|
705
761
|
}).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
|
|
706
762
|
google_business: GOOGLE_BUSINESS_OPTIONS,
|
|
763
|
+
approval_workflow_id: z.string().optional().describe("Route the post through a saved approval workflow (id from list_approval_workflows). The post is created as `in_approval` instead of `scheduled`: its approvers are notified, they review it on the Approvals page, and it publishes at scheduled_at only once the workflow's last step approves it (or approve_post). Use this whenever the user wants to review/approve agent-created posts before they go out. Requires scheduled_at; not allowed with publish_now (use create_post, never create_and_publish_post). A workflow with no approvers on its first step is rejected with validation_error."),
|
|
707
764
|
}, { title: "Create Post", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async (params) => {
|
|
708
765
|
const result = await getClient().createPost({ ...params, source: "mcp" });
|
|
709
766
|
if (result.error) {
|
|
@@ -726,18 +783,26 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
726
783
|
try {
|
|
727
784
|
const schedAt = p.schedule_at ?? p.scheduled_at;
|
|
728
785
|
const channels = selectedChannels(p);
|
|
729
|
-
|
|
786
|
+
const inApproval = p.status === "in_approval";
|
|
787
|
+
let md = inApproval
|
|
788
|
+
? `## Post Submitted for Approval\n\n`
|
|
789
|
+
: `## Draft Created (not yet published)\n\n`;
|
|
730
790
|
md += `| Field | Value |\n`;
|
|
731
791
|
md += `|-------|-------|\n`;
|
|
732
792
|
md += `| **ID** | \`${p.id}\` |\n`;
|
|
733
793
|
md += `| **Status** | ${capitalize(p.status || "draft")} |\n`;
|
|
794
|
+
if (inApproval)
|
|
795
|
+
md += `| **Approval status** | ${capitalize(p.approval_status || "pending")} |\n`;
|
|
734
796
|
if (schedAt)
|
|
735
|
-
md += `|
|
|
797
|
+
md += `| **${inApproval ? "Publishes (once approved)" : "Scheduled"}** | ${formatDateTime(schedAt)} |\n`;
|
|
736
798
|
if (channels.length)
|
|
737
799
|
md += `| **Channels** | ${channels.join(", ")} |\n`;
|
|
738
800
|
if (appUrl)
|
|
739
801
|
md += `| **Open in OmniSocials** | ${appUrl} |\n`;
|
|
740
802
|
md += `\n**Content:** ${truncate(p.content, 100)}\n\n`;
|
|
803
|
+
if (inApproval) {
|
|
804
|
+
md += `_The approvers have been notified. The post publishes at the time above as soon as the workflow's last step approves it (Approvals page in OmniSocials, or approve_post). Nothing else is needed from the user until then._\n\n`;
|
|
805
|
+
}
|
|
741
806
|
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
742
807
|
for (const w of result.warnings) {
|
|
743
808
|
md += `⚠️ ${w.message}\n`;
|
|
@@ -779,9 +844,9 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
779
844
|
- Pinterest: ALWAYS need an image + board_id.
|
|
780
845
|
- Ask the user which media to use. Use list_media to show options.
|
|
781
846
|
- **Max items per platform** (same caps as create_post — exceeding returns 400): Bluesky/X/Mastodon ≤4, Instagram/Threads ≤10, TikTok ≤35 photos, Pinterest carousel 2–5 same-ratio images. Mixing images+video is rejected on Facebook/LinkedIn/X/Bluesky/Mastodon/TikTok; Instagram and Threads mixed carousels are allowed.
|
|
782
|
-
- **Video duration/size caps** (ffprobe at submit — over either returns 400 validation_error with the exact cap + the file's value): X **140s/512MB** (no video+image mix in one tweet), Bluesky 180s, Threads 5min, Instagram 15min, TikTok 10min, YouTube Short 3min, LinkedIn 10min, Facebook Reel 90s.
|
|
847
|
+
- **Video duration/size caps** (ffprobe at submit — over either returns 400 validation_error with the exact cap + the file's value): X **140s/512MB** free/Basic, **125min/16GB** when the connected X account has Premium/Premium+ (auto-detected; no video+image mix in one tweet), Bluesky 180s, Threads 5min, Instagram 15min, TikTok 10min, YouTube Short 3min, LinkedIn 10min, Facebook Reel 90s.
|
|
783
848
|
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.
|
|
784
|
-
5. **X threads vs long-form**: A chained "thread" → pass \`x.thread_parts\` as a 2–25 entry array of \`{ text }\` objects (each ≤ 280 chars). A single long-form post on a Premium / Premium+ account → put the full text (up to 25,000 chars) in \`content\`; no threading needed. On free / Basic, X caps a single post at 280 chars. Do NOT split into "1/", "2/" inside \`content\` — that produces a single tweet, not a thread. The same \`thread_parts\` shape works under \`bluesky\`, \`mastodon\` and \`threads\` (Threads: 2 to 25 parts, 500 characters per part, up to 10 media per part; parts after the first publish as replies to the previous part).
|
|
849
|
+
5. **X threads vs long-form**: A chained "thread" → pass \`x.thread_parts\` as a 2–25 entry array of \`{ text }\` objects (each ≤ 280 chars on free/Basic, 25,000 on Premium/Premium+). A single long-form post on a Premium / Premium+ account → put the full text (up to 25,000 chars) in \`content\`; no threading needed. On free / Basic, X caps a single post at 280 chars. Do NOT split into "1/", "2/" inside \`content\` — that produces a single tweet, not a thread. The same \`thread_parts\` shape works under \`bluesky\`, \`mastodon\` and \`threads\` (Threads: 2 to 25 parts, 500 characters per part, up to 10 media per part; parts after the first publish as replies to the previous part).
|
|
785
850
|
6. **X posts containing a link cost credits**: X bills API posts whose text contains a URL at a premium; OmniSocials passes that through as prepaid credits at X's exact rate (20 credits ≈ $0.20 per URL-containing tweet, threads charged per link-containing part). Because this tool publishes IMMEDIATELY, the debit happens right away — if the company's balance can't cover it, the X target fails with a top-up message while other platforms still publish. Relay any \`x_url_post_credits\` warning and X publish failure to the user; never strip their link to avoid the fee without asking. If the balance (minus credits reserved by scheduled X link posts) can't cover this post, the request is refused up front with a 402 \`x_credits_insufficient\` error instead of failing at publish.
|
|
786
851
|
|
|
787
852
|
Do NOT call without required media — it will fail.`, {
|
|
@@ -900,7 +965,7 @@ Do NOT call without required media — it will fail.`, {
|
|
|
900
965
|
|
|
901
966
|
**Per-platform options (\`youtube\`, \`pinterest\`, \`instagram\`, \`tiktok\`, \`google_business\`)** are accepted here, same shape as in \`create_post\`. Pass an object — never a JSON-encoded string. For YouTube Shorts, the **title** lives at \`youtube.title\`; the **video description** lives in \`content\` (or \`content.youtube\` for a per-platform override). They are not the same field — changing the caption does NOT rename the Short.
|
|
902
967
|
|
|
903
|
-
**X threads**: To convert an existing draft into a chained X thread, pass \`x.thread_parts\` as a 2–25 entry array of \`{ text }\` objects (each ≤ 280 chars). Pass \`null\` to revert to single-tweet mode. Do NOT shove "1/", "2/" into \`content\` — that's a single tweet, not a thread. The same applies to \`bluesky.thread_parts\`, \`mastodon.thread_parts\` and \`threads.thread_parts\` (Threads: 2 to 25 parts, 500 characters per part, up to 10 media per part; \`null\` reverts to a single post).`, {
|
|
968
|
+
**X threads**: To convert an existing draft into a chained X thread, pass \`x.thread_parts\` as a 2–25 entry array of \`{ text }\` objects (each ≤ 280 chars on free/Basic, 25,000 on Premium/Premium+). Pass \`null\` to revert to single-tweet mode. Do NOT shove "1/", "2/" into \`content\` — that's a single tweet, not a thread. The same applies to \`bluesky.thread_parts\`, \`mastodon.thread_parts\` and \`threads.thread_parts\` (Threads: 2 to 25 parts, 500 characters per part, up to 10 media per part; \`null\` reverts to a single post).`, {
|
|
904
969
|
id: z.string().describe("The post ID to update"),
|
|
905
970
|
content: z.union([z.string(), z.record(z.string(), z.string())]).optional().describe("Updated post caption / body text. For YouTube Shorts this becomes the video description, NOT the title — to rename the Short, use `youtube.title`. String or object with platform keys: { \"default\": \"fallback\", \"linkedin\": \"long\" }."),
|
|
906
971
|
scheduled_at: z.string().optional().describe("Updated scheduled date (ISO 8601, must be in the future). A draft becomes scheduled. A post waiting for approval keeps its in_approval status; only its time moves."),
|
|
@@ -1058,6 +1123,38 @@ The retry runs asynchronously (usually within a few minutes). Poll \`get_post\`
|
|
|
1058
1123
|
content: [{ type: "text", text: md }],
|
|
1059
1124
|
};
|
|
1060
1125
|
});
|
|
1126
|
+
server.tool("list_approval_workflows", `List the approval workflows this workspace can use (company-wide ones plus workspace-bound ones), with their steps and named approvers. Workflows are created in the OmniSocials dashboard (Approvals); this tool only lists them.
|
|
1127
|
+
|
|
1128
|
+
Pass a workflow's id as \`approval_workflow_id\` on \`create_post\` to hold a scheduled post for review: it is created as \`in_approval\`, the approvers are notified, and it publishes at \`scheduled_at\` once the last step approves. Call this first whenever the user wants agent-created posts to be reviewed/approved before they go out; if the list is empty, tell the user to create a workflow under Approvals in the dashboard.`, {}, { title: "List Approval Workflows", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async () => {
|
|
1129
|
+
const result = await getClient().listApprovalWorkflows();
|
|
1130
|
+
if (result.error) {
|
|
1131
|
+
return {
|
|
1132
|
+
content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const items = Array.isArray(result.data) ? result.data : [];
|
|
1136
|
+
if (!items.length) {
|
|
1137
|
+
return {
|
|
1138
|
+
content: [{
|
|
1139
|
+
type: "text",
|
|
1140
|
+
text: "No approval workflows yet. The user can create one in the OmniSocials dashboard under Approvals (choose the approvers per step); then create_post with approval_workflow_id routes posts through it.",
|
|
1141
|
+
}],
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
let md = `## Approval Workflows (${items.length})\n\n`;
|
|
1145
|
+
for (const w of items) {
|
|
1146
|
+
md += `### \`${w.id}\` ${w.name}${w.workspace_id ? "" : " _(company-wide)_"}\n\n`;
|
|
1147
|
+
for (const s of w.steps || []) {
|
|
1148
|
+
const approvers = (s.approvers || []).map((a) => a.name || a.email || a.id).join(", ") || "*(no approvers)*";
|
|
1149
|
+
md += `- Step ${s.order}: ${s.name || "Review"} — ${s.require_mode === "all" ? "all must approve" : "any one approves"}: ${approvers}\n`;
|
|
1150
|
+
}
|
|
1151
|
+
md += `\n`;
|
|
1152
|
+
}
|
|
1153
|
+
md += `Use create_post with approval_workflow_id="<id>" (plus scheduled_at) to submit a post for review.`;
|
|
1154
|
+
return {
|
|
1155
|
+
content: [{ type: "text", text: md }],
|
|
1156
|
+
};
|
|
1157
|
+
});
|
|
1061
1158
|
server.tool("approve_post", `Approve the current step of a post's approval workflow. Only works on a post with \`approval_status: "pending"\` (post status \`in_approval\`) — check \`get_post\` first.
|
|
1062
1159
|
|
|
1063
1160
|
IMPORTANT: this only succeeds if the connected user is a listed approver for the workflow's CURRENT step — steps approve in order, so being an approver on a later step is not enough yet. A \`forbidden\` error means the user isn't (or isn't yet) an approver for this step; tell them who needs to approve instead, don't retry.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnisocials/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"mcpName": "io.github.omnisocials/mcp-server",
|
|
5
5
|
"description": "MCP server for OmniSocials API - manage social media posts, media, accounts, analytics, and webhooks",
|
|
6
6
|
"type": "module",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
},
|
|
54
54
|
"overrides": {
|
|
55
55
|
"zod": "4.4.0",
|
|
56
|
-
"fast-uri": "3.1.
|
|
57
|
-
"hono": "4.
|
|
56
|
+
"fast-uri": "^3.1.7",
|
|
57
|
+
"hono": "^4.13.7",
|
|
58
|
+
"qs": "^6.16.0"
|
|
58
59
|
}
|
|
59
60
|
}
|