@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.
- package/README.md +47 -10
- package/index.js +302 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,50 +3,80 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@realaman90/x-mcp)
|
|
4
4
|
[](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
|
-
**
|
|
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) —
|
|
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
|
-
|
|
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
|
-
|
|
|
42
|
-
| `
|
|
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
|
|
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 (~
|
|
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
|
|
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}${
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
515
|
+
// ─── OAuth Write Tools ─────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
// 22. Upload media
|
|
353
518
|
server.tool(
|
|
354
|
-
"
|
|
355
|
-
"
|
|
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
|
-
|
|
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 ({
|
|
360
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
442
|
-
console.error("Set X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET for full
|
|
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