@twitterapis/mcp 0.9.0 → 0.9.2

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.2 (2026-08-18)
4
+
5
+ ### Fixed
6
+
7
+ - **The server no longer exits at startup when `TWITTERAPIS_KEY` is missing.** A registry connectivity scanner (Smithery, Glama, the official MCP registry, Claude Connectors Directory) spins up the server with no real credential just to enumerate `tools/list`. Exiting before the transport connected made every automated scan fail outright and read as a generic connectivity error, HTTP 405 on Smithery, rather than a missing-key error, which is why this listing scored low on registry capability-quality checks that can only run once a scan succeeds. Tools now register and `tools/list` responds regardless of whether a key is present, verified live with the key stripped from the process env. An actual tool call made with no key still fails clearly, at the point of the call, with the same style of message the existing 401 branch already used.
8
+ - Refreshing the vendored spec to verify the fix surfaced 3 endpoints the live API had added with no corresponding tool, and a new `product` parameter on an existing one; the openapi-parity gate refuses a build until every spec change is covered, so both are addressed in this same release rather than left drifting.
9
+
10
+ ### Added
11
+
12
+ - **`twitter_list_followers`**: a public List's followers, a different set from its members.
13
+ - **`twitter_community_search`**: find X Communities by keyword, the discovery step that produces the numeric id the rest of the community family needs.
14
+ - **`twitter_community_about`**: a community's moderators and a member preview as full user profiles, complementing the reduced rows `twitter_community_members` / `twitter_community_moderators` return.
15
+ - **`twitter_list_tweets`** gains a `product` argument (Latest / Top), matching the live API's new search-ranking parameter for that endpoint.
16
+
17
+ Catalog is now **94 tools: 60 reads and 34 write actions**.
18
+
3
19
  ## 0.9.0 (2026-08-17)
4
20
 
5
21
  ### 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
- 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).
94
+ 94 tools: 60 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
 
@@ -125,7 +125,8 @@ Public reads (search, profiles, tweets, followers, likes) work with just your AP
125
125
  | `twitter_tweet_retweeters` | Accounts that retweeted a tweet |
126
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` |
127
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 |
128
+ | `twitter_list_followers` | Accounts that follow a public List (a different set from its members) |
129
+ | `twitter_list_tweets` | Posts by a List's members, search-backed: filterable by `since` / `until` date, `include_replies`, and `product` (Latest / Top), no retweets |
129
130
  | `twitter_list_timeline` | A List's native X feed: retweets and X's own ordering included, no filters, paging only |
130
131
  | `twitter_home_timeline` | Your authenticated account's Home timeline _(session)_ |
131
132
  | `twitter_bookmarks` | Your authenticated account's bookmarks _(session)_ |
@@ -137,7 +138,9 @@ Public reads (search, profiles, tweets, followers, likes) work with just your AP
137
138
  | `twitter_dm_list` | Your DM conversations (inbox), read-only _(session)_ |
138
139
  | `twitter_dm_conversation` | Messages in one DM conversation, read-only _(session)_ |
139
140
  | `twitter_spaces_info` | Metadata and participant roster for one X Space, live or ended (by Space `id`) |
141
+ | `twitter_community_search` | Find X Communities by keyword; the discovery step that produces the numeric id the rest of the community family needs |
140
142
  | `twitter_community_info` | One X Community by numeric id: name, counts, join policy, rules, topic, banners, admin |
143
+ | `twitter_community_about` | A community's moderators and a member preview, each returned as a full user profile, not the reduced row `_members`/`_moderators` return |
141
144
  | `twitter_community_members` | A community's member roster, each row carrying that member's `Admin` / `Moderator` / `Member` role |
142
145
  | `twitter_community_moderators` | A community's moderators and admins, from its own upstream operation (not a filter over the roster) |
143
146
  | `twitter_community_tweets` | A community's post timeline, with the pinned post returned as its own `pinned` field |
@@ -292,7 +295,7 @@ Calls are billed to your twitterapis.com account. Almost every endpoint is $0.00
292
295
 
293
296
  **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.
294
297
 
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.
298
+ **Is it read-only?** No. 60 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.
296
299
 
297
300
  **Which clients are supported?** Claude Desktop, Cursor, Windsurf, and VS Code (Copilot agent mode), or any Model Context Protocol client.
298
301
 
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@twitterapis/mcp",
3
- "version": "0.9.0",
3
+ "mcpName": "io.github.TwitterAPIs/twitterapis-mcp",
4
+ "version": "0.9.2",
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",
@@ -32,6 +33,8 @@
32
33
  "check:body-mode-parity": "node test/body-mode-parity.mjs",
33
34
  "check:firewall": "node test/firewall.mjs",
34
35
  "check:registry": "node test/registry-manifests.mjs",
36
+ "bundle": "npx -y @anthropic-ai/mcpb@2.1.2 pack .",
37
+ "check:mcpb": "npx -y @anthropic-ai/mcpb@2.1.2 validate manifest.json",
35
38
  "check:publish-provenance": "node test/publish-provenance.mjs"
36
39
  },
37
40
  "dependencies": {
package/src/index.js CHANGED
@@ -32,11 +32,19 @@ const BASE_URL = (
32
32
  ).replace(/\/+$/, "");
33
33
  const REQUEST_TIMEOUT_MS = Number(process.env.TWITTERAPIS_TIMEOUT_MS || 30000);
34
34
 
35
+ // Lazy validation, not exit-on-boot: an MCP registry scanner (Smithery, Glama,
36
+ // the official registry, Claude Connectors) connects the stdio transport with
37
+ // no real credential to enumerate tools/list. Exiting here before the server
38
+ // ever registers a tool makes that handshake fail outright and reads as a
39
+ // generic connectivity error, not a missing-key error, on the scanner side --
40
+ // confirmed live 2026-08-18 (Smithery: "Initialization failed... could not be
41
+ // automatically scanned", HTTP 405). Warn and continue; a real tool CALL made
42
+ // with no key still fails clearly, at the point of the call, same as it
43
+ // already does for a bad key (see the 401 branch below).
35
44
  if (!API_KEY) {
36
45
  console.error(
37
- "[twitterapis-mcp] Missing TWITTERAPIS_KEY. Get a key at https://www.twitterapis.com/signup and set it in your MCP client config.",
46
+ "[twitterapis-mcp] Missing TWITTERAPIS_KEY. Get a key at https://www.twitterapis.com/signup and set it in your MCP client config. Tools are registered but every call will fail until it is set.",
38
47
  );
39
- process.exit(1);
40
48
  }
41
49
 
42
50
  // ── REST call ────────────────────────────────────────────────────────────────
@@ -51,6 +59,15 @@ if (!API_KEY) {
51
59
  // before building the query string or body, so a pathParams arg never leaks
52
60
  // into either.
53
61
  async function callEndpoint(path, args, method = "GET", jsonBody = false, pathParams = []) {
62
+ if (!API_KEY) {
63
+ return {
64
+ isError: true,
65
+ content: [{
66
+ type: "text",
67
+ text: "Missing TWITTERAPIS_KEY (invalid or missing API key, get one at https://www.twitterapis.com/signup and set it in your MCP client config).",
68
+ }],
69
+ };
70
+ }
54
71
  // Fill {name} URL segments from args and strip those keys, so a pathParams arg
55
72
  // (e.g. a monitor/webhook id) never also leaks into the query string or JSON
56
73
  // body. A missing value fails loudly rather than shipping a request that still
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: 91 tools (57 reads, 34 writes).
11
+ // Catalog: 94 tools (60 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
@@ -493,6 +493,23 @@ export const TOOLS = [
493
493
  ),
494
494
  },
495
495
  },
496
+ {
497
+ name: "twitter_list_followers",
498
+ path: "/twitter/list/followers",
499
+ description:
500
+ "Fetch a public List's followers by its numeric id, cursor-paginated. Followers and members are different sets of people: members are the accounts the List owner added to it, followers are the accounts that subscribed to read it. A List with hundreds of members commonly has only a handful of followers, so a small count here is normal and is not a truncated page. Use twitter_list_members for the member roster instead.",
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>.",
504
+ ),
505
+ count: z.number().int().min(1).max(100).optional().describe(
506
+ "Max items to return for this page. Defaults to 20 and is clamped to 1-100.",
507
+ ),
508
+ cursor: z.string().optional().describe(
509
+ "Opaque pagination cursor from a previous response's next_cursor field. Omit on the first call. next_cursor is null once X marks the follower list complete.",
510
+ ),
511
+ },
512
+ },
496
513
  {
497
514
  name: "twitter_list_tweets",
498
515
  path: "/twitter/list/tweets",
@@ -511,6 +528,9 @@ export const TOOLS = [
511
528
  include_replies: z.string().optional().describe(
512
529
  "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
530
  ),
531
+ product: z.enum(["Latest","Top"]).optional().describe(
532
+ "Which search ranking to read. 'Latest' (default) is reverse-chronological. 'Top' is X's ranked ordering. Any unrecognised value falls back to Latest rather than erroring.",
533
+ ),
514
534
  count: z.number().int().min(1).max(100).optional().describe(
515
535
  "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
536
  ),
@@ -553,6 +573,20 @@ export const TOOLS = [
553
573
  ),
554
574
  },
555
575
  },
576
+ {
577
+ name: "twitter_community_search",
578
+ path: "/twitter/community/search",
579
+ description:
580
+ "Find X Communities by keyword, cursor-paginated. This is the discovery step the rest of the community family assumes: every other community endpoint starts from a community id, and this is the one that produces one. Each hit is a compact record, id, name, member count, nsfw flag, topic name, banners and the facepile avatars, exactly what X's own search sends and nothing more. Once you have an id, use twitter_community_info or twitter_community_about for detail, twitter_community_members / twitter_community_moderators for the roster, and twitter_community_tweets for its posts.",
581
+ shape: {
582
+ query: z.string().describe(
583
+ "Keyword to search for, 1 to 500 characters, e.g. 'build in public'.",
584
+ ),
585
+ cursor: z.string().optional().describe(
586
+ "Opaque pagination cursor from a previous response's next_cursor field. Omit on the first call.",
587
+ ),
588
+ },
589
+ },
556
590
  {
557
591
  name: "twitter_community_info",
558
592
  path: "/twitter/community/info",
@@ -564,6 +598,17 @@ export const TOOLS = [
564
598
  ),
565
599
  },
566
600
  },
601
+ {
602
+ name: "twitter_community_about",
603
+ path: "/twitter/community/about",
604
+ description:
605
+ "The About tab for one X Community: its moderators, and a preview of its members, both returned as FULL user profiles with bio, follower and following counts, tweet counts, location, website, banner and join date. twitter_community_members and twitter_community_moderators return a reduced row instead, so this is the endpoint that answers who runs a community in one call rather than one call plus a profile lookup per person. Use twitter_community_info instead for the community's own metadata (name, description, rules, join policy); this endpoint is about the PEOPLE, not the community object.",
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
+ },
611
+ },
567
612
  {
568
613
  name: "twitter_community_members",
569
614
  path: "/twitter/community/members",