@codingmantra/mcp-server 0.3.0 β†’ 0.5.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
@@ -2,8 +2,21 @@
2
2
 
3
3
  The official **Model Context Protocol (MCP)** server connecting AI assistants (Claude Desktop, Cursor, Cline, and Antigravity) to CodingMantra's generative AI media engines.
4
4
 
5
- ## What's New in v0.3.0
6
- - **Social media workflows:** 15 tools for connected accounts, caption creation, drafts, gallery images, immediate publishing, scheduling, status, cancellation, and analytics. Uses the same CodingMantra API key.
5
+ ## What's New in v0.5.0
6
+
7
+ - Instagram supports single-image posts and ordered carousels of 2–10 images; Facebook supports up to 10 images in a multi-photo post.
8
+ - New `list_social_library` retrieves reusable complete posts with ordered slides, captions and usage filters. `list_social_media` retains generated-gallery access.
9
+ - Optional `mediaLayout` provides common square or portrait canvases using fit or crop, including reusable uploaded media IDs.
10
+ - Existing single-image publishing and scheduling inputs remain supported.
11
+ - Polling exposes processing details and delivery review flags. Never create a replacement post automatically when final delivery is uncertain.
12
+
13
+ ## Previous v0.4.0 changes
14
+
15
+ - Social publishing and scheduling use the versioned developer API, supporting stable platform-prefixed account IDs, multiple targets, and caller-supplied idempotency keys.
16
+ - New `upload_social_image` and `get_social_image` tools support URL, base64, data URI, local files, and reusable media IDs.
17
+ - Per-account polling includes provider post IDs; cancellation retains delivery history. HTTP error codes and Retry-After are exposed in structured MCP errors.
18
+ - **Migration:** publication and scheduling inputs changed from the v0.3 platform/accountId or platforms/content shape to the contract below. Existing drafts, caption generation, analytics, and editing tools remain available.
19
+ - **Social media workflows:** 19 tools for connected accounts, caption creation, drafts, gallery images, immediate publishing, scheduling, status, cancellation, and analytics. Uses the same CodingMantra API key.
7
20
 
8
21
  - **Current API coverage:** Story Teller, HeyGen Talking Avatar IV, and Jewellery Rate Poster tools.
9
22
  - **Video contract alignment:** Required prompt/model/duration/resolution/aspect ratio, character descriptions, seed, guidance, and local reference-image resolution.
@@ -11,7 +24,7 @@ The official **Model Context Protocol (MCP)** server connecting AI assistants (C
11
24
 
12
25
  - πŸ–ΌοΈ **Native MCP Image Output**: Generated images (including QR codes, logos, try-on shots, and product photos) return standard MCP `ImageContent` so they render directly in your AI assistant's chat window.
13
26
  - πŸ“ **Local File Path Resolution**: Pass local paths (e.g. `./shoe.png` or `/Users/.../portrait.jpg`) directly in tool argumentsβ€”the server automatically reads and encodes them as base64 data URIs.
14
- - ⚑ **Expanded Tool Suite (43 Tools)**: Expanded image generation support including Passport Photos, Baby Milestones, YouTube Thumbnails, Blog Images, Nano Banana, Professional Photography, Sketch-to-Real, Human Builder, Apparel/Product/Jewelry Try-On, Festival Posts, Upscaling, and Discovery.
27
+ - ⚑ **Expanded Tool Suite (47 Tools)**: Expanded image generation support including Passport Photos, Baby Milestones, YouTube Thumbnails, Blog Images, Nano Banana, Professional Photography, Sketch-to-Real, Human Builder, Apparel/Product/Jewelry Try-On, Festival Posts, Upscaling, and Discovery.
15
28
  - 🎯 **Advanced Model & Resolution Parameters**: Direct support for `model`, `modelId`, `promptId`, custom reference images, and server-side credit synchronization.
16
29
  - πŸ›‘οΈ **Graceful Error Handling & 120s Timeout**: Informative error diagnostics when API keys are missing or requests time out, with clean stdio protocol execution.
17
30
 
@@ -19,7 +32,7 @@ The official **Model Context Protocol (MCP)** server connecting AI assistants (C
19
32
 
20
33
  ## Features & Tool Suite
21
34
 
22
- The server exposes **43 specialized generative and utility tools** over standard I/O (`stdio`):
35
+ The server exposes **47 specialized generative, utility and social tools** over standard I/O (`stdio`):
23
36
 
24
37
  ### 1. Synchronous Image Generation (Immediate Inline Image Output)
25
38
  - **`generate_product_photo`** β€” Generates studio-grade commercial product photos with custom background scenes, ambient occlusion contact shadows, camera optics, resolutions (1K, 2K, 4K), aspect ratios, and dynamic logo/text overlays.
@@ -71,13 +84,16 @@ All generations automatically deduct credits from your CodingMantra wallet and p
71
84
  - **`save_social_draft`** β€” Create or fully replace a draft; uploads local images. Does not schedule or publish.
72
85
  - **`delete_social_draft`** β€” Remove a saved draft.
73
86
  - **`list_social_media`** β€” Paginate your generated image gallery.
87
+ - **`upload_social_image`** β€” Upload and normalize an image; returns reusable owner-scoped media ID and URL.
88
+ - **`get_social_image`** β€” Read uploaded media metadata.
89
+ - **`schedule_social_draft`** β€” Atomically queue an approved saved draft; reuse draftId safely.
74
90
  - **`schedule_social_post`** β€” Queue final content for automatic future delivery to selected accounts.
75
91
  - **`list_social_posts`** β€” Filter the posting calendar by status, platform, and date range.
76
- - **`get_social_post`** β€” Inspect status, errors, retries, and published job IDs.
92
+ - **`get_social_post`** β€” Inspect overall/per-account status, errors, retries, job IDs, and provider post IDs.
77
93
  - **`update_social_post`** β€” Edit or reschedule a queued post; retry a failed post with a new future time.
78
- - **`cancel_social_post`** β€” Stop a queued post before publication starts.
94
+ - **`cancel_social_post`** β€” Cancel remaining deliveries; retain history and successful account results.
79
95
  - **`delete_social_post`** β€” Delete an eligible schedule record; does not delete a live platform post.
80
- - **`publish_social_post`** β€” Post immediately to one connected account.
96
+ - **`publish_social_post`** β€” Queue immediate delivery to one or multiple connected accounts; poll to confirm publication.
81
97
  - **`get_social_analytics`** β€” Recorded reach, engagement, totals, top posts, and posting activity.
82
98
 
83
99
  #### Working with Claude, Codex, Antigravity, and other assistants
@@ -88,24 +104,34 @@ Try: β€œCreate three Facebook posts for my bakery’s launch. Use a warm voice a
88
104
 
89
105
  Start with `get_social_workflow` and `list_social_accounts`. Use exact returned account IDs. Supply `brandBrief` to isolate a particular brand; otherwise caption generation uses saved brand context. Provide audience, verified facts, tone, offer, and CTA. Combine caption generation with existing image tools, or write captions directly in your assistant without AI credit charges.
90
106
 
91
- Instagram supports **exactly one image**, with at most **2,200 characters including hashtags**. Facebook supports text and up to 10 images, with 63,206 characters including hashtags. Video/reel publishing and other networks are not implemented. Generated images may use local file paths; the server resolves them and the backend persists them when drafts or schedules are saved.
107
+ Instagram supports **1–10 images (one image or a carousel)**, with at most **2,200 characters including hashtags**. Facebook supports text and up to 10 images, with 63,206 characters including hashtags. Video/reel publishing and other networks are not implemented. Generated images may use local file paths; the server resolves them and the backend persists them when drafts or schedules are saved.
108
+
109
+ Publishing and scheduling accept `accountIds` as one string or up to 20 distinct IDs from `list_social_accounts`, such as `facebook:123456` and `instagram:17890000000000000`. Account names are labels; use `platformAccountId` without its prefix only in existing draft/edit/analytics inputs. Those tools retain their legacy schemas.
92
110
 
93
- Scheduling requires `platforms`, `content.caption` (may be empty if there is an image), and a future `scheduledFor` ISO timestamp with an explicit UTC offset. Do not guess an ambiguous timezone. Use `draftId` when queuing a saved draft: the backend atomically consumes it and repeated requests return the same schedule. Immediate publishing has no idempotency guarantee; inspect publication state before retrying a timeout. Successful schedules publish on the backend cron worker even when the assistant is offline.
111
+ Each intended post requires a caller-chosen `idempotencyKey` (8–128 letters, digits, dot, underscore, colon, or hyphen). Preserve the exact key and body on a retry. A new key intentionally creates a new post. Conflicting reuse returns 409; removing the original history returns 410. Both publication and scheduling are **asynchronous**: queue acceptance does not prove a live post. Poll `get_social_post` for each account's status, job ID and `providerPostId`. Provider acceptance followed by response loss can still be ambiguous; inspect provider history before rescheduling.
112
+
113
+ Call `schedule_social_post` with a future ISO timestamp containing an explicit UTC offset. Resolve an ambiguous timezone first. `publish_social_post` uses the same shape without `scheduledFor`. Drafts remain separate; copy approved caption/images into the request. Standalone scheduling does not consume a saved draft or accept `draftId`/`socialContentId`. Use `schedule_social_draft` for the existing atomic draft-consumption workflow: provide draftId, complete platforms/content and scheduledFor using the draft’s raw platform account IDs. Reusing draftId returns the original schedule. This tool retains the v0.3 scheduling shape for approved saved drafts.
94
114
 
95
115
  ```json
96
116
  {
97
- "draftId": "returned_draft_id",
98
- "platforms": [{ "platform": "facebook", "accountId": "returned_page_id", "accountName": "My Bakery" }],
99
- "content": {
100
- "caption": "Our opening day is almost here. Join us for fresh bread!",
101
- "hashtags": ["bakery", "freshbread"],
102
- "mediaUrls": ["https://example.com/my-generated-image.png"]
103
- },
104
- "scheduledFor": "2026-10-10T09:00:00+05:30"
117
+ "accountIds": ["facebook:123456", "instagram:17890000000000000"],
118
+ "caption": "Meet our new collection.",
119
+ "hashtags": ["NewCollection"],
120
+ "images": [{"url": "https://example.com/product.jpg"}],
121
+ "idempotencyKey": "brand-launch-2027-001",
122
+ "scheduledFor": "2027-01-15T10:00:00+05:30"
105
123
  }
106
124
  ```
107
125
 
108
- Deployment order: deploy the updated web API, verify the cron worker, then release/use MCP v0.3.0. The backend must support bearer API keys; older browser-session-only endpoints return 401. For REST examples and the route map, see the repository’s `docs/social-mcp-api.md`.
126
+ Each `images` item accepts a URL/base64/data-URI/local-path string or `{url}`, `{base64, mimeType?}`, `{path}`, or `{mediaId}`. `upload_social_image` takes `{ "image": <input> }`, excluding an existing media ID; it returns a `data` envelope with media ID, URL, JPEG MIME type, dimensions, bytes, and creation time. For example, upload `{ "image": { "path": "/path/to/product.png" } }`, then use `{ "mediaId": "returned-uuid" }` in a post. `get_social_image` accepts `{ "id": "returned-uuid" }`. Uploads do not publish or run AI generation.
127
+
128
+ Source images must be static JPEG/PNG/WebP and no more than 10 MB/40 million pixels. JSON bodies are bounded to 20 MB. HTTPS URLs must be public port 443, return directly, and contain no credentials. Redirects/private destinations are rejected. The backend normalizes images to JPEG; Instagram requires a 4:5–1.91:1 aspect ratio. Dedicated API assets remain independent of gallery deletion. Use media IDs for large or repeated images.
129
+
130
+ Versioned social responses retain the REST `{data: ...}` envelope in MCP text and structuredContent. List posts accepts `limit` 1–100 (default 20), `offset` 0–10,000, status/platform and ISO date filters. `cancel_social_post` cancels remaining deliveries and retains history; it does not remove remote posts. Updating or deleting history uses the existing owner-scoped hub API and remains blocked for active/published posts. Editing a keyed post does not free its original key; create an intentional new post with a new key.
131
+
132
+ Limits are shared across keys per user: 300 reads and 60 writes per minute. API failures provide structured `error.code`, `error.status`, and optional `error.retryAfter` alongside text. Wait for Retry-After on 429 or request-in-progress conflicts; no automatic retry is performed by the MCP server.
133
+
134
+ Deployment order: deploy the updated web API and Firestore rules, verify the cron worker, then release/use MCP v0.5.0. The package version here is prepared locally; updating source does not publish npm. See [social API documentation](https://codingmantra.com/api-docs/social-media), [OpenAPI contract](https://codingmantra.com/openapi/social-media.json), and repository `docs/social-mcp-api.md`.
109
135
 
110
136
 
111
137
  ## Video parameters
@@ -232,3 +258,16 @@ CODINGMANTRA_API_KEY=your_key node build/index.js
232
258
  ## License
233
259
 
234
260
  Private & Proprietary β€” Part of the CodingMantra Web Portal Β© 2026. All rights reserved.
261
+
262
+ MCP transport note: the server accepts messages up to 24 MB so base64 images within the 20 MB API JSON limit fit with the MCP envelope. Individual clients may impose smaller message limits; use local paths, direct HTTPS URLs, and reusable media IDs in that case. Larger messages exceed the transport bound and can close the connection.
263
+
264
+
265
+ ### Ordered carousels and the Content Library
266
+
267
+ Instagram accepts one image or 2–10 ordered carousel slides. Facebook accepts up to 10 images as a multi-photo feed post; its presentation is controlled by Facebook and may differ from an Instagram slider. Existing single-image requests and `mediaUrl` content remain supported. The first image is the cover. A carousel counts as one published Instagram post; publishing quota is per Instagram account in a rolling 24-hour window, read from Meta's `content_publishing_limit` response rather than assumed globally.
268
+
269
+ `POST /api/v1/social/posts` and MCP `publish_social_post` / `schedule_social_post` accept ordered `images` and optional `mediaLayout`: `original`, `square-fit`, `square-crop`, `portrait-fit`, or `portrait-crop`. Fit preserves the complete image with white padding; crop fills a common canvas. Uploaded media IDs also support canvas conversion. Use the same idempotency key and unchanged body on retry. Post responses include `postType`, `imageCount`, `coverImage`, `processingMessage`, `deliveryNeedsReview`, and per-account publication `permalink`. A queued response confirms acceptance; poll `get_social_post` until delivery completes. When `deliveryNeedsReview` is true, inspect the account before intentionally creating another post; do not automatically retry with a new key.
270
+
271
+ `GET /api/v1/social/library` (API-key authentication) and MCP `list_social_library` return `{ data: { items, total, hasMore } }`. Each item includes `mediaUrl` (cover), ordered `mediaUrls`, `type` (`image`, `carousel`, or `video`), caption, hashtags, usage, and statuses. Filters: `groupBy=post|media` (default `post`), `status=unused|scheduled|published`, `type`, `platform`, raw `accountId` (requires platform), `search`, `offset`, and `limit` (1–100, default 24). `list_social_media` continues to read the generated-image gallery. This library is read-only; reuse creates a new post.
272
+
273
+ The Hub Content Library defaults to Posts, displays slide counts, opens every slide in a navigable preview, and reuses all images in order. Individual media retains the existing deduplicated asset view. The session-authenticated `/api/social/library` defaults to `groupBy=media` for compatibility; the Hub explicitly requests `post`. Saved content POST/PATCH supports `mediaUrls` (1–10), maintains scalar `mediaUrl` as the cover, and stores multi-image records as `carousel`.
@@ -1,6 +1,18 @@
1
1
  import dotenv from 'dotenv';
2
2
  dotenv.config({ quiet: true });
3
3
  const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes for heavy generation tasks
4
+ export class CodingMantraApiError extends Error {
5
+ status;
6
+ code;
7
+ retryAfter;
8
+ constructor(message, status, code, retryAfter) {
9
+ super(message);
10
+ this.status = status;
11
+ this.code = code;
12
+ this.retryAfter = retryAfter;
13
+ this.name = 'CodingMantraApiError';
14
+ }
15
+ }
4
16
  export function getApiKey() {
5
17
  return process.env.CODINGMANTRA_API_KEY;
6
18
  }
@@ -40,15 +52,20 @@ export async function apiClient(endpoint, options = {}) {
40
52
  });
41
53
  }
42
54
  catch (fetchError) {
43
- if (fetchError?.name === 'TimeoutError' || fetchError?.name === 'AbortError') {
44
- throw new Error(`CodingMantra API request timed out after ${DEFAULT_TIMEOUT_MS / 1000}s for ${endpoint}`);
55
+ if (fetchError instanceof Error && (fetchError.name === 'TimeoutError' || fetchError.name === 'AbortError')) {
56
+ const retryAdvice = endpoint === '/v1/social/posts' && options.method === 'POST'
57
+ ? ' Retry the exact body with the same idempotencyKey; do not create a new key.' : '';
58
+ throw new Error(`CodingMantra API request timed out for ${endpoint}.${retryAdvice}`);
45
59
  }
46
- throw new Error(`Failed to connect to CodingMantra API at ${url}: ${fetchError?.message || fetchError}`);
60
+ throw new Error(`Failed to connect to CodingMantra API at ${url}: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`);
47
61
  }
48
62
  if (!response.ok) {
49
63
  let errorMsg = `API request failed (${response.status} ${response.statusText})`;
64
+ let code;
65
+ const text = await response.text();
50
66
  try {
51
- const errorData = await response.json();
67
+ const errorData = JSON.parse(text);
68
+ code = typeof errorData?.error?.code === 'string' ? errorData.error.code : undefined;
52
69
  if (typeof errorData?.error === 'string') {
53
70
  errorMsg += `: ${errorData.error}`;
54
71
  }
@@ -63,9 +80,9 @@ export async function apiClient(endpoint, options = {}) {
63
80
  }
64
81
  }
65
82
  catch {
66
- errorMsg += `: ${await response.text()}`;
83
+ errorMsg += `: ${text.slice(0, 1000)}`;
67
84
  }
68
- throw new Error(errorMsg);
85
+ throw new CodingMantraApiError(errorMsg, response.status, code, response.headers.get('retry-after') || undefined);
69
86
  }
70
87
  return response.json();
71
88
  }
@@ -41,7 +41,7 @@ function getMimeTypeFromExt(filePath) {
41
41
  * reads it and converts to a base64 data URI.
42
42
  * If already a URL or data URI, returns as is.
43
43
  */
44
- export function resolveImageInput(input) {
44
+ export function resolveImageInput(input, maxBytes) {
45
45
  if (!input || typeof input !== 'string')
46
46
  return '';
47
47
  const trimmed = input.trim();
@@ -53,12 +53,16 @@ export function resolveImageInput(input) {
53
53
  try {
54
54
  const resolvedPath = path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
55
55
  if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()) {
56
+ if (maxBytes && fs.statSync(resolvedPath).size > maxBytes)
57
+ throw new RangeError('Source image exceeds the allowed size (10 MB for social API uploads).');
56
58
  const buffer = fs.readFileSync(resolvedPath);
57
59
  const mimeType = getMimeTypeFromExt(resolvedPath);
58
60
  return `data:${mimeType};base64,${buffer.toString('base64')}`;
59
61
  }
60
62
  }
61
- catch {
63
+ catch (error) {
64
+ if (error instanceof RangeError)
65
+ throw error;
62
66
  // If any filesystem read error, fall through
63
67
  }
64
68
  return trimmed;
package/build/index.js CHANGED
@@ -3,13 +3,13 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv";
6
- import { apiClient } from './api-client.js';
6
+ import { apiClient, CodingMantraApiError } from './api-client.js';
7
7
  import { formatImageToolResult, resolveImageInput, resolveImagesArray } from './image-utils.js';
8
8
  const server = new Server({
9
9
  name: "codingmantra-mcp",
10
- version: "0.3.0",
10
+ version: "0.5.0",
11
11
  }, {
12
- instructions: "For social media, start with get_social_workflow and list_social_accounts. Create captions and images, save drafts for review, then schedule or publish according to the user’s request. Use exact connected account IDs and explicit timezone offsets. Only Facebook Pages and Instagram image posts are supported. Keep social provider tokens on the backend.",
12
+ instructions: "For social media, start with get_social_workflow and list_social_accounts. Create captions and images, save drafts for review, then schedule or publish according to the user’s request. Use platform-prefixed account IDs, a stable caller-chosen idempotencyKey for each intended post, and explicit timezone offsets. Publication is asynchronous; poll get_social_post and never claim a queued post is live. Only Facebook Pages and Instagram image posts are supported. Keep social provider tokens on the backend.",
13
13
  capabilities: {
14
14
  tools: {},
15
15
  },
@@ -647,11 +647,11 @@ const EDIT_NANO_BANANA_TOOL = {
647
647
  },
648
648
  model: {
649
649
  type: "string",
650
- description: "Model tier ('flash', 'pro', 'standard'). Default is 'flash' (google/gemini-3.1-flash-image)."
650
+ description: "Model tier ('flash', 'pro', 'standard', 'gemini-nano-banana-2.1', 'hy-image-v3.5-preview'). Default is 'flash' (google/gemini-3.1-flash-image)."
651
651
  },
652
652
  modelId: {
653
653
  type: "string",
654
- description: "The image model ID."
654
+ description: "The image model ID (e.g. 'tencent/hy-image-v3.5-preview', 'google/gemini-nano-banana-2.1', 'google/gemini-3.1-flash-image')."
655
655
  },
656
656
  resolution: {
657
657
  type: "string",
@@ -948,15 +948,16 @@ const CHECK_JOB_STATUS_TOOL = {
948
948
  const SOCIAL_ACCOUNT_SCHEMA = {
949
949
  type: 'object', properties: {
950
950
  platform: { type: 'string', enum: ['facebook', 'instagram'], description: 'Connected social platform.' },
951
- accountId: { type: 'string', minLength: 1, description: 'Exact account ID returned by list_social_accounts.' },
951
+ accountId: { type: 'string', minLength: 1, description: 'Raw platformAccountId returned by list_social_accounts, without the facebook: or instagram: prefix; used only for draft/edit operations.' },
952
952
  accountName: { type: 'string', minLength: 1, description: 'Account display name returned by list_social_accounts.' },
953
953
  }, required: ['platform', 'accountId', 'accountName'],
954
954
  };
955
955
  const SOCIAL_CONTENT_PROPERTIES = {
956
956
  caption: { type: 'string', maxLength: 63206, description: 'Post text. Instagram caption plus hashtags must fit 2200 characters.' },
957
+ mediaLayout: { type: 'string', enum: ['original', 'square-fit', 'square-crop', 'portrait-fit', 'portrait-crop'], description: 'Apply the same canvas to all slides; fit preserves the full image with white padding, crop fills the canvas.' },
957
958
  brandBrief: { type: 'string', maxLength: 4000, description: 'Brand name, audience, voice, offer, facts, and call to action for this post.' },
958
959
  hashtags: { type: 'array', maxItems: 30, items: { type: 'string', description: 'Hashtag with or without a leading #.' }, description: 'Relevant hashtags; duplicates already in the caption are omitted during publication.' },
959
- mediaUrls: { type: 'array', maxItems: 10, items: { type: 'string', description: 'HTTPS image URL, PNG/JPEG/WebP data URI, or local image path.' }, description: 'Images only. Instagram currently requires exactly one image; Facebook accepts up to 10.' },
960
+ mediaUrls: { type: 'array', maxItems: 10, items: { type: 'string', description: 'HTTPS image URL, PNG/JPEG/WebP data URI, or local image path.' }, description: 'Images only. Instagram supports one image or a carousel of 2–10 images; Facebook accepts up to 10.' },
960
961
  };
961
962
  const SOCIAL_SCHEDULE_PROPERTIES = {
962
963
  draftId: { type: 'string', description: 'Optional saved draft ID. Scheduling consumes it atomically; repeating this request returns the same scheduled post.' },
@@ -965,32 +966,57 @@ const SOCIAL_SCHEDULE_PROPERTIES = {
965
966
  content: { type: 'object', properties: SOCIAL_CONTENT_PROPERTIES, required: ['caption'], description: 'Final post caption, hashtags, brand brief, and images.' },
966
967
  scheduledFor: { type: 'string', format: 'date-time', description: 'Future ISO 8601 timestamp with explicit UTC offset, e.g. 2026-10-10T09:00:00+05:30. Resolve the user’s timezone before scheduling.' },
967
968
  };
968
- const SOCIAL_ID = { type: 'string', minLength: 1, pattern: '^[A-Za-z0-9_-]+$', description: 'Exact draft or scheduled post ID returned by a social tool.' };
969
+ const SOCIAL_API_IMAGE = {
970
+ description: 'HTTPS URL, raw base64, data URI, local image path, or explicit image object. Static JPEG/PNG/WebP; 10 MB decoded limit. Uploaded media IDs belong to your API-key owner.',
971
+ oneOf: [
972
+ { type: 'string', minLength: 1, description: 'Public HTTPS URL, raw base64, data URI, or local image file path.' },
973
+ { type: 'object', additionalProperties: false, properties: { url: { type: 'string', minLength: 1, description: 'Direct public HTTPS image URL on port 443; no redirects.' } }, required: ['url'] },
974
+ { type: 'object', additionalProperties: false, properties: { base64: { type: 'string', minLength: 1, description: 'Raw base64 or image data URI.' }, mimeType: { type: 'string', enum: ['image/jpeg', 'image/png', 'image/webp'], description: 'Optional declared type; must match actual bytes.' } }, required: ['base64'] },
975
+ { type: 'object', additionalProperties: false, properties: { path: { type: 'string', minLength: 1, description: 'Local JPEG/PNG/WebP image file to upload.' } }, required: ['path'] },
976
+ { type: 'object', additionalProperties: false, properties: { mediaId: { type: 'string', format: 'uuid', description: 'Reusable UUID returned by upload_social_image.' } }, required: ['mediaId'] },
977
+ ],
978
+ };
979
+ const SOCIAL_POST_PROPERTIES = {
980
+ accountIds: { description: 'One or up to 20 distinct IDs from list_social_accounts: facebook:<Meta Page ID> or instagram:<Meta account ID>.', oneOf: [
981
+ { type: 'string', pattern: '^(facebook|instagram):[0-9]+$', description: 'One platform-prefixed connected account ID.' },
982
+ { type: 'array', minItems: 1, maxItems: 20, uniqueItems: true, items: { type: 'string', pattern: '^(facebook|instagram):[0-9]+$', description: 'Platform-prefixed connected account ID.' } },
983
+ ] },
984
+ mediaLayout: SOCIAL_CONTENT_PROPERTIES.mediaLayout,
985
+ caption: SOCIAL_CONTENT_PROPERTIES.caption,
986
+ hashtags: SOCIAL_CONTENT_PROPERTIES.hashtags,
987
+ images: { type: 'array', maxItems: 10, items: SOCIAL_API_IMAGE, description: 'Image inputs; Instagram requires 1–10 images (single image or carousel). Use reusable mediaId for large images.' },
988
+ idempotencyKey: { type: 'string', minLength: 8, maxLength: 128, pattern: '^[A-Za-z0-9._:-]+$', description: 'Caller-chosen key for this intended post. Preserve the exact key and body on retries; a new key means a new post. Never invent a new key after a timeout.' },
989
+ };
990
+ const SOCIAL_ID = { type: 'string', minLength: 1, pattern: '^[A-Za-z0-9_-]+$', description: 'Exact draft, media UUID or post ID returned by a social tool.' };
969
991
  const socialRead = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
970
992
  const socialWrite = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
971
993
  const socialDelete = { ...socialWrite, destructiveHint: true, idempotentHint: true };
972
994
  const SOCIAL_TOOLS = [
973
995
  { name: 'get_social_workflow', description: 'Read the social workflow, capabilities, timezone guidance, and platform restrictions before planning content or posting. Read-only; no API call.', annotations: { ...socialRead, openWorldHint: false }, inputSchema: { type: 'object', properties: {} } },
974
- { name: 'list_social_accounts', description: 'List connected Facebook Pages and Instagram accounts with IDs, names, permissions, and reconnect status. Read-only; OAuth tokens are never returned. Connect accounts in the web hub first.', annotations: socialRead, inputSchema: { type: 'object', properties: {} } },
996
+ { name: 'list_social_accounts', description: 'Read connected Facebook Pages and Instagram accounts with stable platform-prefixed IDs, raw platformAccountId, names, capabilities and reconnect status. No provider tokens. Connect in the web hub first.', annotations: socialRead, inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
975
997
  { name: 'generate_social_caption', description: 'Synchronously create or refine a caption and hashtags from a brief or images. Uses wallet credits and saved brand context unless brandBrief is supplied. Does not publish. Include verified facts, audience, tone, and CTA in the brief; review the result.', annotations: socialWrite, inputSchema: { type: 'object', properties: { ...SOCIAL_CONTENT_PROPERTIES, platform: { type: 'string', enum: ['facebook', 'instagram'], description: 'Target platform; defaults to Instagram.' } } } },
976
998
  { name: 'list_social_drafts', description: 'Read saved drafts, including captions, images, account selections, and proposed dates. Returns 20 per page; use offset for pagination.', annotations: socialRead, inputSchema: { type: 'object', properties: { offset: { type: 'integer', minimum: 0, description: 'Number of drafts to skip.' } } } },
977
999
  { name: 'save_social_draft', description: 'Save a new draft or replace an existing draft by id. Does not queue or publish. Send the complete draft when editing; omitted fields use API defaults. Local images are uploaded and persisted.', annotations: socialWrite, inputSchema: { type: 'object', properties: { ...SOCIAL_CONTENT_PROPERTIES, id: SOCIAL_ID, platforms: { type: 'array', items: SOCIAL_ACCOUNT_SCHEMA, description: 'Optional target accounts; drafts may have no accounts yet.' }, scheduledFor: { type: ['string', 'null'], format: 'date-time', description: 'Optional proposed date with explicit offset; saving does not schedule.' }, socialContentId: { type: 'string', description: 'Optional media library content ID.' } } } },
978
1000
  { name: 'delete_social_draft', description: 'Delete one saved draft belonging to the API-key owner. Does not delete a scheduled or published post.', annotations: socialDelete, inputSchema: { type: 'object', properties: { id: SOCIAL_ID }, required: ['id'] } },
1001
+ { name: 'list_social_library', description: 'Read reusable saved content and scheduled/published posts with ordered slides. Defaults to grouped posts; groupBy media returns individual assets. Read-only.', annotations: socialRead, inputSchema: { type: 'object', properties: { groupBy: { type: 'string', enum: ['post', 'media'] }, status: { type: 'string', enum: ['unused', 'scheduled', 'published'] }, type: { type: 'string', enum: ['image', 'carousel', 'video'] }, platform: { type: 'string', enum: ['facebook', 'instagram'] }, accountId: { type: 'string', description: 'Raw platform account ID; requires platform.' }, search: { type: 'string', maxLength: 200 }, offset: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 100 } } } },
979
1002
  { name: 'list_social_media', description: 'Read generated images from your gallery for use in posts. Returns public media URLs; use existing image generation tools to create new visuals.', annotations: socialRead, inputSchema: { type: 'object', properties: { page: { type: 'integer', minimum: 1, description: 'Gallery page (default 1).' }, limit: { type: 'integer', minimum: 1, maximum: 50, description: 'Images per page (default 24).' } } } },
980
- { name: 'schedule_social_post', description: 'Queue a final post for automatic future publication to connected accounts. Returns a scheduled post ID. ISO timestamps require a UTC offset; the server scheduler publishes even when this assistant is offline. Use draftId to avoid duplicate draft scheduling.', annotations: socialWrite, inputSchema: { type: 'object', properties: SOCIAL_SCHEDULE_PROPERTIES, required: ['platforms', 'content', 'scheduledFor'] } },
1003
+ { name: 'upload_social_image', description: 'Synchronously upload and normalize an image from HTTPS URL, base64, data URI, or local file. Returns an owner-scoped reusable media ID and JPEG URL; does not publish or generate AI content. Source max 10 MB/40 million pixels; request max 20 MB.', annotations: socialWrite, inputSchema: { type: 'object', additionalProperties: false, properties: { image: { ...SOCIAL_API_IMAGE, oneOf: SOCIAL_API_IMAGE.oneOf.slice(0, 4) } }, required: ['image'] } },
1004
+ { name: 'get_social_image', description: 'Read prepared image metadata by reusable media UUID. Read-only and scoped to your API-key owner.', annotations: socialRead, inputSchema: { type: 'object', additionalProperties: false, properties: { id: { type: 'string', format: 'uuid', description: 'Media UUID returned by upload_social_image.' } }, required: ['id'] } },
1005
+ { name: 'schedule_social_draft', description: 'Asynchronously queue an approved saved hub draft using the existing atomic draft-consumption workflow. Requires draftId and complete final platforms/content/scheduledFor. Reusing draftId returns the original schedule; do not use for new standalone posts. Use raw platformAccountId in platforms.', annotations: { ...socialWrite, idempotentHint: true }, inputSchema: { type: 'object', additionalProperties: false, properties: SOCIAL_SCHEDULE_PROPERTIES, required: ['draftId', 'platforms', 'content', 'scheduledFor'] } },
1006
+ { name: 'schedule_social_post', description: 'Asynchronously queue future delivery to one or multiple owned accounts. Requires a future ISO timestamp with an explicit offset and an idempotencyKey. Acceptance is not publication; poll get_social_post for per-account results. Does not consume a saved draft.', annotations: { ...socialWrite, idempotentHint: true }, inputSchema: { type: 'object', additionalProperties: false, properties: { ...SOCIAL_POST_PROPERTIES, scheduledFor: SOCIAL_SCHEDULE_PROPERTIES.scheduledFor }, required: ['accountIds', 'idempotencyKey', 'scheduledFor'] } },
981
1007
  { name: 'list_social_posts', description: 'Read scheduled, published, failed, or cancelled posts. Filter by platform, status, or date range; use limit/offset for pagination. Dates must include a UTC offset.', annotations: socialRead, inputSchema: { type: 'object', properties: {
982
1008
  status: { type: 'string', enum: ['scheduled', 'published', 'failed', 'cancelled'], description: 'Post status filter.' },
983
1009
  platform: { type: 'string', enum: ['facebook', 'instagram'], description: 'Platform filter.' },
984
1010
  fromDate: { type: 'string', format: 'date-time', description: 'Inclusive start timestamp with offset.' },
985
1011
  toDate: { type: 'string', format: 'date-time', description: 'Inclusive end timestamp with offset.' },
986
- limit: { type: 'integer', minimum: 1, maximum: 500, description: 'Page size, default 50.' },
987
- offset: { type: 'integer', minimum: 0, description: 'Number of posts to skip.' },
1012
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Page size, default 20.' },
1013
+ offset: { type: 'integer', minimum: 0, maximum: 10000, description: 'Number of posts to skip.' },
988
1014
  } } },
989
- { name: 'get_social_post', description: 'Read one scheduled post, including status, error, retries, and published job IDs. Check this before retrying a failed or ambiguous publication.', annotations: socialRead, inputSchema: { type: 'object', properties: { id: SOCIAL_ID }, required: ['id'] } },
1015
+ { name: 'get_social_post', description: 'Read one scheduled post, including overall and per-account delivery status, retries, errors, job IDs and providerPostId receipts. Check this before retrying a failed or ambiguous publication.', annotations: socialRead, inputSchema: { type: 'object', properties: { id: SOCIAL_ID }, required: ['id'] } },
990
1016
  { name: 'update_social_post', description: 'Edit a pending schedule, content, or target accounts. Provide a new future scheduledFor to reschedule or retry a failed post. The API rejects changes to posts that are publishing or already published.', annotations: { ...socialWrite, idempotentHint: true }, inputSchema: { type: 'object', properties: { id: SOCIAL_ID, content: { type: 'object', properties: SOCIAL_CONTENT_PROPERTIES, description: 'Fields to change; omitted fields retain their existing values.' }, platforms: SOCIAL_SCHEDULE_PROPERTIES.platforms, scheduledFor: SOCIAL_SCHEDULE_PROPERTIES.scheduledFor }, required: ['id'] } },
991
- { name: 'cancel_social_post', description: 'Cancel a queued post so it will not publish. The API rejects cancellation while publishing or after publication. Does not remove an already published platform post.', annotations: socialDelete, inputSchema: { type: 'object', properties: { id: SOCIAL_ID }, required: ['id'] } },
1017
+ { name: 'cancel_social_post', description: 'Cancel outstanding deliveries and retain the record. Successful targets remain published; pending targets are cancelled. The API rejects cancellation while publishing or after publication. Does not remove an already published platform post.', annotations: socialDelete, inputSchema: { type: 'object', properties: { id: SOCIAL_ID }, required: ['id'] } },
992
1018
  { name: 'delete_social_post', description: 'Remove a schedule record. The API prevents deletion during publishing or after publication. Does not delete a live Facebook or Instagram post.', annotations: socialDelete, inputSchema: { type: 'object', properties: { id: SOCIAL_ID }, required: ['id'] } },
993
- { name: 'publish_social_post', description: 'Publish immediately to one connected Facebook Page or Instagram account when the user requests posting now. Returns the platform publication result. Images only; Instagram requires exactly one image. Avoid blind retries after timeouts because a post may already be live.', annotations: { ...socialWrite, destructiveHint: true }, inputSchema: { type: 'object', properties: { ...SOCIAL_CONTENT_PROPERTIES, platform: { type: 'string', enum: ['facebook', 'instagram'], description: 'Platform to publish to.' }, accountId: { type: 'string', minLength: 1, description: 'Exact connected account ID from list_social_accounts.' }, socialContentId: { type: 'string', description: 'Optional media library content ID for publication tracking.' }, link: { type: 'string', description: 'Optional HTTPS link for a Facebook post.' } }, required: ['platform', 'accountId', 'caption'] } },
1019
+ { name: 'publish_social_post', description: 'Asynchronously queue immediate delivery to one or multiple owned Facebook/Instagram accounts. Requires idempotencyKey; identical retries return the original post. HTTP acceptance does not mean live publication. Poll get_social_post; remote response loss may still be ambiguous.', annotations: { ...socialWrite, destructiveHint: true, idempotentHint: true }, inputSchema: { type: 'object', additionalProperties: false, properties: SOCIAL_POST_PROPERTIES, required: ['accountIds', 'idempotencyKey'] } },
994
1020
  { name: 'get_social_analytics', description: 'Read recorded publishing totals, reach, engagement, top posts, and posting activity. Metrics reflect stored provider data and may be incomplete; these are not guaranteed optimal posting times.', annotations: socialRead, inputSchema: { type: 'object', properties: { range: { type: 'string', enum: ['7d', '30d', '90d'], description: 'Reporting range, default 30d.' }, platform: { type: 'string', enum: ['facebook', 'instagram'], description: 'Optional platform filter.' }, accountId: { type: 'string', description: 'Optional connected account filter.' } } } },
995
1021
  ];
996
1022
  const socialSchemaValidator = new AjvJsonSchemaValidator();
@@ -1021,6 +1047,17 @@ function socialImage(value) {
1021
1047
  catch { /* invalid source */ }
1022
1048
  throw new Error('Use an HTTPS image URL or a local PNG, JPEG, or WebP file.');
1023
1049
  }
1050
+ function socialApiImage(value) {
1051
+ if (typeof value === 'string')
1052
+ return resolveImageInput(value, 10 * 1024 * 1024);
1053
+ if (value && typeof value === 'object' && 'path' in value) {
1054
+ const image = socialImage(resolveImageInput(String(value.path), 10 * 1024 * 1024));
1055
+ if (!image.startsWith('data:'))
1056
+ throw new Error('The local image path must point to an existing JPEG, PNG or WebP file.');
1057
+ return image;
1058
+ }
1059
+ return value;
1060
+ }
1024
1061
  function socialQuery(args, allowed) {
1025
1062
  const query = new URLSearchParams();
1026
1063
  for (const key of allowed)
@@ -1054,25 +1091,32 @@ async function executeSocialTool(name, rawArgs) {
1054
1091
  }
1055
1092
  args.content = content;
1056
1093
  }
1094
+ if (Array.isArray(args.images))
1095
+ args.images = args.images.map(socialApiImage);
1057
1096
  let result;
1058
1097
  if (name === 'get_social_workflow') {
1059
1098
  result = {
1060
1099
  supportedPlatforms: ['facebook', 'instagram'],
1061
1100
  connectUrl: 'https://codingmantra.com/tools/digital-marketing/social-media-hub',
1062
- steps: ['list_social_accounts', 'generate_social_caption or write a caption with your assistant', 'generate an image with existing MCP tools or list_social_media', 'save_social_draft for review', 'schedule_social_post or publish_social_post as requested', 'list_social_posts / get_social_post / get_social_analytics'],
1063
- limits: { instagram: { images: 1, captionIncludingHashtags: 2200 }, facebook: { maxImages: 10, captionIncludingHashtags: 63206 }, videoPublishing: false },
1101
+ steps: ['list_social_accounts', 'generate_social_caption or write a caption with your assistant', 'generate an image, list_social_media, or upload_social_image for a reusable mediaId', 'save_social_draft for review', 'schedule_social_post or publish_social_post as requested; schedule_social_draft for an approved saved draft', 'list_social_posts / get_social_post / get_social_analytics'],
1102
+ limits: { instagram: { minImages: 1, maxImages: 10, carousel: true, captionIncludingHashtags: 2200 }, facebook: { minImages: 0, maxImages: 10, multiPhoto: true, captionIncludingHashtags: 63206 }, videoPublishing: false },
1064
1103
  timezone: 'Ask for the user’s intended timezone when ambiguous; convert to an ISO 8601 timestamp with an explicit UTC offset. Dates are returned as ISO 8601 UTC timestamps.',
1065
1104
  contentGuidance: 'Use a clear hook, useful verified facts, the intended audience and brand voice, and one CTA. Supply brandBrief to generate_social_caption for per-brand context. Never invent prices, claims, discounts, or business details. Caption generation uses credits; drafts and schedules do not run AI.',
1066
- scheduling: 'The deployed backend requires its cron scheduler to run. The assistant does not need to remain online. Saved drafts are consumed atomically; reuse draftId to make repeated scheduling requests safe.',
1067
- retries: 'Inspect failed post errors and publishedJobIds before rescheduling. Immediate publication has no idempotency guarantee; do not automatically retry ambiguous timeouts.',
1105
+ accountIds: 'Use accounts[].id for publishing/scheduling (facebook:123 or instagram:456); draft/edit/analytics tools use the raw platformAccountId plus platform.',
1106
+ apiDocsUrl: 'https://codingmantra.com/api-docs/social-media',
1107
+ openApiUrl: 'https://codingmantra.com/openapi/social-media.json',
1108
+ rateLimits: { readsPerMinute: 300, writesPerMinute: 60, scope: 'per user, shared across API keys; respect Retry-After on 429' },
1109
+ scheduling: 'The deployed backend requires its cron scheduler to run. Both immediate and future delivery are asynchronous. Poll get_social_post for per-account receipts. Saving a draft does not publish. schedule_social_post copies standalone content; schedule_social_draft atomically consumes an approved saved draft and reuses draftId for safe retries.',
1110
+ retries: 'Preserve idempotencyKey and the exact request body for retries. Same-key retries return the original record; conflicting content returns 409, removed history returns 410. Do not generate a new key after a timeout. Provider acceptance followed by response loss can still be ambiguous; inspect the hub and provider history before rescheduling.',
1068
1111
  };
1069
1112
  }
1070
1113
  else {
1071
1114
  let endpoint = '';
1072
1115
  let method = 'GET';
1073
1116
  let body;
1117
+ const headers = {};
1074
1118
  if (name === 'list_social_accounts')
1075
- endpoint = '/social/meta/connection';
1119
+ endpoint = '/v1/social/accounts';
1076
1120
  if (name === 'generate_social_caption') {
1077
1121
  endpoint = '/social/caption';
1078
1122
  method = 'POST';
@@ -1089,55 +1133,53 @@ async function executeSocialTool(name, rawArgs) {
1089
1133
  endpoint = `/social/drafts?id=${socialId(args)}`;
1090
1134
  method = 'DELETE';
1091
1135
  }
1136
+ if (name === 'list_social_library')
1137
+ endpoint = '/v1/social/library' + socialQuery(args, ['groupBy', 'status', 'type', 'platform', 'accountId', 'search', 'offset', 'limit']);
1092
1138
  if (name === 'list_social_media')
1093
1139
  endpoint = '/social/gallery' + socialQuery({ ...args, type: 'image' }, ['type', 'page', 'limit']);
1094
- if (name === 'schedule_social_post') {
1140
+ if (name === 'schedule_social_draft') {
1095
1141
  endpoint = '/social/schedule';
1096
1142
  method = 'POST';
1097
1143
  body = args;
1098
1144
  }
1145
+ if (name === 'upload_social_image') {
1146
+ endpoint = '/v1/social/media';
1147
+ method = 'POST';
1148
+ body = socialApiImage(args.image);
1149
+ }
1150
+ if (name === 'get_social_image')
1151
+ endpoint = `/v1/social/media/${socialId(args)}`;
1152
+ if (name === 'schedule_social_post' || name === 'publish_social_post') {
1153
+ endpoint = '/v1/social/posts';
1154
+ method = 'POST';
1155
+ const { idempotencyKey, ...post } = args;
1156
+ headers['Idempotency-Key'] = String(idempotencyKey);
1157
+ body = post;
1158
+ }
1099
1159
  if (name === 'list_social_posts')
1100
- endpoint = '/social/schedule' + socialQuery(args, ['status', 'platform', 'fromDate', 'toDate', 'limit', 'offset']);
1101
- if (['get_social_post', 'update_social_post', 'cancel_social_post', 'delete_social_post'].includes(name)) {
1160
+ endpoint = '/v1/social/posts' + socialQuery(args, ['status', 'platform', 'fromDate', 'toDate', 'limit', 'offset']);
1161
+ if (['update_social_post', 'delete_social_post'].includes(name)) {
1102
1162
  endpoint = `/social/schedule/${socialId(args)}`;
1103
1163
  if (name === 'update_social_post') {
1104
1164
  method = 'PATCH';
1105
1165
  const { id: _id, ...updates } = args;
1106
1166
  body = updates;
1107
1167
  }
1108
- if (name === 'cancel_social_post') {
1109
- method = 'PATCH';
1110
- body = { status: 'cancelled' };
1111
- }
1112
1168
  if (name === 'delete_social_post')
1113
1169
  method = 'DELETE';
1114
1170
  }
1115
- if (name === 'publish_social_post') {
1116
- endpoint = '/social/meta/publish';
1117
- method = 'POST';
1118
- const caption = typeof args.caption === 'string' ? args.caption.trim() : '';
1119
- if (args.hashtags !== undefined && (!Array.isArray(args.hashtags) || args.hashtags.some(tag => typeof tag !== 'string' || !/^#?[\p{L}\p{N}_]+$/u.test(tag))))
1120
- throw new Error('Use valid hashtags containing letters, numbers, or underscores.');
1121
- const existing = new Set((caption.match(/#[\p{L}\p{N}_]+/gu) || []).map(tag => tag.toLowerCase()));
1122
- const tags = (args.hashtags || []).map(tag => `#${tag.replace(/^#/, '')}`).filter(tag => {
1123
- if (existing.has(tag.toLowerCase()))
1124
- return false;
1125
- existing.add(tag.toLowerCase());
1126
- return true;
1127
- });
1128
- const text = [caption, tags.join(' ')].filter(Boolean).join('\n\n');
1129
- const images = args.mediaUrls || [];
1130
- if (!['facebook', 'instagram'].includes(String(args.platform)))
1131
- throw new Error('Choose Facebook or Instagram.');
1132
- if (args.platform === 'instagram' && images.length !== 1)
1133
- throw new Error('Instagram requires exactly one image.');
1134
- body = args.platform === 'instagram'
1135
- ? { platform: 'instagram', instagramId: args.accountId, caption: text, imageDataUrl: images[0]?.startsWith('data:') ? images[0] : undefined, imageUrl: images[0]?.startsWith('https:') ? images[0] : undefined, socialContentId: args.socialContentId }
1136
- : { platform: 'facebook', pageId: args.accountId, message: text, imageUrls: images.length ? images : undefined, link: args.link, socialContentId: args.socialContentId };
1171
+ if (name === 'get_social_post' || name === 'cancel_social_post') {
1172
+ endpoint = `/v1/social/posts/${socialId(args)}`;
1173
+ if (name === 'cancel_social_post')
1174
+ method = 'DELETE';
1137
1175
  }
1138
1176
  if (name === 'get_social_analytics')
1139
1177
  endpoint = '/social/analytics' + socialQuery(args, ['range', 'platform', 'accountId']);
1140
- result = await apiClient(endpoint, { method, ...(body ? { body: JSON.stringify(body) } : {}) });
1178
+ const requestBody = body !== undefined ? JSON.stringify(body) : undefined;
1179
+ if (endpoint.startsWith('/v1/social/') && requestBody && Buffer.byteLength(requestBody, 'utf8') > 20 * 1024 * 1024) {
1180
+ throw new Error('Social API JSON requests must be 20 MB or smaller. Upload images separately and reuse media IDs.');
1181
+ }
1182
+ result = await apiClient(endpoint, { method, headers, signal: AbortSignal.timeout(300_000), ...(requestBody !== undefined ? { body: requestBody } : {}) });
1141
1183
  }
1142
1184
  result = normalizeSocialDates(result);
1143
1185
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result, ...(result.success === false ? { isError: true } : {}) };
@@ -1539,17 +1581,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1539
1581
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1540
1582
  }
1541
1583
  catch (error) {
1584
+ const message = error instanceof Error ? error.message : String(error);
1542
1585
  return {
1543
- content: [{ type: "text", text: `❌ **Error:** ${error.message || error}` }],
1586
+ ...(error instanceof CodingMantraApiError ? { structuredContent: { error: { code: error.code, message, status: error.status, retryAfter: error.retryAfter } } } : {}),
1587
+ content: [{ type: "text", text: `❌ **Error:** ${message}` }],
1544
1588
  isError: true,
1545
1589
  };
1546
1590
  }
1547
1591
  });
1548
1592
  // ─── Server Startup ─────────────────────────────────────────────────────────────
1549
1593
  async function run() {
1550
- const transport = new StdioServerTransport();
1594
+ // A 10 MB source becomes ~13.4 MB in base64. Leave room for the 20 MB
1595
+ // API JSON limit plus the MCP request envelope, while still bounding input.
1596
+ const transport = new StdioServerTransport(process.stdin, process.stdout, { maxBufferSize: 24 * 1024 * 1024 });
1551
1597
  await server.connect(transport);
1552
- console.error("[CodingMantra MCP] Server v0.3.0 initialized and running on stdio (43 tools active)");
1598
+ console.error("[CodingMantra MCP] Server v0.5.0 initialized and running on stdio (47 tools active)");
1553
1599
  }
1554
1600
  run().catch((error) => {
1555
1601
  console.error("Fatal error running CodingMantra MCP server:", error);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@codingmantra/mcp-server",
3
- "version": "0.3.0",
4
- "description": "Official Model Context Protocol (MCP) Server for CodingMantra AI Generation Tools",
3
+ "version": "0.5.0",
4
+ "description": "Official MCP server for CodingMantra AI tools and social media publishing and scheduling",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
7
7
  "bin": {