@realaman90/x-mcp 2.0.2 → 2.1.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 (3) hide show
  1. package/README.md +47 -10
  2. package/index.js +302 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,50 +3,80 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@realaman90/x-mcp)](https://www.npmjs.com/package/@realaman90/x-mcp)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- MCP server for X/Twitter API — give Claude (or any MCP client) the ability to search, read, post, like, retweet, follow, bookmark, and more.
6
+ MCP server for X/Twitter API — give Claude (or any MCP client) the ability to search, read, post, like, retweet, follow, bookmark, manage lists, explore communities, and more.
7
7
 
8
- **27 tools total**: 8 read-only (Bearer token) + 19 write/advanced-read (OAuth 1.0a).
8
+ **44 tools total**: 17 read-only (Bearer token) + 27 write/advanced-read (OAuth 1.0a).
9
9
 
10
10
  ## Why
11
11
 
12
12
  - Search and analyze tweets without leaving your AI workflow
13
13
  - Post, reply, quote-tweet, and run polls directly from Claude
14
14
  - Like, retweet, follow, bookmark, block, mute — all from your terminal
15
+ - Manage X Lists — create, update, add/remove members, pin/unpin
16
+ - Explore Communities, trending topics, and news
17
+ - Upload images and attach them to tweets
18
+ - Monitor your API usage
15
19
  - Works with Claude Code, Claude Desktop, Codex, or any MCP client
16
20
  - OAuth credentials optional — runs read-only with just a Bearer token
17
21
 
18
22
  ## Tools
19
23
 
20
- ### Always available (Bearer token only) — 8 tools
24
+ ### Always available (Bearer token only) — 17 tools
21
25
 
22
26
  | Tool | Description |
23
27
  |------|-------------|
28
+ | **Tweets** | |
24
29
  | `search_tweets` | Search recent tweets (last 7 days) with full query operators |
25
30
  | `get_user_profile` | Get user profile by username (bio, followers, etc.) |
26
31
  | `get_user_tweets` | Get a user's recent tweets by user ID |
27
32
  | `get_tweet_replies` | Get replies to a specific tweet |
28
33
  | `get_tweet` | Get a single tweet with full details and metrics |
34
+ | **Users** | |
29
35
  | `get_user_followers` | Get a user's followers |
30
36
  | `get_user_following` | Get who a user is following |
31
37
  | `get_liking_users` | Get users who liked a tweet |
32
-
33
- ### Requires OAuth 1.0a 19 tools (auto-registered when credentials present)
38
+ | **Trends** | |
39
+ | `get_trending_topics` | Get trending topics by location (WOEID) |
40
+ | **Communities** | |
41
+ | `get_community` | Get details for a specific community |
42
+ | `search_communities` | Search communities by keyword |
43
+ | **News** | |
44
+ | `get_news` | Get a news article/cluster by ID |
45
+ | **Usage** | |
46
+ | `get_api_usage` | Get your API tweet consumption stats |
47
+ | **Lists** | |
48
+ | `get_list` | Get list details by ID |
49
+ | `get_user_lists` | Get lists owned by a user |
50
+ | `get_list_members` | Get members of a list |
51
+ | `get_user_list_memberships` | Get lists a user belongs to |
52
+
53
+ ### Requires OAuth 1.0a — 27 tools (auto-registered when credentials present)
34
54
 
35
55
  | Tool | Description |
36
56
  |------|-------------|
57
+ | **Read** | |
37
58
  | `get_my_profile` | Get your own profile |
38
59
  | `get_user_mentions` | Get tweets mentioning a user |
39
60
  | `get_quote_tweets` | Get quote tweets of a tweet |
40
61
  | `get_bookmarks` | Get your bookmarked tweets |
41
- | `get_trending_topics` | Get trending topics by location |
42
- | `create_post` | Post a tweet (text, reply, quote, poll) |
62
+ | **Post** | |
63
+ | `upload_media` | Upload an image (from URL) for use in tweets |
64
+ | `create_post` | Post a tweet (text, reply, quote, poll, media) |
43
65
  | `delete_post` | Delete your own tweet |
66
+ | **Engage** | |
44
67
  | `like_post` / `unlike_post` | Like or unlike a tweet |
45
68
  | `repost` / `unrepost` | Retweet or undo retweet |
46
69
  | `follow_user` / `unfollow_user` | Follow or unfollow a user |
47
70
  | `bookmark_post` / `unbookmark_post` | Bookmark or remove bookmark |
48
71
  | `block_user` / `unblock_user` | Block or unblock a user |
49
72
  | `mute_user` / `unmute_user` | Mute or unmute a user |
73
+ | **Lists** | |
74
+ | `create_list` | Create a new list |
75
+ | `update_list` | Update list name/description/privacy |
76
+ | `delete_list` | Delete a list you own |
77
+ | `add_list_member` | Add a user to your list |
78
+ | `remove_list_member` | Remove a user from your list |
79
+ | `pin_list` / `unpin_list` | Pin or unpin a list |
50
80
 
51
81
  ## Quick Start
52
82
 
@@ -59,7 +89,7 @@ MCP server for X/Twitter API — give Claude (or any MCP client) the ability to
59
89
  - Copy the **API Key** and **API Secret** (for write access)
60
90
  - Generate and copy the **Access Token** and **Access Token Secret** (for write access)
61
91
 
62
- > **Read-only mode**: Only the Bearer Token is required. The 8 read tools work without OAuth.
92
+ > **Read-only mode**: Only the Bearer Token is required. The 17 read tools work without OAuth.
63
93
 
64
94
  ### 2. Add to Claude Code
65
95
 
@@ -145,6 +175,13 @@ Like tweet 1234567890
145
175
  Retweet the latest tweet from @username
146
176
  ```
147
177
 
178
+ ### Lists
179
+ ```
180
+ Create a private list called "AI Builders"
181
+ Add @username to my "AI Builders" list
182
+ Show me the members of list 123456
183
+ ```
184
+
148
185
  ### Chain tools
149
186
  1. `get_user_profile` → get user ID from username
150
187
  2. `get_user_tweets` → get their recent tweets
@@ -168,10 +205,10 @@ Retweet the latest tweet from @username
168
205
 
169
206
  ## How It Works
170
207
 
171
- Single-file MCP server (~430 lines). No build step, no config files, zero extra dependencies. Uses Node.js built-in `crypto` for OAuth signing.
208
+ Single-file MCP server (~720 lines). No build step, no config files, zero extra dependencies. Uses Node.js built-in `crypto` for OAuth signing.
172
209
 
173
210
  ```
174
- Claude ↔ stdio ↔ x-mcp ↔ X API v1.1/v2
211
+ Claude ↔ stdio ↔ x-mcp ↔ X API v2
175
212
 
176
213
  Bearer (read) + OAuth 1.0a (write)
177
214
  ```
package/index.js CHANGED
@@ -35,6 +35,8 @@ const HAS_OAUTH = !!(OAUTH.consumerKey && OAUTH.consumerSecret && OAUTH.token &&
35
35
 
36
36
  const TWEET_FIELDS = "created_at,public_metrics,author_id,conversation_id,in_reply_to_user_id,lang";
37
37
  const USER_FIELDS = "created_at,description,public_metrics,profile_image_url,url,verified";
38
+ const LIST_FIELDS = "created_at,description,follower_count,member_count,owner_id,private";
39
+ const COMMUNITY_FIELDS = "name,description,member_count,created_at,is_private";
38
40
 
39
41
  // ─── Bearer fetch (read-only, no user context) ────────────────────────────────
40
42
 
@@ -95,7 +97,7 @@ function oauthSign(method, url, queryParams = {}) {
95
97
  async function xapiAuth(method, endpoint, body) {
96
98
  const isV1 = endpoint.startsWith("/1.1/");
97
99
  const base = isV1 ? "https://api.x.com" : "https://api.x.com/2";
98
- const fullUrl = `${base}${isV1 ? endpoint : endpoint}`;
100
+ const fullUrl = `${base}${endpoint}`;
99
101
 
100
102
  // Parse any query params from endpoint for signing
101
103
  const urlObj = new URL(fullUrl);
@@ -123,6 +125,23 @@ async function xapiAuth(method, endpoint, body) {
123
125
  return res.json();
124
126
  }
125
127
 
128
+ // ─── OAuth multipart upload ────────────────────────────────────────────────────
129
+
130
+ async function xapiUpload(formData) {
131
+ const url = "https://upload.x.com/2/media/upload";
132
+ const authHeader = oauthSign("POST", url);
133
+ const res = await fetch(url, {
134
+ method: "POST",
135
+ headers: { Authorization: authHeader },
136
+ body: formData,
137
+ });
138
+ if (!res.ok) {
139
+ const text = await res.text();
140
+ throw new Error(`X API upload ${res.status}: ${text}`);
141
+ }
142
+ return res.json();
143
+ }
144
+
126
145
  // ─── Cached authenticated user ID ──────────────────────────────────────────────
127
146
 
128
147
  let _myUserId = null;
@@ -137,7 +156,7 @@ async function getMyUserId() {
137
156
 
138
157
  const server = new McpServer({
139
158
  name: "x-mcp",
140
- version: "2.0.0",
159
+ version: "2.1.0",
141
160
  });
142
161
 
143
162
  const ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] });
@@ -146,6 +165,8 @@ const ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data, nul
146
165
  // BEARER-ONLY TOOLS (always registered)
147
166
  // ═══════════════════════════════════════════════════════════════════════════════
148
167
 
168
+ // ─── Tweets ──────────────────────────────────────────────────────────────────
169
+
149
170
  // 1. Search tweets
150
171
  server.tool(
151
172
  "search_tweets",
@@ -228,6 +249,8 @@ server.tool(
228
249
  }
229
250
  );
230
251
 
252
+ // ─── Users ───────────────────────────────────────────────────────────────────
253
+
231
254
  // 6. Get user's followers
232
255
  server.tool(
233
256
  "get_user_followers",
@@ -279,6 +302,146 @@ server.tool(
279
302
  }
280
303
  );
281
304
 
305
+ // ─── Trends ──────────────────────────────────────────────────────────────────
306
+
307
+ // 9. Get trending topics (v2)
308
+ server.tool(
309
+ "get_trending_topics",
310
+ "Get current trending topics for a location. Default WOEID 1 = worldwide, 23424977 = US, 23424975 = UK.",
311
+ {
312
+ woeid: z.number().default(1).describe("Where On Earth ID (1=worldwide, 23424977=US, 23424975=UK)"),
313
+ },
314
+ async ({ woeid }) => {
315
+ const data = await xapi(`/trends/by/woeid/${woeid}`);
316
+ return ok(data);
317
+ }
318
+ );
319
+
320
+ // ─── Communities ─────────────────────────────────────────────────────────────
321
+
322
+ // 10. Get community
323
+ server.tool(
324
+ "get_community",
325
+ "Get details for a specific X Community by ID. Returns name, description, member count, and privacy status.",
326
+ { community_id: z.string().describe("Community ID") },
327
+ async ({ community_id }) => {
328
+ const data = await xapi(`/communities/${community_id}`, {
329
+ "community.fields": COMMUNITY_FIELDS,
330
+ });
331
+ return ok(data);
332
+ }
333
+ );
334
+
335
+ // 11. Search communities
336
+ server.tool(
337
+ "search_communities",
338
+ "Search for X Communities by keyword.",
339
+ {
340
+ query: z.string().describe("Search query for communities"),
341
+ max_results: z.number().min(1).max(100).default(10).describe("Number of results (1-100)"),
342
+ },
343
+ async ({ query, max_results }) => {
344
+ const data = await xapi("/communities/search", {
345
+ query,
346
+ max_results,
347
+ "community.fields": COMMUNITY_FIELDS,
348
+ });
349
+ return ok(data);
350
+ }
351
+ );
352
+
353
+ // ─── News ────────────────────────────────────────────────────────────────────
354
+
355
+ // 12. Get news
356
+ server.tool(
357
+ "get_news",
358
+ "Get a news article/cluster by ID from X. Returns contexts and related posts.",
359
+ { news_id: z.string().describe("News ID") },
360
+ async ({ news_id }) => {
361
+ const data = await xapi(`/news/${news_id}`, {
362
+ "news.fields": "contexts,cluster_posts_results",
363
+ });
364
+ return ok(data);
365
+ }
366
+ );
367
+
368
+ // ─── Usage ───────────────────────────────────────────────────────────────────
369
+
370
+ // 13. Get API usage
371
+ server.tool(
372
+ "get_api_usage",
373
+ "Get your X API tweet consumption usage. Shows daily usage, app ID, and project cap.",
374
+ {},
375
+ async () => {
376
+ const data = await xapi("/usage/tweets");
377
+ return ok(data);
378
+ }
379
+ );
380
+
381
+ // ─── Lists (read) ────────────────────────────────────────────────────────────
382
+
383
+ // 14. Get list
384
+ server.tool(
385
+ "get_list",
386
+ "Get details for a specific X List by ID. Returns name, description, member/follower counts, and privacy status.",
387
+ { list_id: z.string().describe("List ID") },
388
+ async ({ list_id }) => {
389
+ const data = await xapi(`/lists/${list_id}`, { "list.fields": LIST_FIELDS });
390
+ return ok(data);
391
+ }
392
+ );
393
+
394
+ // 15. Get user's owned lists
395
+ server.tool(
396
+ "get_user_lists",
397
+ "Get all Lists owned by a user. Use get_user_profile first to get the user ID.",
398
+ {
399
+ user_id: z.string().describe("X user ID (numeric string)"),
400
+ max_results: z.number().min(1).max(100).default(100).describe("Number of results (1-100)"),
401
+ },
402
+ async ({ user_id, max_results }) => {
403
+ const data = await xapi(`/users/${user_id}/owned_lists`, {
404
+ max_results,
405
+ "list.fields": LIST_FIELDS,
406
+ });
407
+ return ok(data);
408
+ }
409
+ );
410
+
411
+ // 16. Get list members
412
+ server.tool(
413
+ "get_list_members",
414
+ "Get all members of a specific X List.",
415
+ {
416
+ list_id: z.string().describe("List ID"),
417
+ max_results: z.number().min(1).max(100).default(100).describe("Number of results (1-100)"),
418
+ },
419
+ async ({ list_id, max_results }) => {
420
+ const data = await xapi(`/lists/${list_id}/members`, {
421
+ max_results,
422
+ "user.fields": USER_FIELDS,
423
+ });
424
+ return ok(data);
425
+ }
426
+ );
427
+
428
+ // 17. Get user's list memberships
429
+ server.tool(
430
+ "get_user_list_memberships",
431
+ "Get all Lists a user is a member of. Use get_user_profile first to get the user ID.",
432
+ {
433
+ user_id: z.string().describe("X user ID (numeric string)"),
434
+ max_results: z.number().min(1).max(100).default(100).describe("Number of results (1-100)"),
435
+ },
436
+ async ({ user_id, max_results }) => {
437
+ const data = await xapi(`/users/${user_id}/list_memberships`, {
438
+ max_results,
439
+ "list.fields": LIST_FIELDS,
440
+ });
441
+ return ok(data);
442
+ }
443
+ );
444
+
282
445
  // ═══════════════════════════════════════════════════════════════════════════════
283
446
  // OAUTH TOOLS (only registered when OAuth credentials are present)
284
447
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -287,7 +450,7 @@ if (HAS_OAUTH) {
287
450
 
288
451
  // ─── OAuth Read Tools ──────────────────────────────────────────────────────
289
452
 
290
- // 9. Get my profile
453
+ // 18. Get my profile
291
454
  server.tool(
292
455
  "get_my_profile",
293
456
  "Get the authenticated user's own profile. Requires OAuth — returns your user ID, bio, metrics, etc.",
@@ -298,7 +461,7 @@ if (HAS_OAUTH) {
298
461
  }
299
462
  );
300
463
 
301
- // 10. Get user mentions
464
+ // 19. Get user mentions
302
465
  server.tool(
303
466
  "get_user_mentions",
304
467
  "Get recent tweets mentioning a specific user. Requires OAuth for user-context access.",
@@ -315,7 +478,7 @@ if (HAS_OAUTH) {
315
478
  }
316
479
  );
317
480
 
318
- // 11. Get quote tweets
481
+ // 20. Get quote tweets
319
482
  server.tool(
320
483
  "get_quote_tweets",
321
484
  "Get tweets that quote a specific tweet. Requires OAuth for user-context access.",
@@ -332,7 +495,7 @@ if (HAS_OAUTH) {
332
495
  }
333
496
  );
334
497
 
335
- // 12. Get bookmarks
498
+ // 21. Get bookmarks
336
499
  server.tool(
337
500
  "get_bookmarks",
338
501
  "Get the authenticated user's bookmarked tweets. Bearer token is explicitly forbidden for this endpoint.",
@@ -349,36 +512,50 @@ if (HAS_OAUTH) {
349
512
  }
350
513
  );
351
514
 
352
- // 13. Get trending topics
515
+ // ─── OAuth Write Tools ─────────────────────────────────────────────────────
516
+
517
+ // 22. Upload media
353
518
  server.tool(
354
- "get_trending_topics",
355
- "Get current trending topics for a location. Uses v1.1 API. Default WOEID 1 = worldwide, 23424977 = US.",
519
+ "upload_media",
520
+ "Upload an image for use in tweets. Pass a publicly accessible URL — the image will be downloaded and uploaded to X. Returns a media_id to use with create_post. Max 5MB for images, 15MB for GIFs.",
356
521
  {
357
- woeid: z.number().default(1).describe("Where On Earth ID (1=worldwide, 23424977=US, 23424975=UK)"),
522
+ media_url: z.string().describe("Public URL of the image to upload"),
523
+ media_category: z.enum(["tweet_image", "dm_image"]).default("tweet_image").describe("Media category"),
358
524
  },
359
- async ({ woeid }) => {
360
- const data = await xapiAuth("GET", `/1.1/trends/place.json?id=${woeid}`, null);
525
+ async ({ media_url, media_category }) => {
526
+ // Download the image
527
+ const imgRes = await fetch(media_url);
528
+ if (!imgRes.ok) throw new Error(`Failed to download image: ${imgRes.status}`);
529
+ const blob = await imgRes.blob();
530
+ const contentType = imgRes.headers.get("content-type") || "image/jpeg";
531
+
532
+ // Upload via multipart
533
+ const formData = new FormData();
534
+ formData.append("media", blob, { type: contentType });
535
+ formData.append("media_category", media_category);
536
+
537
+ const data = await xapiUpload(formData);
361
538
  return ok(data);
362
539
  }
363
540
  );
364
541
 
365
- // ─── OAuth Write Tools ─────────────────────────────────────────────────────
366
-
367
- // 14. Create post (most complex write tool)
542
+ // 23. Create post (supports text, reply, quote, poll, media)
368
543
  server.tool(
369
544
  "create_post",
370
- "Create a new tweet/post on X. Supports text, replies, quote tweets, and polls. Max 280 characters for text.",
545
+ "Create a new tweet/post on X. Supports text, replies, quote tweets, polls, and media attachments. Max 280 characters for text.",
371
546
  {
372
547
  text: z.string().max(280).describe("Tweet text (max 280 characters)"),
373
548
  reply_to: z.string().optional().describe("Tweet ID to reply to (makes this a reply)"),
374
549
  quote_tweet_id: z.string().optional().describe("Tweet ID to quote (makes this a quote tweet)"),
550
+ media_ids: z.array(z.string()).min(1).max(4).optional().describe("Media IDs from upload_media (1-4 images)"),
375
551
  poll_options: z.array(z.string()).min(2).max(4).optional().describe("Poll options (2-4 choices). Creates a poll attached to the tweet."),
376
552
  poll_duration_minutes: z.number().min(5).max(10080).optional().describe("Poll duration in minutes (5-10080, default 1440 = 24h). Only used with poll_options."),
377
553
  },
378
- async ({ text, reply_to, quote_tweet_id, poll_options, poll_duration_minutes }) => {
554
+ async ({ text, reply_to, quote_tweet_id, media_ids, poll_options, poll_duration_minutes }) => {
379
555
  const body = { text };
380
556
  if (reply_to) body.reply = { in_reply_to_tweet_id: reply_to };
381
557
  if (quote_tweet_id) body.quote_tweet_id = quote_tweet_id;
558
+ if (media_ids) body.media = { media_ids };
382
559
  if (poll_options) {
383
560
  body.poll = {
384
561
  options: poll_options,
@@ -390,7 +567,7 @@ if (HAS_OAUTH) {
390
567
  }
391
568
  );
392
569
 
393
- // 15. Delete post
570
+ // 24. Delete post
394
571
  server.tool(
395
572
  "delete_post",
396
573
  "Delete one of your own tweets by ID. This action is irreversible.",
@@ -401,7 +578,110 @@ if (HAS_OAUTH) {
401
578
  }
402
579
  );
403
580
 
404
- // 16-27. Toggle tools (like/unlike, repost/unrepost, etc.)
581
+ // ─── Lists (write) ────────────────────────────────────────────────────────
582
+
583
+ // 25. Create list
584
+ server.tool(
585
+ "create_list",
586
+ "Create a new X List.",
587
+ {
588
+ name: z.string().max(25).describe("List name (max 25 characters)"),
589
+ description: z.string().max(100).optional().describe("List description (max 100 characters)"),
590
+ private: z.boolean().default(false).describe("Whether the list is private"),
591
+ },
592
+ async ({ name, description, private: isPrivate }) => {
593
+ const body = { name, private: isPrivate };
594
+ if (description) body.description = description;
595
+ const data = await xapiAuth("POST", "/lists", body);
596
+ return ok(data);
597
+ }
598
+ );
599
+
600
+ // 26. Update list
601
+ server.tool(
602
+ "update_list",
603
+ "Update an existing X List's name, description, or privacy.",
604
+ {
605
+ list_id: z.string().describe("List ID to update"),
606
+ name: z.string().max(25).optional().describe("New list name"),
607
+ description: z.string().max(100).optional().describe("New list description"),
608
+ private: z.boolean().optional().describe("Whether the list is private"),
609
+ },
610
+ async ({ list_id, name, description, private: isPrivate }) => {
611
+ const body = {};
612
+ if (name !== undefined) body.name = name;
613
+ if (description !== undefined) body.description = description;
614
+ if (isPrivate !== undefined) body.private = isPrivate;
615
+ const data = await xapiAuth("PUT", `/lists/${list_id}`, body);
616
+ return ok(data);
617
+ }
618
+ );
619
+
620
+ // 27. Delete list
621
+ server.tool(
622
+ "delete_list",
623
+ "Delete an X List you own. This action is irreversible.",
624
+ { list_id: z.string().describe("List ID to delete") },
625
+ async ({ list_id }) => {
626
+ const data = await xapiAuth("DELETE", `/lists/${list_id}`, null);
627
+ return ok(data);
628
+ }
629
+ );
630
+
631
+ // 28. Add list member
632
+ server.tool(
633
+ "add_list_member",
634
+ "Add a user to an X List you own.",
635
+ {
636
+ list_id: z.string().describe("List ID"),
637
+ user_id: z.string().describe("User ID to add"),
638
+ },
639
+ async ({ list_id, user_id }) => {
640
+ const data = await xapiAuth("POST", `/lists/${list_id}/members`, { user_id });
641
+ return ok(data);
642
+ }
643
+ );
644
+
645
+ // 29. Remove list member
646
+ server.tool(
647
+ "remove_list_member",
648
+ "Remove a user from an X List you own.",
649
+ {
650
+ list_id: z.string().describe("List ID"),
651
+ user_id: z.string().describe("User ID to remove"),
652
+ },
653
+ async ({ list_id, user_id }) => {
654
+ const data = await xapiAuth("DELETE", `/lists/${list_id}/members/${user_id}`, null);
655
+ return ok(data);
656
+ }
657
+ );
658
+
659
+ // 30. Pin list
660
+ server.tool(
661
+ "pin_list",
662
+ "Pin an X List to your profile.",
663
+ { list_id: z.string().describe("List ID to pin") },
664
+ async ({ list_id }) => {
665
+ const myId = await getMyUserId();
666
+ const data = await xapiAuth("POST", `/users/${myId}/pinned_lists`, { list_id });
667
+ return ok(data);
668
+ }
669
+ );
670
+
671
+ // 31. Unpin list
672
+ server.tool(
673
+ "unpin_list",
674
+ "Unpin an X List from your profile.",
675
+ { list_id: z.string().describe("List ID to unpin") },
676
+ async ({ list_id }) => {
677
+ const myId = await getMyUserId();
678
+ const data = await xapiAuth("DELETE", `/users/${myId}/pinned_lists/${list_id}`, null);
679
+ return ok(data);
680
+ }
681
+ );
682
+
683
+ // ─── Toggle tools (like/unlike, repost/unrepost, etc.) ─────────────────────
684
+
405
685
  const toggleTools = [
406
686
  ["like_post", "Like a tweet", "POST", (me) => `/users/${me}/likes`, "tweet_id", (id) => ({ tweet_id: id })],
407
687
  ["unlike_post", "Unlike a previously liked tweet", "DELETE", (me) => `/users/${me}/likes/${"{id}"}`, "tweet_id", null],
@@ -437,9 +717,10 @@ if (HAS_OAUTH) {
437
717
 
438
718
  // ─── Start server ──────────────────────────────────────────────────────────────
439
719
 
720
+ const toolCount = HAS_OAUTH ? 44 : 17;
440
721
  if (!HAS_OAUTH) {
441
- console.error("OAuth credentials not found — running with 8 read-only tools (Bearer token only)");
442
- console.error("Set X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET for full 27-tool access");
722
+ console.error("OAuth credentials not found — running with 17 read-only tools (Bearer token only)");
723
+ console.error("Set X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET for full 44-tool access");
443
724
  }
444
725
 
445
726
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realaman90/x-mcp",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "MCP server for X/Twitter API — read tweets, profiles, timelines + write posts, likes, retweets, follows, bookmarks, and more",
5
5
  "type": "module",
6
6
  "main": "index.js",