@twitterapis/mcp 0.9.8 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 (2026-09-13)
4
+
5
+ ### Added
6
+
7
+ - **Seven compose tools: private drafts and scheduled posts.** `twitter_draft_create`, `twitter_draft_edit`, `twitter_draft_delete`, `twitter_draft_list`, `twitter_scheduled_create`, `twitter_scheduled_delete`, `twitter_scheduled_list`. Catalog is now **106 tools, 65 reads and 41 writes**, still exact parity with the API's endpoint count. A MINOR bump, not a patch: the catalog grew, so a consumer pinned to `0.9.x` opts in rather than receiving seven new tools silently, which is exactly what `scripts/prepublish-version-class.mjs` refuses.
8
+ - **The one thing a model cannot read off a schema is said in every one of the seven descriptions: a DRAFT is private and never posts; a SCHEDULED post WILL publish publicly at its `execute_at` unless it is cancelled first.** A model choosing between `twitter_draft_create` and `twitter_scheduled_create` on the word "create" alone gets it wrong, and the failure is a real post going out. The descriptions also carry the routing to their siblings (`twitter_create_tweet` to post now) and the `execute_at` unit trap: epoch SECONDS, never the milliseconds `Date.now()` returns, with a value at or above 1e12 refused by the API rather than scheduled tens of thousands of years out.
9
+ - **Both list tools document `partial`.** True means X's answer was read but not fully understood; it is ABSENT on a clean read, so an empty array with no flag means the account genuinely has nothing. That distinction is what stops a model reporting "you have no drafts" when the real answer is "we could not read the reply".
10
+
11
+ ### Notes
12
+
13
+ - The routes these tools call were built, merged and deployed on 2026-09-13 behind two env flags (`DRAFT_TWEETS_ENABLED`, `SCHEDULED_TWEETS_ENABLED`, both answering 503 while off), verified end to end against a real customer session, then published and their flags removed. The snapshot in `test/openapi.snapshot.json` was refreshed from the LIVE published spec after the docs site deployed, which is the only order that works: the refresh fetches `docs.twitterapis.com/openapi.json`, never a local file.
14
+
15
+ ## 0.9.9 (2026-09-11)
16
+
17
+ ### Changed
18
+
19
+ - **The server instructions now ask for a control before an "ignored parameter" report.** A generic query on a score-ordered sort returns the site-wide listing, which reads exactly like a dropped parameter and is not one. Before drafting a report that a parameter is ignored or a field is empty, the model is told to re-run with a distinctive value that could only match if the parameter was honoured, and with the phrase quoted, then title the report by what the control showed.
20
+ - **`scripts/prepublish-version-class.mjs`, run by `prepublishOnly`, refuses a patch bump when the tool catalog grew.** It reads the published tarball from npm, counts the catalog on both sides, and blocks a publish whose version is a patch over the published one while the catalog is larger. Fails closed when npm cannot be read. `--selftest` covers both directions offline.
21
+
3
22
  ## 0.9.8 (2026-09-04)
4
23
 
5
24
  ### Added
package/README.md CHANGED
@@ -91,7 +91,7 @@ Restart Claude Desktop. The `twitter_*` tools appear in the tool picker.
91
91
 
92
92
  ## Tools
93
93
 
94
- 99 tools: 63 reads and 36 write actions. Most user endpoints accept `username` (handle without @) **or** `user_id` (`twitter_user_likes` and `twitter_user_tweets_complete` require `user_id`); tweet endpoints accept `id` **or** `url`; paginated endpoints return a `cursor` you pass back to get the next page. Four of the reads are free account lookups (`twitter_account_me`, `twitter_account_payments`, `twitter_feedback_get`, `twitter_feedback_list`); the 14 monitoring tools and `twitter_feedback_send` are also free (account administration, not metered reads).
94
+ 106 tools: 65 reads and 41 write actions. Most user endpoints accept `username` (handle without @) **or** `user_id` (`twitter_user_likes` and `twitter_user_tweets_complete` require `user_id`); tweet endpoints accept `id` **or** `url`; paginated endpoints return a `cursor` you pass back to get the next page. Four of the reads are free account lookups (`twitter_account_me`, `twitter_account_payments`, `twitter_feedback_get`, `twitter_feedback_list`); the 14 monitoring tools and `twitter_feedback_send` are also free (account administration, not metered reads).
95
95
 
96
96
  Public reads (search, profiles, tweets, followers, likes) work with just your API key. The **account-only** reads (bookmarks, DMs, home timeline, followers-you-know) and **most write actions** act AS an authenticated X account, so they need a session linked to your key first (returns HTTP 409 until then). Link a session either by registering your x.com cookies (`twitter_customer_session`) or by logging in with a username/password (`twitter_user_login`). Alternatively, pass **per-call inline credentials** on any of those tools (`auth_token` + `ct0`, with optional `proxy_url` / `user_agent`) to act AS that account for a single call without pre-registering a session, so one API key can act as many accounts. For write actions, set `proxy_url` to a residential proxy, since X soft-blocks writes that egress from datacenter IPs. Each write tool is annotated `readOnlyHint: false`; reversing actions (delete, unfollow, unlike, unretweet, unbookmark, monitor/webhook delete) are annotated `destructiveHint: true` so MCP clients can prompt before running them. The **monitoring** and **feedback** tools (see below) are the exception: they administer your twitterapis.com account, not an X session, so they need only your API key, no linked session and no inline credentials.
97
97
 
@@ -170,6 +170,22 @@ Public reads (search, profiles, tweets, followers, likes) work with just your AP
170
170
  | `twitter_list_add_member` / `twitter_list_remove_member` | Add / remove one account on a List you own; `member_count` comes back as proof the write landed |
171
171
  | `twitter_media_upload` | Upload a base64 image, returns a `media_id` for `twitter_create_tweet` |
172
172
 
173
+ ### Drafts and scheduled posts _(require a linked X session)_
174
+
175
+ A **draft** is private and never posts. A **scheduled** post **will publish publicly** at its `execute_at` unless you cancel it first. Both lists read only your own account.
176
+
177
+ | Tool | What it does |
178
+ |---|---|
179
+ | `twitter_draft_create` | Save a private draft on your account; nothing is posted |
180
+ | `twitter_draft_edit` | Replace one draft's contents by `id`; the fields you send become the draft |
181
+ | `twitter_draft_delete` | Delete a draft by `id`; it was never public, so nothing is retracted |
182
+ | `twitter_draft_list` | List your drafts, each with `text` and `thread_truncated` |
183
+ | `twitter_scheduled_create` | Schedule a post for a future `execute_at` in epoch **seconds**, not milliseconds |
184
+ | `twitter_scheduled_delete` | Cancel a pending scheduled post before it sends |
185
+ | `twitter_scheduled_list` | List your pending queue; `execute_at` comes back in epoch seconds |
186
+
187
+ Both list tools can return `partial: true`, which means X's answer was read but not fully understood. It is **not** the same as an empty list: on a clean read the flag is absent entirely, so an empty array with no `partial` means you genuinely have nothing queued or drafted.
188
+
173
189
  ### Articles _(X's long-form "Notes" feature; writes require a linked X session)_
174
190
 
175
191
  | Tool | What it does |
@@ -310,7 +326,7 @@ count: 50
310
326
 
311
327
  ## Pricing
312
328
 
313
- Calls are billed to your twitterapis.com account. Almost every endpoint is $0.0008/call: all reads (search, profiles, tweets, followers, likes) plus the simple write actions (like, retweet, bookmark, follow and their undos, delete). At the read rate that works out to $0.04 per 1,000 tweets, since each call returns about 20 tweets. The premium endpoints cost a little more: tweet creation, sending a DM (`twitter_dm_send`), and DM reads (`twitter_dm_list`, `twitter_dm_conversation`) at $0.0016/call, full tweet history (`twitter_user_tweets_complete`) at $0.0024/call, a full tweet thread (`twitter_tweet_thread`) and a Grok answer (`twitter_grok_chat`) at $0.004/call, and the article-editing writes (`twitter_article_create`, `twitter_article_update_title`, `twitter_article_update_cover_media`, `twitter_article_update_content`, `twitter_article_publish`, `twitter_article_unpublish`) at $0.0016/call (`twitter_article_get`, `twitter_article_list`, and `twitter_article_delete` stay at the standard $0.0008/call). Your first $0.50 is free. See [twitterapis.com/pricing](https://www.twitterapis.com/pricing).
329
+ Calls are billed to your twitterapis.com account. Most endpoints are $0.0008/call: nearly every read (search, profiles, tweets, followers, likes) plus the simple write actions (like, retweet, bookmark, follow and their undos, delete). At the read rate that works out to $0.04 per 1,000 tweets, since each call returns about 20 tweets. The premium endpoints cost a little more: tweet creation, sending a DM (`twitter_dm_send`), and DM reads (`twitter_dm_list`, `twitter_dm_conversation`) at $0.0016/call, full tweet history (`twitter_user_tweets_complete`) at $0.0024/call, a full tweet thread (`twitter_tweet_thread`) and a Grok answer (`twitter_grok_chat`) at $0.004/call, and the article-editing writes (`twitter_article_create`, `twitter_article_update_title`, `twitter_article_update_cover_media`, `twitter_article_update_content`, `twitter_article_publish`, `twitter_article_unpublish`) at $0.0016. The compose surface is $0.0016 for `twitter_draft_create`, `twitter_draft_edit`, `twitter_scheduled_create` and, note, BOTH LIST READS (`twitter_draft_list`, `twitter_scheduled_list`), which are the two reads that are not at the read rate; `twitter_draft_delete` and `twitter_scheduled_delete` are $0.0008/call (`twitter_article_get`, `twitter_article_list`, and `twitter_article_delete` stay at the standard $0.0008/call). Your first $0.50 is free. See [twitterapis.com/pricing](https://www.twitterapis.com/pricing).
314
330
 
315
331
  ## Links
316
332
 
@@ -323,7 +339,7 @@ Calls are billed to your twitterapis.com account. Almost every endpoint is $0.00
323
339
 
324
340
  **Do I need an X (Twitter) developer account?** No. Get an API key at [twitterapis.com/signup](https://www.twitterapis.com/signup); there is no application or approval step.
325
341
 
326
- **Is it read-only?** No. 63 read tools work with just your API key; 36 write actions (post, like, retweet, follow, DM, media upload, List create/add member/remove member, article create/edit/publish/delete, monitor/webhook create/update/delete, feedback send) act as a linked X account or per-call inline credentials, except monitor/webhook CRUD and feedback, which are account administration and need only your API key.
342
+ **Is it read-only?** No. 65 read tools work with just your API key; 41 write actions (post, save or schedule a post, like, retweet, follow, DM, media upload, List create/add member/remove member, article create/edit/publish/delete, monitor/webhook create/update/delete, feedback send) act as a linked X account or per-call inline credentials, except monitor/webhook CRUD and feedback, which are account administration and need only your API key.
327
343
 
328
344
  **Which clients are supported?** Claude Desktop, Cursor, Windsurf, and VS Code (Copilot agent mode), or any Model Context Protocol client.
329
345
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@twitterapis/mcp",
3
3
  "mcpName": "io.github.TwitterAPIs/twitterapis-mcp",
4
- "version": "0.9.8",
4
+ "version": "0.10.0",
5
5
  "description": "Official MCP server for twitterapis.com, the Twitter/X API (search, users, followers, tweets, threads, lists, likes, bookmarks, DMs) plus write actions (post/like/retweet/follow) as native tools for Claude, Cursor, and any MCP client.",
6
6
  "repository": {
7
7
  "type": "git",
@@ -29,7 +29,7 @@
29
29
  "build:check": "node scripts/gen-tools.mjs --check",
30
30
  "openapi:refresh": "node scripts/openapi-refresh.mjs",
31
31
  "test": "node scripts/gen-tools.mjs --check && node scripts/gen-manifest-tools.mjs --check && node test/gen-tools-endpoints.mjs && node test/catalog-identity.mjs && node test/tools.test.mjs && node test/feedback.test.mjs && node test/hint-for.test.mjs && node test/smoke.mjs && node test/openapi-parity.mjs && node test/body-mode-parity.mjs && node test/readme-parity.mjs && node test/firewall.mjs && node test/registry-manifests.mjs && node scripts/__tests__/reconcile-mcp-publish-chain.test.mjs",
32
- "prepublishOnly": "npm test && node test/publish-provenance.mjs",
32
+ "prepublishOnly": "npm test && node test/publish-provenance.mjs && node scripts/prepublish-version-class.mjs",
33
33
  "check:openapi-parity": "node test/openapi-parity.mjs",
34
34
  "check:body-mode-parity": "node test/body-mode-parity.mjs",
35
35
  "check:firewall": "node test/firewall.mjs",
package/src/index.js CHANGED
@@ -229,7 +229,9 @@ const INSTRUCTIONS =
229
229
  "twitterapis.com MCP server. Read tools cost credits per call (most $0.0008); account, monitoring and feedback tools are free. " +
230
230
  "If a twitterapis tool call fails with an error other than 401/402/409/429 and the user has to work around it, if the user asks for something no twitterapis tool covers, " +
231
231
  "if a documented field comes back empty or wrong, or if the user is clearly frustrated with a result, draft a report with twitter_feedback_send (action \"draft\"). " +
232
- "Drafting is local and silent; never send a draft unless the user names it after reviewing action \"list\".";
232
+ "Drafting is local and silent; never send a draft unless the user names it after reviewing action \"list\". " +
233
+ "Before drafting a report that a parameter is IGNORED or a field is EMPTY, re-run the call with a distinctive value that could only match if the parameter was honoured, and with the phrase quoted; " +
234
+ "if either comes back on topic the issue is ranking or matching, so title it that way and say what the control showed.";
233
235
 
234
236
  const server = new McpServer({ name: "twitterapis", version: VERSION }, { instructions: INSTRUCTIONS });
235
237
 
package/src/tools.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // file in memory and fails if it does not match what is committed, so a hand edit
9
9
  // here is caught rather than shipped.
10
10
  //
11
- // Catalog: 99 tools (63 reads, 36 writes).
11
+ // Catalog: 106 tools (65 reads, 41 writes).
12
12
  //
13
13
  // Each tool maps 1:1 to a REST endpoint at https://api.twitterapis.com. Tool arg
14
14
  // names map 1:1 to endpoint query params (every endpoint, including the POST
@@ -1161,6 +1161,212 @@ export const TOOLS = [
1161
1161
  ),
1162
1162
  },
1163
1163
  },
1164
+ {
1165
+ name: "twitter_draft_create",
1166
+ path: "/twitter/draft/create",
1167
+ method: "POST",
1168
+ write: true,
1169
+ description:
1170
+ "Save a PRIVATE draft tweet on your authenticated account. Nothing is posted and nobody can see it: the draft lands in X's own composer under Drafts until a human publishes or deletes it. Use this when a person still has to approve the wording. Use twitter_create_tweet to post right now, and twitter_scheduled_create when it should go out on its own at a known time. Requires an authenticated session behind your key. Returns ok and draft_tweet_id. A null draft_tweet_id means X refused the create, answers 422, and is not billed.",
1171
+ shape: {
1172
+ text: z.string().min(1).describe(
1173
+ "The draft body text. Required: a draft with no text is refused with 400, so a media-only draft cannot be created through this API.",
1174
+ ),
1175
+ reply_to: z.string().optional().describe(
1176
+ "Optional. Numeric id of the tweet this draft replies to. Send it as a string; X ids are 19 digits and an unquoted number is refused rather than silently rounded to a different tweet.",
1177
+ ),
1178
+ quote: z.string().optional().describe(
1179
+ "Optional. Numeric id of the tweet this draft quotes. Send it as a string, same reason as reply_to.",
1180
+ ),
1181
+ media_ids: z.string().optional().describe(
1182
+ "Optional. Comma-separated media id(s) from a prior media upload to attach. Up to 4.",
1183
+ ),
1184
+ auth_token: z.string().optional().describe(
1185
+ "Optional. The account's auth_token cookie, to act AS that account for this call (must be paired with ct0). Sent as the x-auth-token header; never placed in the URL.",
1186
+ ),
1187
+ ct0: z.string().optional().describe(
1188
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1189
+ ),
1190
+ proxy_url: z.string().optional().describe(
1191
+ "Optional. Residential proxy URL to egress this call through. Recommended for writes: X soft-blocks writes from datacenter IPs as automated. Sent as the x-proxy-url header.",
1192
+ ),
1193
+ user_agent: z.string().optional().describe(
1194
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1195
+ ),
1196
+ },
1197
+ },
1198
+ {
1199
+ name: "twitter_draft_edit",
1200
+ path: "/twitter/draft/edit",
1201
+ method: "POST",
1202
+ write: true,
1203
+ description:
1204
+ "Replace the contents of one existing PRIVATE draft on your authenticated account. The fields you send BECOME the draft rather than merging into it, so anything you leave out is dropped, including media. Get the id from twitter_draft_list or from the twitter_draft_create call that saved it. Still posts nothing. Requires an authenticated session behind your key. Returns ok and the draft_tweet_id you edited.",
1205
+ shape: {
1206
+ id: z.string().describe(
1207
+ "Numeric id of the draft to edit, from twitter_draft_list. Also accepted by the API as draft_tweet_id.",
1208
+ ),
1209
+ text: z.string().min(1).describe(
1210
+ "The replacement draft body text. Required: an edit with no text is refused with 400.",
1211
+ ),
1212
+ reply_to: z.string().optional().describe(
1213
+ "Optional. Numeric id of the tweet this draft replies to. Send it as a string.",
1214
+ ),
1215
+ quote: z.string().optional().describe(
1216
+ "Optional. Numeric id of the tweet this draft quotes. Send it as a string.",
1217
+ ),
1218
+ media_ids: z.string().optional().describe(
1219
+ "Optional. Comma-separated media id(s) to attach. Omitting this drops whatever media the draft had; it is not merged.",
1220
+ ),
1221
+ auth_token: z.string().optional().describe(
1222
+ "Optional. The account's auth_token cookie, to act AS that account for this call (must be paired with ct0). Sent as the x-auth-token header; never placed in the URL.",
1223
+ ),
1224
+ ct0: z.string().optional().describe(
1225
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1226
+ ),
1227
+ proxy_url: z.string().optional().describe(
1228
+ "Optional. Residential proxy URL to egress this call through. Recommended for writes: X soft-blocks writes from datacenter IPs as automated. Sent as the x-proxy-url header.",
1229
+ ),
1230
+ user_agent: z.string().optional().describe(
1231
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1232
+ ),
1233
+ },
1234
+ },
1235
+ {
1236
+ name: "twitter_draft_delete",
1237
+ path: "/twitter/draft/delete",
1238
+ method: "POST",
1239
+ write: true,
1240
+ destructive: true,
1241
+ description:
1242
+ "Delete one PRIVATE draft from your authenticated account by id. Irreversible, but low-stakes in a way twitter_delete_tweet is not: a draft was never public, so this retracts nothing and notifies nobody. Use twitter_delete_tweet for a post that is already live. Requires an authenticated session behind your key. Returns ok, deleted, and the draft_tweet_id you targeted.",
1243
+ shape: {
1244
+ id: z.string().describe(
1245
+ "Numeric id of the draft to delete, from twitter_draft_list. Also accepted by the API as draft_tweet_id.",
1246
+ ),
1247
+ auth_token: z.string().optional().describe(
1248
+ "Optional. The account's auth_token cookie, to act AS that account for this call (must be paired with ct0). Sent as the x-auth-token header; never placed in the URL.",
1249
+ ),
1250
+ ct0: z.string().optional().describe(
1251
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1252
+ ),
1253
+ proxy_url: z.string().optional().describe(
1254
+ "Optional. Residential proxy URL to egress this call through. Recommended for writes: X soft-blocks writes from datacenter IPs as automated. Sent as the x-proxy-url header.",
1255
+ ),
1256
+ user_agent: z.string().optional().describe(
1257
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1258
+ ),
1259
+ },
1260
+ },
1261
+ {
1262
+ name: "twitter_draft_list",
1263
+ path: "/twitter/draft/list",
1264
+ description:
1265
+ "List the PRIVATE drafts saved on your authenticated account. This is where a draft id comes from for an edit or a delete. Reads only your own account: drafts are private to the account that holds them, so there is no way to read anyone else's. Requires an authenticated session behind your key. Returns drafts (each with draft_tweet_id, text, thread_truncated), count, and sometimes partial. thread_truncated true means the draft is a THREAD and text is only its first tweet, which is a parse that succeeded. partial true means X's answer was read but not fully understood, which is NOT 'you have no drafts': it is absent entirely on a clean read, so an empty drafts array with no partial flag means the account genuinely has none. One call returns the whole list; there is no cursor and no timestamp on a draft row.",
1266
+ shape: {
1267
+ ascending: z.string().optional().describe(
1268
+ "Optional. Pass the STRING \"true\" to ask X for the oldest draft first. Anything else, including omitting it, sends ascending=false, which is what X's own composer sends. The resulting order is X's and is not re-sorted, so do not promise a user newest-first.",
1269
+ ),
1270
+ auth_token: z.string().optional().describe(
1271
+ "Optional. The account's auth_token cookie, to act AS that account for this call (must be paired with ct0). Sent as the x-auth-token header; never placed in the URL.",
1272
+ ),
1273
+ ct0: z.string().optional().describe(
1274
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1275
+ ),
1276
+ proxy_url: z.string().optional().describe(
1277
+ "Optional. Residential proxy URL to egress this call through. Recommended for writes: X soft-blocks writes from datacenter IPs as automated. Sent as the x-proxy-url header.",
1278
+ ),
1279
+ user_agent: z.string().optional().describe(
1280
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1281
+ ),
1282
+ },
1283
+ },
1284
+ {
1285
+ name: "twitter_scheduled_create",
1286
+ path: "/twitter/scheduled/create",
1287
+ method: "POST",
1288
+ write: true,
1289
+ description:
1290
+ "Schedule a tweet to POST PUBLICLY at a future instant from your authenticated account. This is NOT a draft: it goes out on its own at execute_at whether or not anyone is watching, unless it is cancelled first with twitter_scheduled_delete. Use twitter_draft_create when a human still has to approve the wording. execute_at is epoch SECONDS, never milliseconds: Date.now() returns milliseconds, so divide by 1000, and a millisecond value is refused with a message naming the unit rather than scheduling the post tens of thousands of years out. It must also be strictly in the future. Requires an authenticated session behind your key. Returns ok, scheduled_tweet_id, and the execute_at you sent.",
1291
+ shape: {
1292
+ text: z.string().min(1).describe(
1293
+ "The tweet body text that will be published. Required: a scheduled post with no text is refused with 400.",
1294
+ ),
1295
+ execute_at: z.number().int().describe(
1296
+ "When to post, as epoch SECONDS in the future (for example 1829752200). NOT milliseconds: a value of 1000000000000 or more is rejected as a millisecond timestamp. Also accepted by the API as schedule_at.",
1297
+ ),
1298
+ reply_to: z.string().optional().describe(
1299
+ "Optional. Numeric id of the tweet this post replies to. Send it as a string.",
1300
+ ),
1301
+ quote: z.string().optional().describe(
1302
+ "Optional. Numeric id of the tweet this post quotes. Send it as a string.",
1303
+ ),
1304
+ media_ids: z.string().optional().describe(
1305
+ "Optional. Comma-separated media id(s) from a prior media upload to attach. Up to 4.",
1306
+ ),
1307
+ auth_token: z.string().optional().describe(
1308
+ "Optional. The account's auth_token cookie, to act AS that account for this call (must be paired with ct0). Sent as the x-auth-token header; never placed in the URL.",
1309
+ ),
1310
+ ct0: z.string().optional().describe(
1311
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1312
+ ),
1313
+ proxy_url: z.string().optional().describe(
1314
+ "Optional. Residential proxy URL to egress this call through. Recommended for writes: X soft-blocks writes from datacenter IPs as automated. Sent as the x-proxy-url header.",
1315
+ ),
1316
+ user_agent: z.string().optional().describe(
1317
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1318
+ ),
1319
+ },
1320
+ },
1321
+ {
1322
+ name: "twitter_scheduled_delete",
1323
+ path: "/twitter/scheduled/delete",
1324
+ method: "POST",
1325
+ write: true,
1326
+ destructive: true,
1327
+ description:
1328
+ "Cancel one PENDING scheduled post on your authenticated account so it never publishes. Only works before its execute_at: once the post has gone out there is no scheduled row left to cancel, and the thing to remove is the resulting tweet, with twitter_delete_tweet. Get the id from twitter_scheduled_list. Requires an authenticated session behind your key. Returns ok, deleted, and the scheduled_tweet_id you targeted.",
1329
+ shape: {
1330
+ id: z.string().describe(
1331
+ "Numeric id of the scheduled post to cancel, from twitter_scheduled_list. Also accepted by the API as scheduled_tweet_id.",
1332
+ ),
1333
+ auth_token: z.string().optional().describe(
1334
+ "Optional. The account's auth_token cookie, to act AS that account for this call (must be paired with ct0). Sent as the x-auth-token header; never placed in the URL.",
1335
+ ),
1336
+ ct0: z.string().optional().describe(
1337
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1338
+ ),
1339
+ proxy_url: z.string().optional().describe(
1340
+ "Optional. Residential proxy URL to egress this call through. Recommended for writes: X soft-blocks writes from datacenter IPs as automated. Sent as the x-proxy-url header.",
1341
+ ),
1342
+ user_agent: z.string().optional().describe(
1343
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1344
+ ),
1345
+ },
1346
+ },
1347
+ {
1348
+ name: "twitter_scheduled_list",
1349
+ path: "/twitter/scheduled/list",
1350
+ description:
1351
+ "List the posts QUEUED to publish on your authenticated account. This is where a scheduled id comes from for a cancel, and it is worth reading before scheduling anything so a retry in your own code does not quietly queue the same post twice. Rows carry X's own state label verbatim (for example Scheduled), and a row that has already published leaves the queue and becomes an ordinary tweet. Requires an authenticated session behind your key. Returns scheduled (each with scheduled_tweet_id, text, thread_truncated, execute_at, state), count, and sometimes partial. thread_truncated is INFERRED on this endpoint rather than captured: a scheduled row carries the same compose payload a draft row does, and the captured scheduled row elides that body, so the flag is sound and fail-safe (an absent key yields false) but has not been seen true. execute_at comes back in epoch SECONDS: X answers this operation in milliseconds and the value is normalised, so a timestamp read here can be passed straight back into twitter_scheduled_create. partial true means X's answer was read but not fully understood, which is not the same as an empty queue; on a clean read it is absent entirely.",
1352
+ shape: {
1353
+ ascending: z.string().optional().describe(
1354
+ "Optional. Pass the STRING \"true\" for the oldest row first. Anything else, including omitting it, returns X's default order.",
1355
+ ),
1356
+ auth_token: z.string().optional().describe(
1357
+ "Optional. The account's auth_token cookie, to act AS that account for this call (must be paired with ct0). Sent as the x-auth-token header; never placed in the URL.",
1358
+ ),
1359
+ ct0: z.string().optional().describe(
1360
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1361
+ ),
1362
+ proxy_url: z.string().optional().describe(
1363
+ "Optional. Residential proxy URL to egress this call through. Recommended for writes: X soft-blocks writes from datacenter IPs as automated. Sent as the x-proxy-url header.",
1364
+ ),
1365
+ user_agent: z.string().optional().describe(
1366
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1367
+ ),
1368
+ },
1369
+ },
1164
1370
  {
1165
1371
  name: "twitter_favorite_tweet",
1166
1372
  path: "/twitter/tweet/favorite",