@omnisocials/mcp-server 1.0.3 → 1.1.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 CHANGED
@@ -130,6 +130,16 @@ Add to your `~/.codeium/windsurf/mcp_config.json`:
130
130
  | `OMNISOCIALS_API_KEY` | Yes | Your API key (`omsk_live_*` or `omsk_test_*`) |
131
131
  | `OMNISOCIALS_BASE_URL` | No | Custom API base URL (defaults to production) |
132
132
 
133
+ ## Alternative: Agent Skills
134
+
135
+ For broader AI agent compatibility (works with Cursor, Windsurf, Codex, Gemini CLI, GitHub Copilot, and more), you can also use the OmniSocials Agent Skills package:
136
+
137
+ ```bash
138
+ npx skills add OmniSocials/omnisocials-agent-skills
139
+ ```
140
+
141
+ The agent-skills package uses a CLI + SKILL.md approach that works in any AI coding tool, not just MCP-compatible ones. See [omnisocials-agent-skills on GitHub](https://github.com/OmniSocials/omnisocials-agent-skills) for details.
142
+
133
143
  ## API Documentation
134
144
 
135
145
  Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
package/build/client.d.ts CHANGED
@@ -1,4 +1,14 @@
1
1
  import type { ApiResponse } from "./types.js";
2
+ export declare function fetchImageAsBase64(url: string): Promise<{
3
+ data: string;
4
+ mimeType: string;
5
+ } | null>;
6
+ export declare function formatNumber(n: number): string;
7
+ export declare function formatBytes(bytes: number): string;
8
+ export declare function formatDate(iso: string | null): string;
9
+ export declare function formatDateTime(iso: string | null): string;
10
+ export declare function truncate(str: string, len: number): string;
11
+ export declare function capitalize(str: string): string;
2
12
  export declare class OmniSocialsClient {
3
13
  private baseUrl;
4
14
  private apiKey;
package/build/client.js CHANGED
@@ -1,3 +1,68 @@
1
+ // ─── Image Helper ───────────────────────────────────────────────────────────
2
+ export async function fetchImageAsBase64(url) {
3
+ try {
4
+ const controller = new AbortController();
5
+ const timeout = setTimeout(() => controller.abort(), 3000);
6
+ const response = await fetch(url, { signal: controller.signal });
7
+ clearTimeout(timeout);
8
+ if (!response.ok)
9
+ return null;
10
+ const buffer = await response.arrayBuffer();
11
+ const base64 = Buffer.from(buffer).toString("base64");
12
+ const mimeType = response.headers.get("content-type") || "image/png";
13
+ return { data: base64, mimeType };
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ // ─── Formatting Helpers ─────────────────────────────────────────────────────
20
+ export function formatNumber(n) {
21
+ if (n >= 1_000_000)
22
+ return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
23
+ if (n >= 1_000)
24
+ return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
25
+ return n.toString();
26
+ }
27
+ export function formatBytes(bytes) {
28
+ if (bytes >= 1_000_000)
29
+ return (bytes / 1_000_000).toFixed(1) + " MB";
30
+ if (bytes >= 1_000)
31
+ return (bytes / 1_000).toFixed(1) + " KB";
32
+ return bytes + " B";
33
+ }
34
+ export function formatDate(iso) {
35
+ if (!iso)
36
+ return "—";
37
+ const d = new Date(iso);
38
+ return d.toLocaleDateString("en-US", {
39
+ month: "short",
40
+ day: "numeric",
41
+ year: "numeric",
42
+ });
43
+ }
44
+ export function formatDateTime(iso) {
45
+ if (!iso)
46
+ return "—";
47
+ const d = new Date(iso);
48
+ return d.toLocaleDateString("en-US", {
49
+ month: "short",
50
+ day: "numeric",
51
+ year: "numeric",
52
+ hour: "numeric",
53
+ minute: "2-digit",
54
+ timeZoneName: "short",
55
+ });
56
+ }
57
+ export function truncate(str, len) {
58
+ if (!str)
59
+ return "";
60
+ const clean = str.replace(/\n/g, " ");
61
+ return clean.length > len ? clean.slice(0, len - 1) + "…" : clean;
62
+ }
63
+ export function capitalize(str) {
64
+ return str.charAt(0).toUpperCase() + str.slice(1);
65
+ }
1
66
  export class OmniSocialsClient {
2
67
  baseUrl;
3
68
  apiKey;
package/build/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
4
5
  import { OmniSocialsClient } from "./client.js";
5
6
  import { registerPostTools } from "./tools/posts.js";
6
7
  import { registerMediaTools } from "./tools/media.js";
@@ -25,6 +26,44 @@ registerMediaTools(server, client);
25
26
  registerAccountTools(server, client);
26
27
  registerAnalyticsTools(server, client);
27
28
  registerWebhookTools(server, client);
29
+ // Register prompts
30
+ server.prompt("weekly-report", "Generate a weekly social media performance report", {}, async () => ({
31
+ messages: [
32
+ {
33
+ role: "user",
34
+ content: {
35
+ type: "text",
36
+ text: "Give me a comprehensive social media performance report for the last 7 days. Include:\n\n1. Analytics overview (impressions, engagements, engagement rate)\n2. Platform-by-platform breakdown\n3. Account analytics (follower counts)\n4. Key observations and recommendations\n\nUse the get_analytics_overview (period: 7d) and get_account_analytics tools to gather the data, then present it in a clear, well-formatted report.",
37
+ },
38
+ },
39
+ ],
40
+ }));
41
+ server.prompt("create-campaign", "Create a multi-platform social media campaign", {
42
+ topic: z.string().describe("What the campaign is about"),
43
+ }, async ({ topic }) => ({
44
+ messages: [
45
+ {
46
+ role: "user",
47
+ content: {
48
+ type: "text",
49
+ text: `Help me create a social media campaign about: ${topic}\n\nPlease:\n1. First, list my connected accounts so we know which platforms are available\n2. Draft tailored content for each platform (respecting character limits and best practices)\n3. Suggest optimal scheduling times\n4. Ask me to review before creating any posts\n\nDon't create any posts until I've approved the content.`,
50
+ },
51
+ },
52
+ ],
53
+ }));
54
+ server.prompt("schedule-week", "Plan and schedule a week of social media content", {
55
+ theme: z.string().optional().describe("Optional theme or focus for the week's content"),
56
+ }, async ({ theme }) => ({
57
+ messages: [
58
+ {
59
+ role: "user",
60
+ content: {
61
+ type: "text",
62
+ text: `Help me plan and schedule social media content for the upcoming week${theme ? ` with a focus on: ${theme}` : ""}.\n\nPlease:\n1. List my connected accounts\n2. Check what's already scheduled\n3. Suggest 5-7 posts spread across the week\n4. For each post, specify: content, platforms, suggested date/time\n5. Wait for my approval before creating any posts\n\nConsider platform best practices and vary content types.`,
63
+ },
64
+ },
65
+ ],
66
+ }));
28
67
  // Start the server
29
68
  async function main() {
30
69
  const transport = new StdioServerTransport();
@@ -1,17 +1,100 @@
1
1
  import { z } from "zod";
2
+ import { fetchImageAsBase64, capitalize } from "../client.js";
2
3
  export function registerAccountTools(server, client) {
3
- server.tool("list_accounts", "List all connected social media accounts in the workspace.", {}, async () => {
4
+ server.tool("list_accounts", "List all connected social media accounts in the workspace. Each account includes its platform, display name, channel ID (used for create_post), and supported content_types (post, story, reel). Pinterest accounts also include a `boards` array with `{id, name}` — use the board `id` as `board_id` when creating Pinterest posts. X accounts with Premium include `platform_details` with `subscription_type` (e.g. \"Premium\", \"PremiumPlus\"). Call this to help users pick which platforms to post to.", {}, async () => {
4
5
  const result = await client.listAccounts();
5
- return {
6
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7
- };
6
+ if (result.error) {
7
+ return {
8
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
9
+ };
10
+ }
11
+ const accounts = Array.isArray(result.data) ? result.data : result.data?.accounts || [];
12
+ if (!accounts.length) {
13
+ return {
14
+ content: [{ type: "text", text: "No connected accounts found." }],
15
+ };
16
+ }
17
+ // Build markdown table
18
+ let md = `## Connected Accounts (${accounts.length})\n\n`;
19
+ md += `| # | Platform | Account | Content Types | Channel ID |\n`;
20
+ md += `|---|----------|---------|---------------|------------|\n`;
21
+ for (let i = 0; i < accounts.length; i++) {
22
+ const a = accounts[i];
23
+ const name = a.username ? `@${a.username}` : a.display_name || "—";
24
+ const types = (a.content_types || []).join(", ");
25
+ const platform = capitalize(a.platform);
26
+ const sub = a.platform_details?.subscription_type && a.platform_details.subscription_type !== "None"
27
+ ? ` (${a.platform_details.subscription_type})`
28
+ : "";
29
+ md += `| ${i + 1} | ${platform}${sub} | ${name} | ${types} | \`${a.id}\` |\n`;
30
+ }
31
+ md += `\nUse the **Channel ID** as the \`channels\` parameter when creating posts.`;
32
+ // Fetch profile pictures
33
+ const content = [];
34
+ const imagePromises = accounts
35
+ .filter((a) => a.profile_picture)
36
+ .slice(0, 6)
37
+ .map(async (a) => {
38
+ const img = await fetchImageAsBase64(a.profile_picture);
39
+ return img ? { account: a, image: img } : null;
40
+ });
41
+ const images = (await Promise.all(imagePromises)).filter(Boolean);
42
+ for (const item of images) {
43
+ if (item) {
44
+ content.push({
45
+ type: "image",
46
+ data: item.image.data,
47
+ mimeType: item.image.mimeType,
48
+ });
49
+ }
50
+ }
51
+ content.push({ type: "text", text: md });
52
+ return { content };
8
53
  });
9
54
  server.tool("get_account", "Get details of a specific connected social media account.", {
10
55
  id: z.string().describe("The account ID"),
11
56
  }, async ({ id }) => {
12
57
  const result = await client.getAccount(id);
13
- return {
14
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
15
- };
58
+ if (result.error) {
59
+ return {
60
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
61
+ };
62
+ }
63
+ const a = result.data;
64
+ if (!a) {
65
+ return {
66
+ content: [{ type: "text", text: "Account not found." }],
67
+ };
68
+ }
69
+ let md = `## ${capitalize(a.platform)} Account\n\n`;
70
+ md += `| Field | Value |\n`;
71
+ md += `|-------|-------|\n`;
72
+ md += `| **Channel ID** | \`${a.id}\` |\n`;
73
+ md += `| **Platform** | ${capitalize(a.platform)} |\n`;
74
+ md += `| **Username** | ${a.username ? `@${a.username}` : "—"} |\n`;
75
+ md += `| **Display Name** | ${a.display_name || "—"} |\n`;
76
+ md += `| **Content Types** | ${(a.content_types || []).join(", ")} |\n`;
77
+ md += `| **Status** | ${a.status} |\n`;
78
+ md += `| **Connected** | ${a.connected_at ? new Date(a.connected_at).toLocaleDateString() : "—"} |\n`;
79
+ if (a.platform_details?.subscription_type) {
80
+ md += `| **Subscription** | ${a.platform_details.subscription_type} |\n`;
81
+ }
82
+ if (a.boards?.length) {
83
+ md += `\n### Pinterest Boards\n\n`;
84
+ md += `| Board Name | Board ID |\n`;
85
+ md += `|------------|----------|\n`;
86
+ for (const b of a.boards) {
87
+ md += `| ${b.name} | \`${b.id}\` |\n`;
88
+ }
89
+ }
90
+ const content = [];
91
+ if (a.profile_picture) {
92
+ const img = await fetchImageAsBase64(a.profile_picture);
93
+ if (img) {
94
+ content.push({ type: "image", data: img.data, mimeType: img.mimeType });
95
+ }
96
+ }
97
+ content.push({ type: "text", text: md });
98
+ return { content };
16
99
  });
17
100
  }
@@ -1,11 +1,52 @@
1
1
  import { z } from "zod";
2
+ import { formatNumber, capitalize } from "../client.js";
2
3
  export function registerAnalyticsTools(server, client) {
3
4
  server.tool("get_post_analytics", "Get analytics/statistics for a specific published post (impressions, engagements, likes, etc.).", {
4
5
  post_id: z.string().describe("The post ID to get analytics for"),
5
6
  }, async ({ post_id }) => {
6
7
  const result = await client.getPostAnalytics(post_id);
8
+ if (result.error) {
9
+ return {
10
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
11
+ };
12
+ }
13
+ const d = result.data;
14
+ if (!d) {
15
+ return {
16
+ content: [{ type: "text", text: "No analytics found for this post." }],
17
+ };
18
+ }
19
+ let md = `## Post Analytics\n\n`;
20
+ md += `| Metric | Value |\n`;
21
+ md += `|--------|-------|\n`;
22
+ md += `| **Impressions** | ${formatNumber(d.impressions ?? 0)} |\n`;
23
+ md += `| **Engagements** | ${formatNumber(d.engagements ?? 0)} |\n`;
24
+ md += `| **Likes** | ${formatNumber(d.likes ?? 0)} |\n`;
25
+ md += `| **Comments** | ${formatNumber(d.comments ?? 0)} |\n`;
26
+ md += `| **Shares** | ${formatNumber(d.shares ?? 0)} |\n`;
27
+ if (d.impressions > 0) {
28
+ const rate = ((d.engagements ?? 0) / d.impressions * 100).toFixed(2);
29
+ md += `| **Engagement Rate** | ${rate}% |\n`;
30
+ }
31
+ if (d.platform_stats && Object.keys(d.platform_stats).length > 0) {
32
+ md += `\n### Per-Platform Breakdown\n\n`;
33
+ for (const [platform, stats] of Object.entries(d.platform_stats)) {
34
+ const s = stats;
35
+ md += `**${capitalize(platform)}:** `;
36
+ const parts = [];
37
+ if (s.impressions !== undefined)
38
+ parts.push(`${formatNumber(s.impressions)} impressions`);
39
+ if (s.engagements !== undefined)
40
+ parts.push(`${formatNumber(s.engagements)} engagements`);
41
+ if (s.likes !== undefined)
42
+ parts.push(`${formatNumber(s.likes)} likes`);
43
+ if (s.comments !== undefined)
44
+ parts.push(`${formatNumber(s.comments)} comments`);
45
+ md += parts.join(", ") + "\n\n";
46
+ }
47
+ }
7
48
  return {
8
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
49
+ content: [{ type: "text", text: md }],
9
50
  };
10
51
  });
11
52
  server.tool("get_analytics_overview", "Get an overview of analytics across all posts and accounts for a time period.", {
@@ -14,8 +55,37 @@ export function registerAnalyticsTools(server, client) {
14
55
  end_date: z.string().optional().describe("Custom end date (ISO 8601)"),
15
56
  }, async (params) => {
16
57
  const result = await client.getAnalyticsOverview(params);
58
+ if (result.error) {
59
+ return {
60
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
61
+ };
62
+ }
63
+ const d = result.data;
64
+ const period = result.period || params.period || "30d";
65
+ let md = `## Analytics Overview (Last ${period})\n\n`;
66
+ md += `**${d.total_posts} posts** across **${d.total_platforms} platforms** | `;
67
+ md += `**${formatNumber(d.total_impressions)}** impressions | `;
68
+ md += `**${formatNumber(d.total_engagement)}** engagements | `;
69
+ md += `**${(d.average_engagement_rate ?? 0).toFixed(2)}%** avg engagement rate\n\n`;
70
+ if (d.top_performing_platform) {
71
+ md += `Top performing platform: **${capitalize(d.top_performing_platform)}**\n\n`;
72
+ }
73
+ if (d.platform_breakdown && Object.keys(d.platform_breakdown).length > 0) {
74
+ md += `### By Platform\n\n`;
75
+ md += `| Platform | Posts | Impressions | Engagements | Eng. Rate |\n`;
76
+ md += `|----------|-------|-------------|-------------|----------|\n`;
77
+ // Sort by impressions descending
78
+ const sorted = Object.entries(d.platform_breakdown).sort((a, b) => (b[1].total_impressions || 0) - (a[1].total_impressions || 0));
79
+ for (const [platform, stats] of sorted) {
80
+ const s = stats;
81
+ const rate = s.engagement_rate !== undefined
82
+ ? `${s.engagement_rate.toFixed(2)}%`
83
+ : "—";
84
+ md += `| ${capitalize(platform)} | ${s.posts} | ${formatNumber(s.total_impressions || 0)} | ${formatNumber(s.total_engagement || 0)} | ${rate} |\n`;
85
+ }
86
+ }
17
87
  return {
18
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
88
+ content: [{ type: "text", text: md }],
19
89
  };
20
90
  });
21
91
  server.tool("get_account_analytics", "Get account-level analytics (followers, subscribers, etc.) for connected social accounts.", {
@@ -23,8 +93,54 @@ export function registerAnalyticsTools(server, client) {
23
93
  date: z.string().optional().describe("Date to get analytics for (YYYY-MM-DD, defaults to today)"),
24
94
  }, async (params) => {
25
95
  const result = await client.getAccountAnalytics(params);
96
+ if (result.error) {
97
+ return {
98
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
99
+ };
100
+ }
101
+ const accounts = result.data || [];
102
+ const date = result.date || params.date || "today";
103
+ if (!accounts.length) {
104
+ return {
105
+ content: [{ type: "text", text: "No account analytics found." }],
106
+ };
107
+ }
108
+ let md = `## Account Analytics (${date})\n\n`;
109
+ md += `| Platform | Followers | Details |\n`;
110
+ md += `|----------|-----------|---------|\n`;
111
+ // Sort by followers descending
112
+ const sorted = [...accounts].sort((a, b) => {
113
+ const fa = a.metrics?.followers ?? 0;
114
+ const fb = b.metrics?.followers ?? 0;
115
+ return fb - fa;
116
+ });
117
+ for (const a of sorted) {
118
+ const m = a.metrics || {};
119
+ const followers = m.followers !== undefined
120
+ ? formatNumber(m.followers)
121
+ : m.subscribers !== undefined
122
+ ? `${formatNumber(m.subscribers)} subs`
123
+ : "—";
124
+ const details = [];
125
+ if (m.posts !== undefined)
126
+ details.push(`${formatNumber(m.posts)} posts`);
127
+ if (m.following !== undefined)
128
+ details.push(`${formatNumber(m.following)} following`);
129
+ if (m.impressions !== undefined)
130
+ details.push(`${formatNumber(m.impressions)} impressions`);
131
+ if (m.engagement !== undefined)
132
+ details.push(`${formatNumber(m.engagement)} engagements`);
133
+ if (m.total_views !== undefined)
134
+ details.push(`${formatNumber(m.total_views)} views`);
135
+ if (m.total_videos !== undefined)
136
+ details.push(`${m.total_videos} videos`);
137
+ if (m.subscribers !== undefined && m.followers !== undefined) {
138
+ details.push(`${formatNumber(m.subscribers)} subscribers`);
139
+ }
140
+ md += `| ${capitalize(a.platform)} | ${followers} | ${details.join(", ") || "—"} |\n`;
141
+ }
26
142
  return {
27
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
143
+ content: [{ type: "text", text: md }],
28
144
  };
29
145
  });
30
146
  }
@@ -1,21 +1,68 @@
1
1
  import { z } from "zod";
2
+ import { fetchImageAsBase64, formatBytes } from "../client.js";
2
3
  export function registerMediaTools(server, client) {
3
- server.tool("list_media", "List all media files in the workspace.", {
4
+ server.tool("list_media", "List all media files in the workspace. Returns each file's ID, URL, type (image/video), and size. Use this to help users pick media for posts, stories, or reels. Media IDs or URLs from this list can be passed to create_post.", {
4
5
  limit: z.string().optional().describe("Max results to return"),
5
6
  offset: z.string().optional().describe("Offset for pagination"),
6
7
  }, async (params) => {
7
8
  const result = await client.listMedia(params);
8
- return {
9
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
10
- };
9
+ if (result.error) {
10
+ return {
11
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
12
+ };
13
+ }
14
+ const items = Array.isArray(result.data) ? result.data : result.data?.media || [];
15
+ if (!items.length) {
16
+ return {
17
+ content: [{ type: "text", text: "No media files found." }],
18
+ };
19
+ }
20
+ let md = `## Media Library (${items.length} files)\n\n`;
21
+ md += `| # | Type | Filename | Size | Media ID |\n`;
22
+ md += `|---|------|----------|------|----------|\n`;
23
+ for (let i = 0; i < items.length; i++) {
24
+ const m = items[i];
25
+ const type = m.type === "video" ? "Video" : "Image";
26
+ md += `| ${i + 1} | ${type} | ${m.filename || "—"} | ${formatBytes(m.size || 0)} | \`${m.id}\` |\n`;
27
+ }
28
+ md += `\nUse the **Media ID** with \`media_ids\` when creating posts.`;
29
+ // Fetch thumbnails for image files (max 4)
30
+ const content = [];
31
+ const imageItems = items.filter((m) => m.type !== "video" && m.url).slice(0, 4);
32
+ const imagePromises = imageItems.map(async (m) => {
33
+ const img = await fetchImageAsBase64(m.url);
34
+ return img ? { ...img, id: m.id } : null;
35
+ });
36
+ const images = (await Promise.all(imagePromises)).filter(Boolean);
37
+ for (const img of images) {
38
+ if (img) {
39
+ content.push({ type: "image", data: img.data, mimeType: img.mimeType });
40
+ }
41
+ }
42
+ content.push({ type: "text", text: md });
43
+ return { content };
11
44
  });
12
45
  server.tool("upload_media", "Upload media from a URL. The file will be downloaded and stored. Max 50MB.", {
13
46
  url: z.string().describe("Public URL of the media file to upload"),
14
47
  filename: z.string().optional().describe("Optional filename for the uploaded media"),
15
48
  }, async (params) => {
16
49
  const result = await client.uploadMedia(params);
50
+ if (result.error) {
51
+ return {
52
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
53
+ };
54
+ }
55
+ const m = result.data;
56
+ let md = `## Media Uploaded\n\n`;
57
+ md += `| Field | Value |\n`;
58
+ md += `|-------|-------|\n`;
59
+ md += `| **Media ID** | \`${m.id}\` |\n`;
60
+ md += `| **Type** | ${m.type || "—"} |\n`;
61
+ md += `| **Filename** | ${m.filename || "—"} |\n`;
62
+ md += `| **Size** | ${formatBytes(m.size || 0)} |\n`;
63
+ md += `\nUse this Media ID with \`media_ids\` when creating posts.`;
17
64
  return {
18
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
65
+ content: [{ type: "text", text: md }],
19
66
  };
20
67
  });
21
68
  server.tool("delete_media", "Delete a media file by ID.", {
@@ -23,7 +70,7 @@ export function registerMediaTools(server, client) {
23
70
  }, async ({ id }) => {
24
71
  const result = await client.deleteMedia(id);
25
72
  return {
26
- content: [{ type: "text", text: result.error ? JSON.stringify(result.error) : "Media deleted successfully." }],
73
+ content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Media deleted successfully." }],
27
74
  };
28
75
  });
29
76
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { formatDateTime, truncate, capitalize } from "../client.js";
2
3
  export function registerPostTools(server, client) {
3
4
  server.tool("list_posts", "List all posts in the workspace. Optionally filter by status.", {
4
5
  status: z.string().optional().describe("Filter by status: draft, scheduled, published, failed"),
@@ -6,19 +7,92 @@ export function registerPostTools(server, client) {
6
7
  offset: z.string().optional().describe("Offset for pagination"),
7
8
  }, async (params) => {
8
9
  const result = await client.listPosts(params);
10
+ if (result.error) {
11
+ return {
12
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
13
+ };
14
+ }
15
+ const posts = Array.isArray(result.data) ? result.data : result.data?.posts || [];
16
+ if (!posts.length) {
17
+ return {
18
+ content: [{ type: "text", text: `No posts found${params.status ? ` with status "${params.status}"` : ""}.` }],
19
+ };
20
+ }
21
+ const statusLabel = params.status ? ` — ${capitalize(params.status)}` : "";
22
+ let md = `## Posts (${posts.length} results)${statusLabel}\n\n`;
23
+ md += `| # | Content | Channels | Status | Date | ID |\n`;
24
+ md += `|---|---------|----------|--------|------|----|\n`;
25
+ for (let i = 0; i < posts.length; i++) {
26
+ const p = posts[i];
27
+ const content = truncate(p.content || "", 50);
28
+ const channels = (p.channels || []).join(", ");
29
+ const status = capitalize(p.status || "—");
30
+ const date = p.scheduled_at
31
+ ? formatDateTime(p.scheduled_at)
32
+ : p.published_at
33
+ ? formatDateTime(p.published_at)
34
+ : formatDateTime(p.created_at);
35
+ md += `| ${i + 1} | ${content} | ${channels} | ${status} | ${date} | \`${p.id}\` |\n`;
36
+ }
9
37
  return {
10
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
38
+ content: [{ type: "text", text: md }],
11
39
  };
12
40
  });
13
41
  server.tool("get_post", "Get details of a specific post by ID.", {
14
42
  id: z.string().describe("The post ID"),
15
43
  }, async ({ id }) => {
16
44
  const result = await client.getPost(id);
45
+ if (result.error) {
46
+ return {
47
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
48
+ };
49
+ }
50
+ const p = result.data;
51
+ if (!p) {
52
+ return {
53
+ content: [{ type: "text", text: "Post not found." }],
54
+ };
55
+ }
56
+ let md = `## Post Details\n\n`;
57
+ md += `| Field | Value |\n`;
58
+ md += `|-------|-------|\n`;
59
+ md += `| **ID** | \`${p.id}\` |\n`;
60
+ md += `| **Status** | ${capitalize(p.status || "—")} |\n`;
61
+ md += `| **Type** | ${p.type || "post"} |\n`;
62
+ md += `| **Channels** | ${(p.channels || []).join(", ") || "—"} |\n`;
63
+ if (p.scheduled_at)
64
+ md += `| **Scheduled** | ${formatDateTime(p.scheduled_at)} |\n`;
65
+ if (p.published_at)
66
+ md += `| **Published** | ${formatDateTime(p.published_at)} |\n`;
67
+ md += `| **Created** | ${formatDateTime(p.created_at)} |\n`;
68
+ if (p.media?.length) {
69
+ md += `| **Media** | ${p.media.length} file(s) |\n`;
70
+ }
71
+ md += `\n### Content\n\n${p.content || "(empty)"}`;
17
72
  return {
18
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
73
+ content: [{ type: "text", text: md }],
19
74
  };
20
75
  });
21
- server.tool("create_post", "Create a new social media post, story, or reel. Specify content, target channels, type, and optional platform-specific options.", {
76
+ server.tool("create_post", `Create a new social media post, story, or reel.
77
+
78
+ IMPORTANT — Before calling this tool, make sure you have all required information from the user. If anything is missing, ASK the user before calling:
79
+
80
+ 1. **Content/caption**: What text should the post have?
81
+ 2. **Channels**: Which platforms to post to? Use list_accounts to show available options if needed.
82
+ 3. **Schedule**: When should it be published? (Or save as draft?)
83
+ 4. **Media** (REQUIRED for some types):
84
+ - 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.
85
+ - Reels: ALWAYS require a video. Ask the user which video to use. Use list_media to find available videos.
86
+ - Instagram posts: ALWAYS require at least one image or video.
87
+ - TikTok posts: ALWAYS require at least one image or video.
88
+ - Pinterest posts: ALWAYS require an image AND a board_id.
89
+ - Other platforms (LinkedIn, X, Bluesky, etc.): Media is optional.
90
+ 5. **Platform-specific options** (ask only when relevant):
91
+ - Pinterest: Which board? Any link to attach?
92
+ - YouTube: Title, privacy status, tags?
93
+ - TikTok: Privacy level?
94
+
95
+ Do NOT call this tool without media when creating stories, reels, Instagram posts, TikTok posts, or Pinterest posts — it will fail.`, {
22
96
  content: z.string().describe("The post content/caption text"),
23
97
  channels: z.array(z.string()).optional().describe("Array of channel IDs to post to"),
24
98
  scheduled_at: z.string().optional().describe("ISO 8601 date for scheduled publishing"),
@@ -29,7 +103,7 @@ export function registerPostTools(server, client) {
29
103
  media_urls: z.union([
30
104
  z.array(z.string()),
31
105
  z.record(z.string(), z.array(z.string())),
32
- ]).optional().describe("External image/video URLs — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...], pinterest: [...] }. Max 10 total."),
106
+ ]).optional().describe("External image/video URLs — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...], pinterest: [...] }. Max 10 total. When using per-platform format, 'default' is the fallback for selected platforms without their own key. Pass an empty array (e.g. facebook: []) to opt a platform out of media."),
33
107
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story' (Instagram/Facebook/Snapchat), 'reel' (Instagram/Facebook/YouTube/TikTok)"),
34
108
  pinterest: z.object({
35
109
  board_id: z.string().optional(),
@@ -64,11 +138,40 @@ export function registerPostTools(server, client) {
64
138
  }).optional().describe("X (Twitter) options"),
65
139
  }, async (params) => {
66
140
  const result = await client.createPost(params);
141
+ if (result.error) {
142
+ return {
143
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
144
+ };
145
+ }
146
+ const p = result.data;
147
+ let md = `## Post Created\n\n`;
148
+ md += `| Field | Value |\n`;
149
+ md += `|-------|-------|\n`;
150
+ md += `| **ID** | \`${p.id}\` |\n`;
151
+ md += `| **Status** | ${capitalize(p.status || "draft")} |\n`;
152
+ if (p.scheduled_at)
153
+ md += `| **Scheduled** | ${formatDateTime(p.scheduled_at)} |\n`;
154
+ if (p.channels?.length)
155
+ md += `| **Channels** | ${p.channels.join(", ")} |\n`;
156
+ md += `\n**Content:** ${truncate(p.content || "", 100)}`;
67
157
  return {
68
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
158
+ content: [{ type: "text", text: md }],
69
159
  };
70
160
  });
71
- server.tool("create_and_publish_post", "Create a new post and publish it immediately to all selected platforms. No scheduling needed.", {
161
+ server.tool("create_and_publish_post", `Create a new post and publish it immediately (no scheduling).
162
+
163
+ IMPORTANT — Before calling this tool, make sure you have all required information. If anything is missing, ASK the user first:
164
+
165
+ 1. **Content/caption**: What text?
166
+ 2. **Channels**: Which platforms? Use list_accounts if needed.
167
+ 3. **Media** (REQUIRED for some types — same rules as create_post):
168
+ - Stories: ALWAYS need an image or video.
169
+ - Reels: ALWAYS need a video.
170
+ - Instagram/TikTok posts: ALWAYS need at least one image or video.
171
+ - Pinterest: ALWAYS need an image + board_id.
172
+ - Ask the user which media to use. Use list_media to show options.
173
+
174
+ Do NOT call without required media — it will fail.`, {
72
175
  content: z.string().describe("The post content/caption text"),
73
176
  channels: z.array(z.string()).optional().describe("Array of channel IDs to post to"),
74
177
  media_ids: z.union([
@@ -78,7 +181,7 @@ export function registerPostTools(server, client) {
78
181
  media_urls: z.union([
79
182
  z.array(z.string()),
80
183
  z.record(z.string(), z.array(z.string())),
81
- ]).optional().describe("External image/video URLs — flat array or per-platform object. Max 10 total."),
184
+ ]).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."),
82
185
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story', 'reel'"),
83
186
  pinterest: z.object({
84
187
  board_id: z.string().optional(),
@@ -95,8 +198,22 @@ export function registerPostTools(server, client) {
95
198
  }).optional().describe("TikTok options"),
96
199
  }, async (params) => {
97
200
  const result = await client.createAndPublishPost(params);
201
+ if (result.error) {
202
+ return {
203
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
204
+ };
205
+ }
206
+ const p = result.data;
207
+ let md = `## Post Created & Publishing\n\n`;
208
+ md += `| Field | Value |\n`;
209
+ md += `|-------|-------|\n`;
210
+ md += `| **ID** | \`${p.id}\` |\n`;
211
+ md += `| **Status** | ${capitalize(p.status || "publishing")} |\n`;
212
+ if (p.channels?.length)
213
+ md += `| **Channels** | ${p.channels.join(", ")} |\n`;
214
+ md += `\n**Content:** ${truncate(p.content || "", 100)}`;
98
215
  return {
99
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
216
+ content: [{ type: "text", text: md }],
100
217
  };
101
218
  });
102
219
  server.tool("update_post", "Update an existing post. Only draft and scheduled posts can be updated.", {
@@ -111,11 +228,24 @@ export function registerPostTools(server, client) {
111
228
  media_urls: z.union([
112
229
  z.array(z.string()),
113
230
  z.record(z.string(), z.array(z.string())),
114
- ]).optional().describe("External URLs — flat array or per-platform object. Max 10 total."),
231
+ ]).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."),
115
232
  }, async ({ id, ...data }) => {
116
233
  const result = await client.updatePost(id, data);
234
+ if (result.error) {
235
+ return {
236
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
237
+ };
238
+ }
239
+ const p = result.data;
240
+ let md = `## Post Updated\n\n`;
241
+ md += `| Field | Value |\n`;
242
+ md += `|-------|-------|\n`;
243
+ md += `| **ID** | \`${p.id}\` |\n`;
244
+ md += `| **Status** | ${capitalize(p.status || "—")} |\n`;
245
+ if (p.scheduled_at)
246
+ md += `| **Scheduled** | ${formatDateTime(p.scheduled_at)} |\n`;
117
247
  return {
118
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
248
+ content: [{ type: "text", text: md }],
119
249
  };
120
250
  });
121
251
  server.tool("delete_post", "Delete a post by ID. This action cannot be undone.", {
@@ -123,15 +253,28 @@ export function registerPostTools(server, client) {
123
253
  }, async ({ id }) => {
124
254
  const result = await client.deletePost(id);
125
255
  return {
126
- content: [{ type: "text", text: result.error ? JSON.stringify(result.error) : "Post deleted successfully." }],
256
+ content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Post deleted successfully." }],
127
257
  };
128
258
  });
129
- server.tool("publish_post", "Publish a draft or scheduled post immediately. The post will be queued for immediate publishing.", {
259
+ server.tool("publish_post", `Publish a draft or scheduled post immediately. The post will be queued for publishing.
260
+
261
+ IMPORTANT: Before publishing, verify the post has all required media. If publishing a story or reel, ensure media was attached when the post was created/updated. If it's missing, use update_post to add media first, or inform the user.`, {
130
262
  id: z.string().describe("The post ID to publish"),
131
263
  }, async ({ id }) => {
132
264
  const result = await client.publishPost(id);
265
+ if (result.error) {
266
+ return {
267
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
268
+ };
269
+ }
270
+ const p = result.data;
271
+ let md = `## Post Queued for Publishing\n\n`;
272
+ md += `| Field | Value |\n`;
273
+ md += `|-------|-------|\n`;
274
+ md += `| **ID** | \`${p.id}\` |\n`;
275
+ md += `| **Status** | ${capitalize(p.status || "publishing")} |\n`;
133
276
  return {
134
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
277
+ content: [{ type: "text", text: md }],
135
278
  };
136
279
  });
137
280
  }
@@ -1,9 +1,31 @@
1
1
  import { z } from "zod";
2
+ import { formatDateTime } from "../client.js";
2
3
  export function registerWebhookTools(server, client) {
3
4
  server.tool("list_webhooks", "List all configured webhooks.", {}, async () => {
4
5
  const result = await client.listWebhooks();
6
+ if (result.error) {
7
+ return {
8
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
9
+ };
10
+ }
11
+ const webhooks = Array.isArray(result.data) ? result.data : result.data?.webhooks || [];
12
+ if (!webhooks.length) {
13
+ return {
14
+ content: [{ type: "text", text: "No webhooks configured." }],
15
+ };
16
+ }
17
+ let md = `## Webhooks (${webhooks.length})\n\n`;
18
+ md += `| # | URL | Events | Status | Last Triggered |\n`;
19
+ md += `|---|-----|--------|--------|----------------|\n`;
20
+ for (let i = 0; i < webhooks.length; i++) {
21
+ const w = webhooks[i];
22
+ const status = w.is_active ? "Active" : "Inactive";
23
+ const events = (w.events || []).join(", ");
24
+ const lastTriggered = w.last_triggered_at ? formatDateTime(w.last_triggered_at) : "Never";
25
+ md += `| ${i + 1} | ${w.url} | ${events} | ${status} | ${lastTriggered} |\n`;
26
+ }
5
27
  return {
6
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
28
+ content: [{ type: "text", text: md }],
7
29
  };
8
30
  });
9
31
  server.tool("create_webhook", "Create a new webhook to receive event notifications. Available events: post.scheduled, post.published, post.failed", {
@@ -11,16 +33,49 @@ export function registerWebhookTools(server, client) {
11
33
  events: z.array(z.string()).describe("Events to subscribe to: post.scheduled, post.published, post.failed"),
12
34
  }, async (params) => {
13
35
  const result = await client.createWebhook(params);
36
+ if (result.error) {
37
+ return {
38
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
39
+ };
40
+ }
41
+ const w = result.data;
42
+ let md = `## Webhook Created\n\n`;
43
+ md += `| Field | Value |\n`;
44
+ md += `|-------|-------|\n`;
45
+ md += `| **ID** | \`${w.id}\` |\n`;
46
+ md += `| **URL** | ${w.url} |\n`;
47
+ md += `| **Events** | ${(w.events || []).join(", ")} |\n`;
48
+ md += `| **Status** | ${w.is_active ? "Active" : "Inactive"} |\n`;
49
+ if (w.secret) {
50
+ md += `| **Secret** | \`${w.secret}\` |\n`;
51
+ md += `\n> **Save this secret** — it won't be shown again. Use it to verify webhook signatures.`;
52
+ }
14
53
  return {
15
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
54
+ content: [{ type: "text", text: md }],
16
55
  };
17
56
  });
18
57
  server.tool("get_webhook", "Get details of a specific webhook by ID.", {
19
58
  id: z.string().describe("The webhook ID"),
20
59
  }, async ({ id }) => {
21
60
  const result = await client.getWebhook(id);
61
+ if (result.error) {
62
+ return {
63
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
64
+ };
65
+ }
66
+ const w = result.data;
67
+ let md = `## Webhook Details\n\n`;
68
+ md += `| Field | Value |\n`;
69
+ md += `|-------|-------|\n`;
70
+ md += `| **ID** | \`${w.id}\` |\n`;
71
+ md += `| **URL** | ${w.url} |\n`;
72
+ md += `| **Events** | ${(w.events || []).join(", ")} |\n`;
73
+ md += `| **Status** | ${w.is_active ? "Active" : "Inactive"} |\n`;
74
+ md += `| **Last Triggered** | ${w.last_triggered_at ? formatDateTime(w.last_triggered_at) : "Never"} |\n`;
75
+ md += `| **Failure Count** | ${w.failure_count ?? 0} |\n`;
76
+ md += `| **Created** | ${formatDateTime(w.created_at)} |\n`;
22
77
  return {
23
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
78
+ content: [{ type: "text", text: md }],
24
79
  };
25
80
  });
26
81
  server.tool("update_webhook", "Update a webhook's URL, events, or active status.", {
@@ -30,8 +85,21 @@ export function registerWebhookTools(server, client) {
30
85
  is_active: z.boolean().optional().describe("Enable or disable the webhook"),
31
86
  }, async ({ id, ...data }) => {
32
87
  const result = await client.updateWebhook(id, data);
88
+ if (result.error) {
89
+ return {
90
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
91
+ };
92
+ }
93
+ const w = result.data;
94
+ let md = `## Webhook Updated\n\n`;
95
+ md += `| Field | Value |\n`;
96
+ md += `|-------|-------|\n`;
97
+ md += `| **ID** | \`${w.id}\` |\n`;
98
+ md += `| **URL** | ${w.url} |\n`;
99
+ md += `| **Events** | ${(w.events || []).join(", ")} |\n`;
100
+ md += `| **Status** | ${w.is_active ? "Active" : "Inactive"} |\n`;
33
101
  return {
34
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
102
+ content: [{ type: "text", text: md }],
35
103
  };
36
104
  });
37
105
  server.tool("delete_webhook", "Delete a webhook by ID. You will stop receiving notifications at this URL.", {
@@ -39,15 +107,29 @@ export function registerWebhookTools(server, client) {
39
107
  }, async ({ id }) => {
40
108
  const result = await client.deleteWebhook(id);
41
109
  return {
42
- content: [{ type: "text", text: result.error ? JSON.stringify(result.error) : "Webhook deleted successfully." }],
110
+ content: [{ type: "text", text: result.error ? `Error: ${result.error.message}` : "Webhook deleted successfully." }],
43
111
  };
44
112
  });
45
113
  server.tool("rotate_webhook_secret", "Rotate the signing secret for a webhook. The new secret will only be shown once.", {
46
114
  id: z.string().describe("The webhook ID"),
47
115
  }, async ({ id }) => {
48
116
  const result = await client.rotateWebhookSecret(id);
117
+ if (result.error) {
118
+ return {
119
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
120
+ };
121
+ }
122
+ const w = result.data;
123
+ let md = `## Webhook Secret Rotated\n\n`;
124
+ md += `| Field | Value |\n`;
125
+ md += `|-------|-------|\n`;
126
+ md += `| **ID** | \`${w.id}\` |\n`;
127
+ if (w.secret) {
128
+ md += `| **New Secret** | \`${w.secret}\` |\n`;
129
+ md += `\n> **Save this secret immediately** — it won't be shown again.`;
130
+ }
49
131
  return {
50
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
132
+ content: [{ type: "text", text: md }],
51
133
  };
52
134
  });
53
135
  }
package/build/types.d.ts CHANGED
@@ -27,8 +27,18 @@ export interface Account {
27
27
  id: string;
28
28
  platform: string;
29
29
  username: string;
30
+ display_name: string;
30
31
  profile_picture: string | null;
31
- is_connected: boolean;
32
+ content_types: string[];
33
+ status: string;
34
+ connected_at: string | null;
35
+ boards?: {
36
+ id: string;
37
+ name: string;
38
+ }[];
39
+ platform_details?: {
40
+ subscription_type?: string;
41
+ };
32
42
  }
33
43
  export interface Webhook {
34
44
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.0.3",
3
+ "version": "1.1.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",