@exileum/meta-mcp 6.0.0 → 7.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 +14 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -58
- package/dist/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/services/meta-client.d.ts +54 -0
- package/dist/services/meta-client.d.ts.map +1 -1
- package/dist/services/meta-client.js +203 -45
- package/dist/services/meta-client.js.map +1 -1
- 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 +63 -33
- package/dist/tools/instagram/comments.js.map +1 -1
- package/dist/tools/instagram/hashtags.d.ts.map +1 -1
- package/dist/tools/instagram/hashtags.js +36 -18
- 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 +46 -24
- 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 +20 -10
- 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 +65 -31
- package/dist/tools/instagram/messaging.js.map +1 -1
- package/dist/tools/instagram/profile.d.ts.map +1 -1
- package/dist/tools/instagram/profile.js +50 -28
- 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 +77 -51
- 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 +47 -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 +20 -10
- 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 +37 -23
- 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 +12 -6
- package/dist/tools/threads/mentions.js.map +1 -1
- package/dist/tools/threads/profile.d.ts.map +1 -1
- package/dist/tools/threads/profile.js +8 -2
- 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 +157 -73
- 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 +40 -22
- package/dist/tools/threads/replies.js.map +1 -1
- 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 +3 -3
package/README.md
CHANGED
|
@@ -90,7 +90,8 @@ The server validates these at startup. Malformed values for `INSTAGRAM_USER_ID`,
|
|
|
90
90
|
- **Meta**: Token exchange/refresh/debug, webhook management
|
|
91
91
|
- **2 resources**: Instagram profile, Threads profile
|
|
92
92
|
- **2 prompts**: Cross-platform content publishing, analytics report
|
|
93
|
-
- Rate limit tracking via `x-app-usage` header
|
|
93
|
+
- Rate limit tracking via `x-app-usage` header — and **automatic client-side throttling** at 80% (1s slowdown) / 90% (5s backoff) so a burst of tool calls stays under Meta's per-app quota
|
|
94
|
+
- **Automatic retry** for transient Meta API failures (HTTP `429`/`500`/`502`/`503`/`504`, network errors, `fetch` timeouts) with exponential backoff and `Retry-After` honoring; tunable via `MetaClient`'s `maxRetries` option (default 3, set to 0 to disable)
|
|
94
95
|
- **Structured error responses** with `error_type` (`auth`, `validation`, `rate_limit`, `server`, `network`, `internal`), HTTP status, Meta API code/subcode/type, and a `remediation` hint where actionable — see [`CHANGELOG.md`](./CHANGELOG.md) for the JSON shape
|
|
95
96
|
|
|
96
97
|
## Tools
|
|
@@ -171,21 +172,22 @@ The server validates these at startup. Malformed values for `INSTAGRAM_USER_ID`,
|
|
|
171
172
|
|------|-------------|
|
|
172
173
|
| `ig_get_conversations` | List DM conversations |
|
|
173
174
|
| `ig_get_messages` | Get messages in a conversation |
|
|
174
|
-
| `ig_send_message` | Send a DM |
|
|
175
|
+
| `ig_send_message` | Send a DM (optional `messaging_type` = `RESPONSE`/`UPDATE`/`MESSAGE_TAG` and `tag` = `HUMAN_AGENT` for the 7-day human-agent window) |
|
|
175
176
|
| `ig_get_message` | Get message details |
|
|
176
177
|
|
|
177
|
-
### Threads — Publishing (
|
|
178
|
+
### Threads — Publishing (9)
|
|
178
179
|
|
|
179
180
|
| Tool | Description |
|
|
180
181
|
|------|-------------|
|
|
181
|
-
| `threads_publish_text` | Publish a text post in a single API call (`auto_publish_text=true`, default; set `auto_publish=false` for the legacy two-step flow). Supports polls, GIFs, link attachments, topic tags, quote posts, spoiler flag, cross-share to IG Stories, geo-gating via `allowlisted_country_codes`, text attachments up to 10K chars with styling |
|
|
182
|
-
| `threads_publish_image` | Publish an image post (supports alt_text, topic tags, spoiler flag, cross-share to IG Stories, geo-gating via `allowlisted_country_codes`) |
|
|
183
|
-
| `threads_publish_video` | Publish a video post (supports alt_text, topic tags, spoiler flag, cross-share to IG Stories, geo-gating via `allowlisted_country_codes`) |
|
|
184
|
-
| `threads_publish_carousel` | Publish a carousel (2-20 items, supports alt_text per item, cross-share to IG Stories, geo-gating via `allowlisted_country_codes` on the parent container) |
|
|
182
|
+
| `threads_publish_text` | Publish a text post in a single API call (`auto_publish_text=true`, default; set `auto_publish=false` for the legacy two-step flow). Supports polls, GIFs, link attachments, topic tags, quote posts, spoiler flag, cross-share to IG Stories, geo-gating via `allowlisted_country_codes`, location tagging via `location_id`, text attachments up to 10K chars with styling |
|
|
183
|
+
| `threads_publish_image` | Publish an image post (supports alt_text, topic tags, spoiler flag, cross-share to IG Stories, geo-gating via `allowlisted_country_codes`, location tagging via `location_id`) |
|
|
184
|
+
| `threads_publish_video` | Publish a video post (supports alt_text, topic tags, spoiler flag, cross-share to IG Stories, geo-gating via `allowlisted_country_codes`, location tagging via `location_id`) |
|
|
185
|
+
| `threads_publish_carousel` | Publish a carousel (2-20 items, supports alt_text per item, cross-share to IG Stories, geo-gating via `allowlisted_country_codes` and location tagging via `location_id` on the parent container) |
|
|
185
186
|
| `threads_delete_post` | Delete a post (max 100/day) |
|
|
186
187
|
| `threads_get_container_status` | Check container processing status (unpublished containers only) |
|
|
187
188
|
| `threads_get_publishing_limit` | Check remaining publishing quota (250 posts/day) |
|
|
188
189
|
| `threads_repost` | Repost an existing thread to your profile (requires `threads_content_publish`) |
|
|
190
|
+
| `threads_search_locations` | Search Threads-supported locations by query (`q`) or coordinates (`latitude`+`longitude`) to obtain a `location_id` for the four `threads_publish_*` tools (requires `threads_location_tagging` permission) |
|
|
189
191
|
|
|
190
192
|
### Threads — Media & Search (3)
|
|
191
193
|
|
|
@@ -355,12 +357,12 @@ What to do:
|
|
|
355
357
|
|
|
356
358
|
### `error_type: "rate_limit"` — application or user quota exhausted
|
|
357
359
|
|
|
358
|
-
Triggered by Meta API codes `4`, `17`, `32`, `341`, `613`, the business-use-case range `80001`–`80008`, or HTTP `429`. Includes any `OAuthException` with code `4` / `17` (these are surfaced as `error_type: "rate_limit"`, **not** `"auth"`, despite the type field).
|
|
360
|
+
Triggered by Meta API codes `4`, `17`, `32`, `341`, `613`, the business-use-case range `80001`–`80008`, or HTTP `429`. Includes any `OAuthException` with code `4` / `17` (these are surfaced as `error_type: "rate_limit"`, **not** `"auth"`, despite the type field). `MetaClient` automatically retries HTTP `429` up to 3 times with exponential backoff and honors any `Retry-After` header — a `rate_limit` error reaching the caller means the retry budget was exhausted.
|
|
359
361
|
|
|
360
362
|
What to do:
|
|
361
363
|
|
|
362
|
-
1. Inspect the `_rateLimit` field on prior successful tool responses. `callCount`, `totalCpuTime`, and `totalTime` come from Meta's `x-app-usage` header; when
|
|
363
|
-
2.
|
|
364
|
+
1. Inspect the `_rateLimit` field on prior successful tool responses. `callCount`, `totalCpuTime`, and `totalTime` come from Meta's `x-app-usage` header; when any approaches `100` you are near the per-app threshold.
|
|
365
|
+
2. meta-mcp already self-throttles once `max(callCount, totalCpuTime, totalTime)` crosses 80% (1s slowdown) or 90% (5s backoff) — see the `[meta-mcp] x-app-usage at N%…` lines on stderr. If you are still hitting `rate_limit` errors despite that, reduce request volume further and cache profile metadata between calls.
|
|
364
366
|
3. Threads has hard daily quotas (250 publishes, 100 deletes) — query the remaining quota with `threads_get_publishing_limit` before bulk operations.
|
|
365
367
|
|
|
366
368
|
### `error_type: "validation"` — bad parameter, wrong ID, or unsupported field
|
|
@@ -375,8 +377,8 @@ Triggered by Meta API codes `100`, `200`, `803`, or any unmapped 4xx HTTP status
|
|
|
375
377
|
|
|
376
378
|
### Other categories
|
|
377
379
|
|
|
378
|
-
- `error_type: "server"` (codes `1`, `2`, HTTP 5xx) — transient Meta outage
|
|
379
|
-
- `error_type: "network"` — `fetch` timed out or failed before reaching Meta.
|
|
380
|
+
- `error_type: "server"` (codes `1`, `2`, HTTP 5xx) — transient Meta outage. `MetaClient` already retried `500`/`502`/`503`/`504` up to 3 times with exponential backoff before surfacing this; check [metastatus.com](https://metastatus.com/) if it persists.
|
|
381
|
+
- `error_type: "network"` — `fetch` timed out or failed before reaching Meta. `MetaClient` already retried thrown network errors up to 3 times; verify outbound connectivity if the error keeps reappearing.
|
|
380
382
|
- `error_type: "internal"` — unexpected condition that did not map to a Meta error code. The `raw` field carries the sanitized original message; `access_token`, `client_secret`, and `input_token` values are scrubbed to `***` before reporting.
|
|
381
383
|
|
|
382
384
|
## API Stability
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAqCpE,wBAAgB,mBAAmB,cAmBlC"}
|
package/dist/index.js
CHANGED
|
@@ -4,27 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { loadConfig } from "./config.js";
|
|
6
6
|
import { MetaClient } from "./services/meta-client.js";
|
|
7
|
-
|
|
8
|
-
import { registerMetaAuthTools } from "./tools/meta/auth.js";
|
|
9
|
-
// Instagram tools
|
|
10
|
-
import { registerIgPublishingTools } from "./tools/instagram/publishing.js";
|
|
11
|
-
import { registerIgMediaTools } from "./tools/instagram/media.js";
|
|
12
|
-
import { registerIgCommentTools } from "./tools/instagram/comments.js";
|
|
13
|
-
import { registerIgProfileTools } from "./tools/instagram/profile.js";
|
|
14
|
-
import { registerIgHashtagTools } from "./tools/instagram/hashtags.js";
|
|
15
|
-
import { registerIgMentionTools } from "./tools/instagram/mentions.js";
|
|
16
|
-
import { registerIgMessagingTools } from "./tools/instagram/messaging.js";
|
|
17
|
-
// Threads tools
|
|
18
|
-
import { registerThreadsPublishingTools } from "./tools/threads/publishing.js";
|
|
19
|
-
import { registerThreadsMediaTools } from "./tools/threads/media.js";
|
|
20
|
-
import { registerThreadsReplyTools } from "./tools/threads/replies.js";
|
|
21
|
-
import { registerThreadsProfileTools } from "./tools/threads/profile.js";
|
|
22
|
-
import { registerThreadsInsightTools } from "./tools/threads/insights.js";
|
|
23
|
-
import { registerThreadsMentionsTools } from "./tools/threads/mentions.js";
|
|
24
|
-
// Resources & Prompts
|
|
25
|
-
import { registerInstagramResources } from "./resources/instagram.js";
|
|
26
|
-
import { registerThreadsResources } from "./resources/threads.js";
|
|
27
|
-
import { registerPrompts } from "./prompts/index.js";
|
|
7
|
+
import { registerAll } from "./register-all.js";
|
|
28
8
|
const require = createRequire(import.meta.url);
|
|
29
9
|
const { version: SERVER_VERSION } = require("../package.json");
|
|
30
10
|
const server = new McpServer({
|
|
@@ -40,26 +20,7 @@ catch (err) {
|
|
|
40
20
|
process.exit(1);
|
|
41
21
|
}
|
|
42
22
|
const client = new MetaClient(config);
|
|
43
|
-
|
|
44
|
-
registerMetaAuthTools(server, client);
|
|
45
|
-
registerIgPublishingTools(server, client);
|
|
46
|
-
registerIgMediaTools(server, client);
|
|
47
|
-
registerIgCommentTools(server, client);
|
|
48
|
-
registerIgProfileTools(server, client);
|
|
49
|
-
registerIgHashtagTools(server, client);
|
|
50
|
-
registerIgMentionTools(server, client);
|
|
51
|
-
registerIgMessagingTools(server, client);
|
|
52
|
-
registerThreadsPublishingTools(server, client);
|
|
53
|
-
registerThreadsMediaTools(server, client);
|
|
54
|
-
registerThreadsReplyTools(server, client);
|
|
55
|
-
registerThreadsProfileTools(server, client);
|
|
56
|
-
registerThreadsInsightTools(server, client);
|
|
57
|
-
registerThreadsMentionsTools(server, client);
|
|
58
|
-
// Register resources
|
|
59
|
-
registerInstagramResources(server, client);
|
|
60
|
-
registerThreadsResources(server, client);
|
|
61
|
-
// Register prompts
|
|
62
|
-
registerPrompts(server);
|
|
23
|
+
registerAll(server, client);
|
|
63
24
|
async function main() {
|
|
64
25
|
const transport = new StdioServerTransport();
|
|
65
26
|
await server.connect(transport);
|
|
@@ -83,23 +44,7 @@ export function createSandboxServer() {
|
|
|
83
44
|
threadsUserId: "",
|
|
84
45
|
};
|
|
85
46
|
const mockClient = new MetaClient(mockConfig);
|
|
86
|
-
|
|
87
|
-
registerIgPublishingTools(sandbox, mockClient);
|
|
88
|
-
registerIgMediaTools(sandbox, mockClient);
|
|
89
|
-
registerIgCommentTools(sandbox, mockClient);
|
|
90
|
-
registerIgProfileTools(sandbox, mockClient);
|
|
91
|
-
registerIgHashtagTools(sandbox, mockClient);
|
|
92
|
-
registerIgMentionTools(sandbox, mockClient);
|
|
93
|
-
registerIgMessagingTools(sandbox, mockClient);
|
|
94
|
-
registerThreadsPublishingTools(sandbox, mockClient);
|
|
95
|
-
registerThreadsMediaTools(sandbox, mockClient);
|
|
96
|
-
registerThreadsReplyTools(sandbox, mockClient);
|
|
97
|
-
registerThreadsProfileTools(sandbox, mockClient);
|
|
98
|
-
registerThreadsInsightTools(sandbox, mockClient);
|
|
99
|
-
registerThreadsMentionsTools(sandbox, mockClient);
|
|
100
|
-
registerInstagramResources(sandbox, mockClient);
|
|
101
|
-
registerThreadsResources(sandbox, mockClient);
|
|
102
|
-
registerPrompts(sandbox);
|
|
47
|
+
registerAll(sandbox, mockClient);
|
|
103
48
|
return sandbox;
|
|
104
49
|
}
|
|
105
50
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAc,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAc,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAwB,CAAC;AAEtF,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,cAAc;CACxB,CAAC,CAAC;AAEH,IAAI,MAAkB,CAAC;AACvB,IAAI,CAAC;IACH,MAAM,GAAG,UAAU,EAAE,CAAC;AACxB,CAAC;AAAC,OAAO,GAAG,EAAE,CAAC;IACb,OAAO,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AACD,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;AAEtC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5B,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,yBAAyB;AAEzB,MAAM,UAAU,mBAAmB;IACjC,MAAM,OAAO,GAAG,IAAI,SAAS,CAAC;QAC5B,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,cAAc;KACxB,CAAC,CAAC;IAEH,MAAM,UAAU,GAAe;QAC7B,KAAK,EAAE,EAAE;QACT,SAAS,EAAE,EAAE;QACb,oBAAoB,EAAE,EAAE;QACxB,eAAe,EAAE,EAAE;QACnB,kBAAkB,EAAE,EAAE;QACtB,aAAa,EAAE,EAAE;KAClB,CAAC;IACF,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC;IAE9C,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-all.d.ts","sourceRoot":"","sources":["../src/register-all.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AA2BvD,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAkBvE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Meta platform tools
|
|
2
|
+
import { registerMetaAuthTools } from "./tools/meta/auth.js";
|
|
3
|
+
// Instagram tools
|
|
4
|
+
import { registerIgPublishingTools } from "./tools/instagram/publishing.js";
|
|
5
|
+
import { registerIgMediaTools } from "./tools/instagram/media.js";
|
|
6
|
+
import { registerIgCommentTools } from "./tools/instagram/comments.js";
|
|
7
|
+
import { registerIgProfileTools } from "./tools/instagram/profile.js";
|
|
8
|
+
import { registerIgHashtagTools } from "./tools/instagram/hashtags.js";
|
|
9
|
+
import { registerIgMentionTools } from "./tools/instagram/mentions.js";
|
|
10
|
+
import { registerIgMessagingTools } from "./tools/instagram/messaging.js";
|
|
11
|
+
// Threads tools
|
|
12
|
+
import { registerThreadsPublishingTools } from "./tools/threads/publishing.js";
|
|
13
|
+
import { registerThreadsMediaTools } from "./tools/threads/media.js";
|
|
14
|
+
import { registerThreadsReplyTools } from "./tools/threads/replies.js";
|
|
15
|
+
import { registerThreadsProfileTools } from "./tools/threads/profile.js";
|
|
16
|
+
import { registerThreadsInsightTools } from "./tools/threads/insights.js";
|
|
17
|
+
import { registerThreadsMentionsTools } from "./tools/threads/mentions.js";
|
|
18
|
+
// Resources & Prompts
|
|
19
|
+
import { registerInstagramResources } from "./resources/instagram.js";
|
|
20
|
+
import { registerThreadsResources } from "./resources/threads.js";
|
|
21
|
+
import { registerPrompts } from "./prompts/index.js";
|
|
22
|
+
export function registerAll(server, client) {
|
|
23
|
+
registerMetaAuthTools(server, client);
|
|
24
|
+
registerIgPublishingTools(server, client);
|
|
25
|
+
registerIgMediaTools(server, client);
|
|
26
|
+
registerIgCommentTools(server, client);
|
|
27
|
+
registerIgProfileTools(server, client);
|
|
28
|
+
registerIgHashtagTools(server, client);
|
|
29
|
+
registerIgMentionTools(server, client);
|
|
30
|
+
registerIgMessagingTools(server, client);
|
|
31
|
+
registerThreadsPublishingTools(server, client);
|
|
32
|
+
registerThreadsMediaTools(server, client);
|
|
33
|
+
registerThreadsReplyTools(server, client);
|
|
34
|
+
registerThreadsProfileTools(server, client);
|
|
35
|
+
registerThreadsInsightTools(server, client);
|
|
36
|
+
registerThreadsMentionsTools(server, client);
|
|
37
|
+
registerInstagramResources(server, client);
|
|
38
|
+
registerThreadsResources(server, client);
|
|
39
|
+
registerPrompts(server);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=register-all.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-all.js","sourceRoot":"","sources":["../src/register-all.ts"],"names":[],"mappings":"AAGA,sBAAsB;AACtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,kBAAkB;AAClB,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAE1E,gBAAgB;AAChB,OAAO,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAE3E,sBAAsB;AACtB,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,UAAU,WAAW,CAAC,MAAiB,EAAE,MAAkB;IAC/D,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,yBAAyB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,wBAAwB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,8BAA8B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,yBAAyB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,yBAAyB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,2BAA2B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,2BAA2B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,4BAA4B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,0BAA0B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,wBAAwB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,eAAe,CAAC,MAAM,CAAC,CAAC;AAC1B,CAAC"}
|
|
@@ -1,15 +1,62 @@
|
|
|
1
1
|
import { MetaConfig } from "../config.js";
|
|
2
2
|
export declare const DEFAULT_META_API_VERSION = "v25.0";
|
|
3
3
|
export declare const DEFAULT_THREADS_API_VERSION = "v1.0";
|
|
4
|
+
export declare const DEFAULT_MAX_RETRIES = 3;
|
|
5
|
+
export declare const DEFAULT_RETRY_BASE_DELAY_MS = 1000;
|
|
6
|
+
export declare const MAX_RETRY_DELAY_MS = 60000;
|
|
7
|
+
/**
|
|
8
|
+
* Parse a `Retry-After` response header value (RFC 7231 §7.1.3) into
|
|
9
|
+
* milliseconds. Returns `undefined` for missing or unparseable values so the
|
|
10
|
+
* caller falls back to the exponential-backoff schedule. Past dates and
|
|
11
|
+
* negative seconds clamp to `0` to absorb server time skew.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseRetryAfter(value: string | null, nowMs: number): number | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Exponential backoff `base * 2^attempt` with 0–25% jitter to spread
|
|
16
|
+
* concurrent clients and avoid thundering-herd reconnects. Capped at
|
|
17
|
+
* {@link MAX_RETRY_DELAY_MS}. `rand` is injectable for deterministic tests.
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeBackoffDelay(attempt: number, baseDelayMs: number, rand?: () => number): number;
|
|
4
20
|
export interface MetaClientOptions {
|
|
5
21
|
metaApiVersion?: string;
|
|
6
22
|
threadsApiVersion?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Maximum number of retry attempts after the initial request fails with a
|
|
25
|
+
* transient error (HTTP 429/500/502/503/504 or a thrown network error).
|
|
26
|
+
* Total request count is `maxRetries + 1`. Set to `0` to disable retries
|
|
27
|
+
* entirely (matches pre-#61 behavior). Defaults to {@link DEFAULT_MAX_RETRIES}.
|
|
28
|
+
*/
|
|
29
|
+
maxRetries?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Base delay (ms) used by the exponential-backoff schedule between retries.
|
|
32
|
+
* Attempt N waits `baseDelayMs * 2^N` plus 0–25% jitter (capped at
|
|
33
|
+
* {@link MAX_RETRY_DELAY_MS}). Defaults to {@link DEFAULT_RETRY_BASE_DELAY_MS}.
|
|
34
|
+
* Tests pass `0` to make retries fire immediately without real waiting.
|
|
35
|
+
*/
|
|
36
|
+
retryBaseDelayMs?: number;
|
|
37
|
+
/**
|
|
38
|
+
* @internal — testing-only seam, not part of the stable API. Injection point
|
|
39
|
+
* for the sleep primitive used between retries. Defaults to a
|
|
40
|
+
* `setTimeout`-backed `Promise`.
|
|
41
|
+
*/
|
|
42
|
+
sleep?: (ms: number) => Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* @internal — testing-only seam, not part of the stable API. Injection point
|
|
45
|
+
* for the current-time source used when parsing the HTTP-date form of the
|
|
46
|
+
* `Retry-After` header. Defaults to `Date.now`.
|
|
47
|
+
*/
|
|
48
|
+
now?: () => number;
|
|
7
49
|
}
|
|
8
50
|
export interface RateLimit {
|
|
9
51
|
callCount?: number;
|
|
10
52
|
totalCpuTime?: number;
|
|
11
53
|
totalTime?: number;
|
|
12
54
|
}
|
|
55
|
+
export declare const RATE_LIMIT_SLOWDOWN_THRESHOLD = 80;
|
|
56
|
+
export declare const RATE_LIMIT_BACKOFF_THRESHOLD = 90;
|
|
57
|
+
export declare const RATE_LIMIT_SLOWDOWN_MS = 1000;
|
|
58
|
+
export declare const RATE_LIMIT_BACKOFF_MS = 5000;
|
|
59
|
+
export declare const RATE_LIMIT_SNAPSHOT_TTL_MS: number;
|
|
13
60
|
export interface ClientResponse {
|
|
14
61
|
data: Record<string, unknown>;
|
|
15
62
|
rateLimit?: RateLimit;
|
|
@@ -54,8 +101,15 @@ export declare class MetaClient {
|
|
|
54
101
|
private igBase;
|
|
55
102
|
private fbBase;
|
|
56
103
|
private threadsBase;
|
|
104
|
+
private lastRateLimit?;
|
|
105
|
+
private lastRateLimitAt?;
|
|
106
|
+
private maxRetries;
|
|
107
|
+
private retryBaseDelayMs;
|
|
108
|
+
private sleep;
|
|
109
|
+
private now;
|
|
57
110
|
constructor(config: MetaConfig, options?: MetaClientOptions);
|
|
58
111
|
private parseRateLimit;
|
|
112
|
+
private maybeThrottle;
|
|
59
113
|
private appendFormParams;
|
|
60
114
|
private request;
|
|
61
115
|
ig(method: string, path: string, params?: FormParams, options?: RequestOptions): Promise<ClientResponse>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta-client.d.ts","sourceRoot":"","sources":["../../src/services/meta-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAa1C,eAAO,MAAM,wBAAwB,UAAU,CAAC;AAChD,eAAO,MAAM,2BAA2B,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"meta-client.d.ts","sourceRoot":"","sources":["../../src/services/meta-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,6 +16,52 @@ const API_VERSION_PATTERN = /^v\d+\.\d+$/;
|
|
|
16
16
|
// surface has no version segment; piping META_API_VERSION here would 404).
|
|
17
17
|
const IG_TOKEN_BASE = "https://graph.instagram.com";
|
|
18
18
|
const THREADS_TOKEN_BASE = "https://graph.threads.net";
|
|
19
|
+
// Retry policy for transient Meta API failures (#61). 429 is the explicit
|
|
20
|
+
// rate-limit signal; 502/503/504 are canonical gateway/availability errors;
|
|
21
|
+
// 500 is included because Meta's infrastructure routinely returns transient
|
|
22
|
+
// 500s during brief overloads alongside the more canonical 5xx codes. Every
|
|
23
|
+
// other 4xx is a permanent caller-side error (auth, validation) and fails fast.
|
|
24
|
+
const RETRYABLE_HTTP_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
25
|
+
export const DEFAULT_MAX_RETRIES = 3;
|
|
26
|
+
export const DEFAULT_RETRY_BASE_DELAY_MS = 1000;
|
|
27
|
+
// Cap so a malformed `Retry-After: 999999` (≈11 days) can't hang a tool.
|
|
28
|
+
export const MAX_RETRY_DELAY_MS = 60_000;
|
|
29
|
+
/**
|
|
30
|
+
* Parse a `Retry-After` response header value (RFC 7231 §7.1.3) into
|
|
31
|
+
* milliseconds. Returns `undefined` for missing or unparseable values so the
|
|
32
|
+
* caller falls back to the exponential-backoff schedule. Past dates and
|
|
33
|
+
* negative seconds clamp to `0` to absorb server time skew.
|
|
34
|
+
*/
|
|
35
|
+
export function parseRetryAfter(value, nowMs) {
|
|
36
|
+
if (value === null)
|
|
37
|
+
return undefined;
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
if (trimmed === "")
|
|
40
|
+
return undefined;
|
|
41
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
42
|
+
const seconds = Number(trimmed);
|
|
43
|
+
if (Number.isFinite(seconds))
|
|
44
|
+
return Math.max(0, seconds * 1000);
|
|
45
|
+
}
|
|
46
|
+
const dateMs = Date.parse(trimmed);
|
|
47
|
+
if (!Number.isNaN(dateMs)) {
|
|
48
|
+
return Math.max(0, dateMs - nowMs);
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Exponential backoff `base * 2^attempt` with 0–25% jitter to spread
|
|
54
|
+
* concurrent clients and avoid thundering-herd reconnects. Capped at
|
|
55
|
+
* {@link MAX_RETRY_DELAY_MS}. `rand` is injectable for deterministic tests.
|
|
56
|
+
*/
|
|
57
|
+
export function computeBackoffDelay(attempt, baseDelayMs, rand = Math.random) {
|
|
58
|
+
const exponential = baseDelayMs * Math.pow(2, attempt);
|
|
59
|
+
const jitter = exponential * 0.25 * rand();
|
|
60
|
+
return Math.min(exponential + jitter, MAX_RETRY_DELAY_MS);
|
|
61
|
+
}
|
|
62
|
+
function defaultSleep(ms) {
|
|
63
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
64
|
+
}
|
|
19
65
|
function resolveApiVersion(envName, fallback, explicit) {
|
|
20
66
|
// Explicit MetaClientOptions value, if any, takes precedence over the env
|
|
21
67
|
// var; both go through the same regex check + warn-fallback so a malformed
|
|
@@ -31,6 +77,17 @@ function resolveApiVersion(envName, fallback, explicit) {
|
|
|
31
77
|
}
|
|
32
78
|
return raw;
|
|
33
79
|
}
|
|
80
|
+
// Pre-request throttle thresholds — Meta throttles at 100% usage on any of
|
|
81
|
+
// callCount / totalCpuTime / totalTime per
|
|
82
|
+
// https://developers.facebook.com/docs/graph-api/overview/rate-limiting/,
|
|
83
|
+
// so we back off well before the cliff.
|
|
84
|
+
export const RATE_LIMIT_SLOWDOWN_THRESHOLD = 80;
|
|
85
|
+
export const RATE_LIMIT_BACKOFF_THRESHOLD = 90;
|
|
86
|
+
export const RATE_LIMIT_SLOWDOWN_MS = 1000;
|
|
87
|
+
export const RATE_LIMIT_BACKOFF_MS = 5000;
|
|
88
|
+
// Meta's rate-limit window is rolling 1h — discard the snapshot after that so
|
|
89
|
+
// a long-idle client doesn't pay a spurious backoff on its first post-idle call.
|
|
90
|
+
export const RATE_LIMIT_SNAPSHOT_TTL_MS = 60 * 60 * 1000;
|
|
34
91
|
function parseMetaErrorBody(text) {
|
|
35
92
|
if (!text)
|
|
36
93
|
return undefined;
|
|
@@ -56,6 +113,12 @@ export class MetaClient {
|
|
|
56
113
|
igBase;
|
|
57
114
|
fbBase;
|
|
58
115
|
threadsBase;
|
|
116
|
+
lastRateLimit;
|
|
117
|
+
lastRateLimitAt;
|
|
118
|
+
maxRetries;
|
|
119
|
+
retryBaseDelayMs;
|
|
120
|
+
sleep;
|
|
121
|
+
now;
|
|
59
122
|
constructor(config, options) {
|
|
60
123
|
this.config = config;
|
|
61
124
|
const metaVersion = resolveApiVersion("META_API_VERSION", DEFAULT_META_API_VERSION, options?.metaApiVersion);
|
|
@@ -63,6 +126,18 @@ export class MetaClient {
|
|
|
63
126
|
this.igBase = `https://graph.instagram.com/${metaVersion}`;
|
|
64
127
|
this.fbBase = `https://graph.facebook.com/${metaVersion}`;
|
|
65
128
|
this.threadsBase = `https://graph.threads.net/${threadsVersion}`;
|
|
129
|
+
// `Math.max(0, Infinity)` is `Infinity` (infinite loop) and `0 <= NaN` is
|
|
130
|
+
// `false` (loop body never runs); reject non-finite values and fall back.
|
|
131
|
+
const rawMaxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
132
|
+
this.maxRetries = Number.isFinite(rawMaxRetries)
|
|
133
|
+
? Math.max(0, rawMaxRetries)
|
|
134
|
+
: DEFAULT_MAX_RETRIES;
|
|
135
|
+
const rawBaseDelay = options?.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
|
|
136
|
+
this.retryBaseDelayMs = Number.isFinite(rawBaseDelay)
|
|
137
|
+
? Math.max(0, rawBaseDelay)
|
|
138
|
+
: DEFAULT_RETRY_BASE_DELAY_MS;
|
|
139
|
+
this.sleep = options?.sleep ?? defaultSleep;
|
|
140
|
+
this.now = options?.now ?? Date.now;
|
|
66
141
|
}
|
|
67
142
|
parseRateLimit(headers) {
|
|
68
143
|
const usage = headers.get("x-app-usage");
|
|
@@ -70,16 +145,54 @@ export class MetaClient {
|
|
|
70
145
|
return undefined;
|
|
71
146
|
try {
|
|
72
147
|
const raw = JSON.parse(usage);
|
|
148
|
+
// `Number(undefined)` is NaN, `Number("92")` is 92 — coerce defensively
|
|
149
|
+
// so a future Meta API tweak (numbers shipped as strings) still produces
|
|
150
|
+
// a usable RateLimit instead of silently leaving fields `undefined`.
|
|
151
|
+
const num = (v) => {
|
|
152
|
+
if (v === undefined || v === null)
|
|
153
|
+
return undefined;
|
|
154
|
+
const n = Number(v);
|
|
155
|
+
return Number.isFinite(n) ? n : undefined;
|
|
156
|
+
};
|
|
73
157
|
return {
|
|
74
|
-
callCount: raw.call_count,
|
|
75
|
-
totalCpuTime: raw.total_cpu_time,
|
|
76
|
-
totalTime: raw.total_time,
|
|
158
|
+
callCount: num(raw.call_count),
|
|
159
|
+
totalCpuTime: num(raw.total_cpu_time),
|
|
160
|
+
totalTime: num(raw.total_time),
|
|
77
161
|
};
|
|
78
162
|
}
|
|
79
163
|
catch {
|
|
80
164
|
return undefined;
|
|
81
165
|
}
|
|
82
166
|
}
|
|
167
|
+
// Take `max(callCount, totalCpuTime, totalTime)` because Meta throttles on
|
|
168
|
+
// whichever quota hits 100% first. Concurrent calls at high usage both
|
|
169
|
+
// sleep their full delay and then fire together — acceptable for MCP's
|
|
170
|
+
// typically sequential call pattern, still safer than no throttling.
|
|
171
|
+
async maybeThrottle() {
|
|
172
|
+
const rl = this.lastRateLimit;
|
|
173
|
+
if (!rl)
|
|
174
|
+
return;
|
|
175
|
+
if (this.lastRateLimitAt !== undefined && Date.now() - this.lastRateLimitAt > RATE_LIMIT_SNAPSHOT_TTL_MS) {
|
|
176
|
+
this.lastRateLimit = undefined;
|
|
177
|
+
this.lastRateLimitAt = undefined;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const max = Math.max(rl.callCount ?? 0, rl.totalCpuTime ?? 0, rl.totalTime ?? 0);
|
|
181
|
+
let delay;
|
|
182
|
+
if (max >= RATE_LIMIT_BACKOFF_THRESHOLD) {
|
|
183
|
+
delay = RATE_LIMIT_BACKOFF_MS;
|
|
184
|
+
}
|
|
185
|
+
else if (max >= RATE_LIMIT_SLOWDOWN_THRESHOLD) {
|
|
186
|
+
delay = RATE_LIMIT_SLOWDOWN_MS;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.error(`[meta-mcp] x-app-usage at ${max}% (callCount=${rl.callCount ?? "?"}, ` +
|
|
192
|
+
`totalTime=${rl.totalTime ?? "?"}, totalCpuTime=${rl.totalCpuTime ?? "?"}); ` +
|
|
193
|
+
`delaying next request by ${delay}ms to stay under Meta's per-app quota.`);
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
195
|
+
}
|
|
83
196
|
// Used for both form bodies (POST/PUT) and query strings (GET/DELETE) — every
|
|
84
197
|
// entry ends up URL-encoded in `qs`. Skipping `""` must precede the typeof
|
|
85
198
|
// check so the type narrowing below stays correct after the filter.
|
|
@@ -98,7 +211,6 @@ export class MetaClient {
|
|
|
98
211
|
}
|
|
99
212
|
async request(baseUrl, token, method, path, params, options) {
|
|
100
213
|
let url = `${baseUrl}${path}`;
|
|
101
|
-
const init = { method, signal: AbortSignal.timeout(30_000) };
|
|
102
214
|
const isWrite = method !== "GET" && method !== "DELETE";
|
|
103
215
|
const useJson = isWrite && options?.jsonBody !== undefined;
|
|
104
216
|
// `params` always lands in the URL query string (form body for POST/PUT,
|
|
@@ -113,61 +225,107 @@ export class MetaClient {
|
|
|
113
225
|
qs.set("access_token", token);
|
|
114
226
|
if (params)
|
|
115
227
|
this.appendFormParams(qs, params);
|
|
228
|
+
// Body/headers are stable strings reusable across retry attempts; only
|
|
229
|
+
// `signal` must be fresh per attempt — sharing an exhausted
|
|
230
|
+
// `AbortSignal.timeout` would silently abort every subsequent retry.
|
|
231
|
+
const baseInit = { method };
|
|
116
232
|
if (useJson) {
|
|
117
233
|
url += (url.includes("?") ? "&" : "?") + qs.toString();
|
|
118
|
-
|
|
119
|
-
|
|
234
|
+
baseInit.headers = { "Content-Type": "application/json" };
|
|
235
|
+
baseInit.body = JSON.stringify(options.jsonBody);
|
|
120
236
|
}
|
|
121
237
|
else if (isWrite) {
|
|
122
|
-
|
|
123
|
-
|
|
238
|
+
baseInit.headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
239
|
+
baseInit.body = qs.toString();
|
|
124
240
|
}
|
|
125
241
|
else {
|
|
126
242
|
url += (url.includes("?") ? "&" : "?") + qs.toString();
|
|
127
243
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
244
|
+
// Retry loop (#61): total request count is `maxRetries + 1`. `attempt` is
|
|
245
|
+
// 0-based — `attempt = 0` is the initial request, `attempt = N` is the Nth
|
|
246
|
+
// retry. `computeBackoffDelay(attempt, …)` therefore produces the correct
|
|
247
|
+
// 1s / 2s / 4s schedule on attempts 0 / 1 / 2 before retries 1 / 2 / 3.
|
|
248
|
+
// The trailing `throw` after the loop is for TS control-flow narrowing.
|
|
249
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
250
|
+
// Pre-request throttle from #60 — runs before every attempt so a retry
|
|
251
|
+
// after a 429 still consults the most recent x-app-usage snapshot.
|
|
252
|
+
await this.maybeThrottle();
|
|
253
|
+
// Arm the 30s abort timer *after* the throttle sleep so a backoff at
|
|
254
|
+
// high x-app-usage doesn't eat into the actual network window (#60 review).
|
|
255
|
+
const init = { ...baseInit, signal: AbortSignal.timeout(30_000) };
|
|
256
|
+
let res;
|
|
257
|
+
try {
|
|
258
|
+
res = await fetch(url, init);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
if (attempt < this.maxRetries) {
|
|
262
|
+
await this.sleep(computeBackoffDelay(attempt, this.retryBaseDelayMs));
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
// Parse rate-limit on every response (a 429 still carries `x-app-usage`);
|
|
268
|
+
// header-less responses (OAuth token endpoints) leave state intact.
|
|
269
|
+
const rateLimit = this.parseRateLimit(res.headers);
|
|
270
|
+
if (rateLimit) {
|
|
271
|
+
this.lastRateLimit = rateLimit;
|
|
272
|
+
this.lastRateLimitAt = Date.now();
|
|
273
|
+
}
|
|
274
|
+
if (!res.ok &&
|
|
275
|
+
RETRYABLE_HTTP_STATUSES.has(res.status) &&
|
|
276
|
+
attempt < this.maxRetries) {
|
|
277
|
+
const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"), this.now());
|
|
278
|
+
const delay = retryAfterMs !== undefined
|
|
279
|
+
? Math.min(retryAfterMs, MAX_RETRY_DELAY_MS)
|
|
280
|
+
: computeBackoffDelay(attempt, this.retryBaseDelayMs);
|
|
281
|
+
// Release the connection — `cancel()` rejects on already-consumed bodies.
|
|
282
|
+
res.body?.cancel().catch(() => { });
|
|
283
|
+
await this.sleep(delay);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!res.ok) {
|
|
287
|
+
const text = await res.text().catch(() => "");
|
|
288
|
+
const parsed = parseMetaErrorBody(text);
|
|
289
|
+
const detail = parsed?.message ?? text;
|
|
156
290
|
throw new MetaApiError({
|
|
157
|
-
message: `Meta API
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
291
|
+
message: `Meta API ${method} ${path} (${res.status}): ${detail}`,
|
|
292
|
+
httpStatus: res.status,
|
|
293
|
+
apiCode: parsed?.code,
|
|
294
|
+
apiSubcode: parsed?.subcode,
|
|
295
|
+
apiType: parsed?.type,
|
|
296
|
+
fbtraceId: parsed?.fbtraceId,
|
|
162
297
|
endpoint: path,
|
|
163
298
|
method,
|
|
164
|
-
body:
|
|
299
|
+
body: text,
|
|
165
300
|
});
|
|
166
301
|
}
|
|
167
|
-
|
|
302
|
+
const contentType = res.headers.get("content-type") || "";
|
|
303
|
+
if (contentType.includes("application/json")) {
|
|
304
|
+
const data = (await res.json());
|
|
305
|
+
if (data.error) {
|
|
306
|
+
const err = data.error;
|
|
307
|
+
const apiCode = typeof err.code === "number" ? err.code : undefined;
|
|
308
|
+
const apiSubcode = typeof err.error_subcode === "number" ? err.error_subcode : undefined;
|
|
309
|
+
const apiType = typeof err.type === "string" ? err.type : undefined;
|
|
310
|
+
const apiMessage = typeof err.message === "string" ? err.message : String(err.message ?? "");
|
|
311
|
+
const fbtraceId = typeof err.fbtrace_id === "string" ? err.fbtrace_id : undefined;
|
|
312
|
+
throw new MetaApiError({
|
|
313
|
+
message: `Meta API error: ${apiMessage} (code ${apiCode ?? "?"})`,
|
|
314
|
+
apiCode,
|
|
315
|
+
apiSubcode,
|
|
316
|
+
apiType,
|
|
317
|
+
fbtraceId,
|
|
318
|
+
endpoint: path,
|
|
319
|
+
method,
|
|
320
|
+
body: JSON.stringify(err),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return { data, rateLimit };
|
|
324
|
+
}
|
|
325
|
+
const text = await res.text();
|
|
326
|
+
return { data: { raw: text, success: true }, rateLimit };
|
|
168
327
|
}
|
|
169
|
-
|
|
170
|
-
return { data: { raw: text, success: true }, rateLimit };
|
|
328
|
+
throw new Error("MetaClient: retry loop exited without resolution (unreachable)");
|
|
171
329
|
}
|
|
172
330
|
async ig(method, path, params, options) {
|
|
173
331
|
if (!this.config.instagramAccessToken) {
|