@twitterapis/mcp 0.7.8 → 0.9.1

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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 (2026-08-17)
4
+
5
+ ### Added
6
+
7
+ - **The X List operation family, 5 tools** (task #2251). Two reads that look like two spellings of one capability and are not:
8
+ - **`twitter_list_tweets`** (GET /twitter/list/tweets): posts written by the members of a public List, newest first, read through X's SEARCH INDEX. This is the filterable half of the List feed: it accepts `since` and `until` date bounds (`until` is EXCLUSIVE, matching X's own `until:` operator) and an `include_replies` toggle. It does not carry retweets, and search-index lag applies, so a post made moments ago can be missing for a short while.
9
+ - **`twitter_list_timeline`** (GET /twitter/list/timeline): the same List as X's OWN native feed, carrying members' retweets and X's List ordering. It takes only `list_id`, `count` and `cursor`, because a native timeline cannot honour search operators, so no date range and no reply filter exist on it.
10
+
11
+ The two are separated by their PARAMETER SETS, not by their names, and each tool description says so and names the other, so a model picking between them cannot silently answer a different question than the one asked. A regression check in `test/tools.test.mjs` pins the difference so a later edit cannot harmonise it away.
12
+
13
+ Three writes that run on the CUSTOMER'S REGISTERED X SESSION rather than a pooled account, because a List belongs to a specific account:
14
+ - **`twitter_list_create`** (POST /twitter/list/create): create a List with a `name` and an optional `description` and `is_private`. A List is public unless you explicitly ask otherwise, and a private List is not readable by the public List read tools.
15
+ - **`twitter_list_add_member`** and **`twitter_list_remove_member`** (POST /twitter/list/add_member, /twitter/list/remove_member): add or remove one account on a List you own. Both return the List's `member_count` read back from X after the write, which is the field to check rather than `ok` alone: X returns a populated `errors[]` on 100% of successful calls to these ops, so the error array cannot tell you whether your write applied, while the count can. `member_count` is null when X returned no list object at all, which is itself the not-applied signal. A write that does not apply comes back with the SAME field layout plus a 422 and a machine-readable reason, and is not billed.
16
+
17
+ All 5 cost $0.0008 per call. Register a session once with `twitter_customer_session`, or pass `auth_token` and `ct0` per call, for the three writes. Their backend handlers read every field through the dual-mode query-or-body helper, verified against the backend's own route-body-modes manifest by `test/body-mode-parity.mjs`, so none of them sets `jsonBody`. The catalog is now **91 tools: 57 reads and 34 write actions**.
18
+
19
+ ## 0.8.0 (2026-08-17)
20
+
21
+ ### Added
22
+
23
+ - **6 new reads: X Communities and tweet quotes.** `twitter_community_info` (one Community by numeric id: name, counts, join policy, rules, topic, banners, admin), `twitter_community_members` (the member roster, each row carrying that member's `Admin` / `Moderator` / `Member` role), `twitter_community_moderators` (moderators and admins from their own upstream operation, not a filter over the roster), `twitter_community_tweets` (the post timeline, with the pinned post returned as its own `pinned` field), `twitter_community_memberships` (the inverse lookup: every Community a given numeric `user_id` belongs to), and `twitter_tweet_quotes` (tweets that quote a tweet, with their text). The five Community reads are served by the account pool rather than by your session, which is why `role`, `can_join`, `is_pinned` and `viewer_relationship_type` come back null on them: those four describe the account that made the upstream call, and on a pooled read that is a rotating account you have never heard of. `twitter_tweet_quotes` is search-backed, so its `count` is what search returned rather than the tweet's true `quote_count`. The catalog moved to **86 tools: 55 reads and 31 write actions**. (This entry was written on 2026-08-17: the 0.8.0 release bumped the package version and shipped the tools but left no changelog entry behind it.)
24
+
3
25
  ## 0.7.8 (2026-08-16)
4
26
 
5
27
  ### 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
- 80 tools: 49 reads and 31 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. Two of the reads are free account/billing lookups (`twitter_account_me`, `twitter_account_payments`); the 14 monitoring tools are also free (account administration, not metered reads).
94
+ 91 tools: 57 reads and 34 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. Two of the reads are free account/billing lookups (`twitter_account_me`, `twitter_account_payments`); the 14 monitoring tools 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** tools (see below) are the one 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
 
@@ -123,7 +123,10 @@ Public reads (search, profiles, tweets, followers, likes) work with just your AP
123
123
  | `twitter_tweet_replies` | Replies to a tweet |
124
124
  | `twitter_tweet_thread` | Full author thread (connected tweet chain by same author) |
125
125
  | `twitter_tweet_retweeters` | Accounts that retweeted a tweet |
126
+ | `twitter_tweet_quotes` | Tweets that quote a tweet, with their text. Search-backed, so `count` is what search returned, not the tweet's true `quote_count` |
126
127
  | `twitter_list_members` | Members of a Twitter/X List |
128
+ | `twitter_list_tweets` | Posts by a List's members, search-backed: filterable by `since` / `until` date and `include_replies`, no retweets |
129
+ | `twitter_list_timeline` | A List's native X feed: retweets and X's own ordering included, no filters, paging only |
127
130
  | `twitter_home_timeline` | Your authenticated account's Home timeline _(session)_ |
128
131
  | `twitter_bookmarks` | Your authenticated account's bookmarks _(session)_ |
129
132
  | `twitter_blocking` | Accounts your authenticated account has blocked (your own list only) _(session)_ |
@@ -134,6 +137,11 @@ Public reads (search, profiles, tweets, followers, likes) work with just your AP
134
137
  | `twitter_dm_list` | Your DM conversations (inbox), read-only _(session)_ |
135
138
  | `twitter_dm_conversation` | Messages in one DM conversation, read-only _(session)_ |
136
139
  | `twitter_spaces_info` | Metadata and participant roster for one X Space, live or ended (by Space `id`) |
140
+ | `twitter_community_info` | One X Community by numeric id: name, counts, join policy, rules, topic, banners, admin |
141
+ | `twitter_community_members` | A community's member roster, each row carrying that member's `Admin` / `Moderator` / `Member` role |
142
+ | `twitter_community_moderators` | A community's moderators and admins, from its own upstream operation (not a filter over the roster) |
143
+ | `twitter_community_tweets` | A community's post timeline, with the pinned post returned as its own `pinned` field |
144
+ | `twitter_community_memberships` | The inverse lookup: every community a given numeric `user_id` belongs to |
137
145
  | `twitter_grok_chat` | Ask X's own Grok, grounded in live X data, and get the answer plus the sources it cited |
138
146
  | `twitter_grok_config` | Whether the authenticated account can use Grok, and which models it may pick |
139
147
  | `twitter_trends` | Current top trends for a location (by `country` or `woeid`) |
@@ -155,6 +163,8 @@ Public reads (search, profiles, tweets, followers, likes) work with just your AP
155
163
  | `twitter_bookmark_tweet` / `twitter_unbookmark_tweet` | Bookmark / remove bookmark |
156
164
  | `twitter_follow_user` / `twitter_unfollow_user` | Follow / unfollow a user by id |
157
165
  | `twitter_dm_send` | Send a Direct Message to a user by their numeric `recipient_id` |
166
+ | `twitter_list_create` | Create a Twitter/X List owned by your session (`name`, optional `description` / `is_private`) |
167
+ | `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 |
158
168
  | `twitter_media_upload` | Upload a base64 image, returns a `media_id` for `twitter_create_tweet` |
159
169
 
160
170
  ### Articles _(X's long-form "Notes" feature; writes require a linked X session)_
@@ -282,7 +292,7 @@ Calls are billed to your twitterapis.com account. Almost every endpoint is $0.00
282
292
 
283
293
  **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.
284
294
 
285
- **Is it read-only?** No. 49 read tools work with just your API key; 31 write actions (post, like, retweet, follow, DM, media upload, article create/edit/publish/delete, monitor/webhook create/update/delete) act as a linked X account or per-call inline credentials, except monitor/webhook CRUD, which is account administration and needs only your API key.
295
+ **Is it read-only?** No. 57 read tools work with just your API key; 34 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) act as a linked X account or per-call inline credentials, except monitor/webhook CRUD, which is account administration and needs only your API key.
286
296
 
287
297
  **Which clients are supported?** Claude Desktop, Cursor, Windsurf, and VS Code (Copilot agent mode), or any Model Context Protocol client.
288
298
 
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@twitterapis/mcp",
3
- "version": "0.7.8",
3
+ "mcpName": "io.github.TwitterAPIs/twitterapis-mcp",
4
+ "version": "0.9.1",
4
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.",
5
6
  "repository": {
6
7
  "type": "git",
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: 80 tools (49 reads, 31 writes).
11
+ // Catalog: 91 tools (57 reads, 34 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
@@ -450,6 +450,32 @@ export const TOOLS = [
450
450
  ),
451
451
  },
452
452
  },
453
+ {
454
+ name: "twitter_tweet_quotes",
455
+ path: "/twitter/tweet/quotes",
456
+ description:
457
+ "List the tweets that QUOTE a specific tweet, cursor-paginated as full tweet objects, so you get the commentary people attached rather than just a number. Different from twitter_tweet_retweeters (a plain retweet carries no text) and from twitter_tweet_replies (a reply is not a quote). IMPORTANT, state this to the user whenever you report a number from it: this endpoint is SEARCH-BACKED, because X exposes no dedicated quote-tweets operation, so it runs the query quoted_tweet_id:<id> against X's search index. The returned 'count' is therefore how many quotes THIS SEARCH returned, never the tweet's true total; the authoritative total is 'quote_count' on the tweet object from twitter_tweet_detail, and the two WILL differ because of index lag and because deleted, protected, suspended and region-withheld quotes are absent from search. Every response carries 'source' (always \"search\"), 'search_query' (the exact query sent), and 'quote_matched' (how many returned tweets demonstrably quote the requested id). quote_matched equal to count means every row is genuine; quote_matched 0 on a NON-EMPTY page means X stopped honouring the operator and the rows are junk, so discard that page rather than reporting it.",
458
+ shape: {
459
+ id: z.string().optional().describe(
460
+ "Tweet/post numeric id (e.g. \"1789012345678901234\"). Provide exactly one of id or url.",
461
+ ),
462
+ url: z.string().optional().describe(
463
+ "Full tweet URL, e.g. \"https://x.com/elonmusk/status/1789012345678901234\". Provide exactly one of id or url.",
464
+ ),
465
+ product: z.enum(["Latest","Top"]).optional().describe(
466
+ "Search ordering. 'Latest' (default) is reverse-chronological and cheap. 'Top' is X's ranked ordering and is materially slower upstream. Any other value falls back to Latest rather than changing what the tool means.",
467
+ ),
468
+ strict: z.string().optional().describe(
469
+ "Set true to DROP every returned row that does not demonstrably quote the requested tweet, instead of only counting them in quote_matched. Default false, because X does not embed the quoted original on every search result, so strict trades a false-positive risk for a false-negative one. Billing follows what you receive, so rows dropped by strict are not charged.",
470
+ ),
471
+ count: z.number().int().min(1).max(100).optional().describe(
472
+ "Max quote tweets to request for this page. Defaults to 20 and is clamped to 1-100 by the underlying search, so a larger number returns at most 100 rather than erroring.",
473
+ ),
474
+ cursor: z.string().optional().describe(
475
+ "Opaque pagination cursor from a previous response's next_cursor field. Omit on the first call.",
476
+ ),
477
+ },
478
+ },
453
479
  {
454
480
  name: "twitter_list_members",
455
481
  path: "/twitter/list/members",
@@ -467,6 +493,49 @@ export const TOOLS = [
467
493
  ),
468
494
  },
469
495
  },
496
+ {
497
+ name: "twitter_list_tweets",
498
+ path: "/twitter/list/tweets",
499
+ description:
500
+ "Read the posts written by the members of a public Twitter/X List, newest first, through X's search index. This is the FILTERABLE List feed: it accepts since and until date bounds and an include_replies toggle. It does NOT return retweets, and search-index lag applies, so a post made moments ago can be missing for a short while. Use twitter_list_timeline instead when you want the List exactly as X shows it, retweets and native ordering included, and accept that it takes no filters. Paginate with cursor. The list_id appears in the X.com list URL (x.com/i/lists/<list_id>).",
501
+ shape: {
502
+ list_id: z.string().describe(
503
+ "Numeric Twitter/X List id. Found in the list URL: x.com/i/lists/<list_id>. The List must be public.",
504
+ ),
505
+ since: z.string().optional().describe(
506
+ "Optional. Only posts on or after this date, as YYYY-MM-DD (e.g. \"2026-08-01\"). Any other format is rejected with a 400.",
507
+ ),
508
+ until: z.string().optional().describe(
509
+ "Optional. Only posts BEFORE this date, as YYYY-MM-DD. EXCLUSIVE, matching X's own until: search operator, so a post made on the until date is not returned. Any other format is rejected with a 400.",
510
+ ),
511
+ include_replies: z.string().optional().describe(
512
+ "Optional. Whether to include replies written by List members. Pass the string \"true\" or \"false\"; defaults to true when omitted. Any other value is rejected with a 400 rather than read as false.",
513
+ ),
514
+ count: z.number().int().min(1).max(100).optional().describe(
515
+ "Max posts to return for this page. Defaults to 20 and is clamped to 1-100, so a larger number returns at most 100 rather than erroring.",
516
+ ),
517
+ cursor: z.string().optional().describe(
518
+ "Opaque pagination cursor from a previous response's next_cursor field. Omit on the first call.",
519
+ ),
520
+ },
521
+ },
522
+ {
523
+ name: "twitter_list_timeline",
524
+ path: "/twitter/list/timeline",
525
+ description:
526
+ "Read a public Twitter/X List's NATIVE feed, the same posts and the same ordering the List shows on x.com, including members' retweets. It takes only list_id, count and cursor: no date range and no reply filter exist on this endpoint, because a native timeline cannot honour search operators. Use twitter_list_tweets when you need a date range or want replies filtered out, and accept that it drops retweets in exchange. Paginate with cursor until the tweets array comes back empty.",
527
+ shape: {
528
+ list_id: z.string().describe(
529
+ "Numeric Twitter/X List id. Found in the list URL: x.com/i/lists/<list_id>. The List must be public.",
530
+ ),
531
+ count: z.number().int().min(1).max(100).optional().describe(
532
+ "Max posts to return for this page. Defaults to 20 and is clamped to 1-100, so a larger number returns at most 100 rather than erroring.",
533
+ ),
534
+ cursor: z.string().optional().describe(
535
+ "Opaque pagination cursor from a previous response's next_cursor field. Omit on the first call.",
536
+ ),
537
+ },
538
+ },
470
539
  {
471
540
  name: "twitter_spaces_info",
472
541
  path: "/twitter/spaces/info",
@@ -484,6 +553,88 @@ export const TOOLS = [
484
553
  ),
485
554
  },
486
555
  },
556
+ {
557
+ name: "twitter_community_info",
558
+ path: "/twitter/community/info",
559
+ description:
560
+ "Get the metadata for one X Community by its numeric id: name, description, member_count, moderator_count, join_policy, invites_policy, the join question, primary topic, search tags, the posted rules, both the custom and the default banner plus a resolved banner_url, the permalink, the admin and creator profiles, and the facepile member ids. The community id is the digits in a x.com/i/communities/<id> URL. IMPORTANT: role, can_join, is_pinned and viewer_relationship_type are ALWAYS null here and that is deliberate, not an error, because they describe the account that made the call and this is a pooled read served by a rotating account. rules[].description is also always null: X sends only the rule id and name on this payload. Use twitter_community_members for the roster and twitter_community_tweets for the posts.",
561
+ shape: {
562
+ community_id: z.string().describe(
563
+ "Numeric X community id, the digits in a x.com/i/communities/<id> URL, e.g. '1493446837214187523'. Digits only. This is NOT a Space id (those are base-62 tokens) and NOT a user id.",
564
+ ),
565
+ },
566
+ },
567
+ {
568
+ name: "twitter_community_members",
569
+ path: "/twitter/community/members",
570
+ description:
571
+ "List the member roster of an X Community, cursor-paginated, with each row carrying that member's own role in the community: 'Admin', 'Moderator' or 'Member'. Rows are { user, role }. The user object is deliberately REDUCED (id, username, name, profile_image_url, is_blue_verified, verified, is_protected) because X's roster operation sends no bio, no follower or following counts and no created_at; call twitter_user_info with an id when the full profile is needed. Note that the role on a member ROW is NOT caller-relative and is returned in full, unlike the role field on the community object itself. Admins and moderators are interleaved through this list at arbitrary positions, so do NOT derive a moderator list by filtering the first page: use twitter_community_moderators. Paging is a bare next_cursor with no total count from X; stop when members comes back empty or has_more is false.",
572
+ shape: {
573
+ community_id: z.string().describe(
574
+ "Numeric X community id, the digits in a x.com/i/communities/<id> URL, e.g. '1493446837214187523'.",
575
+ ),
576
+ count: z.number().int().min(1).max(100).optional().describe(
577
+ "Max roster rows to return for this page. Defaults to 20 and is clamped to 1-100, so a larger number returns 100 rather than erroring.",
578
+ ),
579
+ cursor: z.string().optional().describe(
580
+ "Opaque pagination cursor from a previous response's next_cursor field. Omit on the first call. Absence of next_cursor is the only end-of-list signal X gives on this operation.",
581
+ ),
582
+ },
583
+ },
584
+ {
585
+ name: "twitter_community_moderators",
586
+ path: "/twitter/community/moderators",
587
+ description:
588
+ "List the moderators and admins of an X Community, cursor-paginated, in the same { user, role } row shape twitter_community_members returns (the array is also called members, deliberately, so the two cannot drift apart). This is a SEPARATE upstream operation, not a filter over the member roster, and that matters for correctness: moderators sit at arbitrary positions inside the full roster, so filtering one page of twitter_community_members would return 'the moderators among the first 20 members' while looking like a complete answer. Read each row's role rather than assuming every row is a Moderator, since admins appear here too. Paging is a bare next_cursor with no total count from X.",
589
+ shape: {
590
+ community_id: z.string().describe(
591
+ "Numeric X community id, the digits in a x.com/i/communities/<id> URL, e.g. '1493446837214187523'.",
592
+ ),
593
+ count: z.number().int().min(1).max(100).optional().describe(
594
+ "Max rows to return for this page. Defaults to 20 and is clamped to 1-100.",
595
+ ),
596
+ cursor: z.string().optional().describe(
597
+ "Opaque pagination cursor from a previous response's next_cursor field. Omit on the first call.",
598
+ ),
599
+ },
600
+ },
601
+ {
602
+ name: "twitter_community_tweets",
603
+ path: "/twitter/community/tweets",
604
+ description:
605
+ "Read an X Community's own post timeline, cursor-paginated as full tweet objects, with the community's PINNED post returned as its own separate 'pinned' field rather than as an item inside 'tweets'. That split is not cosmetic: X delivers the pinned post under a different timeline instruction and does not repeat it in the feed, so a client that iterates only 'tweets' silently loses it, and it is very often the community's rules post, the single most useful item in the response. To build one flat list, read 'pinned' first if non-null, then 'tweets' (the pinned post is excluded from 'tweets', so there is no duplicate). ranking_mode is a REAL upstream parameter, not a local sort. Use twitter_advanced_search instead when the search should span all of X rather than one community.",
606
+ shape: {
607
+ community_id: z.string().describe(
608
+ "Numeric X community id, the digits in a x.com/i/communities/<id> URL, e.g. '1493446837214187523'.",
609
+ ),
610
+ ranking_mode: z.enum(["Recency","Relevance"]).optional().describe(
611
+ "Ordering, sent to X as a real request parameter. 'Recency' is the default and the only value confirmed against a live capture. 'Relevance' is accepted because X's own community tab offers exactly two orderings, but it is NOT confirmed live, so do not depend on it. Any other value is rejected with a 400.",
612
+ ),
613
+ count: z.number().int().min(1).max(100).optional().describe(
614
+ "Max posts to return for this page. Defaults to 20 and is clamped to 1-100.",
615
+ ),
616
+ cursor: z.string().optional().describe(
617
+ "Opaque pagination cursor from a previous response's next_cursor field. Omit on the first call.",
618
+ ),
619
+ },
620
+ },
621
+ {
622
+ name: "twitter_community_memberships",
623
+ path: "/twitter/community/memberships",
624
+ description:
625
+ "The INVERSE community lookup: given a numeric X USER id, list the communities that account belongs to, cursor-paginated. Every other community tool starts from a community; this one starts from an account, which makes it the tool for profiling which audiences a person sits inside. Each row is the FULL community object (the same shape twitter_community_info returns, with member counts, rules, topic, policies, admin and creator), so no follow-up call per community is needed. Takes a numeric user id ONLY, not a @handle: resolve a handle with twitter_user_info first, because resolving it here would silently cost a second call. An EMPTY communities array is a real, successful answer (the account is in no communities), not a not-found. As on twitter_community_info, role / can_join / is_pinned / viewer_relationship_type are always null on every community returned, because this is a pooled read.",
626
+ shape: {
627
+ user_id: z.string().describe(
628
+ "Numeric X user id, e.g. '1281109705495130113'. NOT a @handle and NOT a community id. Resolve a handle to its id with twitter_user_info first.",
629
+ ),
630
+ count: z.number().int().min(1).max(100).optional().describe(
631
+ "Max communities to return for this page. Defaults to 20 and is clamped to 1-100.",
632
+ ),
633
+ cursor: z.string().optional().describe(
634
+ "Opaque pagination cursor from a previous response's next_cursor field. Omit on the first call.",
635
+ ),
636
+ },
637
+ },
487
638
  {
488
639
  name: "twitter_grok_chat",
489
640
  path: "/twitter/grok/chat",
@@ -1114,6 +1265,94 @@ export const TOOLS = [
1114
1265
  ),
1115
1266
  },
1116
1267
  },
1268
+ {
1269
+ name: "twitter_list_add_member",
1270
+ path: "/twitter/list/add_member",
1271
+ method: "POST",
1272
+ write: true,
1273
+ description:
1274
+ "Add one account to a Twitter/X List that YOUR registered X session owns, by numeric list id and numeric user id. Use it to curate a List from code, for example adding each speaker at a conference to a List as they are announced. Returns ok, action, list_id, user_id, the List's member_count read back from X after the write, and the full list object. Read member_count to confirm the change landed: it is null when X returned no list object at all, which is itself the not-applied signal. A write that does not apply (the account is already a member, the List is not yours) comes back with the SAME field layout plus a 422 and a machine-readable reason, and is not billed. Reverse with twitter_list_remove_member.",
1275
+ shape: {
1276
+ list_id: z.string().describe(
1277
+ "Numeric id of the List you own. Found in the list URL: x.com/i/lists/<list_id>.",
1278
+ ),
1279
+ user_id: z.string().describe(
1280
+ "Numeric user id of the account to add. Resolve a handle to a user_id first with twitter_user_info.",
1281
+ ),
1282
+ auth_token: z.string().optional().describe(
1283
+ "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.",
1284
+ ),
1285
+ ct0: z.string().optional().describe(
1286
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1287
+ ),
1288
+ proxy_url: z.string().optional().describe(
1289
+ "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.",
1290
+ ),
1291
+ user_agent: z.string().optional().describe(
1292
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1293
+ ),
1294
+ },
1295
+ },
1296
+ {
1297
+ name: "twitter_list_remove_member",
1298
+ path: "/twitter/list/remove_member",
1299
+ method: "POST",
1300
+ write: true,
1301
+ destructive: true,
1302
+ description:
1303
+ "Remove one account from a Twitter/X List that YOUR registered X session owns, by numeric list id and numeric user id. Use it to prune a curated List, for example dropping accounts that have gone quiet. Returns ok, action, list_id, user_id, the List's member_count read back from X after the write, and the full list object. Read member_count to confirm the removal landed: it is null when X returned no list object at all, which is itself the not-applied signal. A write that does not apply (the account was never a member, the List is not yours) comes back with the SAME field layout plus a 422 and a machine-readable reason, and is not billed. Reverse with twitter_list_add_member.",
1304
+ shape: {
1305
+ list_id: z.string().describe(
1306
+ "Numeric id of the List you own. Found in the list URL: x.com/i/lists/<list_id>.",
1307
+ ),
1308
+ user_id: z.string().describe(
1309
+ "Numeric user id of the account to remove. Resolve a handle to a user_id first with twitter_user_info.",
1310
+ ),
1311
+ auth_token: z.string().optional().describe(
1312
+ "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.",
1313
+ ),
1314
+ ct0: z.string().optional().describe(
1315
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1316
+ ),
1317
+ proxy_url: z.string().optional().describe(
1318
+ "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.",
1319
+ ),
1320
+ user_agent: z.string().optional().describe(
1321
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1322
+ ),
1323
+ },
1324
+ },
1325
+ {
1326
+ name: "twitter_list_create",
1327
+ path: "/twitter/list/create",
1328
+ method: "POST",
1329
+ write: true,
1330
+ description:
1331
+ "Create a new Twitter/X List owned by YOUR registered X session, with a name and an optional description and privacy flag. This is the starting point for building a List from code: create it here, then fill it with twitter_list_add_member using the list id this returns. Returns ok, action, the new list_id, member_count, and the full list object X returned. A List is PUBLIC unless you explicitly ask for a private one, and a private List is not readable by the public List read tools (twitter_list_members, twitter_list_tweets, twitter_list_timeline).",
1332
+ shape: {
1333
+ name: z.string().min(1).describe(
1334
+ "Display name for the new List, e.g. \"Founders\". Required; an empty or whitespace-only name is rejected with a 400.",
1335
+ ),
1336
+ description: z.string().optional().describe(
1337
+ "Optional. Description shown on the List, e.g. \"People building in public\". Defaults to empty.",
1338
+ ),
1339
+ is_private: z.string().optional().describe(
1340
+ "Optional. Pass the string \"true\" to create a PRIVATE List. Defaults to false (public), because a public List can be made private later while a leak cannot be undone. Note a private List is not readable by the public List read tools.",
1341
+ ),
1342
+ auth_token: z.string().optional().describe(
1343
+ "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.",
1344
+ ),
1345
+ ct0: z.string().optional().describe(
1346
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
1347
+ ),
1348
+ proxy_url: z.string().optional().describe(
1349
+ "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.",
1350
+ ),
1351
+ user_agent: z.string().optional().describe(
1352
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
1353
+ ),
1354
+ },
1355
+ },
1117
1356
  {
1118
1357
  name: "twitter_customer_session",
1119
1358
  path: "/twitter/customer/session",