@omnisocials/mcp-server 1.3.0 → 1.3.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/build/client.d.ts +24 -3
- package/build/tools/posts.js +92 -6
- package/build/tools/workspaces.js +2 -2
- package/build/types.d.ts +11 -0
- package/package.json +1 -1
package/build/client.d.ts
CHANGED
|
@@ -9,6 +9,27 @@ export declare function formatDate(iso: string | null): string;
|
|
|
9
9
|
export declare function formatDateTime(iso: string | null): string;
|
|
10
10
|
export declare function truncate(str: string, len: number): string;
|
|
11
11
|
export declare function capitalize(str: string): string;
|
|
12
|
+
export interface XThreadPartInput {
|
|
13
|
+
/** Tweet text — ≤ 280 chars (the API enforces 280 even for X Premium). */
|
|
14
|
+
text: string;
|
|
15
|
+
/** Optional per-part media URLs (max 4). Overrides top-level media_urls.x. */
|
|
16
|
+
media_urls?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface XPostOptions {
|
|
19
|
+
reply_settings?: "" | "following" | "mentionedUsers";
|
|
20
|
+
paid_partnership?: boolean;
|
|
21
|
+
made_with_ai?: boolean;
|
|
22
|
+
/** Provide 2–25 parts to publish as a thread. Omit for a single tweet. */
|
|
23
|
+
thread_parts?: XThreadPartInput[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Update-side variant: passing `thread_parts: null` explicitly clears the
|
|
27
|
+
* thread shape on the post (reverting to single-tweet mode). Passing
|
|
28
|
+
* `undefined` (omit it) leaves the existing thread untouched.
|
|
29
|
+
*/
|
|
30
|
+
export interface XPostOptionsUpdate extends Omit<XPostOptions, "thread_parts"> {
|
|
31
|
+
thread_parts?: XThreadPartInput[] | null;
|
|
32
|
+
}
|
|
12
33
|
export declare class OmniSocialsClient {
|
|
13
34
|
private baseUrl;
|
|
14
35
|
private apiKey;
|
|
@@ -32,7 +53,7 @@ export declare class OmniSocialsClient {
|
|
|
32
53
|
youtube?: Record<string, unknown>;
|
|
33
54
|
instagram?: Record<string, unknown>;
|
|
34
55
|
tiktok?: Record<string, unknown>;
|
|
35
|
-
x?:
|
|
56
|
+
x?: XPostOptions;
|
|
36
57
|
}): Promise<ApiResponse<unknown>>;
|
|
37
58
|
createAndPublishPost(data: {
|
|
38
59
|
content: string | Record<string, string>;
|
|
@@ -45,7 +66,7 @@ export declare class OmniSocialsClient {
|
|
|
45
66
|
youtube?: Record<string, unknown>;
|
|
46
67
|
instagram?: Record<string, unknown>;
|
|
47
68
|
tiktok?: Record<string, unknown>;
|
|
48
|
-
x?:
|
|
69
|
+
x?: XPostOptions;
|
|
49
70
|
}): Promise<ApiResponse<unknown>>;
|
|
50
71
|
updatePost(id: string, data: {
|
|
51
72
|
content?: string | Record<string, string>;
|
|
@@ -58,7 +79,7 @@ export declare class OmniSocialsClient {
|
|
|
58
79
|
youtube?: Record<string, unknown>;
|
|
59
80
|
instagram?: Record<string, unknown>;
|
|
60
81
|
tiktok?: Record<string, unknown>;
|
|
61
|
-
x?:
|
|
82
|
+
x?: XPostOptionsUpdate;
|
|
62
83
|
}): Promise<ApiResponse<unknown>>;
|
|
63
84
|
deletePost(id: string): Promise<ApiResponse<unknown>>;
|
|
64
85
|
publishPost(id: string): Promise<ApiResponse<unknown>>;
|
package/build/tools/posts.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatDateTime, truncate, capitalize } from "../client.js";
|
|
3
|
+
// Render full content for get_post — preserves per-platform overrides so Claude
|
|
4
|
+
// can see custom captions (e.g. a shorter X version) instead of only `default`.
|
|
5
|
+
function renderContent(content) {
|
|
6
|
+
if (!content)
|
|
7
|
+
return "(empty)";
|
|
8
|
+
if (typeof content === "string")
|
|
9
|
+
return content;
|
|
10
|
+
if (typeof content !== "object")
|
|
11
|
+
return "(empty)";
|
|
12
|
+
const entries = Object.entries(content).filter(([, v]) => typeof v === "string" && v);
|
|
13
|
+
if (!entries.length)
|
|
14
|
+
return "(empty)";
|
|
15
|
+
if (entries.length === 1)
|
|
16
|
+
return entries[0][1];
|
|
17
|
+
return entries
|
|
18
|
+
.map(([key, value]) => `**${key === "default" ? "Default (fallback)" : capitalize(key.replace(/_/g, " "))}**\n\n${value}`)
|
|
19
|
+
.join("\n\n---\n\n");
|
|
20
|
+
}
|
|
3
21
|
export function registerPostTools(server, getClient) {
|
|
4
|
-
server.tool("list_posts", "List all posts in the workspace. Optionally filter by status.", {
|
|
22
|
+
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.", {
|
|
5
23
|
status: z.string().optional().describe("Filter by status: draft, scheduled, published, failed"),
|
|
6
24
|
limit: z.string().optional().describe("Max results to return (default: 20)"),
|
|
7
25
|
offset: z.string().optional().describe("Offset for pagination"),
|
|
@@ -24,10 +42,21 @@ export function registerPostTools(server, getClient) {
|
|
|
24
42
|
md += `|---|---------|----------|--------|------|----|\n`;
|
|
25
43
|
for (let i = 0; i < posts.length; i++) {
|
|
26
44
|
const p = posts[i];
|
|
27
|
-
const
|
|
45
|
+
const isObj = typeof p.content === "object" && p.content !== null;
|
|
46
|
+
const rawContent = isObj
|
|
28
47
|
? (p.content.default || Object.values(p.content).find((v) => typeof v === "string" && v) || "")
|
|
29
48
|
: (p.content || "");
|
|
30
|
-
const
|
|
49
|
+
const hasOverrides = isObj && Object.keys(p.content).filter((k) => k !== "default" && typeof p.content[k] === "string" && p.content[k]).length > 0;
|
|
50
|
+
const threadParts = Array.isArray(p.x?.thread_parts) ? p.x.thread_parts : [];
|
|
51
|
+
const isThread = threadParts.length >= 2;
|
|
52
|
+
let content;
|
|
53
|
+
if (isThread && !rawContent) {
|
|
54
|
+
const first = typeof threadParts[0]?.text === "string" ? threadParts[0].text : "";
|
|
55
|
+
content = truncate(first, 32) + ` *(X thread, ${threadParts.length} parts)*`;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
content = truncate(rawContent, hasOverrides ? 35 : 50) + (hasOverrides ? " *(per-platform)*" : "");
|
|
59
|
+
}
|
|
31
60
|
const channels = (p.channels || []).join(", ");
|
|
32
61
|
const status = capitalize(p.status || "—");
|
|
33
62
|
const date = p.scheduled_at
|
|
@@ -41,7 +70,7 @@ export function registerPostTools(server, getClient) {
|
|
|
41
70
|
content: [{ type: "text", text: md }],
|
|
42
71
|
};
|
|
43
72
|
});
|
|
44
|
-
server.tool("get_post", "Get details of a specific post by ID.", {
|
|
73
|
+
server.tool("get_post", "Get details of a specific post by ID. When a post has per-platform caption overrides (e.g. a shorter X version alongside the default), every variant is rendered as its own labeled block under `### Content` so you can see exactly what each platform will publish. X threads are rendered under `### X Thread` with each tweet labeled in publish order — read this to see the full chained tweet text, since thread-only posts have no caption in `content`. After publishing, includes `published_urls` — a map of platform → live URL for each platform that successfully posted (e.g. facebook, instagram, linkedin, x). Useful for polling: when a post's status is `published`, read `published_urls` to surface the live links.", {
|
|
45
74
|
id: z.string().describe("The post ID"),
|
|
46
75
|
}, async ({ id }) => {
|
|
47
76
|
const result = await getClient().getPost(id);
|
|
@@ -71,7 +100,34 @@ export function registerPostTools(server, getClient) {
|
|
|
71
100
|
if (p.media?.length) {
|
|
72
101
|
md += `| **Media** | ${p.media.length} file(s) |\n`;
|
|
73
102
|
}
|
|
74
|
-
md += `\n### Content\n\n${p.content
|
|
103
|
+
md += `\n### Content\n\n${renderContent(p.content)}`;
|
|
104
|
+
// X thread: when present, the canonical tweet text lives here, not in `content`.
|
|
105
|
+
const threadParts = Array.isArray(p.x?.thread_parts)
|
|
106
|
+
? p.x.thread_parts
|
|
107
|
+
: [];
|
|
108
|
+
if (threadParts.length >= 2) {
|
|
109
|
+
md += `\n\n### X Thread (${threadParts.length} parts)\n\n`;
|
|
110
|
+
for (let i = 0; i < threadParts.length; i++) {
|
|
111
|
+
const part = threadParts[i] || {};
|
|
112
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
113
|
+
md += `**${i + 1}/${threadParts.length}.** ${text}\n`;
|
|
114
|
+
if (Array.isArray(part.media_urls) && part.media_urls.length) {
|
|
115
|
+
for (const url of part.media_urls)
|
|
116
|
+
md += ` - ${url}\n`;
|
|
117
|
+
}
|
|
118
|
+
md += `\n`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const publishedUrls = (p.published_urls && typeof p.published_urls === "object" && !Array.isArray(p.published_urls))
|
|
122
|
+
? p.published_urls
|
|
123
|
+
: {};
|
|
124
|
+
const urlEntries = Object.entries(publishedUrls);
|
|
125
|
+
if (urlEntries.length) {
|
|
126
|
+
md += `\n\n### Published URLs\n\n`;
|
|
127
|
+
for (const [platform, url] of urlEntries) {
|
|
128
|
+
md += `- **${capitalize(platform.replace(/_/g, " "))}**: ${url}\n`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
75
131
|
return {
|
|
76
132
|
content: [{ type: "text", text: md }],
|
|
77
133
|
};
|
|
@@ -80,6 +136,7 @@ export function registerPostTools(server, getClient) {
|
|
|
80
136
|
|
|
81
137
|
IMPORTANT — Before calling this tool, make sure you have all required information from the user. If anything is missing, ASK the user before calling:
|
|
82
138
|
|
|
139
|
+
0. **Workspace**: If the user has only one workspace, just use it (no need to ask). If the user has multiple workspaces and has NOT named one in their request, call list_workspaces and ask which workspace to post to before calling this tool. If the user has named a workspace, switch to it once and remember the choice for the rest of the conversation. Never silently pick a workspace when more than one exists. After a successful create/schedule, mention the workspace name in your reply to the user for clarity (e.g. "Scheduled to the Acme workspace for Tuesday 3pm").
|
|
83
140
|
1. **Content/caption**: What text should the post have? If captions differ per platform, use one call with an object: { "default": "fallback text", "linkedin": "long version", "threads": "short version" }. Always prefer one call per topic.
|
|
84
141
|
2. **Channels**: Which platforms to post to? Use list_accounts to show available options if needed.
|
|
85
142
|
3. **Schedule**: When should it be published? (Or save as draft?)
|
|
@@ -94,6 +151,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
94
151
|
- Pinterest: Which board? Any link to attach?
|
|
95
152
|
- YouTube: Title, privacy status, tags?
|
|
96
153
|
- TikTok: Privacy level?
|
|
154
|
+
- **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.
|
|
97
155
|
|
|
98
156
|
Do NOT call this tool without media when creating stories, reels, Instagram posts, TikTok posts, or Pinterest posts — it will fail.
|
|
99
157
|
|
|
@@ -140,6 +198,12 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
140
198
|
}).optional().describe("TikTok options"),
|
|
141
199
|
x: z.object({
|
|
142
200
|
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
201
|
+
paid_partnership: z.boolean().optional().describe("Mark as paid partnership disclosure"),
|
|
202
|
+
made_with_ai: z.boolean().optional().describe("Mark as AI-generated content"),
|
|
203
|
+
thread_parts: z.array(z.object({
|
|
204
|
+
text: z.string().describe("Tweet text (≤ 280 chars)"),
|
|
205
|
+
media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media URLs (max 4)"),
|
|
206
|
+
})).min(2).max(25).optional().describe("Publish as a chained X thread instead of a single tweet. Provide 2–25 parts; each is posted in order via in_reply_to_tweet_id. Per-part media_urls override top-level media for that specific tweet. For a single tweet, omit thread_parts and use content."),
|
|
143
207
|
}).optional().describe("X (Twitter) options"),
|
|
144
208
|
}, async (params) => {
|
|
145
209
|
const result = await getClient().createPost({ ...params, source: "mcp" });
|
|
@@ -167,6 +231,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
167
231
|
|
|
168
232
|
IMPORTANT — Before calling this tool, make sure you have all required information. If anything is missing, ASK the user first:
|
|
169
233
|
|
|
234
|
+
0. **Workspace**: If the user has only one workspace, just use it (no need to ask). If the user has multiple workspaces and has NOT named one, call list_workspaces and ask before publishing. If the user has named a workspace, switch to it once and remember it. Never silently pick a workspace when more than one exists — published posts are public. After publishing, mention the workspace name in your reply (e.g. "Published to the Acme workspace LinkedIn Page").
|
|
170
235
|
1. **Content/caption**: What text? If captions differ per platform, use one call with an object: { "default": "fallback", "linkedin": "long", "threads": "short" }.
|
|
171
236
|
2. **Channels**: Which platforms? Use list_accounts if needed.
|
|
172
237
|
3. **Media** (REQUIRED for some types — same rules as create_post):
|
|
@@ -175,6 +240,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
175
240
|
- Instagram/TikTok posts: ALWAYS need at least one image or video.
|
|
176
241
|
- Pinterest: ALWAYS need an image + board_id.
|
|
177
242
|
- Ask the user which media to use. Use list_media to show options.
|
|
243
|
+
4. **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.
|
|
178
244
|
|
|
179
245
|
Do NOT call without required media — it will fail.`, {
|
|
180
246
|
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."),
|
|
@@ -201,6 +267,15 @@ Do NOT call without required media — it will fail.`, {
|
|
|
201
267
|
tiktok: z.object({
|
|
202
268
|
privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
|
|
203
269
|
}).optional().describe("TikTok options"),
|
|
270
|
+
x: z.object({
|
|
271
|
+
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
272
|
+
paid_partnership: z.boolean().optional().describe("Mark as paid partnership disclosure"),
|
|
273
|
+
made_with_ai: z.boolean().optional().describe("Mark as AI-generated content"),
|
|
274
|
+
thread_parts: z.array(z.object({
|
|
275
|
+
text: z.string().describe("Tweet text (≤ 280 chars)"),
|
|
276
|
+
media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media URLs (max 4)"),
|
|
277
|
+
})).min(2).max(25).optional().describe("Publish as a chained X thread (2–25 parts). Each is posted in order via in_reply_to_tweet_id. Per-part media_urls override top-level media."),
|
|
278
|
+
}).optional().describe("X (Twitter) options"),
|
|
204
279
|
}, async (params) => {
|
|
205
280
|
const result = await getClient().createAndPublishPost({ ...params, source: "mcp" });
|
|
206
281
|
if (result.error) {
|
|
@@ -221,7 +296,9 @@ Do NOT call without required media — it will fail.`, {
|
|
|
221
296
|
content: [{ type: "text", text: md }],
|
|
222
297
|
};
|
|
223
298
|
});
|
|
224
|
-
server.tool("update_post",
|
|
299
|
+
server.tool("update_post", `Update an existing post. Only draft and scheduled posts can be updated.
|
|
300
|
+
|
|
301
|
+
**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.`, {
|
|
225
302
|
id: z.string().describe("The post ID to update"),
|
|
226
303
|
content: z.union([z.string(), z.record(z.string(), z.string())]).optional().describe("Updated post content. String or object with platform keys: { \"default\": \"fallback\", \"linkedin\": \"long\" }."),
|
|
227
304
|
scheduled_at: z.string().optional().describe("Updated scheduled date (ISO 8601)"),
|
|
@@ -234,6 +311,15 @@ Do NOT call without required media — it will fail.`, {
|
|
|
234
311
|
z.array(z.string()),
|
|
235
312
|
z.record(z.string(), z.array(z.string())),
|
|
236
313
|
]).optional().describe("External URLs — flat array or per-platform object. Max 10 total. 'default' key is fallback for platforms without their own key. Empty array opts out."),
|
|
314
|
+
x: z.object({
|
|
315
|
+
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
316
|
+
paid_partnership: z.boolean().optional(),
|
|
317
|
+
made_with_ai: z.boolean().optional(),
|
|
318
|
+
thread_parts: z.array(z.object({
|
|
319
|
+
text: z.string().describe("Tweet text (≤ 280 chars)"),
|
|
320
|
+
media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media URLs (max 4)"),
|
|
321
|
+
})).min(2).max(25).nullable().optional().describe("Replace the X thread shape on this post. Pass an array (2–25 parts) to update/create the thread, or `null` to revert to single-tweet mode."),
|
|
322
|
+
}).optional().describe("X (Twitter) options"),
|
|
237
323
|
}, async ({ id, ...data }) => {
|
|
238
324
|
const result = await getClient().updatePost(id, data);
|
|
239
325
|
if (result.error) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export function registerWorkspaceTools(server, workspaceClients, sessionState) {
|
|
3
|
-
server.tool("list_workspaces", "List all workspaces available in this session. Each workspace corresponds to an API key provided at connection time. Shows which workspace is currently active.", {}, async () => {
|
|
3
|
+
server.tool("list_workspaces", "List all workspaces available in this session. Each workspace corresponds to an API key provided at connection time. Shows which workspace is currently active. Workspace selection rule: if only one workspace is available, just use it (no need to ask). If the user has named a workspace in their request (e.g. 'post to my Acme workspace'), proceed with that workspace and remember it for the rest of the conversation. If multiple workspaces exist and the user has NOT named one, call this tool, present the list, and ASK the user which one to use before posting, switching, or fetching analytics. After a successful action, mention the workspace name in your reply for clarity.", {}, async () => {
|
|
4
4
|
const workspaces = await Promise.all(workspaceClients.map(async (wc, index) => {
|
|
5
5
|
try {
|
|
6
6
|
const result = await wc.client.listAccounts();
|
|
@@ -39,7 +39,7 @@ export function registerWorkspaceTools(server, workspaceClients, sessionState) {
|
|
|
39
39
|
md += `\nUse **switch_workspace** with the workspace number to change the active workspace.`;
|
|
40
40
|
return { content: [{ type: "text", text: md }] };
|
|
41
41
|
});
|
|
42
|
-
server.tool("switch_workspace", "Switch the active workspace. All subsequent tool calls (posts, media, analytics, etc.) will operate on the selected workspace.
|
|
42
|
+
server.tool("switch_workspace", "Switch the active workspace. All subsequent tool calls (posts, media, analytics, etc.) will operate on the selected workspace. Only call this when the user has explicitly named the target workspace, or after the user has picked one from list_workspaces. Never call switch_workspace silently when the user hasn't specified a workspace — ask first.", {
|
|
43
43
|
workspace_number: z.string().describe("The workspace number from list_workspaces (1, 2, 3, etc.)"),
|
|
44
44
|
}, async ({ workspace_number }) => {
|
|
45
45
|
const index = parseInt(workspace_number, 10) - 1;
|
package/build/types.d.ts
CHANGED
|
@@ -14,6 +14,17 @@ export interface Post {
|
|
|
14
14
|
channels: string[];
|
|
15
15
|
media: string[];
|
|
16
16
|
created_at: string;
|
|
17
|
+
x?: {
|
|
18
|
+
reply_settings?: string;
|
|
19
|
+
paid_partnership?: boolean;
|
|
20
|
+
made_with_ai?: boolean;
|
|
21
|
+
thread_parts?: Array<{
|
|
22
|
+
id?: string;
|
|
23
|
+
text: string;
|
|
24
|
+
media_urls?: string[];
|
|
25
|
+
}>;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
17
28
|
}
|
|
18
29
|
export interface MediaItem {
|
|
19
30
|
id: string;
|
package/package.json
CHANGED