postproxy-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +635 -0
  3. package/dist/api/client.d.ts +71 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +432 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/auth/credentials.d.ts +19 -0
  8. package/dist/auth/credentials.d.ts.map +1 -0
  9. package/dist/auth/credentials.js +40 -0
  10. package/dist/auth/credentials.js.map +1 -0
  11. package/dist/index.d.ts +6 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +44 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/server.d.ts +162 -0
  16. package/dist/server.d.ts.map +1 -0
  17. package/dist/server.js +220 -0
  18. package/dist/server.js.map +1 -0
  19. package/dist/setup-cli.d.ts +6 -0
  20. package/dist/setup-cli.d.ts.map +1 -0
  21. package/dist/setup-cli.js +10 -0
  22. package/dist/setup-cli.js.map +1 -0
  23. package/dist/setup.d.ts +8 -0
  24. package/dist/setup.d.ts.map +1 -0
  25. package/dist/setup.js +143 -0
  26. package/dist/setup.js.map +1 -0
  27. package/dist/tools/accounts.d.ts +11 -0
  28. package/dist/tools/accounts.d.ts.map +1 -0
  29. package/dist/tools/accounts.js +53 -0
  30. package/dist/tools/accounts.js.map +1 -0
  31. package/dist/tools/auth.d.ts +11 -0
  32. package/dist/tools/auth.d.ts.map +1 -0
  33. package/dist/tools/auth.js +35 -0
  34. package/dist/tools/auth.js.map +1 -0
  35. package/dist/tools/history.d.ts +13 -0
  36. package/dist/tools/history.d.ts.map +1 -0
  37. package/dist/tools/history.js +79 -0
  38. package/dist/tools/history.js.map +1 -0
  39. package/dist/tools/post.d.ts +44 -0
  40. package/dist/tools/post.d.ts.map +1 -0
  41. package/dist/tools/post.js +251 -0
  42. package/dist/tools/post.js.map +1 -0
  43. package/dist/tools/profiles.d.ts +11 -0
  44. package/dist/tools/profiles.d.ts.map +1 -0
  45. package/dist/tools/profiles.js +52 -0
  46. package/dist/tools/profiles.js.map +1 -0
  47. package/dist/types/index.d.ts +147 -0
  48. package/dist/types/index.d.ts.map +1 -0
  49. package/dist/types/index.js +5 -0
  50. package/dist/types/index.js.map +1 -0
  51. package/dist/utils/errors.d.ts +21 -0
  52. package/dist/utils/errors.d.ts.map +1 -0
  53. package/dist/utils/errors.js +33 -0
  54. package/dist/utils/errors.js.map +1 -0
  55. package/dist/utils/idempotency.d.ts +8 -0
  56. package/dist/utils/idempotency.d.ts.map +1 -0
  57. package/dist/utils/idempotency.js +23 -0
  58. package/dist/utils/idempotency.js.map +1 -0
  59. package/dist/utils/logger.d.ts +20 -0
  60. package/dist/utils/logger.d.ts.map +1 -0
  61. package/dist/utils/logger.js +68 -0
  62. package/dist/utils/logger.js.map +1 -0
  63. package/dist/utils/validation.d.ts +555 -0
  64. package/dist/utils/validation.d.ts.map +1 -0
  65. package/dist/utils/validation.js +145 -0
  66. package/dist/utils/validation.js.map +1 -0
  67. package/package.json +39 -0
  68. package/src/api/client.ts +497 -0
  69. package/src/auth/credentials.ts +43 -0
  70. package/src/index.ts +57 -0
  71. package/src/server.ts +235 -0
  72. package/src/setup-cli.ts +11 -0
  73. package/src/setup.ts +187 -0
  74. package/src/tools/auth.ts +45 -0
  75. package/src/tools/history.ts +89 -0
  76. package/src/tools/post.ts +338 -0
  77. package/src/tools/profiles.ts +69 -0
  78. package/src/types/index.ts +161 -0
  79. package/src/utils/errors.ts +38 -0
  80. package/src/utils/idempotency.ts +31 -0
  81. package/src/utils/logger.ts +75 -0
  82. package/src/utils/validation.ts +171 -0
  83. package/tsconfig.json +19 -0
  84. package/worker/index.ts +901 -0
  85. package/wrangler.toml +11 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Validation schemas using Zod
3
+ */
4
+ import { z } from "zod";
5
+ /**
6
+ * Schema for ISO 8601 date strings
7
+ */
8
+ export const ISO8601DateSchema = z.string().refine((date) => {
9
+ const d = new Date(date);
10
+ return !isNaN(d.getTime()) && date.includes("T");
11
+ }, {
12
+ message: "Must be a valid ISO 8601 date string (e.g., 2024-01-01T12:00:00Z)",
13
+ });
14
+ /**
15
+ * Schema for URL strings
16
+ */
17
+ export const URLSchema = z.string().url({
18
+ message: "Must be a valid URL",
19
+ });
20
+ /**
21
+ * Check if a string looks like a file path
22
+ */
23
+ export function isFilePath(value) {
24
+ // Absolute paths, relative paths, or home directory paths
25
+ return (value.startsWith("/") ||
26
+ value.startsWith("./") ||
27
+ value.startsWith("../") ||
28
+ value.startsWith("~/") ||
29
+ // Windows absolute paths
30
+ /^[A-Za-z]:[\\/]/.test(value));
31
+ }
32
+ /**
33
+ * Schema for media items (URLs or file paths)
34
+ */
35
+ export const MediaItemSchema = z.string().refine((value) => {
36
+ // Allow file paths
37
+ if (isFilePath(value)) {
38
+ return true;
39
+ }
40
+ // Or valid URLs
41
+ try {
42
+ new URL(value);
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }, {
49
+ message: "Must be a valid URL or file path",
50
+ });
51
+ /**
52
+ * Platform-specific validation schemas
53
+ */
54
+ // Instagram parameters validation
55
+ export const InstagramParamsSchema = z.object({
56
+ format: z.enum(["post", "reel", "story"], {
57
+ errorMap: () => ({ message: "Instagram format must be 'post', 'reel', or 'story'" }),
58
+ }).optional(),
59
+ collaborators: z.array(z.string()).max(10, {
60
+ message: "Instagram allows up to 10 collaborators for posts, 3 for reels",
61
+ }).optional(),
62
+ first_comment: z.string().optional(),
63
+ cover_url: URLSchema.optional(),
64
+ audio_name: z.string().optional(),
65
+ trial_strategy: z.enum(["MANUAL", "SS_PERFORMANCE"], {
66
+ errorMap: () => ({ message: "Instagram trial_strategy must be 'MANUAL' or 'SS_PERFORMANCE'" }),
67
+ }).optional(),
68
+ thumb_offset: z.string().optional(),
69
+ }).strict();
70
+ // YouTube parameters validation
71
+ export const YouTubeParamsSchema = z.object({
72
+ title: z.string().optional(),
73
+ privacy_status: z.enum(["public", "unlisted", "private"], {
74
+ errorMap: () => ({ message: "YouTube privacy_status must be 'public', 'unlisted', or 'private'" }),
75
+ }).optional(),
76
+ cover_url: URLSchema.optional(),
77
+ }).strict();
78
+ // TikTok parameters validation
79
+ export const TikTokParamsSchema = z.object({
80
+ privacy_status: z.enum([
81
+ "PUBLIC_TO_EVERYONE",
82
+ "MUTUAL_FOLLOW_FRIENDS",
83
+ "FOLLOWER_OF_CREATOR",
84
+ "SELF_ONLY"
85
+ ], {
86
+ errorMap: () => ({ message: "TikTok privacy_status must be one of: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY" }),
87
+ }).optional(),
88
+ photo_cover_index: z.number().int().nonnegative({
89
+ message: "TikTok photo_cover_index must be a non-negative integer",
90
+ }).optional(),
91
+ auto_add_music: z.boolean().optional(),
92
+ made_with_ai: z.boolean().optional(),
93
+ disable_comment: z.boolean().optional(),
94
+ disable_duet: z.boolean().optional(),
95
+ disable_stitch: z.boolean().optional(),
96
+ brand_content_toggle: z.boolean().optional(),
97
+ brand_organic_toggle: z.boolean().optional(),
98
+ }).strict();
99
+ // Facebook parameters validation
100
+ export const FacebookParamsSchema = z.object({
101
+ format: z.enum(["post", "story"], {
102
+ errorMap: () => ({ message: "Facebook format must be 'post' or 'story'" }),
103
+ }).optional(),
104
+ first_comment: z.string().optional(),
105
+ page_id: z.string().optional(),
106
+ }).strict();
107
+ // LinkedIn parameters validation
108
+ export const LinkedInParamsSchema = z.object({
109
+ organization_id: z.string().optional(),
110
+ }).strict();
111
+ // Twitter/X and Threads don't have platform-specific parameters
112
+ export const TwitterParamsSchema = z.object({}).strict();
113
+ export const ThreadsParamsSchema = z.object({}).strict();
114
+ // Combined platform parameters schema
115
+ export const PlatformParamsSchema = z.object({
116
+ instagram: InstagramParamsSchema.optional(),
117
+ youtube: YouTubeParamsSchema.optional(),
118
+ tiktok: TikTokParamsSchema.optional(),
119
+ facebook: FacebookParamsSchema.optional(),
120
+ linkedin: LinkedInParamsSchema.optional(),
121
+ twitter: TwitterParamsSchema.optional(),
122
+ threads: ThreadsParamsSchema.optional(),
123
+ }).strict().optional();
124
+ /**
125
+ * Schema for post.publish parameters
126
+ */
127
+ export const PostPublishSchema = z.object({
128
+ content: z.string().min(1, "Content cannot be empty"),
129
+ targets: z.array(z.string()).min(1, "At least one target is required"),
130
+ schedule: ISO8601DateSchema.optional(),
131
+ media: z.array(MediaItemSchema).optional(),
132
+ idempotency_key: z.string().optional(),
133
+ require_confirmation: z.boolean().optional(),
134
+ draft: z.boolean().optional(),
135
+ platforms: PlatformParamsSchema,
136
+ });
137
+ /**
138
+ * Validate that a schedule date is in the future
139
+ */
140
+ export function validateScheduleInFuture(schedule) {
141
+ const scheduleDate = new Date(schedule);
142
+ const now = new Date();
143
+ return scheduleDate > now;
144
+ }
145
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "postproxy-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for PostProxy API integration with Claude Code",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "postproxy-mcp": "./dist/index.js",
9
+ "postproxy-mcp-setup": "./dist/setup-cli.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsc --watch",
14
+ "dev:worker": "wrangler dev",
15
+ "deploy": "wrangler deploy",
16
+ "prepare": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "postproxy",
21
+ "claude",
22
+ "social-media"
23
+ ],
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.0.0",
27
+ "workers-mcp": "0.1.0-3",
28
+ "zod": "^3.22.4"
29
+ },
30
+ "devDependencies": {
31
+ "@cloudflare/workers-types": "^4.20250124.0",
32
+ "@types/node": "^20.10.0",
33
+ "typescript": "^5.3.3",
34
+ "wrangler": "^4.61.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ }
39
+ }
@@ -0,0 +1,497 @@
1
+ /**
2
+ * PostProxy API HTTP client
3
+ */
4
+
5
+ import { readFileSync } from "node:fs";
6
+ import { basename } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import type {
9
+ ProfileGroup,
10
+ Profile,
11
+ CreatePostParams,
12
+ CreatePostResponse,
13
+ PostDetails,
14
+ Post,
15
+ } from "../types/index.js";
16
+ import { createError, ErrorCodes, formatError, type ErrorCode } from "../utils/errors.js";
17
+ import { log, logError } from "../utils/logger.js";
18
+ import { isFilePath } from "../utils/validation.js";
19
+
20
+ export class PostProxyClient {
21
+ private apiKey: string;
22
+ private baseUrl: string;
23
+
24
+ constructor(apiKey: string, baseUrl: string) {
25
+ this.apiKey = apiKey;
26
+ this.baseUrl = baseUrl.replace(/\/$/, ""); // Remove trailing slash
27
+ }
28
+
29
+ /**
30
+ * Extract array from API response
31
+ * API returns either:
32
+ * - Direct array: [...]
33
+ * - Object with data field: {"data": [...]}
34
+ * - Paginated response: {"total": N, "data": [...]}
35
+ */
36
+ private extractArray<T>(response: any): T[] {
37
+ if (Array.isArray(response)) {
38
+ return response;
39
+ }
40
+ if (response && typeof response === "object" && Array.isArray(response.data)) {
41
+ return response.data;
42
+ }
43
+ // Return empty array if response is not in expected format
44
+ return [];
45
+ }
46
+
47
+ /**
48
+ * Expand ~ to home directory in file paths
49
+ */
50
+ private expandPath(filePath: string): string {
51
+ if (filePath.startsWith("~/")) {
52
+ return filePath.replace("~", homedir());
53
+ }
54
+ return filePath;
55
+ }
56
+
57
+ /**
58
+ * Get MIME type based on file extension
59
+ */
60
+ private getMimeType(filePath: string): string {
61
+ const ext = filePath.toLowerCase().split(".").pop();
62
+ const mimeTypes: Record<string, string> = {
63
+ jpg: "image/jpeg",
64
+ jpeg: "image/jpeg",
65
+ png: "image/png",
66
+ gif: "image/gif",
67
+ webp: "image/webp",
68
+ mp4: "video/mp4",
69
+ mov: "video/quicktime",
70
+ avi: "video/x-msvideo",
71
+ webm: "video/webm",
72
+ };
73
+ return mimeTypes[ext || ""] || "application/octet-stream";
74
+ }
75
+
76
+ /**
77
+ * Check if any media items are file paths (vs URLs)
78
+ */
79
+ private hasFilePaths(media: string[]): boolean {
80
+ return media.some((item) => isFilePath(item));
81
+ }
82
+
83
+ /**
84
+ * Create post using multipart/form-data (for file uploads)
85
+ * Uses form field names with brackets: post[body], profiles[], media[]
86
+ */
87
+ private async createPostWithFiles(
88
+ params: CreatePostParams,
89
+ extraHeaders: Record<string, string>
90
+ ): Promise<CreatePostResponse> {
91
+ const url = `${this.baseUrl}/posts`;
92
+ const formData = new FormData();
93
+
94
+ // Add post body
95
+ formData.append("post[body]", params.content);
96
+
97
+ // Add scheduled_at if provided
98
+ if (params.schedule) {
99
+ formData.append("post[scheduled_at]", params.schedule);
100
+ }
101
+
102
+ // Add draft if provided
103
+ if (params.draft !== undefined) {
104
+ formData.append("post[draft]", String(params.draft));
105
+ }
106
+
107
+ // Add profiles (platform names)
108
+ for (const profile of params.profiles) {
109
+ formData.append("profiles[]", profile);
110
+ }
111
+
112
+ // Add media files and URLs
113
+ if (params.media && params.media.length > 0) {
114
+ for (const mediaItem of params.media) {
115
+ if (isFilePath(mediaItem)) {
116
+ // It's a file path - read and upload the file
117
+ const expandedPath = this.expandPath(mediaItem);
118
+ try {
119
+ const fileContent = readFileSync(expandedPath);
120
+ const fileName = basename(expandedPath);
121
+ const mimeType = this.getMimeType(expandedPath);
122
+ const blob = new Blob([fileContent], { type: mimeType });
123
+ formData.append("media[]", blob, fileName);
124
+ if (process.env.POSTPROXY_MCP_DEBUG === "1") {
125
+ log(`Adding file to upload: ${fileName} (${mimeType}, ${fileContent.length} bytes)`);
126
+ }
127
+ } catch (error) {
128
+ throw createError(
129
+ ErrorCodes.VALIDATION_ERROR,
130
+ `Failed to read file: ${mediaItem} - ${(error as Error).message}`
131
+ );
132
+ }
133
+ } else {
134
+ // It's a URL - pass it as-is
135
+ formData.append("media[]", mediaItem);
136
+ }
137
+ }
138
+ }
139
+
140
+ // Add platform-specific parameters as JSON
141
+ if (params.platforms && Object.keys(params.platforms).length > 0) {
142
+ formData.append("platforms", JSON.stringify(params.platforms));
143
+ }
144
+
145
+ // Build headers (no Content-Type - fetch will set it with boundary for multipart)
146
+ const headers: Record<string, string> = {
147
+ Authorization: `Bearer ${this.apiKey}`,
148
+ ...extraHeaders,
149
+ };
150
+
151
+ if (process.env.POSTPROXY_MCP_DEBUG === "1") {
152
+ log(`Creating post with file upload (multipart/form-data)`);
153
+ log(`Profiles: ${params.profiles.join(", ")}`);
154
+ log(`Media count: ${params.media?.length || 0}`);
155
+ }
156
+
157
+ try {
158
+ const response = await fetch(url, {
159
+ method: "POST",
160
+ headers,
161
+ body: formData,
162
+ signal: AbortSignal.timeout(60000), // 60 second timeout for file uploads
163
+ });
164
+
165
+ const requestId = response.headers.get("x-request-id");
166
+
167
+ if (!response.ok) {
168
+ let errorMessage = `API request failed with status ${response.status}`;
169
+ let errorDetails: any = { status: response.status, requestId };
170
+
171
+ try {
172
+ const errorBody = await response.json();
173
+ if (Array.isArray(errorBody.errors)) {
174
+ errorMessage = errorBody.errors.join("; ");
175
+ errorDetails = { ...errorDetails, errors: errorBody.errors };
176
+ } else if (errorBody.message) {
177
+ errorMessage = errorBody.message;
178
+ errorDetails = { ...errorDetails, ...errorBody };
179
+ } else if (errorBody.error) {
180
+ errorMessage = typeof errorBody.error === "string"
181
+ ? errorBody.error
182
+ : errorBody.message || errorMessage;
183
+ errorDetails = { ...errorDetails, ...errorBody };
184
+ }
185
+ } catch {
186
+ errorMessage = response.statusText || errorMessage;
187
+ }
188
+
189
+ let errorCode: ErrorCode = ErrorCodes.API_ERROR;
190
+ if (response.status === 401) {
191
+ errorCode = ErrorCodes.AUTH_INVALID;
192
+ } else if (response.status === 404) {
193
+ errorCode = ErrorCodes.TARGET_NOT_FOUND;
194
+ } else if (response.status >= 400 && response.status < 500) {
195
+ errorCode = ErrorCodes.VALIDATION_ERROR;
196
+ }
197
+
198
+ logError(createError(errorCode, errorMessage, errorDetails), `API POST /posts (multipart)`);
199
+ throw createError(errorCode, errorMessage, errorDetails);
200
+ }
201
+
202
+ const jsonResponse = await response.json();
203
+ if (process.env.POSTPROXY_MCP_DEBUG === "1") {
204
+ log(`Response POST /posts (multipart)`, JSON.stringify(jsonResponse, null, 2));
205
+ }
206
+ return jsonResponse as CreatePostResponse;
207
+ } catch (error) {
208
+ if (error instanceof Error && error.name === "TimeoutError") {
209
+ throw createError(
210
+ ErrorCodes.API_ERROR,
211
+ "Request timeout - API did not respond within 60 seconds"
212
+ );
213
+ }
214
+ if (error instanceof Error && "code" in error) {
215
+ throw error;
216
+ }
217
+ logError(error as Error, `API POST /posts (multipart)`);
218
+ throw formatError(error as Error, ErrorCodes.API_ERROR, { method: "POST", path: "/posts" });
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Make an HTTP request to the PostProxy API
224
+ */
225
+ private async request<T>(
226
+ method: string,
227
+ path: string,
228
+ body?: any,
229
+ extraHeaders?: Record<string, string>
230
+ ): Promise<T> {
231
+ const url = `${this.baseUrl}${path}`;
232
+ const headers: Record<string, string> = {
233
+ Authorization: `Bearer ${this.apiKey}`,
234
+ "Content-Type": "application/json",
235
+ };
236
+
237
+ // Merge extra headers if provided
238
+ if (extraHeaders) {
239
+ Object.assign(headers, extraHeaders);
240
+ }
241
+
242
+ const options: RequestInit = {
243
+ method,
244
+ headers,
245
+ signal: AbortSignal.timeout(30000), // 30 second timeout
246
+ };
247
+
248
+ if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
249
+ options.body = JSON.stringify(body);
250
+ // Log request payload in debug mode
251
+ if (process.env.POSTPROXY_MCP_DEBUG === "1") {
252
+ log(`Request ${method} ${path}`, JSON.stringify(body, null, 2));
253
+ }
254
+ }
255
+
256
+ try {
257
+ const response = await fetch(url, options);
258
+ const requestId = response.headers.get("x-request-id");
259
+
260
+ if (!response.ok) {
261
+ let errorMessage = `API request failed with status ${response.status}`;
262
+ let errorDetails: any = { status: response.status, requestId };
263
+
264
+ try {
265
+ const errorBody = await response.json();
266
+
267
+ // Handle different error response formats
268
+ if (Array.isArray(errorBody.errors)) {
269
+ // 422 validation errors: {"errors": ["...", "..."]}
270
+ errorMessage = errorBody.errors.join("; ");
271
+ errorDetails = { ...errorDetails, errors: errorBody.errors };
272
+ } else if (errorBody.message) {
273
+ // 400 errors: {"status":400,"error":"Bad Request","message":"..."}
274
+ errorMessage = errorBody.message;
275
+ errorDetails = { ...errorDetails, ...errorBody };
276
+ } else if (errorBody.error) {
277
+ // Some errors use "error" field
278
+ errorMessage = typeof errorBody.error === "string"
279
+ ? errorBody.error
280
+ : errorBody.message || errorMessage;
281
+ errorDetails = { ...errorDetails, ...errorBody };
282
+ } else {
283
+ // Fallback: use any available message field
284
+ errorMessage = errorBody.message || errorBody.error || errorMessage;
285
+ errorDetails = { ...errorDetails, ...errorBody };
286
+ }
287
+ } catch {
288
+ // If response is not JSON, use status text
289
+ errorMessage = response.statusText || errorMessage;
290
+ }
291
+
292
+ // Map HTTP status codes to error codes
293
+ let errorCode: ErrorCode = ErrorCodes.API_ERROR;
294
+ if (response.status === 401) {
295
+ errorCode = ErrorCodes.AUTH_INVALID;
296
+ } else if (response.status === 404) {
297
+ errorCode = ErrorCodes.TARGET_NOT_FOUND;
298
+ } else if (response.status >= 400 && response.status < 500) {
299
+ errorCode = ErrorCodes.VALIDATION_ERROR;
300
+ }
301
+
302
+ logError(createError(errorCode, errorMessage, errorDetails), `API ${method} ${path}`);
303
+ throw createError(errorCode, errorMessage, errorDetails);
304
+ }
305
+
306
+ // Handle empty responses
307
+ const contentType = response.headers.get("content-type");
308
+ if (contentType && contentType.includes("application/json")) {
309
+ const jsonResponse = await response.json();
310
+ // Log response in debug mode
311
+ if (process.env.POSTPROXY_MCP_DEBUG === "1") {
312
+ log(`Response ${method} ${path}`, JSON.stringify(jsonResponse, null, 2));
313
+ }
314
+ return jsonResponse;
315
+ }
316
+
317
+ return {} as T;
318
+ } catch (error) {
319
+ if (error instanceof Error && error.name === "TimeoutError") {
320
+ throw createError(
321
+ ErrorCodes.API_ERROR,
322
+ "Request timeout - API did not respond within 30 seconds"
323
+ );
324
+ }
325
+
326
+ if (error instanceof Error && "code" in error && error.code === "AUTH_INVALID") {
327
+ throw error;
328
+ }
329
+
330
+ logError(error as Error, `API ${method} ${path}`);
331
+ throw formatError(error as Error, ErrorCodes.API_ERROR, { method, path });
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Get all profile groups
337
+ */
338
+ async getProfileGroups(): Promise<ProfileGroup[]> {
339
+ const response = await this.request<any>("GET", "/profile_groups/");
340
+ return this.extractArray<ProfileGroup>(response);
341
+ }
342
+
343
+ /**
344
+ * Get profiles, optionally filtered by group ID
345
+ */
346
+ async getProfiles(groupId?: string | number): Promise<Profile[]> {
347
+ const path = groupId
348
+ ? `/profiles?group_id=${groupId}`
349
+ : "/profiles";
350
+ const response = await this.request<any>("GET", path);
351
+ return this.extractArray<Profile>(response);
352
+ }
353
+
354
+ /**
355
+ * Create a new post
356
+ * API expects: { post: { body, scheduled_at, draft }, profiles: [...], media: [...], platforms: {...} }
357
+ * Note: draft parameter must be inside the post object, not at the top level
358
+ * If media contains file paths, uses multipart/form-data for file upload
359
+ */
360
+ async createPost(params: CreatePostParams): Promise<CreatePostResponse> {
361
+ // Check if we need to use multipart/form-data for file uploads
362
+ if (params.media && params.media.length > 0 && this.hasFilePaths(params.media)) {
363
+ const extraHeaders: Record<string, string> = {};
364
+ if (params.idempotency_key) {
365
+ extraHeaders["Idempotency-Key"] = params.idempotency_key;
366
+ }
367
+ return this.createPostWithFiles(params, extraHeaders);
368
+ }
369
+
370
+ // Transform to API format (JSON request for URLs only)
371
+ const apiPayload: any = {
372
+ post: {
373
+ body: params.content,
374
+ },
375
+ profiles: params.profiles, // Array of platform names (e.g., ["twitter"])
376
+ media: params.media || [], // Always include media field, even if empty
377
+ };
378
+
379
+ if (params.schedule) {
380
+ apiPayload.post.scheduled_at = params.schedule;
381
+ }
382
+
383
+ // Draft parameter must be inside the post object, not at the top level
384
+ if (params.draft !== undefined) {
385
+ apiPayload.post.draft = params.draft;
386
+ }
387
+
388
+ // Platform-specific parameters
389
+ // Only include platforms if it's a non-empty object with at least one platform
390
+ // Supports all platform parameter types: strings, numbers, booleans, arrays (e.g., collaborators)
391
+ // Empty platform objects (e.g., {"linkedin": {}}) are also supported
392
+ if (
393
+ params.platforms &&
394
+ typeof params.platforms === "object" &&
395
+ !Array.isArray(params.platforms) &&
396
+ Object.keys(params.platforms).length > 0
397
+ ) {
398
+ // Validate structure: each key should be a platform name (string), value should be an object
399
+ // Note: We only validate the top-level structure. Detailed validation happens in validation.ts
400
+ // Platform parameter objects can contain: strings, numbers, booleans, arrays (e.g., collaborators), or be empty {}
401
+ const isValidPlatforms = Object.entries(params.platforms).every(
402
+ ([key, value]) =>
403
+ typeof key === "string" &&
404
+ typeof value === "object" &&
405
+ value !== null &&
406
+ !Array.isArray(value)
407
+ );
408
+
409
+ if (isValidPlatforms) {
410
+ apiPayload.platforms = params.platforms;
411
+ } else {
412
+ log("WARNING: Invalid platforms structure, skipping platform parameters");
413
+ }
414
+ }
415
+
416
+ // Log payload in debug mode to troubleshoot draft issues
417
+ if (process.env.POSTPROXY_MCP_DEBUG === "1") {
418
+ log("Creating post with payload:", JSON.stringify(apiPayload, null, 2));
419
+ log(`Draft parameter: requested=${params.draft}, sending=${apiPayload.draft}`);
420
+ if (params.media && params.media.length > 0) {
421
+ log(`Post includes ${params.media.length} media file(s)`);
422
+ }
423
+ if (params.platforms) {
424
+ log(`Platform parameters received: ${JSON.stringify(params.platforms, null, 2)}`);
425
+ log(`Platform parameter keys: ${Object.keys(params.platforms).join(", ")}`);
426
+ if (apiPayload.platforms) {
427
+ log(`Platform parameters sent to API: ${JSON.stringify(apiPayload.platforms, null, 2)}`);
428
+ } else {
429
+ log("WARNING: Platform parameters were provided but not included in API payload (invalid structure or empty)");
430
+ }
431
+ } else {
432
+ log("No platform parameters provided");
433
+ }
434
+ }
435
+
436
+ // Add idempotency key as header if provided
437
+ const extraHeaders: Record<string, string> = {};
438
+ if (params.idempotency_key) {
439
+ extraHeaders["Idempotency-Key"] = params.idempotency_key;
440
+ }
441
+
442
+ const response = await this.request<CreatePostResponse>("POST", "/posts", apiPayload, extraHeaders);
443
+
444
+ // Log response in debug mode, especially draft status
445
+ if (process.env.POSTPROXY_MCP_DEBUG === "1") {
446
+ log(`Post created: id=${response.id}, status=${response.status}, draft=${response.draft}`);
447
+ if (params.draft === true && response.draft === false) {
448
+ log("WARNING: Draft was requested but API returned draft=false. API may have ignored the draft parameter.");
449
+ }
450
+ }
451
+
452
+ return response;
453
+ }
454
+
455
+ /**
456
+ * Get post details by ID
457
+ */
458
+ async getPost(postId: string): Promise<PostDetails> {
459
+ return this.request<PostDetails>("GET", `/posts/${postId}`);
460
+ }
461
+
462
+ /**
463
+ * List posts with optional pagination
464
+ */
465
+ async listPosts(limit?: number, page?: number, perPage?: number): Promise<Post[]> {
466
+ const params = new URLSearchParams();
467
+ // Map limit to per_page (API expects per_page, not limit)
468
+ if (limit !== undefined) {
469
+ params.append("per_page", String(limit));
470
+ }
471
+ if (page !== undefined) {
472
+ params.append("page", String(page));
473
+ }
474
+ if (perPage !== undefined) {
475
+ params.append("per_page", String(perPage));
476
+ }
477
+ const queryString = params.toString();
478
+ const path = queryString ? `/posts?${queryString}` : "/posts";
479
+ const response = await this.request<any>("GET", path);
480
+ return this.extractArray<Post>(response);
481
+ }
482
+
483
+ /**
484
+ * Delete a post by ID
485
+ */
486
+ async deletePost(postId: string): Promise<void> {
487
+ await this.request<void>("DELETE", `/posts/${postId}`);
488
+ }
489
+
490
+ /**
491
+ * Publish a draft post
492
+ * Only posts with status "draft" can be published using this endpoint
493
+ */
494
+ async publishPost(postId: string): Promise<PostDetails> {
495
+ return this.request<PostDetails>("POST", `/posts/${postId}/publish`);
496
+ }
497
+ }