@exileum/meta-mcp 6.0.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -18
- package/dist/constants/fields.d.ts +6 -0
- package/dist/constants/fields.d.ts.map +1 -0
- package/dist/constants/fields.js +25 -0
- package/dist/constants/fields.js.map +1 -0
- package/dist/http-transport.d.ts +20 -0
- package/dist/http-transport.d.ts.map +1 -0
- package/dist/http-transport.js +228 -0
- package/dist/http-transport.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -82
- package/dist/index.js.map +1 -1
- package/dist/prompts/index.d.ts +56 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +264 -41
- package/dist/prompts/index.js.map +1 -1
- package/dist/register-all.d.ts +4 -0
- package/dist/register-all.d.ts.map +1 -0
- package/dist/register-all.js +41 -0
- package/dist/register-all.js.map +1 -0
- package/dist/resources/instagram.d.ts.map +1 -1
- package/dist/resources/instagram.js +13 -4
- package/dist/resources/instagram.js.map +1 -1
- package/dist/resources/threads.d.ts.map +1 -1
- package/dist/resources/threads.js +13 -4
- package/dist/resources/threads.js.map +1 -1
- package/dist/services/meta-client.d.ts +76 -3
- package/dist/services/meta-client.d.ts.map +1 -1
- package/dist/services/meta-client.js +330 -46
- package/dist/services/meta-client.js.map +1 -1
- package/dist/shutdown.d.ts +18 -0
- package/dist/shutdown.d.ts.map +1 -0
- package/dist/shutdown.js +60 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/tools/annotations.d.ts +6 -0
- package/dist/tools/annotations.d.ts.map +1 -0
- package/dist/tools/annotations.js +27 -0
- package/dist/tools/annotations.js.map +1 -0
- package/dist/tools/instagram/comments.d.ts.map +1 -1
- package/dist/tools/instagram/comments.js +75 -50
- package/dist/tools/instagram/comments.js.map +1 -1
- package/dist/tools/instagram/hashtags.d.ts +1 -0
- package/dist/tools/instagram/hashtags.d.ts.map +1 -1
- package/dist/tools/instagram/hashtags.js +54 -36
- package/dist/tools/instagram/hashtags.js.map +1 -1
- package/dist/tools/instagram/media.d.ts.map +1 -1
- package/dist/tools/instagram/media.js +53 -34
- package/dist/tools/instagram/media.js.map +1 -1
- package/dist/tools/instagram/mentions.d.ts.map +1 -1
- package/dist/tools/instagram/mentions.js +22 -17
- package/dist/tools/instagram/mentions.js.map +1 -1
- package/dist/tools/instagram/messaging.d.ts.map +1 -1
- package/dist/tools/instagram/messaging.js +74 -50
- package/dist/tools/instagram/messaging.js.map +1 -1
- package/dist/tools/instagram/profile.d.ts +2 -0
- package/dist/tools/instagram/profile.d.ts.map +1 -1
- package/dist/tools/instagram/profile.js +63 -43
- package/dist/tools/instagram/profile.js.map +1 -1
- package/dist/tools/instagram/publishing.d.ts.map +1 -1
- package/dist/tools/instagram/publishing.js +202 -139
- package/dist/tools/instagram/publishing.js.map +1 -1
- package/dist/tools/meta/auth.d.ts.map +1 -1
- package/dist/tools/meta/auth.js +63 -21
- package/dist/tools/meta/auth.js.map +1 -1
- package/dist/tools/threads/insights.d.ts.map +1 -1
- package/dist/tools/threads/insights.js +22 -15
- package/dist/tools/threads/insights.js.map +1 -1
- package/dist/tools/threads/media.d.ts.map +1 -1
- package/dist/tools/threads/media.js +42 -70
- package/dist/tools/threads/media.js.map +1 -1
- package/dist/tools/threads/mentions.d.ts.map +1 -1
- package/dist/tools/threads/mentions.js +15 -16
- package/dist/tools/threads/mentions.js.map +1 -1
- package/dist/tools/threads/profile.d.ts +2 -0
- package/dist/tools/threads/profile.d.ts.map +1 -1
- package/dist/tools/threads/profile.js +18 -3
- package/dist/tools/threads/profile.js.map +1 -1
- package/dist/tools/threads/publishing.d.ts.map +1 -1
- package/dist/tools/threads/publishing.js +260 -157
- package/dist/tools/threads/publishing.js.map +1 -1
- package/dist/tools/threads/replies.d.ts.map +1 -1
- package/dist/tools/threads/replies.js +55 -45
- package/dist/tools/threads/replies.js.map +1 -1
- package/dist/utils/container.d.ts +16 -4
- package/dist/utils/container.d.ts.map +1 -1
- package/dist/utils/container.js +36 -6
- package/dist/utils/container.js.map +1 -1
- package/dist/utils/errors.d.ts +23 -3
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +32 -2
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/logger.d.ts +28 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/params.d.ts +3 -0
- package/dist/utils/params.d.ts.map +1 -0
- package/dist/utils/params.js +14 -0
- package/dist/utils/params.js.map +1 -0
- package/dist/utils/progress.d.ts +42 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +35 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/response.d.ts +11 -0
- package/dist/utils/response.d.ts.map +1 -0
- package/dist/utils/response.js +6 -0
- package/dist/utils/response.js.map +1 -0
- package/package.json +5 -4
|
@@ -2,6 +2,11 @@ import { z } from "zod";
|
|
|
2
2
|
import { httpsUrl, metaId } from "../../schemas.js";
|
|
3
3
|
import { waitForThreadsContainer, IMAGE_PROCESSING_TIMEOUT, VIDEO_PROCESSING_TIMEOUT } from "../../utils/container.js";
|
|
4
4
|
import { formatErrorResponse, validationError } from "../../utils/errors.js";
|
|
5
|
+
import { formatResponse } from "../../utils/response.js";
|
|
6
|
+
import { buildParams } from "../../utils/params.js";
|
|
7
|
+
import { makeProgressNotifier } from "../../utils/progress.js";
|
|
8
|
+
import { READ_ONLY_TOOL, DESTRUCTIVE_TOOL, WRITE_TOOL } from "../annotations.js";
|
|
9
|
+
import { THREADS_PROFILE_CACHE_PREFIX } from "./profile.js";
|
|
5
10
|
export const topicTagSchema = z.string().min(1).max(50).regex(/^[^.&]+$/, "Topic tags cannot contain periods or ampersands").optional().describe("Topic tag for the post (1-50 chars, no periods or ampersands)");
|
|
6
11
|
export const shareToIgStorySchema = z.enum(["light", "dark"]).optional().describe("Cross-share this post to linked Instagram as a Story. 'light' = normal, 'dark' = dark mode. Requires threads_share_to_instagram permission and a linked Instagram account. The Threads post still publishes even if cross-share fails.");
|
|
7
12
|
export const pollOptionsSchema = z.array(z.string().min(1).max(25)).min(2).max(4).optional().describe("Poll options (2-4 choices, each 1-25 chars). Creates a poll attachment. Cannot be combined with text_attachment, link_attachment, or gif_id+gif_provider.");
|
|
@@ -34,24 +39,31 @@ function applyAllowlistedCountryCodes(params, codes) {
|
|
|
34
39
|
}
|
|
35
40
|
export function registerThreadsPublishingTools(server, client) {
|
|
36
41
|
// ─── threads_publish_text ────────────────────────────────────
|
|
37
|
-
server.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
server.registerTool("threads_publish_text", {
|
|
43
|
+
description: "Publish a text-only post on Threads. By default publishes in a single API call via auto_publish_text=true (faster and avoids the 4279009 'container not propagated' race condition). Supports optional link attachment, poll, GIF, topic tag, quote post, cross-share to Instagram Stories, geo-gating via allowlisted_country_codes, location tagging via location_id, and text_attachment for long-form content (up to 10,000 chars with optional styling and link). Only one attachment type per post — text_attachment, poll_options, link_attachment, and gif_id+gif_provider are mutually exclusive. Set auto_publish=false to fall back to the legacy two-step create-then-publish flow.",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
text: z.string().max(500).describe("Post text (max 500 chars)"),
|
|
46
|
+
reply_control: replyControlSchema,
|
|
47
|
+
link_attachment: z.string().url().optional().describe("URL to attach as a link preview card (max 5 links per post). Cannot be combined with text_attachment, poll_options, or gif_id+gif_provider."),
|
|
48
|
+
topic_tag: topicTagSchema,
|
|
49
|
+
quote_post_id: z.string().optional().describe("ID of a post to quote"),
|
|
50
|
+
poll_options: pollOptionsSchema,
|
|
51
|
+
gif_id: z.string().min(1).optional().describe("GIPHY GIF ID. Must be provided together with gif_provider — providing only one returns an error."),
|
|
52
|
+
gif_provider: z.enum(["GIPHY"]).optional().describe("GIF provider. Only GIPHY is currently supported. Must be provided together with gif_id — providing only one returns an error."),
|
|
53
|
+
is_spoiler: z.boolean().optional().describe("Mark content as spoiler"),
|
|
54
|
+
share_to_ig_story: shareToIgStorySchema,
|
|
55
|
+
allowlisted_country_codes: allowlistedCountryCodesSchema,
|
|
56
|
+
location_id: z.string().optional().describe("Location ID for tagging the post. Use threads_search_locations to find IDs. Requires the threads_location_tagging permission on the access token."),
|
|
57
|
+
text_attachment: z.string().min(1).max(10000).optional().describe("Long-form text attachment (max 10,000 chars). Renders as expandable 'Read more' block beneath the primary text. Cannot be combined with poll_options, link_attachment, or gif_id+gif_provider."),
|
|
58
|
+
text_attachment_link: z.string().url().optional().describe("URL to include inside the text attachment card. Requires text_attachment."),
|
|
59
|
+
text_attachment_styling: textAttachmentStylingSchema,
|
|
60
|
+
auto_publish: z.boolean().optional().default(true).describe("When true (default), combine container creation and publishing into a single API call via auto_publish_text=true — one HTTP request instead of two, and no risk of the 4279009 'container not propagated yet' race. Set to false to fall back to the legacy two-step flow (POST /threads, then POST /threads_publish)."),
|
|
61
|
+
alt_text: z.never("alt_text is not supported on text-only Threads posts (media_type=TEXT). Use threads_publish_image, threads_publish_video, or threads_publish_carousel for accessibility labels — those tools accept alt_text on IMAGE/VIDEO/CAROUSEL containers.").optional().describe("Reserved — must be omitted. alt_text is only supported on image, video, and carousel posts; passing it here raises a Zod schema error."),
|
|
62
|
+
},
|
|
63
|
+
annotations: WRITE_TOOL,
|
|
64
|
+
}, async ({ text, reply_control, link_attachment, topic_tag, quote_post_id, poll_options, gif_id, gif_provider, is_spoiler, share_to_ig_story, allowlisted_country_codes, location_id, text_attachment, text_attachment_link, text_attachment_styling, auto_publish }) => {
|
|
65
|
+
let step = "container creation";
|
|
66
|
+
let containerId;
|
|
55
67
|
try {
|
|
56
68
|
// Validate mutual exclusions
|
|
57
69
|
if (text_attachment && poll_options) {
|
|
@@ -79,8 +91,9 @@ export function registerThreadsPublishingTools(server, client) {
|
|
|
79
91
|
const sorted = [...text_attachment_styling].sort((a, b) => a.offset - b.offset);
|
|
80
92
|
for (let i = 1; i < sorted.length; i++) {
|
|
81
93
|
const prev = sorted[i - 1];
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
const curr = sorted[i];
|
|
95
|
+
if (curr.offset < prev.offset + prev.length) {
|
|
96
|
+
return validationError(`text_attachment_styling ranges must not overlap: range at offset ${curr.offset} overlaps with range at offset ${prev.offset}`);
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
}
|
|
@@ -91,15 +104,7 @@ export function registerThreadsPublishingTools(server, client) {
|
|
|
91
104
|
if (gif_id && (text_attachment || poll_options || link_attachment)) {
|
|
92
105
|
return validationError("GIF attachment cannot be combined with text_attachment, poll_options, or link_attachment");
|
|
93
106
|
}
|
|
94
|
-
|
|
95
|
-
if (reply_control)
|
|
96
|
-
params.reply_control = reply_control;
|
|
97
|
-
if (link_attachment)
|
|
98
|
-
params.link_attachment = link_attachment;
|
|
99
|
-
if (topic_tag)
|
|
100
|
-
params.topic_tag = topic_tag;
|
|
101
|
-
if (quote_post_id)
|
|
102
|
-
params.quote_post_id = quote_post_id;
|
|
107
|
+
let pollAttachment;
|
|
103
108
|
if (poll_options) {
|
|
104
109
|
const pollObj = {};
|
|
105
110
|
poll_options.forEach((opt, i) => {
|
|
@@ -107,13 +112,12 @@ export function registerThreadsPublishingTools(server, client) {
|
|
|
107
112
|
if (key)
|
|
108
113
|
pollObj[key] = opt;
|
|
109
114
|
});
|
|
110
|
-
|
|
115
|
+
pollAttachment = JSON.stringify(pollObj);
|
|
111
116
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
params.is_spoiler_media = true;
|
|
117
|
+
const gifAttachment = gif_id && gif_provider
|
|
118
|
+
? JSON.stringify({ gif_id, provider: gif_provider })
|
|
119
|
+
: undefined;
|
|
120
|
+
let textAttachmentJson;
|
|
117
121
|
if (text_attachment) {
|
|
118
122
|
const obj = { plaintext: text_attachment };
|
|
119
123
|
if (text_attachment_link)
|
|
@@ -127,199 +131,258 @@ export function registerThreadsPublishingTools(server, client) {
|
|
|
127
131
|
styling_info: s.styles,
|
|
128
132
|
}));
|
|
129
133
|
}
|
|
130
|
-
|
|
134
|
+
textAttachmentJson = JSON.stringify(obj);
|
|
131
135
|
}
|
|
132
|
-
applyShareToIgStory(params, share_to_ig_story);
|
|
133
|
-
applyAllowlistedCountryCodes(params, allowlisted_country_codes);
|
|
134
136
|
// Treat `undefined` as the default (true) so the behavior is stable even
|
|
135
137
|
// if a caller bypasses Zod's schema-level default (e.g., direct handler
|
|
136
138
|
// invocation in tests). Only an explicit `false` takes the legacy path.
|
|
137
139
|
const useAutoPublish = auto_publish !== false;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
const params = buildParams({ media_type: "TEXT", text }, {
|
|
141
|
+
reply_control,
|
|
142
|
+
link_attachment,
|
|
143
|
+
topic_tag,
|
|
144
|
+
quote_post_id,
|
|
145
|
+
poll_attachment: pollAttachment,
|
|
146
|
+
gif_attachment: gifAttachment,
|
|
147
|
+
is_spoiler_media: is_spoiler ? true : undefined,
|
|
148
|
+
text_attachment: textAttachmentJson,
|
|
149
|
+
location_id,
|
|
150
|
+
auto_publish_text: useAutoPublish ? true : undefined,
|
|
151
|
+
});
|
|
152
|
+
applyShareToIgStory(params, share_to_ig_story);
|
|
153
|
+
applyAllowlistedCountryCodes(params, allowlisted_country_codes);
|
|
140
154
|
// `createResponse.id` is the published post id when useAutoPublish is true,
|
|
141
155
|
// and the unpublished container id otherwise.
|
|
142
156
|
const { data: createResponse, rateLimit: createRateLimit } = await client.threads("POST", `/${client.threadsUserId}/threads`, params);
|
|
143
157
|
if (typeof createResponse.id !== "string")
|
|
144
158
|
throw new Error("Container creation did not return a valid id");
|
|
145
159
|
if (useAutoPublish) {
|
|
146
|
-
|
|
160
|
+
client.invalidateCache(THREADS_PROFILE_CACHE_PREFIX);
|
|
161
|
+
return formatResponse(createResponse, createRateLimit);
|
|
147
162
|
}
|
|
163
|
+
containerId = createResponse.id;
|
|
164
|
+
step = "publishing";
|
|
148
165
|
const { data, rateLimit } = await client.threads("POST", `/${client.threadsUserId}/threads_publish`, {
|
|
149
|
-
creation_id:
|
|
166
|
+
creation_id: containerId,
|
|
150
167
|
});
|
|
151
|
-
|
|
168
|
+
client.invalidateCache(THREADS_PROFILE_CACHE_PREFIX);
|
|
169
|
+
return formatResponse(data, rateLimit);
|
|
152
170
|
}
|
|
153
171
|
catch (error) {
|
|
154
|
-
return formatErrorResponse(error, "Publish text");
|
|
172
|
+
return formatErrorResponse(error, "Publish text", { step, containerId });
|
|
155
173
|
}
|
|
156
174
|
});
|
|
157
175
|
// ─── threads_publish_image ───────────────────────────────────
|
|
158
|
-
server.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
server.registerTool("threads_publish_image", {
|
|
177
|
+
description: "Publish an image post on Threads. Supports topic tag, quote post, alt text, spoiler flag, cross-share to Instagram Stories, geo-gating via allowlisted_country_codes, and location tagging via location_id.",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
image_url: httpsUrl.describe("Public HTTPS URL of the image (JPEG/PNG, max 8MB)"),
|
|
180
|
+
text: z.string().max(500).optional().describe("Caption text"),
|
|
181
|
+
reply_control: replyControlSchema,
|
|
182
|
+
topic_tag: topicTagSchema,
|
|
183
|
+
quote_post_id: z.string().optional().describe("ID of a post to quote"),
|
|
184
|
+
alt_text: z.string().max(1000).optional().describe("Alt text for accessibility (max 1000 chars)"),
|
|
185
|
+
is_spoiler: z.boolean().optional().describe("Mark content as spoiler"),
|
|
186
|
+
share_to_ig_story: shareToIgStorySchema,
|
|
187
|
+
allowlisted_country_codes: allowlistedCountryCodesSchema,
|
|
188
|
+
location_id: z.string().optional().describe("Location ID for tagging the post. Use threads_search_locations to find IDs. Requires the threads_location_tagging permission on the access token."),
|
|
189
|
+
},
|
|
190
|
+
annotations: WRITE_TOOL,
|
|
191
|
+
}, async ({ image_url, text, reply_control, topic_tag, quote_post_id, alt_text, is_spoiler, share_to_ig_story, allowlisted_country_codes, location_id }, extra) => {
|
|
192
|
+
let step = "container creation";
|
|
193
|
+
let containerId;
|
|
169
194
|
try {
|
|
170
|
-
const params = { media_type: "IMAGE", image_url }
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (alt_text)
|
|
180
|
-
params.alt_text = alt_text;
|
|
181
|
-
if (is_spoiler)
|
|
182
|
-
params.is_spoiler_media = true;
|
|
195
|
+
const params = buildParams({ media_type: "IMAGE", image_url }, {
|
|
196
|
+
text,
|
|
197
|
+
reply_control,
|
|
198
|
+
topic_tag,
|
|
199
|
+
quote_post_id,
|
|
200
|
+
alt_text,
|
|
201
|
+
is_spoiler_media: is_spoiler ? true : undefined,
|
|
202
|
+
location_id,
|
|
203
|
+
});
|
|
183
204
|
applyShareToIgStory(params, share_to_ig_story);
|
|
184
205
|
applyAllowlistedCountryCodes(params, allowlisted_country_codes);
|
|
185
206
|
const { data: container } = await client.threads("POST", `/${client.threadsUserId}/threads`, params);
|
|
186
207
|
if (typeof container.id !== "string")
|
|
187
208
|
throw new Error("Container creation did not return a valid id");
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
containerId = container.id;
|
|
210
|
+
step = "processing";
|
|
211
|
+
await waitForThreadsContainer(client, containerId, IMAGE_PROCESSING_TIMEOUT, { onProgress: makeProgressNotifier(extra) });
|
|
212
|
+
step = "publishing";
|
|
190
213
|
const { data, rateLimit } = await client.threads("POST", `/${client.threadsUserId}/threads_publish`, {
|
|
191
214
|
creation_id: containerId,
|
|
192
215
|
});
|
|
193
|
-
|
|
216
|
+
client.invalidateCache(THREADS_PROFILE_CACHE_PREFIX);
|
|
217
|
+
return formatResponse(data, rateLimit);
|
|
194
218
|
}
|
|
195
219
|
catch (error) {
|
|
196
|
-
return formatErrorResponse(error, "Publish image");
|
|
220
|
+
return formatErrorResponse(error, "Publish image", { step, containerId });
|
|
197
221
|
}
|
|
198
222
|
});
|
|
199
223
|
// ─── threads_publish_video ───────────────────────────────────
|
|
200
|
-
server.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
server.registerTool("threads_publish_video", {
|
|
225
|
+
description: "Publish a video post on Threads. Waits for video processing. Supports topic tag, quote post, alt text, spoiler flag, cross-share to Instagram Stories, geo-gating via allowlisted_country_codes, and location tagging via location_id. Note: cross-share to IG Stories may silently fail for video posts (the Threads post still publishes).",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
video_url: httpsUrl.describe("Public HTTPS URL of the video (MP4/MOV, max 1GB, up to 5 min)"),
|
|
228
|
+
text: z.string().max(500).optional().describe("Caption text"),
|
|
229
|
+
reply_control: replyControlSchema,
|
|
230
|
+
topic_tag: topicTagSchema,
|
|
231
|
+
quote_post_id: z.string().optional().describe("ID of a post to quote"),
|
|
232
|
+
alt_text: z.string().max(1000).optional().describe("Alt text for accessibility (max 1000 chars)"),
|
|
233
|
+
is_spoiler: z.boolean().optional().describe("Mark content as spoiler"),
|
|
234
|
+
share_to_ig_story: shareToIgStorySchema,
|
|
235
|
+
allowlisted_country_codes: allowlistedCountryCodesSchema,
|
|
236
|
+
location_id: z.string().optional().describe("Location ID for tagging the post. Use threads_search_locations to find IDs. Requires the threads_location_tagging permission on the access token."),
|
|
237
|
+
},
|
|
238
|
+
annotations: WRITE_TOOL,
|
|
239
|
+
}, async ({ video_url, text, reply_control, topic_tag, quote_post_id, alt_text, is_spoiler, share_to_ig_story, allowlisted_country_codes, location_id }, extra) => {
|
|
240
|
+
let step = "container creation";
|
|
241
|
+
let containerId;
|
|
211
242
|
try {
|
|
212
|
-
const params = { media_type: "VIDEO", video_url }
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (alt_text)
|
|
222
|
-
params.alt_text = alt_text;
|
|
223
|
-
if (is_spoiler)
|
|
224
|
-
params.is_spoiler_media = true;
|
|
243
|
+
const params = buildParams({ media_type: "VIDEO", video_url }, {
|
|
244
|
+
text,
|
|
245
|
+
reply_control,
|
|
246
|
+
topic_tag,
|
|
247
|
+
quote_post_id,
|
|
248
|
+
alt_text,
|
|
249
|
+
is_spoiler_media: is_spoiler ? true : undefined,
|
|
250
|
+
location_id,
|
|
251
|
+
});
|
|
225
252
|
applyShareToIgStory(params, share_to_ig_story);
|
|
226
253
|
applyAllowlistedCountryCodes(params, allowlisted_country_codes);
|
|
227
254
|
const { data: container } = await client.threads("POST", `/${client.threadsUserId}/threads`, params);
|
|
228
255
|
if (typeof container.id !== "string")
|
|
229
256
|
throw new Error("Container creation did not return a valid id");
|
|
230
|
-
|
|
231
|
-
|
|
257
|
+
containerId = container.id;
|
|
258
|
+
step = "processing";
|
|
259
|
+
await waitForThreadsContainer(client, containerId, VIDEO_PROCESSING_TIMEOUT, { onProgress: makeProgressNotifier(extra) });
|
|
260
|
+
step = "publishing";
|
|
232
261
|
const { data, rateLimit } = await client.threads("POST", `/${client.threadsUserId}/threads_publish`, {
|
|
233
262
|
creation_id: containerId,
|
|
234
263
|
});
|
|
235
|
-
|
|
264
|
+
client.invalidateCache(THREADS_PROFILE_CACHE_PREFIX);
|
|
265
|
+
return formatResponse(data, rateLimit);
|
|
236
266
|
}
|
|
237
267
|
catch (error) {
|
|
238
|
-
return formatErrorResponse(error, "Publish video");
|
|
268
|
+
return formatErrorResponse(error, "Publish video", { step, containerId });
|
|
239
269
|
}
|
|
240
270
|
});
|
|
241
271
|
// ─── threads_publish_carousel ────────────────────────────────
|
|
242
|
-
server.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
272
|
+
server.registerTool("threads_publish_carousel", {
|
|
273
|
+
description: "Publish a carousel post on Threads with 2-20 images/videos. Supports cross-share to Instagram Stories, geo-gating via allowlisted_country_codes, and location tagging via location_id (parent container only).",
|
|
274
|
+
inputSchema: {
|
|
275
|
+
items: z.array(z.object({
|
|
276
|
+
type: z.enum(["IMAGE", "VIDEO"]).describe("Media type"),
|
|
277
|
+
url: httpsUrl.describe("Public HTTPS URL"),
|
|
278
|
+
alt_text: z.string().max(1000).optional().describe("Alt text for this item"),
|
|
279
|
+
})).min(2).max(20).describe("Array of media items"),
|
|
280
|
+
text: z.string().max(500).optional().describe("Caption text"),
|
|
281
|
+
reply_control: replyControlSchema,
|
|
282
|
+
topic_tag: topicTagSchema,
|
|
283
|
+
quote_post_id: z.string().optional().describe("ID of a post to quote"),
|
|
284
|
+
share_to_ig_story: shareToIgStorySchema,
|
|
285
|
+
allowlisted_country_codes: allowlistedCountryCodesSchema,
|
|
286
|
+
location_id: z.string().optional().describe("Location ID for tagging the post. Use threads_search_locations to find IDs. Requires the threads_location_tagging permission on the access token."),
|
|
287
|
+
},
|
|
288
|
+
annotations: WRITE_TOOL,
|
|
289
|
+
}, async ({ items, text, reply_control, topic_tag, quote_post_id, share_to_ig_story, allowlisted_country_codes, location_id }, extra) => {
|
|
290
|
+
// See ig_publish_carousel — shared notifier keeps progress monotonic
|
|
291
|
+
// across parallel child polls.
|
|
292
|
+
const onProgress = makeProgressNotifier(extra, "shared");
|
|
293
|
+
let step = "child container creation";
|
|
294
|
+
let containerId;
|
|
295
|
+
// Promise.all rejects with the first child to throw; JS is single-threaded,
|
|
296
|
+
// so the first catch wins the `=== undefined` race here.
|
|
297
|
+
let firstFailingChildStep;
|
|
298
|
+
let firstFailingChildId;
|
|
255
299
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
300
|
+
// Children are independent — create them in parallel. Errors propagate
|
|
301
|
+
// unwrapped so formatErrorResponse keeps MetaApiError categorization.
|
|
302
|
+
const childIds = await Promise.all(items.map(async (item) => {
|
|
303
|
+
let myStep = "child container creation";
|
|
304
|
+
let myContainerId;
|
|
305
|
+
try {
|
|
306
|
+
const params = buildParams({ media_type: item.type, is_carousel_item: true }, {
|
|
307
|
+
image_url: item.type === "IMAGE" ? item.url : undefined,
|
|
308
|
+
video_url: item.type === "VIDEO" ? item.url : undefined,
|
|
309
|
+
alt_text: item.alt_text,
|
|
310
|
+
});
|
|
311
|
+
const { data: child } = await client.threads("POST", `/${client.threadsUserId}/threads`, params);
|
|
312
|
+
if (typeof child.id !== "string")
|
|
313
|
+
throw new Error("Container creation did not return a valid id");
|
|
314
|
+
myContainerId = child.id;
|
|
315
|
+
myStep = "child processing";
|
|
316
|
+
await waitForThreadsContainer(client, myContainerId, item.type === "VIDEO" ? VIDEO_PROCESSING_TIMEOUT : IMAGE_PROCESSING_TIMEOUT, { onProgress });
|
|
317
|
+
return myContainerId;
|
|
261
318
|
}
|
|
262
|
-
|
|
263
|
-
|
|
319
|
+
catch (e) {
|
|
320
|
+
if (firstFailingChildStep === undefined) {
|
|
321
|
+
firstFailingChildStep = myStep;
|
|
322
|
+
firstFailingChildId = myContainerId;
|
|
323
|
+
}
|
|
324
|
+
throw e;
|
|
264
325
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (typeof child.id !== "string")
|
|
269
|
-
throw new Error("Container creation did not return a valid id");
|
|
270
|
-
const childId = child.id;
|
|
271
|
-
await waitForThreadsContainer(client, childId, item.type === "VIDEO" ? VIDEO_PROCESSING_TIMEOUT : IMAGE_PROCESSING_TIMEOUT);
|
|
272
|
-
childIds.push(childId);
|
|
273
|
-
}
|
|
274
|
-
const carouselParams = {
|
|
275
|
-
media_type: "CAROUSEL",
|
|
276
|
-
children: childIds.join(","),
|
|
277
|
-
};
|
|
278
|
-
if (text)
|
|
279
|
-
carouselParams.text = text;
|
|
280
|
-
if (reply_control)
|
|
281
|
-
carouselParams.reply_control = reply_control;
|
|
282
|
-
if (topic_tag)
|
|
283
|
-
carouselParams.topic_tag = topic_tag;
|
|
284
|
-
if (quote_post_id)
|
|
285
|
-
carouselParams.quote_post_id = quote_post_id;
|
|
326
|
+
}));
|
|
327
|
+
step = "parent container creation";
|
|
328
|
+
const carouselParams = buildParams({ media_type: "CAROUSEL", children: childIds.join(",") }, { text, reply_control, topic_tag, quote_post_id, location_id });
|
|
286
329
|
applyShareToIgStory(carouselParams, share_to_ig_story);
|
|
287
330
|
applyAllowlistedCountryCodes(carouselParams, allowlisted_country_codes);
|
|
288
331
|
const { data: carousel } = await client.threads("POST", `/${client.threadsUserId}/threads`, carouselParams);
|
|
289
332
|
if (typeof carousel.id !== "string")
|
|
290
333
|
throw new Error("Container creation did not return a valid id");
|
|
291
|
-
|
|
292
|
-
|
|
334
|
+
containerId = carousel.id;
|
|
335
|
+
step = "parent processing";
|
|
336
|
+
await waitForThreadsContainer(client, containerId, IMAGE_PROCESSING_TIMEOUT, { onProgress });
|
|
337
|
+
step = "publishing";
|
|
293
338
|
const { data, rateLimit } = await client.threads("POST", `/${client.threadsUserId}/threads_publish`, {
|
|
294
|
-
creation_id:
|
|
339
|
+
creation_id: containerId,
|
|
295
340
|
});
|
|
296
|
-
|
|
341
|
+
client.invalidateCache(THREADS_PROFILE_CACHE_PREFIX);
|
|
342
|
+
return formatResponse(data, rateLimit);
|
|
297
343
|
}
|
|
298
344
|
catch (error) {
|
|
299
|
-
return formatErrorResponse(error, "Publish carousel"
|
|
345
|
+
return formatErrorResponse(error, "Publish carousel", {
|
|
346
|
+
step: firstFailingChildStep ?? step,
|
|
347
|
+
containerId: firstFailingChildId ?? containerId,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
// Sibling polls still in-flight after a Promise.all rejection must
|
|
352
|
+
// not keep emitting progress for the now-completed request.
|
|
353
|
+
onProgress?.stop();
|
|
300
354
|
}
|
|
301
355
|
});
|
|
302
356
|
// ─── threads_delete_post ──────────────────────────────────────
|
|
303
|
-
server.
|
|
304
|
-
|
|
357
|
+
server.registerTool("threads_delete_post", {
|
|
358
|
+
description: "Delete a Threads post. This action is irreversible. Rate limited to 100 deletions per 24 hours.",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
post_id: metaId.describe("Threads post ID to delete"),
|
|
361
|
+
},
|
|
362
|
+
annotations: DESTRUCTIVE_TOOL,
|
|
305
363
|
}, async ({ post_id }) => {
|
|
306
364
|
try {
|
|
307
365
|
const { data, rateLimit } = await client.threads("DELETE", `/${post_id}`);
|
|
308
|
-
|
|
366
|
+
client.invalidateCache(THREADS_PROFILE_CACHE_PREFIX);
|
|
367
|
+
return formatResponse({ success: true, ...data }, rateLimit);
|
|
309
368
|
}
|
|
310
369
|
catch (error) {
|
|
311
370
|
return formatErrorResponse(error, "Delete post");
|
|
312
371
|
}
|
|
313
372
|
});
|
|
314
373
|
// ─── threads_get_container_status ────────────────────────────
|
|
315
|
-
server.
|
|
316
|
-
|
|
374
|
+
server.registerTool("threads_get_container_status", {
|
|
375
|
+
description: "Check the processing status of a Threads media container. Only works with unpublished container IDs (returned from container creation endpoints) — not with published post IDs.",
|
|
376
|
+
inputSchema: {
|
|
377
|
+
container_id: metaId.describe("Unpublished container ID to check (from container creation, not a published post ID)"),
|
|
378
|
+
},
|
|
379
|
+
annotations: READ_ONLY_TOOL,
|
|
317
380
|
}, async ({ container_id }) => {
|
|
318
381
|
try {
|
|
319
382
|
const { data, rateLimit } = await client.threads("GET", `/${container_id}`, {
|
|
320
383
|
fields: "id,status,error_message",
|
|
321
384
|
});
|
|
322
|
-
return
|
|
385
|
+
return formatResponse(data, rateLimit);
|
|
323
386
|
}
|
|
324
387
|
catch (error) {
|
|
325
388
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -330,28 +393,68 @@ export function registerThreadsPublishingTools(server, client) {
|
|
|
330
393
|
}
|
|
331
394
|
});
|
|
332
395
|
// ─── threads_get_publishing_limit ────────────────────────────
|
|
333
|
-
server.
|
|
396
|
+
server.registerTool("threads_get_publishing_limit", {
|
|
397
|
+
description: "Check how many posts you can still publish within the current 24-hour window (max 250 posts/day).",
|
|
398
|
+
inputSchema: {},
|
|
399
|
+
annotations: READ_ONLY_TOOL,
|
|
400
|
+
}, async () => {
|
|
334
401
|
try {
|
|
335
402
|
const { data, rateLimit } = await client.threads("GET", `/${client.threadsUserId}/threads_publishing_limit`, {
|
|
336
403
|
fields: "quota_usage,config",
|
|
337
404
|
});
|
|
338
|
-
return
|
|
405
|
+
return formatResponse(data, rateLimit);
|
|
339
406
|
}
|
|
340
407
|
catch (error) {
|
|
341
408
|
return formatErrorResponse(error, "Get publishing limit");
|
|
342
409
|
}
|
|
343
410
|
});
|
|
344
411
|
// ─── threads_repost ──────────────────────────────────────────
|
|
345
|
-
server.
|
|
346
|
-
|
|
412
|
+
server.registerTool("threads_repost", {
|
|
413
|
+
description: "Repost an existing Threads post to your own profile. Reposts appear under the Reposts tab on your profile. Requires the threads_content_publish permission. Note: this is a simple repost — for quote-reposts use threads_publish_text with quote_post_id.",
|
|
414
|
+
inputSchema: {
|
|
415
|
+
post_id: metaId.describe("Threads post ID to repost"),
|
|
416
|
+
},
|
|
417
|
+
annotations: WRITE_TOOL,
|
|
347
418
|
}, async ({ post_id }) => {
|
|
348
419
|
try {
|
|
349
420
|
const { data, rateLimit } = await client.threads("POST", `/${post_id}/repost`, {});
|
|
350
|
-
|
|
421
|
+
client.invalidateCache(THREADS_PROFILE_CACHE_PREFIX);
|
|
422
|
+
return formatResponse(data, rateLimit);
|
|
351
423
|
}
|
|
352
424
|
catch (error) {
|
|
353
425
|
return formatErrorResponse(error, "Repost");
|
|
354
426
|
}
|
|
355
427
|
});
|
|
428
|
+
// ─── threads_search_locations ────────────────────────────────
|
|
429
|
+
server.registerTool("threads_search_locations", {
|
|
430
|
+
description: "Search Threads-supported locations by keyword or coordinates. Returns a list of location objects (id, name, address, city, country, latitude, longitude, postal_code) whose id can be passed as location_id to the four threads_publish_* tools to tag a post. Either q or both latitude+longitude must be provided. Requires the threads_location_tagging permission; without app approval, the endpoint restricts results to the literal query 'Menlo Park' for testing.",
|
|
431
|
+
inputSchema: {
|
|
432
|
+
q: z.string().min(1).optional().describe("Search query (e.g., 'Menlo Park'). Either q or both latitude+longitude must be provided. If your app does not yet have the threads_location_tagging permission, the API restricts results to the literal query 'Menlo Park' for testing."),
|
|
433
|
+
latitude: z.number().min(-90).max(90).optional().describe("Latitude in decimal degrees (-90..90). Must be provided together with longitude."),
|
|
434
|
+
longitude: z.number().min(-180).max(180).optional().describe("Longitude in decimal degrees (-180..180). Must be provided together with latitude."),
|
|
435
|
+
},
|
|
436
|
+
annotations: READ_ONLY_TOOL,
|
|
437
|
+
}, async ({ q, latitude, longitude }) => {
|
|
438
|
+
try {
|
|
439
|
+
const hasLat = latitude !== undefined;
|
|
440
|
+
const hasLng = longitude !== undefined;
|
|
441
|
+
const hasCoords = hasLat || hasLng;
|
|
442
|
+
if (q === undefined && !hasCoords) {
|
|
443
|
+
return validationError("threads_search_locations requires either q or both latitude and longitude.");
|
|
444
|
+
}
|
|
445
|
+
if (q !== undefined && hasCoords) {
|
|
446
|
+
return validationError("threads_search_locations accepts either q or latitude+longitude, not both.");
|
|
447
|
+
}
|
|
448
|
+
if (hasCoords && !(hasLat && hasLng)) {
|
|
449
|
+
return validationError("threads_search_locations requires both latitude and longitude when searching by coordinates.");
|
|
450
|
+
}
|
|
451
|
+
const params = buildParams({}, { q, latitude, longitude });
|
|
452
|
+
const { data, rateLimit } = await client.threads("GET", "/location_search", params);
|
|
453
|
+
return formatResponse(data, rateLimit);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return formatErrorResponse(error, "Search locations");
|
|
457
|
+
}
|
|
458
|
+
});
|
|
356
459
|
}
|
|
357
460
|
//# sourceMappingURL=publishing.js.map
|