@omnisocials/mcp-server 1.0.4 → 1.2.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
@@ -75,6 +75,24 @@ Add to your `~/.codeium/windsurf/mcp_config.json`:
75
75
  }
76
76
  ```
77
77
 
78
+ ## Supported Channels
79
+
80
+ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publish_post`, and related tools. Use `list_accounts` to see which channels a workspace currently has connected.
81
+
82
+ | Channel ID | Platform | Notes |
83
+ |------------|----------|-------|
84
+ | `instagram` | Instagram | Posts, stories, reels |
85
+ | `facebook` | Facebook | Posts, stories, reels |
86
+ | `linkedin` | LinkedIn Profile | Personal profile posts |
87
+ | `linkedin_page` | LinkedIn Page | Company page posts. Independent from `linkedin`; a workspace can have both connected at once |
88
+ | `youtube` | YouTube | Shorts |
89
+ | `tiktok` | TikTok | Posts and reels |
90
+ | `pinterest` | Pinterest | Pins (requires `board_id`) |
91
+ | `x` | X (Twitter) | Posts |
92
+ | `threads` | Threads | Posts |
93
+ | `bluesky` | Bluesky | Posts |
94
+ | `mastodon` | Mastodon | Posts |
95
+
78
96
  ## Available Tools
79
97
 
80
98
  ### Posts (7 tools)
@@ -130,6 +148,16 @@ Add to your `~/.codeium/windsurf/mcp_config.json`:
130
148
  | `OMNISOCIALS_API_KEY` | Yes | Your API key (`omsk_live_*` or `omsk_test_*`) |
131
149
  | `OMNISOCIALS_BASE_URL` | No | Custom API base URL (defaults to production) |
132
150
 
151
+ ## Alternative: Agent Skills
152
+
153
+ 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:
154
+
155
+ ```bash
156
+ npx skills add OmniSocials/omnisocials-agent-skills
157
+ ```
158
+
159
+ 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.
160
+
133
161
  ## API Documentation
134
162
 
135
163
  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;
@@ -17,6 +27,7 @@ export declare class OmniSocialsClient {
17
27
  media_ids?: string[] | Record<string, string[]>;
18
28
  media_urls?: string[] | Record<string, string[]>;
19
29
  type?: string;
30
+ source?: string;
20
31
  pinterest?: Record<string, unknown>;
21
32
  youtube?: Record<string, unknown>;
22
33
  instagram?: Record<string, unknown>;
@@ -29,6 +40,7 @@ export declare class OmniSocialsClient {
29
40
  media_ids?: string[] | Record<string, string[]>;
30
41
  media_urls?: string[] | Record<string, string[]>;
31
42
  type?: string;
43
+ source?: string;
32
44
  pinterest?: Record<string, unknown>;
33
45
  youtube?: Record<string, unknown>;
34
46
  instagram?: Record<string, unknown>;
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. Each account includes its platform, display name, channel ID (used for create_post), and supported content_types (post, story, reel). Call this to help users pick which platforms to post to.", {}, 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\"). LinkedIn appears as two independent platforms: `linkedin` for a personal profile and `linkedin_page` for a company page. A workspace can have both connected at the same time and post to each separately. 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
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,16 +7,70 @@ 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
76
  server.tool("create_post", `Create a new social media post, story, or reel.
@@ -31,7 +86,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
31
86
  - Instagram posts: ALWAYS require at least one image or video.
32
87
  - TikTok posts: ALWAYS require at least one image or video.
33
88
  - Pinterest posts: ALWAYS require an image AND a board_id.
34
- - Other platforms (LinkedIn, X, Bluesky, etc.): Media is optional.
89
+ - Other platforms (LinkedIn, LinkedIn Page, X, Bluesky, etc.): Media is optional.
35
90
  5. **Platform-specific options** (ask only when relevant):
36
91
  - Pinterest: Which board? Any link to attach?
37
92
  - YouTube: Title, privacy status, tags?
@@ -39,7 +94,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
39
94
 
40
95
  Do NOT call this tool without media when creating stories, reels, Instagram posts, TikTok posts, or Pinterest posts — it will fail.`, {
41
96
  content: z.string().describe("The post content/caption text"),
42
- channels: z.array(z.string()).optional().describe("Array of channel IDs to post to"),
97
+ channels: z.array(z.string()).optional().describe("Array of channel IDs to post to. Get the available channel IDs from list_accounts. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels. A workspace can have both connected and post to each separately."),
43
98
  scheduled_at: z.string().optional().describe("ISO 8601 date for scheduled publishing"),
44
99
  media_ids: z.union([
45
100
  z.array(z.string()),
@@ -48,7 +103,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
48
103
  media_urls: z.union([
49
104
  z.array(z.string()),
50
105
  z.record(z.string(), z.array(z.string())),
51
- ]).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."),
52
107
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story' (Instagram/Facebook/Snapchat), 'reel' (Instagram/Facebook/YouTube/TikTok)"),
53
108
  pinterest: z.object({
54
109
  board_id: z.string().optional(),
@@ -82,9 +137,25 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
82
137
  reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
83
138
  }).optional().describe("X (Twitter) options"),
84
139
  }, async (params) => {
85
- const result = await client.createPost(params);
140
+ const result = await client.createPost({ ...params, source: "mcp" });
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)}`;
86
157
  return {
87
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
158
+ content: [{ type: "text", text: md }],
88
159
  };
89
160
  });
90
161
  server.tool("create_and_publish_post", `Create a new post and publish it immediately (no scheduling).
@@ -102,7 +173,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
102
173
 
103
174
  Do NOT call without required media — it will fail.`, {
104
175
  content: z.string().describe("The post content/caption text"),
105
- channels: z.array(z.string()).optional().describe("Array of channel IDs to post to"),
176
+ channels: z.array(z.string()).optional().describe("Array of channel IDs to post to. Get the available channel IDs from list_accounts. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels. A workspace can have both connected and post to each separately."),
106
177
  media_ids: z.union([
107
178
  z.array(z.string()),
108
179
  z.record(z.string(), z.array(z.string())),
@@ -110,7 +181,7 @@ Do NOT call without required media — it will fail.`, {
110
181
  media_urls: z.union([
111
182
  z.array(z.string()),
112
183
  z.record(z.string(), z.array(z.string())),
113
- ]).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."),
114
185
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story', 'reel'"),
115
186
  pinterest: z.object({
116
187
  board_id: z.string().optional(),
@@ -126,16 +197,30 @@ Do NOT call without required media — it will fail.`, {
126
197
  privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
127
198
  }).optional().describe("TikTok options"),
128
199
  }, async (params) => {
129
- const result = await client.createAndPublishPost(params);
200
+ const result = await client.createAndPublishPost({ ...params, source: "mcp" });
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)}`;
130
215
  return {
131
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
216
+ content: [{ type: "text", text: md }],
132
217
  };
133
218
  });
134
219
  server.tool("update_post", "Update an existing post. Only draft and scheduled posts can be updated.", {
135
220
  id: z.string().describe("The post ID to update"),
136
221
  content: z.string().optional().describe("Updated post content"),
137
222
  scheduled_at: z.string().optional().describe("Updated scheduled date (ISO 8601)"),
138
- channels: z.array(z.string()).optional().describe("Updated channel IDs"),
223
+ channels: z.array(z.string()).optional().describe("Updated channel IDs. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels."),
139
224
  media_ids: z.union([
140
225
  z.array(z.string()),
141
226
  z.record(z.string(), z.array(z.string())),
@@ -143,11 +228,24 @@ Do NOT call without required media — it will fail.`, {
143
228
  media_urls: z.union([
144
229
  z.array(z.string()),
145
230
  z.record(z.string(), z.array(z.string())),
146
- ]).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."),
147
232
  }, async ({ id, ...data }) => {
148
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`;
149
247
  return {
150
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
248
+ content: [{ type: "text", text: md }],
151
249
  };
152
250
  });
153
251
  server.tool("delete_post", "Delete a post by ID. This action cannot be undone.", {
@@ -155,7 +253,7 @@ Do NOT call without required media — it will fail.`, {
155
253
  }, async ({ id }) => {
156
254
  const result = await client.deletePost(id);
157
255
  return {
158
- 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." }],
159
257
  };
160
258
  });
161
259
  server.tool("publish_post", `Publish a draft or scheduled post immediately. The post will be queued for publishing.
@@ -164,8 +262,19 @@ IMPORTANT: Before publishing, verify the post has all required media. If publish
164
262
  id: z.string().describe("The post ID to publish"),
165
263
  }, async ({ id }) => {
166
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`;
167
276
  return {
168
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
277
+ content: [{ type: "text", text: md }],
169
278
  };
170
279
  });
171
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.4",
3
+ "version": "1.2.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",