@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.
Files changed (63) hide show
  1. package/README.md +14 -12
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -58
  4. package/dist/index.js.map +1 -1
  5. package/dist/register-all.d.ts +4 -0
  6. package/dist/register-all.d.ts.map +1 -0
  7. package/dist/register-all.js +41 -0
  8. package/dist/register-all.js.map +1 -0
  9. package/dist/services/meta-client.d.ts +54 -0
  10. package/dist/services/meta-client.d.ts.map +1 -1
  11. package/dist/services/meta-client.js +203 -45
  12. package/dist/services/meta-client.js.map +1 -1
  13. package/dist/tools/annotations.d.ts +6 -0
  14. package/dist/tools/annotations.d.ts.map +1 -0
  15. package/dist/tools/annotations.js +27 -0
  16. package/dist/tools/annotations.js.map +1 -0
  17. package/dist/tools/instagram/comments.d.ts.map +1 -1
  18. package/dist/tools/instagram/comments.js +63 -33
  19. package/dist/tools/instagram/comments.js.map +1 -1
  20. package/dist/tools/instagram/hashtags.d.ts.map +1 -1
  21. package/dist/tools/instagram/hashtags.js +36 -18
  22. package/dist/tools/instagram/hashtags.js.map +1 -1
  23. package/dist/tools/instagram/media.d.ts.map +1 -1
  24. package/dist/tools/instagram/media.js +46 -24
  25. package/dist/tools/instagram/media.js.map +1 -1
  26. package/dist/tools/instagram/mentions.d.ts.map +1 -1
  27. package/dist/tools/instagram/mentions.js +20 -10
  28. package/dist/tools/instagram/mentions.js.map +1 -1
  29. package/dist/tools/instagram/messaging.d.ts.map +1 -1
  30. package/dist/tools/instagram/messaging.js +65 -31
  31. package/dist/tools/instagram/messaging.js.map +1 -1
  32. package/dist/tools/instagram/profile.d.ts.map +1 -1
  33. package/dist/tools/instagram/profile.js +50 -28
  34. package/dist/tools/instagram/profile.js.map +1 -1
  35. package/dist/tools/instagram/publishing.d.ts.map +1 -1
  36. package/dist/tools/instagram/publishing.js +77 -51
  37. package/dist/tools/instagram/publishing.js.map +1 -1
  38. package/dist/tools/meta/auth.d.ts.map +1 -1
  39. package/dist/tools/meta/auth.js +47 -21
  40. package/dist/tools/meta/auth.js.map +1 -1
  41. package/dist/tools/threads/insights.d.ts.map +1 -1
  42. package/dist/tools/threads/insights.js +20 -10
  43. package/dist/tools/threads/insights.js.map +1 -1
  44. package/dist/tools/threads/media.d.ts.map +1 -1
  45. package/dist/tools/threads/media.js +37 -23
  46. package/dist/tools/threads/media.js.map +1 -1
  47. package/dist/tools/threads/mentions.d.ts.map +1 -1
  48. package/dist/tools/threads/mentions.js +12 -6
  49. package/dist/tools/threads/mentions.js.map +1 -1
  50. package/dist/tools/threads/profile.d.ts.map +1 -1
  51. package/dist/tools/threads/profile.js +8 -2
  52. package/dist/tools/threads/profile.js.map +1 -1
  53. package/dist/tools/threads/publishing.d.ts.map +1 -1
  54. package/dist/tools/threads/publishing.js +157 -73
  55. package/dist/tools/threads/publishing.js.map +1 -1
  56. package/dist/tools/threads/replies.d.ts.map +1 -1
  57. package/dist/tools/threads/replies.js +40 -22
  58. package/dist/tools/threads/replies.js.map +1 -1
  59. package/dist/utils/response.d.ts +11 -0
  60. package/dist/utils/response.d.ts.map +1 -0
  61. package/dist/utils/response.js +6 -0
  62. package/dist/utils/response.js.map +1 -0
  63. 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 (8)
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 `callCount` approaches `100` you are near the per-app threshold.
363
- 2. Back off with exponential delay; reduce request volume; cache profile metadata between calls.
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; retry with exponential backoff. Check [metastatus.com](https://metastatus.com/) if it persists.
379
- - `error_type: "network"` — `fetch` timed out or failed before reaching Meta. Verify outbound connectivity and retry.
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAkFpE,wBAAgB,mBAAmB,cAmClC"}
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
- // Meta platform tools
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
- // Register tools
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
- registerMetaAuthTools(sandbox, mockClient);
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":"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"}
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,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { MetaClient } from "./services/meta-client.js";
3
+ export declare function registerAll(server: McpServer, client: MetaClient): void;
4
+ //# sourceMappingURL=register-all.d.ts.map
@@ -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;AA8BlD,MAAM,WAAW,iBAAiB;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;AAC1E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AA4BD,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAS;gBAEhB,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,iBAAiB;IAiB3D,OAAO,CAAC,cAAc;IAkBtB,OAAO,CAAC,gBAAgB;YAeV,OAAO;IAuFf,EAAE,CACN,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,UAAU,EACnB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,cAAc,CAAC;IAOpB,OAAO,CACX,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,UAAU,EACnB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,cAAc,CAAC;IAOpB,IAAI,CACR,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,UAAU,EACnB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,cAAc,CAAC;IAQ1B,0EAA0E;IACpE,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAUlE,sFAAsF;IAChF,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAMhE,wEAAwE;IAClE,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAUvE,oFAAoF;IAC9E,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAMrE,oBAAoB;IACd,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAU7D,IAAI,QAAQ,IAAI,MAAM,CAKrB;IAED,IAAI,aAAa,IAAI,MAAM,CAK1B;CACF"}
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
- init.headers = { "Content-Type": "application/json" };
119
- init.body = JSON.stringify(options.jsonBody);
234
+ baseInit.headers = { "Content-Type": "application/json" };
235
+ baseInit.body = JSON.stringify(options.jsonBody);
120
236
  }
121
237
  else if (isWrite) {
122
- init.headers = { "Content-Type": "application/x-www-form-urlencoded" };
123
- init.body = qs.toString();
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
- const res = await fetch(url, init);
129
- if (!res.ok) {
130
- const text = await res.text().catch(() => "");
131
- const parsed = parseMetaErrorBody(text);
132
- const detail = parsed?.message ?? text;
133
- throw new MetaApiError({
134
- message: `Meta API ${method} ${path} (${res.status}): ${detail}`,
135
- httpStatus: res.status,
136
- apiCode: parsed?.code,
137
- apiSubcode: parsed?.subcode,
138
- apiType: parsed?.type,
139
- fbtraceId: parsed?.fbtraceId,
140
- endpoint: path,
141
- method,
142
- body: text,
143
- });
144
- }
145
- const rateLimit = this.parseRateLimit(res.headers);
146
- const contentType = res.headers.get("content-type") || "";
147
- if (contentType.includes("application/json")) {
148
- const data = (await res.json());
149
- if (data.error) {
150
- const err = data.error;
151
- const apiCode = typeof err.code === "number" ? err.code : undefined;
152
- const apiSubcode = typeof err.error_subcode === "number" ? err.error_subcode : undefined;
153
- const apiType = typeof err.type === "string" ? err.type : undefined;
154
- const apiMessage = typeof err.message === "string" ? err.message : String(err.message ?? "");
155
- const fbtraceId = typeof err.fbtrace_id === "string" ? err.fbtrace_id : undefined;
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 error: ${apiMessage} (code ${apiCode ?? "?"})`,
158
- apiCode,
159
- apiSubcode,
160
- apiType,
161
- fbtraceId,
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: JSON.stringify(err),
299
+ body: text,
165
300
  });
166
301
  }
167
- return { data, rateLimit };
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
- const text = await res.text();
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) {