@omnisocials/mcp-server 1.5.0 → 1.7.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 +7 -4
- package/build/client.d.ts +36 -0
- package/build/client.js +10 -0
- package/build/index.js +1 -1
- package/build/tools/analytics.js +1 -1
- package/build/tools/media.js +77 -13
- package/build/tools/posts.js +26 -2
- package/build/tools/workspaces.js +88 -38
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -128,13 +128,16 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
|
|
|
128
128
|
| `publish_post` | Publish a draft or scheduled post now |
|
|
129
129
|
| `search_locations` | Find an Instagram location to tag (returns place IDs for `location_id`) |
|
|
130
130
|
|
|
131
|
-
### Media (
|
|
131
|
+
### Media & folders (6 tools)
|
|
132
132
|
|
|
133
133
|
| Tool | Description |
|
|
134
134
|
|------|-------------|
|
|
135
|
-
| `list_media` | List
|
|
136
|
-
| `upload_media` | Upload media from a URL (max 50MB) |
|
|
135
|
+
| `list_media` | List media files; filter by `search` (name) or `folder_id` |
|
|
136
|
+
| `upload_media` | Upload media from a URL (max 50MB); set a `name` and a `folder` |
|
|
137
|
+
| `update_media` | Rename a media file or move it to a folder |
|
|
137
138
|
| `delete_media` | Delete a media file |
|
|
139
|
+
| `list_folders` | List the workspace's media folders |
|
|
140
|
+
| `create_folder` | Create a media folder (optionally nested) |
|
|
138
141
|
|
|
139
142
|
### Accounts (2 tools)
|
|
140
143
|
|
|
@@ -156,7 +159,7 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
|
|
|
156
159
|
| Tool | Description |
|
|
157
160
|
|------|-------------|
|
|
158
161
|
| `list_workspaces` | List all workspaces available in this session |
|
|
159
|
-
| `switch_workspace` | Switch the active workspace |
|
|
162
|
+
| `switch_workspace` | Switch the active workspace **by name** (id or list number also accepted) |
|
|
160
163
|
|
|
161
164
|
### Webhooks (5 tools)
|
|
162
165
|
|
package/build/client.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export declare class OmniSocialsClient {
|
|
|
56
56
|
link_description?: string;
|
|
57
57
|
link_thumbnail_url?: string;
|
|
58
58
|
location_id?: string;
|
|
59
|
+
collaborators?: string[];
|
|
60
|
+
user_tags?: Array<{
|
|
61
|
+
username: string;
|
|
62
|
+
x: number;
|
|
63
|
+
y: number;
|
|
64
|
+
image_index?: number;
|
|
65
|
+
}>;
|
|
59
66
|
pinterest?: Record<string, unknown>;
|
|
60
67
|
youtube?: Record<string, unknown>;
|
|
61
68
|
instagram?: Record<string, unknown>;
|
|
@@ -75,6 +82,13 @@ export declare class OmniSocialsClient {
|
|
|
75
82
|
link_description?: string;
|
|
76
83
|
link_thumbnail_url?: string;
|
|
77
84
|
location_id?: string;
|
|
85
|
+
collaborators?: string[];
|
|
86
|
+
user_tags?: Array<{
|
|
87
|
+
username: string;
|
|
88
|
+
x: number;
|
|
89
|
+
y: number;
|
|
90
|
+
image_index?: number;
|
|
91
|
+
}>;
|
|
78
92
|
pinterest?: Record<string, unknown>;
|
|
79
93
|
youtube?: Record<string, unknown>;
|
|
80
94
|
instagram?: Record<string, unknown>;
|
|
@@ -90,6 +104,13 @@ export declare class OmniSocialsClient {
|
|
|
90
104
|
media_urls?: string[] | Record<string, string[]>;
|
|
91
105
|
type?: string;
|
|
92
106
|
location_id?: string;
|
|
107
|
+
collaborators?: string[];
|
|
108
|
+
user_tags?: Array<{
|
|
109
|
+
username: string;
|
|
110
|
+
x: number;
|
|
111
|
+
y: number;
|
|
112
|
+
image_index?: number;
|
|
113
|
+
}>;
|
|
93
114
|
pinterest?: Record<string, unknown>;
|
|
94
115
|
youtube?: Record<string, unknown>;
|
|
95
116
|
instagram?: Record<string, unknown>;
|
|
@@ -104,17 +125,32 @@ export declare class OmniSocialsClient {
|
|
|
104
125
|
listMedia(params?: {
|
|
105
126
|
limit?: string;
|
|
106
127
|
offset?: string;
|
|
128
|
+
search?: string;
|
|
129
|
+
folder_id?: string;
|
|
107
130
|
}): Promise<ApiResponse<unknown>>;
|
|
108
131
|
uploadMedia(data: {
|
|
109
132
|
url: string;
|
|
110
133
|
filename?: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
folder?: string;
|
|
111
136
|
}): Promise<ApiResponse<unknown>>;
|
|
112
137
|
uploadMediaFromBase64(data: {
|
|
113
138
|
data: string;
|
|
114
139
|
mime_type: string;
|
|
115
140
|
filename?: string;
|
|
141
|
+
name?: string;
|
|
142
|
+
folder?: string;
|
|
116
143
|
}): Promise<ApiResponse<unknown>>;
|
|
117
144
|
deleteMedia(id: string): Promise<ApiResponse<unknown>>;
|
|
145
|
+
updateMedia(id: string, data: {
|
|
146
|
+
name?: string;
|
|
147
|
+
folder_id?: string | null;
|
|
148
|
+
}): Promise<ApiResponse<unknown>>;
|
|
149
|
+
listFolders(): Promise<ApiResponse<unknown>>;
|
|
150
|
+
createFolder(data: {
|
|
151
|
+
name: string;
|
|
152
|
+
parent_id?: string;
|
|
153
|
+
}): Promise<ApiResponse<unknown>>;
|
|
118
154
|
listAccounts(): Promise<ApiResponse<unknown>>;
|
|
119
155
|
getAccount(id: string): Promise<ApiResponse<unknown>>;
|
|
120
156
|
getPostAnalytics(postId: string): Promise<ApiResponse<unknown>>;
|
package/build/client.js
CHANGED
|
@@ -170,6 +170,16 @@ export class OmniSocialsClient {
|
|
|
170
170
|
async deleteMedia(id) {
|
|
171
171
|
return this.request("DELETE", `/media/${id}`);
|
|
172
172
|
}
|
|
173
|
+
async updateMedia(id, data) {
|
|
174
|
+
return this.request("PATCH", `/media/${id}`, data);
|
|
175
|
+
}
|
|
176
|
+
// Folders
|
|
177
|
+
async listFolders() {
|
|
178
|
+
return this.request("GET", "/folders");
|
|
179
|
+
}
|
|
180
|
+
async createFolder(data) {
|
|
181
|
+
return this.request("POST", "/folders", data);
|
|
182
|
+
}
|
|
173
183
|
// Accounts
|
|
174
184
|
async listAccounts() {
|
|
175
185
|
return this.request("GET", "/accounts");
|
package/build/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const sessionState = { activeIndex: 0 };
|
|
|
27
27
|
const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
|
|
28
28
|
const server = new McpServer({
|
|
29
29
|
name: "OmniSocials",
|
|
30
|
-
version: "1.
|
|
30
|
+
version: "1.7.0",
|
|
31
31
|
});
|
|
32
32
|
// Register all tools - pass getter function so tools always use the active workspace's client
|
|
33
33
|
registerPostTools(server, getActiveClient);
|
package/build/tools/analytics.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { formatNumber, capitalize } from "../client.js";
|
|
3
3
|
export function registerAnalyticsTools(server, getClient) {
|
|
4
4
|
server.tool("get_post_analytics", "Get analytics/statistics for a specific published post (impressions, engagements, likes, etc.).", {
|
|
5
|
-
post_id: z.string().describe("The post
|
|
5
|
+
post_id: z.string().describe("The numeric OmniSocials post id (the `id` field from list_posts). NOT a platform post id such as a YouTube video id or tweet id."),
|
|
6
6
|
}, async ({ post_id }) => {
|
|
7
7
|
const result = await getClient().getPostAnalytics(post_id);
|
|
8
8
|
if (result.error) {
|
package/build/tools/media.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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
|
|
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.", {
|
|
5
5
|
limit: z.string().optional().describe("Max results to return"),
|
|
6
6
|
offset: z.string().optional().describe("Offset for pagination"),
|
|
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.'),
|
|
8
|
+
folder_id: z.string().optional().describe('Only return media in this folder id (from list_folders). Use "root" for unfiled items.'),
|
|
7
9
|
}, async (params) => {
|
|
8
10
|
const result = await getClient().listMedia(params);
|
|
9
11
|
if (result.error) {
|
|
@@ -13,22 +15,26 @@ export function registerMediaTools(server, getClient) {
|
|
|
13
15
|
}
|
|
14
16
|
const items = Array.isArray(result.data) ? result.data : result.data?.media || [];
|
|
15
17
|
if (!items.length) {
|
|
18
|
+
const hint = params?.search
|
|
19
|
+
? `No media found matching "${params.search}". It hasn't been uploaded yet — upload it (and pass a \`name\`) so it's findable next time.`
|
|
20
|
+
: "No media files found.";
|
|
16
21
|
return {
|
|
17
|
-
content: [{ type: "text", text:
|
|
22
|
+
content: [{ type: "text", text: hint }],
|
|
18
23
|
};
|
|
19
24
|
}
|
|
20
25
|
let md = `## Media Library (${items.length} files)\n\n`;
|
|
21
|
-
md += `| # | Type |
|
|
22
|
-
md +=
|
|
26
|
+
md += `| # | Type | Name | Folder | Size | Media ID |\n`;
|
|
27
|
+
md += `|---|------|------|--------|------|----------|\n`;
|
|
23
28
|
for (let i = 0; i < items.length; i++) {
|
|
24
29
|
const m = items[i];
|
|
25
30
|
// `type` may be "video"/"image" (normalized) or a raw MIME like "video/mp4".
|
|
26
31
|
const rawType = typeof m.type === "string" ? m.type : "";
|
|
27
32
|
const isVideo = rawType === "video" || rawType.startsWith("video/");
|
|
28
33
|
const type = isVideo ? "Video" : "Image";
|
|
29
|
-
|
|
34
|
+
const folder = m.folder_id ? `\`${m.folder_id}\`` : "—";
|
|
35
|
+
md += `| ${i + 1} | ${type} | ${m.name || m.filename || "—"} | ${folder} | ${formatBytes(m.size || 0)} | \`${m.id}\` |\n`;
|
|
30
36
|
}
|
|
31
|
-
md += `\nUse the **Media ID** with \`media_ids\` when creating posts
|
|
37
|
+
md += `\nUse the **Media ID** with \`media_ids\` when creating posts. Rename or move a file with \`update_media\`. See folders with \`list_folders\`.`;
|
|
32
38
|
// Fetch thumbnails for image files (max 4)
|
|
33
39
|
const content = [];
|
|
34
40
|
const imageItems = items.filter((m) => {
|
|
@@ -56,6 +62,8 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
56
62
|
base64_data: z.string().optional().describe("Base64-encoded file data. Use when the user provides an image directly in chat rather than a URL."),
|
|
57
63
|
mime_type: z.string().optional().describe("MIME type of the file (e.g. 'image/jpeg', 'image/png'). Required when using base64_data."),
|
|
58
64
|
filename: z.string().optional().describe("Optional filename for the uploaded media"),
|
|
65
|
+
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.'),
|
|
66
|
+
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.'),
|
|
59
67
|
}, async (params) => {
|
|
60
68
|
let result;
|
|
61
69
|
if (params.base64_data && params.mime_type) {
|
|
@@ -63,10 +71,12 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
63
71
|
data: params.base64_data,
|
|
64
72
|
mime_type: params.mime_type,
|
|
65
73
|
filename: params.filename,
|
|
74
|
+
name: params.name || params.filename,
|
|
75
|
+
folder: params.folder,
|
|
66
76
|
});
|
|
67
77
|
}
|
|
68
78
|
else if (params.url) {
|
|
69
|
-
result = await getClient().uploadMedia({ url: params.url, filename: params.filename });
|
|
79
|
+
result = await getClient().uploadMedia({ url: params.url, filename: params.filename, name: params.name || params.filename, folder: params.folder });
|
|
70
80
|
}
|
|
71
81
|
else {
|
|
72
82
|
return {
|
|
@@ -83,8 +93,10 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
83
93
|
md += `| Field | Value |\n`;
|
|
84
94
|
md += `|-------|-------|\n`;
|
|
85
95
|
md += `| **Media ID** | \`${m.id}\` |\n`;
|
|
96
|
+
md += `| **Name** | ${m.name || m.filename || "—"} |\n`;
|
|
86
97
|
md += `| **Type** | ${m.type || "—"} |\n`;
|
|
87
|
-
|
|
98
|
+
if (m.folder_id)
|
|
99
|
+
md += `| **Folder** | \`${m.folder_id}\` |\n`;
|
|
88
100
|
md += `| **Size** | ${formatBytes(m.size || 0)} |\n`;
|
|
89
101
|
if (m.url)
|
|
90
102
|
md += `| **URL** | ${m.url} |\n`;
|
|
@@ -93,12 +105,64 @@ When the user provides an image in the conversation (not a URL), use base64_data
|
|
|
93
105
|
content: [{ type: "text", text: md }],
|
|
94
106
|
};
|
|
95
107
|
});
|
|
96
|
-
server.tool("
|
|
97
|
-
id: z.string().describe("The media ID to
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
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.", {
|
|
109
|
+
id: z.string().describe("The media ID to update"),
|
|
110
|
+
name: z.string().optional().describe('New human-readable label, e.g. "pp-play5get50". Pass an empty string to clear it.'),
|
|
111
|
+
folder_id: z.string().optional().describe('Move the file into this folder id (from list_folders). Pass an empty string to move it to the root ("All media").'),
|
|
112
|
+
}, async ({ id, name, folder_id }) => {
|
|
113
|
+
const body = {};
|
|
114
|
+
if (name !== undefined)
|
|
115
|
+
body.name = name;
|
|
116
|
+
if (folder_id !== undefined)
|
|
117
|
+
body.folder_id = folder_id === "" ? null : folder_id;
|
|
118
|
+
const result = await getClient().updateMedia(id, body);
|
|
119
|
+
if (result.error) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const m = result.data;
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: `Updated media \`${m.id}\`.\n\n| Field | Value |\n|-------|-------|\n| **Name** | ${m.name || "—"} |\n| **Folder** | ${m.folder_id ? `\`${m.folder_id}\`` : "root"} |`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
server.tool("list_folders", "List the media-library folders in this workspace (Finder-style organization). Returns each folder's id, name, parent, and item count. Use a folder id with list_media (folder_id) or update_media (folder_id), or a folder name with upload_media (folder).", {}, async () => {
|
|
135
|
+
const result = await getClient().listFolders();
|
|
136
|
+
if (result.error) {
|
|
137
|
+
return { content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }] };
|
|
138
|
+
}
|
|
139
|
+
const items = Array.isArray(result.data) ? result.data : result.data?.data || [];
|
|
140
|
+
if (!items.length) {
|
|
141
|
+
return { content: [{ type: "text", text: "No folders yet. Create one with create_folder." }] };
|
|
142
|
+
}
|
|
143
|
+
let md = `## Folders (${items.length})\n\n`;
|
|
144
|
+
md += `| Folder ID | Name | Parent | Items |\n|-----------|------|--------|-------|\n`;
|
|
145
|
+
for (const f of items) {
|
|
146
|
+
md += `| \`${f.id}\` | ${f.name} | ${f.parent_id ? `\`${f.parent_id}\`` : "—"} | ${f.item_count ?? 0} |\n`;
|
|
147
|
+
}
|
|
148
|
+
return { content: [{ type: "text", text: md }] };
|
|
149
|
+
});
|
|
150
|
+
server.tool("create_folder", "Create a media-library folder. Use it to organize assets (e.g. a 'win-graphics' folder), then upload into it with upload_media (folder) or move files with update_media (folder_id).", {
|
|
151
|
+
name: z.string().describe("Folder name, e.g. 'win-graphics'"),
|
|
152
|
+
parent_id: z.string().optional().describe("Optional parent folder id to nest under (from list_folders). Omit for a top-level folder."),
|
|
153
|
+
}, async ({ name, parent_id }) => {
|
|
154
|
+
const result = await getClient().createFolder({ name, parent_id });
|
|
155
|
+
if (result.error) {
|
|
156
|
+
return { content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }] };
|
|
157
|
+
}
|
|
158
|
+
const f = result.data;
|
|
100
159
|
return {
|
|
101
|
-
content: [
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: `Created folder \`${f.id}\` "${f.name}". Upload into it with upload_media (folder="${f.name}").`,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
102
166
|
};
|
|
103
167
|
});
|
|
104
168
|
}
|
package/build/tools/posts.js
CHANGED
|
@@ -180,7 +180,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
180
180
|
3. **Schedule**: When should it be published? (Or save as draft?)
|
|
181
181
|
4. **Media** (REQUIRED for some types):
|
|
182
182
|
- Stories: ALWAYS require an image or video. Ask the user which image/video to use. Use list_media to show their uploaded media, or ask for an external URL.
|
|
183
|
-
- Reels: ALWAYS require a video. Ask the user which video to use
|
|
183
|
+
- Reels: ALWAYS require a video (MP4/MOV) — NOT an image. Passing an image to a Reel returns a 400 validation_error. To share an image, use post_type "Post" instead. Ask the user which video to use; use list_media to find available videos.
|
|
184
184
|
- Instagram posts: ALWAYS require at least one image or video.
|
|
185
185
|
- TikTok posts: ALWAYS require at least one image or video.
|
|
186
186
|
- Pinterest posts: ALWAYS require an image AND a board_id.
|
|
@@ -228,6 +228,13 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
228
228
|
link_description: z.string().optional().describe("Optional description for the link-share preview. LinkedIn uses this when set; Facebook auto-fetches the OG description."),
|
|
229
229
|
link_thumbnail_url: z.string().optional().describe("Optional thumbnail image URL for the preview card. Reserved for future use — currently not yet applied to LinkedIn (would require uploading to LinkedIn's image API first)."),
|
|
230
230
|
location_id: z.string().optional().describe("Instagram only. Facebook Place ID of a single physical venue (with a street address) to tag the post's location. Applied to single-image and carousel Instagram feed posts. To get a valid ID, call the `search_locations` tool with the place name and let the user pick. Ignored by other platforms."),
|
|
231
|
+
collaborators: z.array(z.string()).max(3).optional().describe("Instagram only. Up to 3 public Instagram usernames to invite as co-authors (the 'Collab' feature). Works on image, carousel, and reel posts — NOT Stories. Invited users get an invite in the Instagram app; once they accept, the post also appears on their profile and feed. A leading '@' is stripped; usernames are case-insensitive. Private or non-existent usernames are rejected by Instagram at publish time with a clear error. Ignored by other platforms."),
|
|
232
|
+
user_tags: z.array(z.object({
|
|
233
|
+
username: z.string().describe("Public Instagram username to tag. Leading '@' is stripped."),
|
|
234
|
+
x: z.number().min(0).max(1).describe("Horizontal position 0.0–1.0 from the photo's left edge."),
|
|
235
|
+
y: z.number().min(0).max(1).describe("Vertical position 0.0–1.0 from the photo's top edge."),
|
|
236
|
+
image_index: z.number().int().min(0).optional().describe("0-based carousel slide this tag belongs to. Omit (or 0) for a single image."),
|
|
237
|
+
})).optional().describe("Instagram only. Tag public accounts at x/y positions on a PHOTO (not video/reels/stories). For a single image omit image_index; for a carousel, set image_index to the slide each tag belongs to. Private/non-existent usernames are rejected at publish time. Ignored by other platforms."),
|
|
231
238
|
pinterest: z.object({
|
|
232
239
|
board_id: z.string().optional().describe("Pinterest board ID. Required for Pinterest. Use get_account to list boards."),
|
|
233
240
|
title: z.string().optional().describe("Pin title (max 100 characters). For carousel pins (2–5 images) this title applies to the whole pin, not individual slides."),
|
|
@@ -257,6 +264,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
|
|
|
257
264
|
disable_stitch: z.boolean().optional(),
|
|
258
265
|
is_aigc: z.boolean().optional(),
|
|
259
266
|
brand_content_toggle: z.boolean().optional(),
|
|
267
|
+
auto_add_music: z.boolean().optional().describe("Photo carousels only. When true, TikTok auto-selects a soundtrack. Defaults to false to avoid unsuitable tracks."),
|
|
260
268
|
}).optional().describe("TikTok options"),
|
|
261
269
|
x: z.object({
|
|
262
270
|
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
@@ -346,7 +354,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
|
|
|
346
354
|
2. **Channels**: Which platforms? Use list_accounts if needed.
|
|
347
355
|
3. **Media** (REQUIRED for some types — same rules as create_post):
|
|
348
356
|
- Stories: ALWAYS need an image or video.
|
|
349
|
-
- Reels: ALWAYS need a video.
|
|
357
|
+
- Reels: ALWAYS need a video (MP4/MOV), NOT an image — passing an image returns a 400 validation_error. Use post_type "Post" to share an image.
|
|
350
358
|
- Instagram/TikTok posts: ALWAYS need at least one image or video.
|
|
351
359
|
- Pinterest: ALWAYS need an image + board_id.
|
|
352
360
|
- Ask the user which media to use. Use list_media to show options.
|
|
@@ -367,6 +375,14 @@ Do NOT call without required media — it will fail.`, {
|
|
|
367
375
|
z.record(z.string(), z.array(z.string())),
|
|
368
376
|
]).optional().describe("External image/video URLs — flat array or per-platform object. Max 10 total. 'default' key is fallback for platforms without their own key. Empty array opts out."),
|
|
369
377
|
type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story', 'reel'"),
|
|
378
|
+
location_id: z.string().optional().describe("Instagram only. Facebook Place ID of a single physical venue to tag the post's location. Applied to single-image and carousel Instagram feed posts. Use the `search_locations` tool to find a valid ID. Ignored by other platforms."),
|
|
379
|
+
collaborators: z.array(z.string()).max(3).optional().describe("Instagram only. Up to 3 public Instagram usernames to invite as co-authors (the 'Collab' feature). Works on image, carousel, and reel posts — NOT Stories. A leading '@' is stripped; usernames are case-insensitive. Private or non-existent usernames are rejected by Instagram at publish time. Ignored by other platforms."),
|
|
380
|
+
user_tags: z.array(z.object({
|
|
381
|
+
username: z.string().describe("Public Instagram username to tag. Leading '@' is stripped."),
|
|
382
|
+
x: z.number().min(0).max(1).describe("Horizontal position 0.0–1.0 from the photo's left edge."),
|
|
383
|
+
y: z.number().min(0).max(1).describe("Vertical position 0.0–1.0 from the photo's top edge."),
|
|
384
|
+
image_index: z.number().int().min(0).optional().describe("0-based carousel slide this tag belongs to. Omit (or 0) for a single image."),
|
|
385
|
+
})).optional().describe("Instagram only. Tag public accounts at x/y positions on a PHOTO (not video/reels/stories). For a single image omit image_index; for a carousel, set image_index to the slide each tag belongs to. Private/non-existent usernames are rejected at publish time. Ignored by other platforms."),
|
|
370
386
|
pinterest: z.object({
|
|
371
387
|
board_id: z.string().optional().describe("Pinterest board ID. Required for Pinterest. Use get_account to list boards."),
|
|
372
388
|
title: z.string().optional().describe("Pin title (max 100 characters). For carousel pins (2–5 images) this title applies to the whole pin, not individual slides."),
|
|
@@ -462,6 +478,13 @@ Do NOT call without required media — it will fail.`, {
|
|
|
462
478
|
z.record(z.string(), z.array(z.string())),
|
|
463
479
|
]).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."),
|
|
464
480
|
location_id: z.string().optional().describe("Instagram only. Facebook Place/Page ID to tag the post's location with. Send an empty string to clear an existing location tag. Ignored by other platforms."),
|
|
481
|
+
collaborators: z.array(z.string()).max(3).optional().describe("Instagram only. Up to 3 public Instagram usernames to invite as co-authors. Replaces the existing collaborator list. Send an empty array to clear collaborators. Works on image, carousel, and reel posts — NOT Stories. Ignored by other platforms."),
|
|
482
|
+
user_tags: z.array(z.object({
|
|
483
|
+
username: z.string().describe("Public Instagram username to tag. Leading '@' is stripped."),
|
|
484
|
+
x: z.number().min(0).max(1).describe("Horizontal position 0.0–1.0 from the photo's left edge."),
|
|
485
|
+
y: z.number().min(0).max(1).describe("Vertical position 0.0–1.0 from the photo's top edge."),
|
|
486
|
+
image_index: z.number().int().min(0).optional().describe("0-based carousel slide this tag belongs to. Omit (or 0) for a single image."),
|
|
487
|
+
})).optional().describe("Instagram only. Tag public accounts at x/y positions on a PHOTO (not video/reels/stories). Replaces the existing tag list. Send an empty array to clear. For carousels set image_index per tag. Ignored by other platforms."),
|
|
465
488
|
youtube: z.object({
|
|
466
489
|
title: z.string().optional().describe("Short title shown on YouTube. To change the Short's title on an existing draft, set this — do NOT use `content` (which is the description)."),
|
|
467
490
|
tags: z.array(z.string()).optional(),
|
|
@@ -491,6 +514,7 @@ Do NOT call without required media — it will fail.`, {
|
|
|
491
514
|
disable_stitch: z.boolean().optional(),
|
|
492
515
|
is_aigc: z.boolean().optional(),
|
|
493
516
|
brand_content_toggle: z.boolean().optional(),
|
|
517
|
+
auto_add_music: z.boolean().optional().describe("Photo carousels only. When true, TikTok auto-selects a soundtrack. Defaults to false to avoid unsuitable tracks."),
|
|
494
518
|
}).optional().describe("TikTok options"),
|
|
495
519
|
x: z.object({
|
|
496
520
|
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
@@ -1,25 +1,67 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** Fetch each workspace's name + id so we can match a switch request to it. */
|
|
3
|
+
async function describeWorkspaces(workspaceClients, sessionState) {
|
|
4
|
+
return Promise.all(workspaceClients.map(async (wc, index) => {
|
|
5
|
+
try {
|
|
6
|
+
const result = await wc.client.listAccounts();
|
|
7
|
+
return {
|
|
8
|
+
index,
|
|
9
|
+
workspace_id: result.workspace_id != null ? String(result.workspace_id) : null,
|
|
10
|
+
workspace_name: result.workspace_name || `Workspace ${index + 1}`,
|
|
11
|
+
accounts_count: Array.isArray(result.data) ? result.data.length : 0,
|
|
12
|
+
active: index === sessionState.activeIndex,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {
|
|
17
|
+
index,
|
|
18
|
+
workspace_id: null,
|
|
19
|
+
workspace_name: `Workspace ${index + 1}`,
|
|
20
|
+
accounts_count: 0,
|
|
21
|
+
active: index === sessionState.activeIndex,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a user-supplied workspace reference (name, id, or positional number)
|
|
28
|
+
* to an index. Order of precedence puts stable identity first so ordering can
|
|
29
|
+
* never send work to the wrong workspace:
|
|
30
|
+
* 1. exact name (case-insensitive)
|
|
31
|
+
* 2. exact workspace id
|
|
32
|
+
* 3. unique partial name match (case-insensitive)
|
|
33
|
+
* 4. positional number fallback (1-based) for back-compat
|
|
34
|
+
* Returns { index } on success, { ambiguous } when a name matches several, or
|
|
35
|
+
* { notFound: true }.
|
|
36
|
+
*/
|
|
37
|
+
function resolveWorkspaceIndex(entries, query) {
|
|
38
|
+
const q = String(query ?? "").trim();
|
|
39
|
+
if (!q)
|
|
40
|
+
return { notFound: true };
|
|
41
|
+
const qLower = q.toLowerCase();
|
|
42
|
+
const exactName = entries.filter((e) => e.workspace_name.toLowerCase() === qLower);
|
|
43
|
+
if (exactName.length === 1)
|
|
44
|
+
return { index: exactName[0].index };
|
|
45
|
+
if (exactName.length > 1)
|
|
46
|
+
return { ambiguous: exactName };
|
|
47
|
+
const byId = entries.filter((e) => e.workspace_id === q);
|
|
48
|
+
if (byId.length === 1)
|
|
49
|
+
return { index: byId[0].index };
|
|
50
|
+
const partial = entries.filter((e) => e.workspace_name.toLowerCase().includes(qLower));
|
|
51
|
+
if (partial.length === 1)
|
|
52
|
+
return { index: partial[0].index };
|
|
53
|
+
if (partial.length > 1)
|
|
54
|
+
return { ambiguous: partial };
|
|
55
|
+
if (/^\d+$/.test(q)) {
|
|
56
|
+
const idx = parseInt(q, 10) - 1;
|
|
57
|
+
if (idx >= 0 && idx < entries.length)
|
|
58
|
+
return { index: idx };
|
|
59
|
+
}
|
|
60
|
+
return { notFound: true };
|
|
61
|
+
}
|
|
2
62
|
export function registerWorkspaceTools(server, workspaceClients, sessionState) {
|
|
3
63
|
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
|
-
const workspaces = await
|
|
5
|
-
try {
|
|
6
|
-
const result = await wc.client.listAccounts();
|
|
7
|
-
return {
|
|
8
|
-
index,
|
|
9
|
-
workspace_name: result.workspace_name || `Workspace ${index + 1}`,
|
|
10
|
-
active: index === sessionState.activeIndex,
|
|
11
|
-
accounts_count: (Array.isArray(result.data) ? result.data : []).length,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return {
|
|
16
|
-
index,
|
|
17
|
-
workspace_name: `Workspace ${index + 1}`,
|
|
18
|
-
active: index === sessionState.activeIndex,
|
|
19
|
-
accounts_count: 0,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
}));
|
|
64
|
+
const workspaces = await describeWorkspaces(workspaceClients, sessionState);
|
|
23
65
|
if (workspaces.length === 1) {
|
|
24
66
|
const ws = workspaces[0];
|
|
25
67
|
return {
|
|
@@ -30,40 +72,48 @@ export function registerWorkspaceTools(server, workspaceClients, sessionState) {
|
|
|
30
72
|
};
|
|
31
73
|
}
|
|
32
74
|
let md = `## Workspaces (${workspaces.length})\n\n`;
|
|
33
|
-
md += `|
|
|
34
|
-
md +=
|
|
75
|
+
md += `| Workspace | Accounts | Status |\n`;
|
|
76
|
+
md += `|-----------|----------|--------|\n`;
|
|
35
77
|
for (const ws of workspaces) {
|
|
36
78
|
const status = ws.active ? "**Active**" : "";
|
|
37
|
-
md += `| ${ws.
|
|
79
|
+
md += `| ${ws.workspace_name} | ${ws.accounts_count} | ${status} |\n`;
|
|
38
80
|
}
|
|
39
|
-
md += `\nUse **switch_workspace** with the workspace
|
|
81
|
+
md += `\nUse **switch_workspace** with the workspace **name** to change the active workspace (e.g. \`switch_workspace workspace: "${workspaces[0].workspace_name}"\`).`;
|
|
40
82
|
return { content: [{ type: "text", text: md }] };
|
|
41
83
|
});
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
84
|
+
server.tool("switch_workspace", "Switch the active workspace by NAME. All subsequent tool calls (posts, media, analytics, etc.) will operate on the selected workspace. Pass the workspace name exactly as shown in list_workspaces (its id or list position also work, but the name is preferred and unambiguous). 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.", {
|
|
85
|
+
workspace: z
|
|
86
|
+
.string()
|
|
87
|
+
.describe('The workspace to switch to: its name (preferred, e.g. "Daily Edge Sports"), or its id/list number.'),
|
|
88
|
+
}, async ({ workspace }) => {
|
|
89
|
+
const entries = await describeWorkspaces(workspaceClients, sessionState);
|
|
90
|
+
const resolved = resolveWorkspaceIndex(entries, workspace);
|
|
91
|
+
if ("ambiguous" in resolved) {
|
|
92
|
+
const names = resolved.ambiguous
|
|
93
|
+
.map((e) => `- ${e.workspace_name}${e.workspace_id ? ` (id ${e.workspace_id})` : ""}`)
|
|
94
|
+
.join("\n");
|
|
47
95
|
return {
|
|
48
96
|
content: [{
|
|
49
97
|
type: "text",
|
|
50
|
-
text: `
|
|
98
|
+
text: `"${workspace}" matches more than one workspace:\n\n${names}\n\nPlease specify the exact name or id.`,
|
|
51
99
|
}],
|
|
52
100
|
};
|
|
53
101
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
102
|
+
if ("notFound" in resolved) {
|
|
103
|
+
const avail = entries.map((e) => e.workspace_name).join(", ");
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: `Error: No workspace matches "${workspace}". Available workspaces: ${avail}. Use list_workspaces to see them.`,
|
|
108
|
+
}],
|
|
109
|
+
};
|
|
61
110
|
}
|
|
62
|
-
|
|
111
|
+
sessionState.activeIndex = resolved.index;
|
|
112
|
+
const ws = entries[resolved.index];
|
|
63
113
|
return {
|
|
64
114
|
content: [{
|
|
65
115
|
type: "text",
|
|
66
|
-
text: `Switched to **${
|
|
116
|
+
text: `Switched to **${ws.workspace_name}**. All subsequent calls will use this workspace.`,
|
|
67
117
|
}],
|
|
68
118
|
};
|
|
69
119
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnisocials/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "MCP server for OmniSocials API - manage social media posts, media, accounts, analytics, and webhooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -36,5 +36,10 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.3.3",
|
|
38
38
|
"typescript": "^5.8.2"
|
|
39
|
+
},
|
|
40
|
+
"overrides": {
|
|
41
|
+
"zod": "4.4.0",
|
|
42
|
+
"fast-uri": "3.1.1",
|
|
43
|
+
"hono": "4.12.18"
|
|
39
44
|
}
|
|
40
45
|
}
|