postproxy-mcp 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/worker/index.ts CHANGED
@@ -36,15 +36,26 @@ interface PlatformOutcome {
36
36
  insights?: any;
37
37
  }
38
38
 
39
+ interface MediaAttachment {
40
+ id: string;
41
+ status: "pending" | "processed" | "failed";
42
+ error_message: string | null;
43
+ content_type: string;
44
+ source_url: string | null;
45
+ url: string | null;
46
+ }
47
+
39
48
  interface Post {
40
49
  id: string;
41
50
  body?: string;
42
51
  content?: string;
43
- status: "draft" | "pending" | "processing" | "processed" | "scheduled";
52
+ status: "draft" | "pending" | "processing" | "processed" | "scheduled" | "media_processing_failed";
44
53
  draft: boolean;
45
54
  scheduled_at: string | null;
46
55
  created_at: string;
56
+ media?: MediaAttachment[];
47
57
  platforms: PlatformOutcome[];
58
+ thread?: Array<{ id: string; body: string; media?: MediaAttachment[] }>;
48
59
  }
49
60
 
50
61
  export default class PostProxyMCP extends WorkerEntrypoint<Env> {
@@ -177,7 +188,8 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
177
188
  schedule?: string,
178
189
  draft?: boolean,
179
190
  platformParams?: Record<string, Record<string, any>>,
180
- idempotencyKey?: string
191
+ idempotencyKey?: string,
192
+ threadChildren?: Array<{ body: string; media?: string[] }>
181
193
  ): Promise<any> {
182
194
  const baseUrl = this.env.POSTPROXY_BASE_URL.replace(/\/$/, "");
183
195
  const url = `${baseUrl}/posts`;
@@ -224,6 +236,11 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
224
236
  formData.append("platforms", JSON.stringify(platformParams));
225
237
  }
226
238
 
239
+ // Add thread children
240
+ if (threadChildren && threadChildren.length > 0) {
241
+ formData.append("thread", JSON.stringify(threadChildren));
242
+ }
243
+
227
244
  // Build headers
228
245
  const headers: Record<string, string> = {
229
246
  Authorization: `Bearer ${this.getApiKey()}`,
@@ -290,7 +307,10 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
290
307
  */
291
308
  private determineOverallStatus(
292
309
  post: Post
293
- ): "pending" | "processing" | "complete" | "failed" | "draft" {
310
+ ): "pending" | "processing" | "complete" | "failed" | "draft" | "media_processing_failed" {
311
+ if (post.status === "media_processing_failed") {
312
+ return "media_processing_failed";
313
+ }
294
314
  if (post.status === "draft" || post.draft === true) {
295
315
  return "draft";
296
316
  }
@@ -382,6 +402,9 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
382
402
  * @param draft {boolean} If true, creates a draft post that won't publish automatically
383
403
  * @param platforms {string} Optional JSON string of platform-specific parameters
384
404
  * @param media_files {string} Optional JSON array of file objects with {filename, data (base64), content_type?}
405
+ * @param thread {string} Optional JSON array of thread child posts [{body, media?}]. Supported on X and Threads only.
406
+ * @param queue_id {string} Optional queue ID to add the post to
407
+ * @param queue_priority {string} Optional priority when adding to a queue (high, medium, low)
385
408
  * @return {Promise<string>} Post creation result as JSON
386
409
  */
387
410
  async postPublish(
@@ -393,7 +416,10 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
393
416
  require_confirmation?: boolean,
394
417
  draft?: boolean,
395
418
  platforms?: string,
396
- media_files?: string
419
+ media_files?: string,
420
+ thread?: string,
421
+ queue_id?: string,
422
+ queue_priority?: string
397
423
  ): Promise<string> {
398
424
  this.getApiKey(); // Validate API key is present
399
425
 
@@ -432,6 +458,27 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
432
458
  }
433
459
  }
434
460
 
461
+ // Parse thread JSON if provided
462
+ let threadChildren: Array<{ body: string; media?: string[] }> | undefined;
463
+ if (thread) {
464
+ try {
465
+ threadChildren = JSON.parse(thread);
466
+ if (!Array.isArray(threadChildren)) {
467
+ throw new Error("thread must be an array");
468
+ }
469
+ for (const child of threadChildren) {
470
+ if (!child.body) {
471
+ throw new Error("Each thread child must have a 'body' property");
472
+ }
473
+ }
474
+ } catch (e: any) {
475
+ if (e.message.includes("thread")) {
476
+ throw e;
477
+ }
478
+ throw new Error("Invalid thread parameter: must be valid JSON array");
479
+ }
480
+ }
481
+
435
482
  // Validate input
436
483
  if (!content || content.trim() === "") {
437
484
  throw new Error("Content cannot be empty");
@@ -452,6 +499,7 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
452
499
  schedule_time: schedule,
453
500
  draft: draft || false,
454
501
  platforms: platformParams || {},
502
+ thread: threadChildren || [],
455
503
  },
456
504
  },
457
505
  null,
@@ -503,7 +551,8 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
503
551
  schedule,
504
552
  draft,
505
553
  platformParams,
506
- finalIdempotencyKey
554
+ finalIdempotencyKey,
555
+ threadChildren
507
556
  );
508
557
  } else {
509
558
  // Create post with JSON (URLs only)
@@ -527,6 +576,17 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
527
576
  apiPayload.platforms = platformParams;
528
577
  }
529
578
 
579
+ if (threadChildren && threadChildren.length > 0) {
580
+ apiPayload.thread = threadChildren;
581
+ }
582
+
583
+ if (queue_id) {
584
+ apiPayload.queue_id = queue_id;
585
+ if (queue_priority) {
586
+ apiPayload.queue_priority = queue_priority;
587
+ }
588
+ }
589
+
530
590
  const extraHeaders: Record<string, string> = {
531
591
  "Idempotency-Key": finalIdempotencyKey,
532
592
  };
@@ -579,17 +639,23 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
579
639
 
580
640
  const overallStatus = this.determineOverallStatus(postDetails);
581
641
 
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
- );
642
+ const result: any = {
643
+ job_id: job_id,
644
+ overall_status: overallStatus,
645
+ draft: postDetails.draft || false,
646
+ status: postDetails.status,
647
+ platforms,
648
+ };
649
+
650
+ if (postDetails.media && postDetails.media.length > 0) {
651
+ result.media = postDetails.media;
652
+ }
653
+
654
+ if (postDetails.thread && postDetails.thread.length > 0) {
655
+ result.thread = postDetails.thread;
656
+ }
657
+
658
+ return JSON.stringify(result, null, 2);
593
659
  }
594
660
 
595
661
  /**
@@ -731,6 +797,171 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
731
797
  return JSON.stringify({ placements }, null, 2);
732
798
  }
733
799
 
800
+ /**
801
+ * List all queues
802
+ * @param profile_group_id {string} Optional profile group ID to filter queues
803
+ * @return {Promise<string>} List of queues as JSON
804
+ */
805
+ async queuesList(profile_group_id?: string): Promise<string> {
806
+ this.getApiKey();
807
+
808
+ const path = profile_group_id
809
+ ? `/post_queues?profile_group_id=${profile_group_id}`
810
+ : "/post_queues";
811
+ const response = await this.apiRequest<any>("GET", path);
812
+ const queues = this.extractArray<any>(response);
813
+
814
+ return JSON.stringify({ queues }, null, 2);
815
+ }
816
+
817
+ /**
818
+ * Get a single queue by ID
819
+ * @param queue_id {string} Queue ID
820
+ * @return {Promise<string>} Queue details as JSON
821
+ */
822
+ async queuesGet(queue_id: string): Promise<string> {
823
+ if (!queue_id) {
824
+ throw new Error("queue_id is required");
825
+ }
826
+
827
+ const queue = await this.apiRequest<any>("GET", `/post_queues/${queue_id}`);
828
+ return JSON.stringify(queue, null, 2);
829
+ }
830
+
831
+ /**
832
+ * Create a new posting queue
833
+ * @param profile_group_id {string} Profile group ID to connect the queue to
834
+ * @param name {string} Queue name
835
+ * @param description {string} Optional description
836
+ * @param timezone {string} Optional IANA timezone (default: UTC)
837
+ * @param jitter {number} Optional random offset in minutes (0-60)
838
+ * @param timeslots {string} Optional JSON array of timeslots [{day, time}]
839
+ * @return {Promise<string>} Created queue as JSON
840
+ */
841
+ async queuesCreate(
842
+ profile_group_id: string,
843
+ name: string,
844
+ description?: string,
845
+ timezone?: string,
846
+ jitter?: number,
847
+ timeslots?: string
848
+ ): Promise<string> {
849
+ this.getApiKey();
850
+
851
+ if (!profile_group_id) {
852
+ throw new Error("profile_group_id is required");
853
+ }
854
+ if (!name) {
855
+ throw new Error("name is required");
856
+ }
857
+
858
+ const apiPayload: any = {
859
+ profile_group_id,
860
+ post_queue: { name },
861
+ };
862
+ if (description !== undefined) {
863
+ apiPayload.post_queue.description = description;
864
+ }
865
+ if (timezone) {
866
+ apiPayload.post_queue.timezone = timezone;
867
+ }
868
+ if (jitter !== undefined) {
869
+ apiPayload.post_queue.jitter = jitter;
870
+ }
871
+ if (timeslots) {
872
+ try {
873
+ const parsed = JSON.parse(timeslots);
874
+ if (Array.isArray(parsed)) {
875
+ apiPayload.post_queue.queue_timeslots_attributes = parsed;
876
+ }
877
+ } catch {
878
+ throw new Error("Invalid timeslots parameter: must be valid JSON array");
879
+ }
880
+ }
881
+
882
+ const queue = await this.apiRequest<any>("POST", "/post_queues", apiPayload);
883
+ return JSON.stringify({ ...queue, message: "Queue created successfully" }, null, 2);
884
+ }
885
+
886
+ /**
887
+ * Update a queue's settings
888
+ * @param queue_id {string} Queue ID to update
889
+ * @param name {string} Optional new name
890
+ * @param description {string} Optional new description
891
+ * @param timezone {string} Optional IANA timezone
892
+ * @param enabled {boolean} Optional pause/unpause
893
+ * @param jitter {number} Optional random offset in minutes (0-60)
894
+ * @param timeslots {string} Optional JSON array of timeslots to add/remove
895
+ * @return {Promise<string>} Updated queue as JSON
896
+ */
897
+ async queuesUpdate(
898
+ queue_id: string,
899
+ name?: string,
900
+ description?: string,
901
+ timezone?: string,
902
+ enabled?: boolean,
903
+ jitter?: number,
904
+ timeslots?: string
905
+ ): Promise<string> {
906
+ this.getApiKey();
907
+
908
+ if (!queue_id) {
909
+ throw new Error("queue_id is required");
910
+ }
911
+
912
+ const apiPayload: any = { post_queue: {} };
913
+ if (name !== undefined) apiPayload.post_queue.name = name;
914
+ if (description !== undefined) apiPayload.post_queue.description = description;
915
+ if (timezone !== undefined) apiPayload.post_queue.timezone = timezone;
916
+ if (enabled !== undefined) apiPayload.post_queue.enabled = enabled;
917
+ if (jitter !== undefined) apiPayload.post_queue.jitter = jitter;
918
+ if (timeslots) {
919
+ try {
920
+ const parsed = JSON.parse(timeslots);
921
+ if (Array.isArray(parsed)) {
922
+ apiPayload.post_queue.queue_timeslots_attributes = parsed;
923
+ }
924
+ } catch {
925
+ throw new Error("Invalid timeslots parameter: must be valid JSON array");
926
+ }
927
+ }
928
+
929
+ const queue = await this.apiRequest<any>("PATCH", `/post_queues/${queue_id}`, apiPayload);
930
+ return JSON.stringify({ ...queue, message: "Queue updated successfully" }, null, 2);
931
+ }
932
+
933
+ /**
934
+ * Delete a posting queue
935
+ * @param queue_id {string} Queue ID to delete
936
+ * @return {Promise<string>} Deletion confirmation as JSON
937
+ */
938
+ async queuesDelete(queue_id: string): Promise<string> {
939
+ this.getApiKey();
940
+
941
+ if (!queue_id) {
942
+ throw new Error("queue_id is required");
943
+ }
944
+
945
+ await this.apiRequest<void>("DELETE", `/post_queues/${queue_id}`);
946
+ return JSON.stringify({ queue_id, deleted: true }, null, 2);
947
+ }
948
+
949
+ /**
950
+ * Get the next available timeslot for a queue
951
+ * @param queue_id {string} Queue ID
952
+ * @return {Promise<string>} Next slot as JSON
953
+ */
954
+ async queuesNextSlot(queue_id: string): Promise<string> {
955
+ this.getApiKey();
956
+
957
+ if (!queue_id) {
958
+ throw new Error("queue_id is required");
959
+ }
960
+
961
+ const result = await this.apiRequest<any>("GET", `/post_queues/${queue_id}/next_slot`);
962
+ return JSON.stringify(result, null, 2);
963
+ }
964
+
734
965
  /**
735
966
  * MCP tool definitions
736
967
  */
@@ -748,7 +979,7 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
748
979
  },
749
980
  {
750
981
  name: "postPublish",
751
- description: "Publish a post to specified targets",
982
+ description: "Publish a post to specified targets. Supports threads (X and Threads only).",
752
983
  inputSchema: {
753
984
  type: "object",
754
985
  properties: {
@@ -759,8 +990,11 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
759
990
  idempotency_key: { type: "string", description: "Optional idempotency key for deduplication" },
760
991
  require_confirmation: { type: "boolean", description: "If true, return summary without publishing" },
761
992
  draft: { type: "boolean", description: "If true, creates a draft post" },
762
- platforms: { type: "string", description: "Optional JSON string of platform-specific parameters" },
993
+ platforms: { type: "string", description: "Optional JSON string of platform-specific parameters. YouTube supports: title, privacy_status, cover_url, made_for_kids. TikTok supports: format (video|image), privacy_status, and more." },
763
994
  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...\"}]" },
995
+ thread: { type: "string", description: "Optional JSON array of thread child posts. Supported on X and Threads only. Each object must have 'body' (string), optionally 'media' (array of URLs). Example: [{\"body\":\"Reply 1\"},{\"body\":\"Reply 2\",\"media\":[\"https://...\"]}]" },
996
+ queue_id: { type: "string", description: "Optional queue ID to add the post to. The queue will automatically assign a timeslot. Do not use together with 'schedule'." },
997
+ queue_priority: { type: "string", description: "Optional priority when adding to a queue: high, medium (default), or low" },
764
998
  },
765
999
  required: ["content", "targets"],
766
1000
  },
@@ -834,6 +1068,83 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
834
1068
  required: ["profile_id"],
835
1069
  },
836
1070
  },
1071
+ {
1072
+ name: "queuesList",
1073
+ description: "List all posting queues. Queues automatically schedule posts into recurring weekly timeslots.",
1074
+ inputSchema: {
1075
+ type: "object",
1076
+ properties: {
1077
+ profile_group_id: { type: "string", description: "Optional profile group ID to filter queues" },
1078
+ },
1079
+ required: [],
1080
+ },
1081
+ },
1082
+ {
1083
+ name: "queuesGet",
1084
+ description: "Get details of a single posting queue including its timeslots and post count",
1085
+ inputSchema: {
1086
+ type: "object",
1087
+ properties: {
1088
+ queue_id: { type: "string", description: "Queue ID" },
1089
+ },
1090
+ required: ["queue_id"],
1091
+ },
1092
+ },
1093
+ {
1094
+ name: "queuesCreate",
1095
+ description: "Create a new posting queue with weekly timeslots",
1096
+ inputSchema: {
1097
+ type: "object",
1098
+ properties: {
1099
+ profile_group_id: { type: "string", description: "Profile group ID to connect the queue to" },
1100
+ name: { type: "string", description: "Queue name" },
1101
+ description: { type: "string", description: "Optional description" },
1102
+ timezone: { type: "string", description: "IANA timezone name (default: UTC)" },
1103
+ jitter: { type: "number", description: "Random offset in minutes (0-60, default: 0)" },
1104
+ timeslots: { type: "string", description: "Optional JSON array of timeslots [{\"day\":1,\"time\":\"09:00\"}]. Day: 0=Sun..6=Sat" },
1105
+ },
1106
+ required: ["profile_group_id", "name"],
1107
+ },
1108
+ },
1109
+ {
1110
+ name: "queuesUpdate",
1111
+ description: "Update a queue's settings, timeslots, or pause/unpause it",
1112
+ inputSchema: {
1113
+ type: "object",
1114
+ properties: {
1115
+ queue_id: { type: "string", description: "Queue ID to update" },
1116
+ name: { type: "string", description: "New queue name" },
1117
+ description: { type: "string", description: "New description" },
1118
+ timezone: { type: "string", description: "IANA timezone name" },
1119
+ enabled: { type: "boolean", description: "Set false to pause, true to unpause" },
1120
+ jitter: { type: "number", description: "Random offset in minutes (0-60)" },
1121
+ timeslots: { type: "string", description: "JSON array of timeslots to add [{\"day\":1,\"time\":\"09:00\"}] or remove [{\"id\":42,\"_destroy\":true}]" },
1122
+ },
1123
+ required: ["queue_id"],
1124
+ },
1125
+ },
1126
+ {
1127
+ name: "queuesDelete",
1128
+ description: "Delete a posting queue. Posts in the queue will not be deleted.",
1129
+ inputSchema: {
1130
+ type: "object",
1131
+ properties: {
1132
+ queue_id: { type: "string", description: "Queue ID to delete" },
1133
+ },
1134
+ required: ["queue_id"],
1135
+ },
1136
+ },
1137
+ {
1138
+ name: "queuesNextSlot",
1139
+ description: "Get the next available timeslot for a queue",
1140
+ inputSchema: {
1141
+ type: "object",
1142
+ properties: {
1143
+ queue_id: { type: "string", description: "Queue ID" },
1144
+ },
1145
+ required: ["queue_id"],
1146
+ },
1147
+ },
837
1148
  ];
838
1149
  }
839
1150
 
@@ -890,7 +1201,10 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
890
1201
  args.require_confirmation,
891
1202
  args.draft,
892
1203
  args.platforms,
893
- args.media_files
1204
+ args.media_files,
1205
+ args.thread,
1206
+ args.queue_id,
1207
+ args.queue_priority
894
1208
  );
895
1209
  break;
896
1210
  case "postStatus":
@@ -911,6 +1225,39 @@ export default class PostProxyMCP extends WorkerEntrypoint<Env> {
911
1225
  case "profilesPlacements":
912
1226
  result = await this.profilesPlacements(args.profile_id);
913
1227
  break;
1228
+ case "queuesList":
1229
+ result = await this.queuesList(args?.profile_group_id);
1230
+ break;
1231
+ case "queuesGet":
1232
+ result = await this.queuesGet(args.queue_id);
1233
+ break;
1234
+ case "queuesCreate":
1235
+ result = await this.queuesCreate(
1236
+ args.profile_group_id,
1237
+ args.name,
1238
+ args.description,
1239
+ args.timezone,
1240
+ args.jitter,
1241
+ args.timeslots
1242
+ );
1243
+ break;
1244
+ case "queuesUpdate":
1245
+ result = await this.queuesUpdate(
1246
+ args.queue_id,
1247
+ args.name,
1248
+ args.description,
1249
+ args.timezone,
1250
+ args.enabled,
1251
+ args.jitter,
1252
+ args.timeslots
1253
+ );
1254
+ break;
1255
+ case "queuesDelete":
1256
+ result = await this.queuesDelete(args.queue_id);
1257
+ break;
1258
+ case "queuesNextSlot":
1259
+ result = await this.queuesNextSlot(args.queue_id);
1260
+ break;
914
1261
  default:
915
1262
  return {
916
1263
  jsonrpc: "2.0",