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,901 @@
1
+ /**
2
+ * PostProxy MCP - Cloudflare Worker Entry Point
3
+ *
4
+ * This worker provides the same MCP functionality as the local stdio version,
5
+ * but runs on Cloudflare Workers for remote access.
6
+ *
7
+ * API key is passed via X-PostProxy-API-Key header from the client.
8
+ */
9
+
10
+ import { WorkerEntrypoint } from "cloudflare:workers";
11
+
12
+ interface Env {
13
+ POSTPROXY_BASE_URL: string;
14
+ }
15
+
16
+ interface ProfileGroup {
17
+ id: string;
18
+ name: string;
19
+ profiles_count: number;
20
+ }
21
+
22
+ interface Profile {
23
+ id: string;
24
+ name: string;
25
+ platform: string;
26
+ profile_group_id: string;
27
+ }
28
+
29
+ interface PlatformOutcome {
30
+ platform: string;
31
+ status: "pending" | "processing" | "published" | "failed" | "deleted";
32
+ url?: string;
33
+ post_id?: string;
34
+ error?: string | null;
35
+ attempted_at: string | null;
36
+ insights?: any;
37
+ }
38
+
39
+ interface Post {
40
+ id: string;
41
+ body?: string;
42
+ content?: string;
43
+ status: "draft" | "pending" | "processing" | "processed" | "scheduled";
44
+ draft: boolean;
45
+ scheduled_at: string | null;
46
+ created_at: string;
47
+ platforms: PlatformOutcome[];
48
+ }
49
+
50
+ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
51
+ private apiKey: string | null = null;
52
+
53
+ /**
54
+ * Get API key from request context
55
+ */
56
+ private getApiKey(): string {
57
+ if (!this.apiKey) {
58
+ throw new Error("API key not configured. Pass X-PostProxy-API-Key header.");
59
+ }
60
+ return this.apiKey;
61
+ }
62
+
63
+ /**
64
+ * Make an HTTP request to the PostProxy API
65
+ */
66
+ private async apiRequest<T>(
67
+ method: string,
68
+ path: string,
69
+ body?: any,
70
+ extraHeaders?: Record<string, string>
71
+ ): Promise<T> {
72
+ const baseUrl = this.env.POSTPROXY_BASE_URL.replace(/\/$/, "");
73
+ const url = `${baseUrl}${path}`;
74
+
75
+ const headers: Record<string, string> = {
76
+ Authorization: `Bearer ${this.getApiKey()}`,
77
+ "Content-Type": "application/json",
78
+ "Accept": "application/json",
79
+ };
80
+
81
+ if (extraHeaders) {
82
+ Object.assign(headers, extraHeaders);
83
+ }
84
+
85
+ const options: RequestInit = {
86
+ method,
87
+ headers,
88
+ };
89
+
90
+ if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
91
+ options.body = JSON.stringify(body);
92
+ }
93
+
94
+ const response = await fetch(url, options);
95
+
96
+ if (!response.ok) {
97
+ let errorMessage = `API request failed with status ${response.status}`;
98
+ try {
99
+ const errorBody = await response.json() as any;
100
+ if (Array.isArray(errorBody.errors)) {
101
+ errorMessage = errorBody.errors.join("; ");
102
+ } else if (errorBody.message) {
103
+ errorMessage = errorBody.message;
104
+ } else if (errorBody.error) {
105
+ errorMessage = typeof errorBody.error === "string" ? errorBody.error : errorMessage;
106
+ }
107
+ } catch {
108
+ errorMessage = response.statusText || errorMessage;
109
+ }
110
+ throw new Error(errorMessage);
111
+ }
112
+
113
+ const contentType = response.headers.get("content-type");
114
+ if (contentType && contentType.includes("application/json")) {
115
+ return (await response.json()) as T;
116
+ }
117
+
118
+ return {} as T;
119
+ }
120
+
121
+ /**
122
+ * Extract array from API response
123
+ */
124
+ private extractArray<T>(response: any): T[] {
125
+ if (Array.isArray(response)) {
126
+ return response;
127
+ }
128
+ if (response && typeof response === "object" && Array.isArray(response.data)) {
129
+ return response.data;
130
+ }
131
+ return [];
132
+ }
133
+
134
+ /**
135
+ * Get all profiles
136
+ */
137
+ private async getAllProfiles(): Promise<Profile[]> {
138
+ const groupsResponse = await this.apiRequest<any>("GET", "/profile_groups/");
139
+ const groups = this.extractArray<ProfileGroup>(groupsResponse);
140
+
141
+ const allProfiles: Profile[] = [];
142
+ for (const group of groups) {
143
+ const profilesResponse = await this.apiRequest<any>("GET", `/profiles?group_id=${group.id}`);
144
+ const profiles = this.extractArray<Profile>(profilesResponse);
145
+ allProfiles.push(...profiles);
146
+ }
147
+
148
+ return allProfiles;
149
+ }
150
+
151
+ /**
152
+ * Get MIME type based on file extension
153
+ */
154
+ private getMimeType(filename: string): string {
155
+ const ext = filename.toLowerCase().split(".").pop();
156
+ const mimeTypes: Record<string, string> = {
157
+ jpg: "image/jpeg",
158
+ jpeg: "image/jpeg",
159
+ png: "image/png",
160
+ gif: "image/gif",
161
+ webp: "image/webp",
162
+ mp4: "video/mp4",
163
+ mov: "video/quicktime",
164
+ avi: "video/x-msvideo",
165
+ webm: "video/webm",
166
+ };
167
+ return mimeTypes[ext || ""] || "application/octet-stream";
168
+ }
169
+
170
+ /**
171
+ * Create post with file uploads using multipart/form-data
172
+ */
173
+ private async createPostWithFiles(
174
+ content: string,
175
+ platformNames: string[],
176
+ mediaFiles: Array<{ filename: string; data: string; content_type?: string }>,
177
+ schedule?: string,
178
+ draft?: boolean,
179
+ platformParams?: Record<string, Record<string, any>>,
180
+ idempotencyKey?: string
181
+ ): Promise<any> {
182
+ const baseUrl = this.env.POSTPROXY_BASE_URL.replace(/\/$/, "");
183
+ const url = `${baseUrl}/posts`;
184
+ const formData = new FormData();
185
+
186
+ // Add post body
187
+ formData.append("post[body]", content);
188
+
189
+ // Add scheduled_at if provided
190
+ if (schedule) {
191
+ formData.append("post[scheduled_at]", schedule);
192
+ }
193
+
194
+ // Add draft if provided
195
+ if (draft !== undefined) {
196
+ formData.append("post[draft]", String(draft));
197
+ }
198
+
199
+ // Add profiles (platform names)
200
+ for (const profile of platformNames) {
201
+ formData.append("profiles[]", profile);
202
+ }
203
+
204
+ // Add media files from base64
205
+ for (const file of mediaFiles) {
206
+ try {
207
+ // Decode base64 to binary
208
+ const binaryString = atob(file.data);
209
+ const bytes = new Uint8Array(binaryString.length);
210
+ for (let i = 0; i < binaryString.length; i++) {
211
+ bytes[i] = binaryString.charCodeAt(i);
212
+ }
213
+
214
+ const mimeType = file.content_type || this.getMimeType(file.filename);
215
+ const blob = new Blob([bytes], { type: mimeType });
216
+ formData.append("media[]", blob, file.filename);
217
+ } catch (error) {
218
+ throw new Error(`Failed to decode file ${file.filename}: ${(error as Error).message}`);
219
+ }
220
+ }
221
+
222
+ // Add platform-specific parameters as JSON
223
+ if (platformParams && Object.keys(platformParams).length > 0) {
224
+ formData.append("platforms", JSON.stringify(platformParams));
225
+ }
226
+
227
+ // Build headers
228
+ const headers: Record<string, string> = {
229
+ Authorization: `Bearer ${this.getApiKey()}`,
230
+ };
231
+
232
+ if (idempotencyKey) {
233
+ headers["Idempotency-Key"] = idempotencyKey;
234
+ }
235
+
236
+ const response = await fetch(url, {
237
+ method: "POST",
238
+ headers,
239
+ body: formData,
240
+ });
241
+
242
+ if (!response.ok) {
243
+ let errorMessage = `API request failed with status ${response.status}`;
244
+ try {
245
+ const errorBody = await response.json() as any;
246
+ if (Array.isArray(errorBody.errors)) {
247
+ errorMessage = errorBody.errors.join("; ");
248
+ } else if (errorBody.message) {
249
+ errorMessage = errorBody.message;
250
+ } else if (errorBody.error) {
251
+ errorMessage = typeof errorBody.error === "string" ? errorBody.error : errorMessage;
252
+ }
253
+ } catch {
254
+ errorMessage = response.statusText || errorMessage;
255
+ }
256
+ throw new Error(errorMessage);
257
+ }
258
+
259
+ return await response.json();
260
+ }
261
+
262
+ /**
263
+ * Generate idempotency key from post data
264
+ */
265
+ private async generateIdempotencyKey(
266
+ content: string,
267
+ targets: string[],
268
+ schedule?: string
269
+ ): Promise<string> {
270
+ const normalizedContent = content.trim();
271
+ const normalizedTargets = [...targets].sort();
272
+ const normalizedSchedule = schedule || "";
273
+
274
+ const data = JSON.stringify({
275
+ content: normalizedContent,
276
+ targets: normalizedTargets,
277
+ schedule: normalizedSchedule,
278
+ });
279
+
280
+ // Use Web Crypto API (available in Workers)
281
+ const encoder = new TextEncoder();
282
+ const dataBuffer = encoder.encode(data);
283
+ const hashBuffer = await crypto.subtle.digest("SHA-256", dataBuffer);
284
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
285
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
286
+ }
287
+
288
+ /**
289
+ * Determine overall status from post and platform statuses
290
+ */
291
+ private determineOverallStatus(
292
+ post: Post
293
+ ): "pending" | "processing" | "complete" | "failed" | "draft" {
294
+ if (post.status === "draft" || post.draft === true) {
295
+ return "draft";
296
+ }
297
+ if (post.status === "scheduled") {
298
+ return "pending";
299
+ }
300
+ if (post.status === "processing") {
301
+ return "processing";
302
+ }
303
+ if (post.status === "processed") {
304
+ const platforms = post.platforms || [];
305
+ if (platforms.length === 0) {
306
+ return "pending";
307
+ }
308
+ const allPublished = platforms.every((p) => p.status === "published");
309
+ const allFailed = platforms.every((p) => p.status === "failed");
310
+ const anyPending = platforms.some((p) => p.status === "pending" || p.status === "processing");
311
+
312
+ if (anyPending) {
313
+ return "processing";
314
+ } else if (allPublished) {
315
+ return "complete";
316
+ } else if (allFailed) {
317
+ return "failed";
318
+ } else {
319
+ return "complete";
320
+ }
321
+ }
322
+ if (post.status === "pending") {
323
+ return "pending";
324
+ }
325
+ return "pending";
326
+ }
327
+
328
+ /**
329
+ * Check authentication status, API configuration, and workspace information
330
+ * @return {Promise<string>} Authentication status and workspace info as JSON
331
+ */
332
+ async authStatus(): Promise<string> {
333
+ const hasApiKey = !!this.apiKey;
334
+ const result: {
335
+ authenticated: boolean;
336
+ base_url: string;
337
+ profile_groups_count?: number;
338
+ } = {
339
+ authenticated: hasApiKey,
340
+ base_url: this.env.POSTPROXY_BASE_URL,
341
+ };
342
+
343
+ if (hasApiKey) {
344
+ try {
345
+ const groupsResponse = await this.apiRequest<any>("GET", "/profile_groups/");
346
+ const groups = this.extractArray<ProfileGroup>(groupsResponse);
347
+ result.profile_groups_count = groups.length;
348
+ } catch {
349
+ // Ignore errors, just return without count
350
+ }
351
+ }
352
+
353
+ return JSON.stringify(result, null, 2);
354
+ }
355
+
356
+ /**
357
+ * List all available social media profiles (targets) for posting
358
+ * @return {Promise<string>} List of available profiles as JSON
359
+ */
360
+ async profilesList(): Promise<string> {
361
+ this.getApiKey(); // Validate API key is present
362
+
363
+ const profiles = await this.getAllProfiles();
364
+ const targets = profiles.map((profile) => ({
365
+ id: profile.id,
366
+ name: profile.name,
367
+ platform: profile.platform,
368
+ profile_group_id: profile.profile_group_id,
369
+ }));
370
+
371
+ return JSON.stringify({ targets }, null, 2);
372
+ }
373
+
374
+ /**
375
+ * Publish a post to specified targets
376
+ * @param content {string} Post content text
377
+ * @param targets {string} Comma-separated list of target profile IDs
378
+ * @param schedule {string} Optional ISO 8601 scheduled time
379
+ * @param media {string} Optional comma-separated list of media URLs
380
+ * @param idempotency_key {string} Optional idempotency key for deduplication
381
+ * @param require_confirmation {boolean} If true, return summary without publishing
382
+ * @param draft {boolean} If true, creates a draft post that won't publish automatically
383
+ * @param platforms {string} Optional JSON string of platform-specific parameters
384
+ * @param media_files {string} Optional JSON array of file objects with {filename, data (base64), content_type?}
385
+ * @return {Promise<string>} Post creation result as JSON
386
+ */
387
+ async postPublish(
388
+ content: string,
389
+ targets: string,
390
+ schedule?: string,
391
+ media?: string,
392
+ idempotency_key?: string,
393
+ require_confirmation?: boolean,
394
+ draft?: boolean,
395
+ platforms?: string,
396
+ media_files?: string
397
+ ): Promise<string> {
398
+ this.getApiKey(); // Validate API key is present
399
+
400
+ // Parse comma-separated values
401
+ const targetIds = targets.split(",").map((t) => t.trim()).filter(Boolean);
402
+ const mediaUrls = media ? media.split(",").map((m) => m.trim()).filter(Boolean) : [];
403
+
404
+ // Parse media_files JSON if provided
405
+ let mediaFilesArray: Array<{ filename: string; data: string; content_type?: string }> = [];
406
+ if (media_files) {
407
+ try {
408
+ mediaFilesArray = JSON.parse(media_files);
409
+ if (!Array.isArray(mediaFilesArray)) {
410
+ throw new Error("media_files must be an array");
411
+ }
412
+ for (const file of mediaFilesArray) {
413
+ if (!file.filename || !file.data) {
414
+ throw new Error("Each media file must have 'filename' and 'data' (base64) properties");
415
+ }
416
+ }
417
+ } catch (e: any) {
418
+ if (e.message.includes("media_files")) {
419
+ throw e;
420
+ }
421
+ throw new Error("Invalid media_files parameter: must be valid JSON array");
422
+ }
423
+ }
424
+
425
+ // Parse platforms JSON if provided
426
+ let platformParams: Record<string, Record<string, any>> | undefined;
427
+ if (platforms) {
428
+ try {
429
+ platformParams = JSON.parse(platforms);
430
+ } catch {
431
+ throw new Error("Invalid platforms parameter: must be valid JSON");
432
+ }
433
+ }
434
+
435
+ // Validate input
436
+ if (!content || content.trim() === "") {
437
+ throw new Error("Content cannot be empty");
438
+ }
439
+ if (targetIds.length === 0) {
440
+ throw new Error("At least one target is required");
441
+ }
442
+
443
+ // If require_confirmation, return summary without publishing
444
+ if (require_confirmation) {
445
+ return JSON.stringify(
446
+ {
447
+ summary: {
448
+ targets: targetIds,
449
+ content_preview: content.substring(0, 100) + (content.length > 100 ? "..." : ""),
450
+ media_count: mediaUrls.length + mediaFilesArray.length,
451
+ media_files_count: mediaFilesArray.length,
452
+ schedule_time: schedule,
453
+ draft: draft || false,
454
+ platforms: platformParams || {},
455
+ },
456
+ },
457
+ null,
458
+ 2
459
+ );
460
+ }
461
+
462
+ // Get profiles to convert target IDs to platform names
463
+ const profiles = await this.getAllProfiles();
464
+ const profilesMap = new Map<string, Profile>();
465
+ for (const profile of profiles) {
466
+ profilesMap.set(profile.id, profile);
467
+ }
468
+
469
+ // Convert target IDs to platform names
470
+ const platformNames: string[] = [];
471
+ for (const targetId of targetIds) {
472
+ const profile = profilesMap.get(targetId);
473
+ if (!profile) {
474
+ throw new Error(`Target ${targetId} not found`);
475
+ }
476
+ platformNames.push(profile.platform);
477
+ }
478
+
479
+ // Validate platforms keys match target platforms
480
+ if (platformParams) {
481
+ const invalidPlatforms = Object.keys(platformParams).filter(
482
+ (key) => !platformNames.includes(key)
483
+ );
484
+ if (invalidPlatforms.length > 0) {
485
+ throw new Error(
486
+ `Platform parameters specified for platforms not in targets: ${invalidPlatforms.join(", ")}. Available platforms: ${platformNames.join(", ")}`
487
+ );
488
+ }
489
+ }
490
+
491
+ // Generate idempotency key if not provided
492
+ const finalIdempotencyKey =
493
+ idempotency_key || (await this.generateIdempotencyKey(content, targetIds, schedule));
494
+
495
+ let response: any;
496
+
497
+ // Use multipart upload if media files are provided
498
+ if (mediaFilesArray.length > 0) {
499
+ response = await this.createPostWithFiles(
500
+ content,
501
+ platformNames,
502
+ mediaFilesArray,
503
+ schedule,
504
+ draft,
505
+ platformParams,
506
+ finalIdempotencyKey
507
+ );
508
+ } else {
509
+ // Create post with JSON (URLs only)
510
+ const apiPayload: any = {
511
+ post: {
512
+ body: content,
513
+ },
514
+ profiles: platformNames,
515
+ media: mediaUrls,
516
+ };
517
+
518
+ if (schedule) {
519
+ apiPayload.post.scheduled_at = schedule;
520
+ }
521
+
522
+ if (draft !== undefined) {
523
+ apiPayload.post.draft = draft;
524
+ }
525
+
526
+ if (platformParams && Object.keys(platformParams).length > 0) {
527
+ apiPayload.platforms = platformParams;
528
+ }
529
+
530
+ const extraHeaders: Record<string, string> = {
531
+ "Idempotency-Key": finalIdempotencyKey,
532
+ };
533
+
534
+ response = await this.apiRequest<any>("POST", "/posts", apiPayload, extraHeaders);
535
+ }
536
+
537
+ // Check if draft was requested but ignored
538
+ const wasDraftRequested = draft === true;
539
+ const isDraftInResponse = Boolean(response.draft) === true;
540
+ const wasProcessedImmediately = response.status === "processed" && wasDraftRequested;
541
+ const draftIgnored = wasDraftRequested && (!isDraftInResponse || wasProcessedImmediately);
542
+
543
+ const responseData: any = {
544
+ job_id: response.id,
545
+ status: response.status,
546
+ draft: response.draft,
547
+ scheduled_at: response.scheduled_at,
548
+ created_at: response.created_at,
549
+ };
550
+
551
+ if (draftIgnored) {
552
+ responseData.warning = "Warning: Draft was requested but API returned draft: false. The post may have been processed immediately.";
553
+ }
554
+
555
+ return JSON.stringify(responseData, null, 2);
556
+ }
557
+
558
+ /**
559
+ * Get status of a published post by job ID
560
+ * @param job_id {string} Job ID from post.publish response
561
+ * @return {Promise<string>} Post status as JSON
562
+ */
563
+ async postStatus(job_id: string): Promise<string> {
564
+ if (!job_id) {
565
+ throw new Error("job_id is required");
566
+ }
567
+
568
+ const postDetails = await this.apiRequest<Post>("GET", `/posts/${job_id}`);
569
+
570
+ const platforms = (postDetails.platforms || []).map((platform) => ({
571
+ platform: platform.platform,
572
+ status: platform.status,
573
+ url: platform.url,
574
+ post_id: platform.post_id,
575
+ error: platform.error || null,
576
+ attempted_at: platform.attempted_at,
577
+ insights: platform.insights,
578
+ }));
579
+
580
+ const overallStatus = this.determineOverallStatus(postDetails);
581
+
582
+ return JSON.stringify(
583
+ {
584
+ job_id: job_id,
585
+ overall_status: overallStatus,
586
+ draft: postDetails.draft || false,
587
+ status: postDetails.status,
588
+ platforms,
589
+ },
590
+ null,
591
+ 2
592
+ );
593
+ }
594
+
595
+ /**
596
+ * Publish a draft post
597
+ * @param job_id {string} Job ID of the draft post to publish
598
+ * @return {Promise<string>} Published post result as JSON
599
+ */
600
+ async postPublishDraft(job_id: string): Promise<string> {
601
+ if (!job_id) {
602
+ throw new Error("job_id is required");
603
+ }
604
+
605
+ // First check if the post exists and is a draft
606
+ const postDetails = await this.apiRequest<Post>("GET", `/posts/${job_id}`);
607
+
608
+ if (!postDetails.draft && postDetails.status !== "draft") {
609
+ throw new Error(`Post ${job_id} is not a draft and cannot be published using this endpoint`);
610
+ }
611
+
612
+ // Publish the draft post
613
+ const publishedPost = await this.apiRequest<Post>("POST", `/posts/${job_id}/publish`);
614
+
615
+ return JSON.stringify(
616
+ {
617
+ job_id: publishedPost.id,
618
+ status: publishedPost.status,
619
+ draft: publishedPost.draft,
620
+ scheduled_at: publishedPost.scheduled_at,
621
+ created_at: publishedPost.created_at,
622
+ message: "Draft post published successfully",
623
+ },
624
+ null,
625
+ 2
626
+ );
627
+ }
628
+
629
+ /**
630
+ * Delete a post by job ID
631
+ * @param job_id {string} Job ID to delete
632
+ * @return {Promise<string>} Deletion confirmation as JSON
633
+ */
634
+ async postDelete(job_id: string): Promise<string> {
635
+ if (!job_id) {
636
+ throw new Error("job_id is required");
637
+ }
638
+
639
+ await this.apiRequest<void>("DELETE", `/posts/${job_id}`);
640
+
641
+ return JSON.stringify(
642
+ {
643
+ job_id: job_id,
644
+ deleted: true,
645
+ },
646
+ null,
647
+ 2
648
+ );
649
+ }
650
+
651
+ /**
652
+ * List recent post jobs
653
+ * @param limit {number} Maximum number of jobs to return (default: 10)
654
+ * @return {Promise<string>} List of recent jobs as JSON
655
+ */
656
+ async historyList(limit?: number): Promise<string> {
657
+ const effectiveLimit = limit || 10;
658
+
659
+ const response = await this.apiRequest<any>("GET", `/posts?per_page=${effectiveLimit}`);
660
+ const posts = this.extractArray<Post>(response);
661
+
662
+ const jobs = posts.map((post) => {
663
+ const overallStatus = this.determineOverallStatus(post);
664
+ const content = post.body || post.content || "";
665
+
666
+ return {
667
+ job_id: post.id,
668
+ content_preview: content.substring(0, 100) + (content.length > 100 ? "..." : ""),
669
+ created_at: post.created_at,
670
+ overall_status: overallStatus,
671
+ draft: post.draft || false,
672
+ platforms_count: post.platforms?.length || 0,
673
+ };
674
+ });
675
+
676
+ return JSON.stringify({ jobs }, null, 2);
677
+ }
678
+
679
+ /**
680
+ * MCP tool definitions
681
+ */
682
+ private getTools() {
683
+ return [
684
+ {
685
+ name: "authStatus",
686
+ description: "Check authentication status, API configuration, and workspace information",
687
+ inputSchema: { type: "object", properties: {}, required: [] },
688
+ },
689
+ {
690
+ name: "profilesList",
691
+ description: "List all available social media profiles (targets) for posting",
692
+ inputSchema: { type: "object", properties: {}, required: [] },
693
+ },
694
+ {
695
+ name: "postPublish",
696
+ description: "Publish a post to specified targets",
697
+ inputSchema: {
698
+ type: "object",
699
+ properties: {
700
+ content: { type: "string", description: "Post content text" },
701
+ targets: { type: "string", description: "Comma-separated list of target profile IDs" },
702
+ schedule: { type: "string", description: "Optional ISO 8601 scheduled time" },
703
+ media: { type: "string", description: "Optional comma-separated list of media URLs" },
704
+ idempotency_key: { type: "string", description: "Optional idempotency key for deduplication" },
705
+ require_confirmation: { type: "boolean", description: "If true, return summary without publishing" },
706
+ draft: { type: "boolean", description: "If true, creates a draft post" },
707
+ platforms: { type: "string", description: "Optional JSON string of platform-specific parameters" },
708
+ media_files: { type: "string", description: "Optional JSON array of file objects for direct upload. Each object must have 'filename' and 'data' (base64-encoded file content), optionally 'content_type'. Example: [{\"filename\":\"photo.jpg\",\"data\":\"base64...\"}]" },
709
+ },
710
+ required: ["content", "targets"],
711
+ },
712
+ },
713
+ {
714
+ name: "postStatus",
715
+ description: "Get status of a published post by job ID",
716
+ inputSchema: {
717
+ type: "object",
718
+ properties: {
719
+ job_id: { type: "string", description: "Job ID from post.publish response" },
720
+ },
721
+ required: ["job_id"],
722
+ },
723
+ },
724
+ {
725
+ name: "postPublishDraft",
726
+ description: "Publish a draft post",
727
+ inputSchema: {
728
+ type: "object",
729
+ properties: {
730
+ job_id: { type: "string", description: "Job ID of the draft post to publish" },
731
+ },
732
+ required: ["job_id"],
733
+ },
734
+ },
735
+ {
736
+ name: "postDelete",
737
+ description: "Delete a post by job ID",
738
+ inputSchema: {
739
+ type: "object",
740
+ properties: {
741
+ job_id: { type: "string", description: "Job ID to delete" },
742
+ },
743
+ required: ["job_id"],
744
+ },
745
+ },
746
+ {
747
+ name: "historyList",
748
+ description: "List recent post jobs",
749
+ inputSchema: {
750
+ type: "object",
751
+ properties: {
752
+ limit: { type: "number", description: "Maximum number of jobs to return (default: 10)" },
753
+ },
754
+ required: [],
755
+ },
756
+ },
757
+ ];
758
+ }
759
+
760
+ /**
761
+ * Handle MCP JSON-RPC request
762
+ */
763
+ private async handleMcpRequest(body: any): Promise<any> {
764
+ const { jsonrpc, method, params, id } = body;
765
+
766
+ if (jsonrpc !== "2.0") {
767
+ return { jsonrpc: "2.0", error: { code: -32600, message: "Invalid Request" }, id };
768
+ }
769
+
770
+ switch (method) {
771
+ case "initialize":
772
+ return {
773
+ jsonrpc: "2.0",
774
+ result: {
775
+ protocolVersion: "2024-11-05",
776
+ capabilities: { tools: {} },
777
+ serverInfo: { name: "postproxy-mcp", version: "0.1.0" },
778
+ },
779
+ id,
780
+ };
781
+
782
+ case "notifications/initialized":
783
+ return null; // No response for notifications
784
+
785
+ case "tools/list":
786
+ return {
787
+ jsonrpc: "2.0",
788
+ result: { tools: this.getTools() },
789
+ id,
790
+ };
791
+
792
+ case "tools/call": {
793
+ const { name, arguments: args } = params || {};
794
+ try {
795
+ let result: string;
796
+ switch (name) {
797
+ case "authStatus":
798
+ result = await this.authStatus();
799
+ break;
800
+ case "profilesList":
801
+ result = await this.profilesList();
802
+ break;
803
+ case "postPublish":
804
+ result = await this.postPublish(
805
+ args.content,
806
+ args.targets,
807
+ args.schedule,
808
+ args.media,
809
+ args.idempotency_key,
810
+ args.require_confirmation,
811
+ args.draft,
812
+ args.platforms,
813
+ args.media_files
814
+ );
815
+ break;
816
+ case "postStatus":
817
+ result = await this.postStatus(args.job_id);
818
+ break;
819
+ case "postPublishDraft":
820
+ result = await this.postPublishDraft(args.job_id);
821
+ break;
822
+ case "postDelete":
823
+ result = await this.postDelete(args.job_id);
824
+ break;
825
+ case "historyList":
826
+ result = await this.historyList(args?.limit);
827
+ break;
828
+ default:
829
+ return {
830
+ jsonrpc: "2.0",
831
+ error: { code: -32601, message: `Unknown tool: ${name}` },
832
+ id,
833
+ };
834
+ }
835
+ return {
836
+ jsonrpc: "2.0",
837
+ result: { content: [{ type: "text", text: result }] },
838
+ id,
839
+ };
840
+ } catch (e: any) {
841
+ return {
842
+ jsonrpc: "2.0",
843
+ result: { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true },
844
+ id,
845
+ };
846
+ }
847
+ }
848
+
849
+ default:
850
+ return {
851
+ jsonrpc: "2.0",
852
+ error: { code: -32601, message: `Method not found: ${method}` },
853
+ id,
854
+ };
855
+ }
856
+ }
857
+
858
+ /**
859
+ * Handle incoming requests
860
+ */
861
+ async fetch(request: Request): Promise<Response> {
862
+ const url = new URL(request.url);
863
+
864
+ // Only handle /mcp path
865
+ if (url.pathname !== "/mcp") {
866
+ return new Response("Not Found", { status: 404 });
867
+ }
868
+
869
+ // Extract API key from header or query parameter
870
+ this.apiKey = request.headers.get("X-PostProxy-API-Key") || url.searchParams.get("api_key");
871
+
872
+ // Handle POST requests (MCP JSON-RPC)
873
+ if (request.method === "POST") {
874
+ try {
875
+ const body = await request.json();
876
+ const result = await this.handleMcpRequest(body);
877
+
878
+ // Notifications don't get a response
879
+ if (result === null) {
880
+ return new Response(null, { status: 204 });
881
+ }
882
+
883
+ return Response.json(result);
884
+ } catch (e: any) {
885
+ return Response.json({
886
+ jsonrpc: "2.0",
887
+ error: { code: -32700, message: "Parse error" },
888
+ id: null,
889
+ }, { status: 400 });
890
+ }
891
+ }
892
+
893
+ // GET request - return server info
894
+ return Response.json({
895
+ name: "postproxy-mcp",
896
+ version: "0.1.0",
897
+ description: "MCP server for PostProxy API",
898
+ tools: this.getTools().map(t => t.name),
899
+ });
900
+ }
901
+ }