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.
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Queue tools: queues.list, queues.get, queues.create, queues.update, queues.delete, queues.next_slot
3
+ */
4
+
5
+ import type { PostProxyClient } from "../api/client.js";
6
+ import { createError, ErrorCodes } from "../utils/errors.js";
7
+ import { logError } from "../utils/logger.js";
8
+
9
+ const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
10
+
11
+ function formatTimeslots(timeslots: Array<{ id: number; day: number; time: string }>): string[] {
12
+ return timeslots.map((ts) => `${DAY_NAMES[ts.day]} at ${ts.time} (id: ${ts.id})`);
13
+ }
14
+
15
+ export async function handleQueuesList(
16
+ client: PostProxyClient,
17
+ args: { profile_group_id?: string }
18
+ ) {
19
+ try {
20
+ const queues = await client.listQueues(args.profile_group_id);
21
+
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: JSON.stringify(
27
+ {
28
+ queues: queues.map((q) => ({
29
+ id: q.id,
30
+ name: q.name,
31
+ description: q.description,
32
+ timezone: q.timezone,
33
+ enabled: q.enabled,
34
+ jitter: q.jitter,
35
+ profile_group_id: q.profile_group_id,
36
+ timeslots: formatTimeslots(q.timeslots),
37
+ posts_count: q.posts_count,
38
+ })),
39
+ },
40
+ null,
41
+ 2
42
+ ),
43
+ },
44
+ ],
45
+ };
46
+ } catch (error) {
47
+ logError(error as Error, "queues.list");
48
+ throw createError(ErrorCodes.API_ERROR, `Failed to list queues: ${(error as Error).message}`);
49
+ }
50
+ }
51
+
52
+ export async function handleQueuesGet(
53
+ client: PostProxyClient,
54
+ args: { queue_id: string }
55
+ ) {
56
+ if (!args.queue_id) {
57
+ throw createError(ErrorCodes.VALIDATION_ERROR, "queue_id is required");
58
+ }
59
+
60
+ try {
61
+ const queue = await client.getQueue(args.queue_id);
62
+
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: JSON.stringify(
68
+ {
69
+ ...queue,
70
+ timeslots_formatted: formatTimeslots(queue.timeslots),
71
+ },
72
+ null,
73
+ 2
74
+ ),
75
+ },
76
+ ],
77
+ };
78
+ } catch (error) {
79
+ logError(error as Error, "queues.get");
80
+ throw createError(ErrorCodes.API_ERROR, `Failed to get queue: ${(error as Error).message}`);
81
+ }
82
+ }
83
+
84
+ export async function handleQueuesCreate(
85
+ client: PostProxyClient,
86
+ args: {
87
+ profile_group_id: string;
88
+ name: string;
89
+ description?: string;
90
+ timezone?: string;
91
+ jitter?: number;
92
+ timeslots?: Array<{ day: number; time: string }>;
93
+ }
94
+ ) {
95
+ if (!args.profile_group_id) {
96
+ throw createError(ErrorCodes.VALIDATION_ERROR, "profile_group_id is required");
97
+ }
98
+ if (!args.name) {
99
+ throw createError(ErrorCodes.VALIDATION_ERROR, "name is required");
100
+ }
101
+
102
+ try {
103
+ const queue = await client.createQueue({
104
+ profile_group_id: args.profile_group_id,
105
+ name: args.name,
106
+ description: args.description,
107
+ timezone: args.timezone,
108
+ jitter: args.jitter,
109
+ timeslots: args.timeslots,
110
+ });
111
+
112
+ return {
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: JSON.stringify(
117
+ {
118
+ ...queue,
119
+ timeslots_formatted: formatTimeslots(queue.timeslots),
120
+ message: "Queue created successfully",
121
+ },
122
+ null,
123
+ 2
124
+ ),
125
+ },
126
+ ],
127
+ };
128
+ } catch (error) {
129
+ logError(error as Error, "queues.create");
130
+ throw createError(ErrorCodes.API_ERROR, `Failed to create queue: ${(error as Error).message}`);
131
+ }
132
+ }
133
+
134
+ export async function handleQueuesUpdate(
135
+ client: PostProxyClient,
136
+ args: {
137
+ queue_id: string;
138
+ name?: string;
139
+ description?: string;
140
+ timezone?: string;
141
+ enabled?: boolean;
142
+ jitter?: number;
143
+ timeslots?: Array<{ day: number; time: string } | { id: number; _destroy: true }>;
144
+ }
145
+ ) {
146
+ if (!args.queue_id) {
147
+ throw createError(ErrorCodes.VALIDATION_ERROR, "queue_id is required");
148
+ }
149
+
150
+ try {
151
+ const queue = await client.updateQueue(args.queue_id, {
152
+ name: args.name,
153
+ description: args.description,
154
+ timezone: args.timezone,
155
+ enabled: args.enabled,
156
+ jitter: args.jitter,
157
+ timeslots: args.timeslots,
158
+ });
159
+
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: JSON.stringify(
165
+ {
166
+ ...queue,
167
+ timeslots_formatted: formatTimeslots(queue.timeslots),
168
+ message: "Queue updated successfully",
169
+ },
170
+ null,
171
+ 2
172
+ ),
173
+ },
174
+ ],
175
+ };
176
+ } catch (error) {
177
+ logError(error as Error, "queues.update");
178
+ throw createError(ErrorCodes.API_ERROR, `Failed to update queue: ${(error as Error).message}`);
179
+ }
180
+ }
181
+
182
+ export async function handleQueuesDelete(
183
+ client: PostProxyClient,
184
+ args: { queue_id: string }
185
+ ) {
186
+ if (!args.queue_id) {
187
+ throw createError(ErrorCodes.VALIDATION_ERROR, "queue_id is required");
188
+ }
189
+
190
+ try {
191
+ await client.deleteQueue(args.queue_id);
192
+
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: JSON.stringify({ queue_id: args.queue_id, deleted: true }, null, 2),
198
+ },
199
+ ],
200
+ };
201
+ } catch (error) {
202
+ logError(error as Error, "queues.delete");
203
+ throw createError(ErrorCodes.API_ERROR, `Failed to delete queue: ${(error as Error).message}`);
204
+ }
205
+ }
206
+
207
+ export async function handleQueuesNextSlot(
208
+ client: PostProxyClient,
209
+ args: { queue_id: string }
210
+ ) {
211
+ if (!args.queue_id) {
212
+ throw createError(ErrorCodes.VALIDATION_ERROR, "queue_id is required");
213
+ }
214
+
215
+ try {
216
+ const result = await client.getQueueNextSlot(args.queue_id);
217
+
218
+ return {
219
+ content: [
220
+ {
221
+ type: "text",
222
+ text: JSON.stringify(result, null, 2),
223
+ },
224
+ ],
225
+ };
226
+ } catch (error) {
227
+ logError(error as Error, "queues.next_slot");
228
+ throw createError(ErrorCodes.API_ERROR, `Failed to get next slot: ${(error as Error).message}`);
229
+ }
230
+ }
@@ -21,6 +21,20 @@ export interface Profile {
21
21
  avatar_url?: string;
22
22
  }
23
23
 
24
+ export interface ThreadChild {
25
+ body: string;
26
+ media?: string[];
27
+ }
28
+
29
+ export interface MediaAttachment {
30
+ id: string;
31
+ status: "pending" | "processed" | "failed";
32
+ error_message: string | null;
33
+ content_type: string;
34
+ source_url: string | null;
35
+ url: string | null;
36
+ }
37
+
24
38
  export interface CreatePostParams {
25
39
  content: string;
26
40
  profile_group_id?: number; // Not used by API, kept for compatibility
@@ -30,17 +44,22 @@ export interface CreatePostParams {
30
44
  idempotency_key?: string;
31
45
  draft?: boolean; // If true, creates a draft post that won't publish automatically
32
46
  platforms?: PlatformParams; // Platform-specific parameters
47
+ thread?: ThreadChild[]; // Thread posts (supported on X and Threads)
48
+ queue_id?: string; // Queue ID to add post to
49
+ queue_priority?: "high" | "medium" | "low"; // Queue priority (default: medium)
33
50
  }
34
51
 
35
52
  export interface CreatePostResponse {
36
53
  id: string;
37
54
  body?: string; // API returns "body" field
38
55
  content?: string; // Some responses use "content"
39
- status: "draft" | "pending" | "processing" | "processed" | "scheduled";
56
+ status: "draft" | "pending" | "processing" | "processed" | "scheduled" | "media_processing_failed";
40
57
  draft: boolean;
41
58
  scheduled_at: string | null;
42
59
  created_at: string;
60
+ media?: MediaAttachment[];
43
61
  platforms: PlatformOutcome[];
62
+ thread?: Array<{ id: string; body: string; media?: MediaAttachment[] }>;
44
63
  }
45
64
 
46
65
  export interface PlatformOutcome {
@@ -62,24 +81,28 @@ export interface PostDetails {
62
81
  id: string;
63
82
  body?: string; // API returns "body" field
64
83
  content?: string; // Some responses use "content"
65
- status: "draft" | "pending" | "processing" | "processed" | "scheduled";
84
+ status: "draft" | "pending" | "processing" | "processed" | "scheduled" | "media_processing_failed";
66
85
  draft: boolean;
67
86
  scheduled_at: string | null;
68
87
  created_at: string;
69
88
  updated_at?: string;
89
+ media?: MediaAttachment[];
70
90
  platforms: PlatformOutcome[];
91
+ thread?: Array<{ id: string; body: string; media?: MediaAttachment[] }>;
71
92
  }
72
93
 
73
94
  export interface Post {
74
95
  id: string;
75
96
  body?: string; // API returns "body" field
76
97
  content?: string; // Some responses use "content"
77
- status: "draft" | "pending" | "processing" | "processed" | "scheduled";
98
+ status: "draft" | "pending" | "processing" | "processed" | "scheduled" | "media_processing_failed";
78
99
  draft: boolean;
79
100
  scheduled_at: string | null;
80
101
  created_at: string;
81
102
  updated_at?: string;
103
+ media?: MediaAttachment[];
82
104
  platforms: PlatformOutcome[];
105
+ thread?: Array<{ id: string; body: string; media?: MediaAttachment[] }>;
83
106
  }
84
107
 
85
108
  /**
@@ -102,21 +125,23 @@ export interface YouTubeParams {
102
125
  title?: string; // Video title
103
126
  privacy_status?: "public" | "unlisted" | "private"; // Video visibility
104
127
  cover_url?: string; // Custom thumbnail URL
128
+ made_for_kids?: boolean; // Whether the video is made for kids
105
129
  }
106
130
 
107
131
  /**
108
132
  * Platform-specific parameters for TikTok
109
133
  */
110
134
  export interface TikTokParams {
135
+ format?: "video" | "image"; // Content format (video is default)
111
136
  privacy_status?: "PUBLIC_TO_EVERYONE" | "MUTUAL_FOLLOW_FRIENDS" | "FOLLOWER_OF_CREATOR" | "SELF_ONLY";
112
- photo_cover_index?: number; // Index (0-based) of photo to use as cover
113
- auto_add_music?: boolean; // Enable automatic music
114
- made_with_ai?: boolean; // Mark content as AI-generated
115
- disable_comment?: boolean; // Disable comments on the post
116
- disable_duet?: boolean; // Disable duets
117
- disable_stitch?: boolean; // Disable stitches
118
- brand_content_toggle?: boolean; // Mark video as paid partnership promoting a third-party business
119
- brand_organic_toggle?: boolean; // Mark video as paid partnership promoting your own brand
137
+ photo_cover_index?: number; // Index (0-based) of photo to use as cover (image format)
138
+ auto_add_music?: boolean; // Enable automatic music (image format)
139
+ made_with_ai?: boolean; // Mark content as AI-generated (video format)
140
+ disable_comment?: boolean; // Disable comments
141
+ disable_duet?: boolean; // Disable duets (video format)
142
+ disable_stitch?: boolean; // Disable stitches (video format)
143
+ brand_content_toggle?: boolean; // Mark as paid partnership promoting a third-party business
144
+ brand_organic_toggle?: boolean; // Mark as paid partnership promoting your own brand
120
145
  }
121
146
 
122
147
  /**
@@ -198,3 +223,51 @@ export interface PostStats {
198
223
  export interface StatsResponse {
199
224
  data: Record<string, PostStats>;
200
225
  }
226
+
227
+ /**
228
+ * Queue timeslot definition
229
+ */
230
+ export interface QueueTimeslot {
231
+ id: number;
232
+ day: number; // 0=Sunday through 6=Saturday
233
+ time: string; // HH:MM format
234
+ }
235
+
236
+ /**
237
+ * Post queue
238
+ */
239
+ export interface PostQueue {
240
+ id: string;
241
+ name: string;
242
+ description: string | null;
243
+ timezone: string;
244
+ enabled: boolean;
245
+ jitter: number;
246
+ profile_group_id: string;
247
+ timeslots: QueueTimeslot[];
248
+ posts_count: number;
249
+ }
250
+
251
+ /**
252
+ * Parameters for creating a queue
253
+ */
254
+ export interface CreateQueueParams {
255
+ profile_group_id: string;
256
+ name: string;
257
+ description?: string;
258
+ timezone?: string;
259
+ jitter?: number;
260
+ timeslots?: Array<{ day: number; time: string }>;
261
+ }
262
+
263
+ /**
264
+ * Parameters for updating a queue
265
+ */
266
+ export interface UpdateQueueParams {
267
+ name?: string;
268
+ description?: string;
269
+ timezone?: string;
270
+ enabled?: boolean;
271
+ jitter?: number;
272
+ timeslots?: Array<{ day: number; time: string } | { id: number; _destroy: true }>;
273
+ }
@@ -89,10 +89,14 @@ export const YouTubeParamsSchema = z.object({
89
89
  errorMap: () => ({ message: "YouTube privacy_status must be 'public', 'unlisted', or 'private'" }),
90
90
  }).optional(),
91
91
  cover_url: URLSchema.optional(),
92
+ made_for_kids: z.boolean().optional(),
92
93
  }).strict();
93
94
 
94
95
  // TikTok parameters validation
95
96
  export const TikTokParamsSchema = z.object({
97
+ format: z.enum(["video", "image"], {
98
+ errorMap: () => ({ message: "TikTok format must be 'video' or 'image'" }),
99
+ }).optional(),
96
100
  privacy_status: z.enum([
97
101
  "PUBLIC_TO_EVERYONE",
98
102
  "MUTUAL_FOLLOW_FRIENDS",
@@ -142,6 +146,14 @@ export const PlatformParamsSchema = z.object({
142
146
  threads: ThreadsParamsSchema.optional(),
143
147
  }).strict().optional();
144
148
 
149
+ /**
150
+ * Schema for thread child posts
151
+ */
152
+ export const ThreadChildSchema = z.object({
153
+ body: z.string().min(1, "Thread child body cannot be empty"),
154
+ media: z.array(MediaItemSchema).optional(),
155
+ });
156
+
145
157
  /**
146
158
  * Schema for post.publish parameters
147
159
  */
@@ -154,6 +166,7 @@ export const PostPublishSchema = z.object({
154
166
  require_confirmation: z.boolean().optional(),
155
167
  draft: z.boolean().optional(),
156
168
  platforms: PlatformParamsSchema,
169
+ thread: z.array(ThreadChildSchema).optional(),
157
170
  });
158
171
 
159
172
  /**