@twitterapis/mcp 0.2.0 → 0.3.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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-06-29)
4
+
5
+ ### Added
6
+
7
+ - **Per-call inline credentials** for multi-account use: the 16 session tools (all writes + account-only reads) accept optional `auth_token` + `ct0` (plus optional `proxy_url` / `user_agent`) to act AS that account for a single call, with no pre-registered session, so one API key can act as many accounts. Sent as `x-auth-token` / `x-ct0` / `x-proxy-url` / `x-user-agent` request headers, never in the URL or query string.
8
+ - For write actions, set `proxy_url` to a residential proxy: X soft-blocks writes that egress from datacenter IPs.
9
+
3
10
  ## 0.2.0 (2026-06-25)
4
11
 
5
12
  ### Added (full API parity)
package/README.md CHANGED
@@ -89,7 +89,7 @@ Restart Claude Desktop. The `twitter_*` tools appear in the tool picker.
89
89
 
90
90
  37 tools: 27 reads and 10 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.
91
91
 
92
- Public reads (search, profiles, tweets, followers) work with just your API key. The **account-only** reads (likes, bookmarks, DMs, home timeline, followers-you-know) and **all write actions** act AS an authenticated X account, so they need a session linked to your key first (returns HTTP 409 until then). Each write tool is annotated `readOnlyHint: false`; reversing actions (delete, unfollow, unlike, unretweet, unbookmark) are annotated `destructiveHint: true` so MCP clients can prompt before running them.
92
+ 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 **all write actions** act AS an authenticated X account, so they need a session linked to your key first (returns HTTP 409 until then). 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) are annotated `destructiveHint: true` so MCP clients can prompt before running them.
93
93
 
94
94
  ### Reads
95
95
 
@@ -201,7 +201,7 @@ count: 50
201
201
 
202
202
  ## Pricing
203
203
 
204
- Calls are billed to your twitterapis.com account at the standard read rate ($0.0008/call, or $0.04 per 1,000 tweets (each call returns about 20 tweets)); your first $0.50 is free. See [twitterapis.com/pricing](https://www.twitterapis.com/pricing).
204
+ Calls are billed to your twitterapis.com account, every price 20% below getxapi.com per endpoint. Almost every endpoint is $0.0008/call: all reads (search, profiles, tweets, followers, likes) plus the simple write actions (like, retweet, bookmark, follow and their undos, delete, media upload). At the read rate that works out to $0.04 per 1,000 tweets, since each call returns about 20 tweets. The premium endpoints cost a little more: tweet creation and DM reads (`twitter_dm_list`, `twitter_dm_conversation`) at $0.0016/call, profile updates (name, avatar, banner) at $0.0016/call, full tweet history (`twitter_user_tweets_complete`) at $0.0024/call, and a full tweet thread (`twitter_tweet_thread`) at $0.004/call. Your first $0.50 is free. See [twitterapis.com/pricing](https://www.twitterapis.com/pricing).
205
205
 
206
206
  ## Links
207
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twitterapis/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "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
5
  "repository": {
6
6
  "type": "git",
package/src/index.js CHANGED
@@ -38,21 +38,35 @@ if (!API_KEY) {
38
38
  // query string, so the same buildQuery path serves both; only the HTTP method
39
39
  // differs per tool.
40
40
  async function callEndpoint(path, args, method = "GET") {
41
- const q = buildQuery(args);
41
+ // Pull per-call inline credentials out of args so they travel as request
42
+ // headers, never the query string (the API reads x-auth-token / x-ct0; passing
43
+ // them as query params would leak them into URLs and access logs). When
44
+ // supplied, this one API key acts as that account; otherwise the key's linked
45
+ // session is used. Lets a single key act as many accounts.
46
+ const { auth_token, ct0, user_agent, proxy_url, ...rest } = args || {};
47
+ const q = buildQuery(rest);
42
48
  const url = `${BASE_URL}${path}${q ? `?${q}` : ""}`;
43
49
 
50
+ const headers = {
51
+ // The API accepts either header; send both for maximum compatibility.
52
+ Authorization: `Bearer ${API_KEY}`,
53
+ "x-api-key": API_KEY,
54
+ accept: "application/json",
55
+ "user-agent": "twitterapis-mcp/0.3.0",
56
+ };
57
+ if (auth_token && ct0) {
58
+ headers["x-auth-token"] = auth_token;
59
+ headers["x-ct0"] = ct0;
60
+ if (user_agent) headers["x-user-agent"] = user_agent;
61
+ if (proxy_url) headers["x-proxy-url"] = proxy_url;
62
+ }
63
+
44
64
  const ctrl = new AbortController();
45
65
  const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
46
66
  try {
47
67
  const res = await fetch(url, {
48
68
  method,
49
- headers: {
50
- // The API accepts either header; send both for maximum compatibility.
51
- Authorization: `Bearer ${API_KEY}`,
52
- "x-api-key": API_KEY,
53
- accept: "application/json",
54
- "user-agent": "twitterapis-mcp/0.2.0",
55
- },
69
+ headers,
56
70
  signal: ctrl.signal,
57
71
  });
58
72
  const body = await res.text();
@@ -85,7 +99,7 @@ async function callEndpoint(path, args, method = "GET") {
85
99
  }
86
100
 
87
101
  // ── MCP server ───────────────────────────────────────────────────────────────
88
- const server = new McpServer({ name: "twitterapis", version: "0.2.0" });
102
+ const server = new McpServer({ name: "twitterapis", version: "0.3.0" });
89
103
 
90
104
  for (const tool of TOOLS) {
91
105
  const method = tool.method || "GET";
package/src/tools.js CHANGED
@@ -42,6 +42,25 @@ const ACCOUNT = {
42
42
  "Optional. The @handle (without @) of the authenticated account to act AS, when your key manages more than one session. Omit to use your key's default session.",
43
43
  ),
44
44
  };
45
+ // Per-call inline credentials. Pass an account's own X session cookies to act AS
46
+ // that account for this one call, without pre-registering a session, so a single
47
+ // API key can act as many accounts (e.g. polling several inboxes or posting from
48
+ // a pool). Sent as request headers, never in the URL. Omit to use the key's
49
+ // linked session.
50
+ const INLINE = {
51
+ auth_token: z.string().optional().describe(
52
+ "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.",
53
+ ),
54
+ ct0: z.string().optional().describe(
55
+ "Optional. The account's ct0 cookie, paired with auth_token. Sent as the x-ct0 header.",
56
+ ),
57
+ proxy_url: z.string().optional().describe(
58
+ "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.",
59
+ ),
60
+ user_agent: z.string().optional().describe(
61
+ "Optional. User-Agent string to send for this session. Sent as the x-user-agent header.",
62
+ ),
63
+ };
45
64
 
46
65
  // ── Tool catalog. Reads are GET (default); writes set method:"POST". ─────────
47
66
  // write:true -> action mutates account/Twitter state (annotated readOnlyHint:false)
@@ -223,6 +242,7 @@ export const TOOLS = [
223
242
  "Numeric user id of the target account to compute shared followers against.",
224
243
  ),
225
244
  ...PAGINATION,
245
+ ...INLINE,
226
246
  },
227
247
  },
228
248
  // ── Reads: a single tweet + its conversation ───────────────────────────────
@@ -272,14 +292,14 @@ export const TOOLS = [
272
292
  path: "/twitter/user/home_timeline",
273
293
  description:
274
294
  "Get YOUR authenticated account's Home timeline (the 'Following'/'For you' feed), most recent first. Requires an authenticated session behind your key. Returns tweets with author and metrics plus a cursor. Use this to read what your account would see when it opens X.",
275
- shape: { ...PAGINATION },
295
+ shape: { ...PAGINATION, ...INLINE },
276
296
  },
277
297
  {
278
298
  name: "twitter_bookmarks",
279
299
  path: "/twitter/user/bookmarks",
280
300
  description:
281
301
  "List YOUR authenticated account's bookmarked tweets, most recent first. Requires an authenticated session behind your key. Returns each bookmarked tweet with author and metrics plus a cursor.",
282
- shape: { ...PAGINATION },
302
+ shape: { ...PAGINATION, ...INLINE },
283
303
  },
284
304
  {
285
305
  name: "twitter_bookmark_search",
@@ -291,6 +311,7 @@ export const TOOLS = [
291
311
  "Search terms to match against your bookmarked tweets' text.",
292
312
  ),
293
313
  ...PAGINATION,
314
+ ...INLINE,
294
315
  },
295
316
  },
296
317
  {
@@ -298,7 +319,7 @@ export const TOOLS = [
298
319
  path: "/twitter/dm/list",
299
320
  description:
300
321
  "List YOUR authenticated account's Direct Message conversations (inbox), each with the participant and a conversation_id you can pass to twitter_dm_conversation. Requires an authenticated session behind your key. Read-only: this does not send DMs.",
301
- shape: {},
322
+ shape: { ...INLINE },
302
323
  },
303
324
  {
304
325
  name: "twitter_dm_conversation",
@@ -309,6 +330,7 @@ export const TOOLS = [
309
330
  conversation_id: z.string().describe(
310
331
  "The conversation_id from a twitter_dm_list entry identifying which DM thread to read.",
311
332
  ),
333
+ ...INLINE,
312
334
  },
313
335
  },
314
336
 
@@ -333,7 +355,7 @@ export const TOOLS = [
333
355
  media_ids: z.string().optional().describe(
334
356
  "Optional. Comma-separated media id(s) from a prior media upload to attach (images/video).",
335
357
  ),
336
- ...ACCOUNT,
358
+ ...ACCOUNT, ...INLINE,
337
359
  },
338
360
  },
339
361
  {
@@ -344,7 +366,7 @@ export const TOOLS = [
344
366
  destructive: true,
345
367
  description:
346
368
  "Delete a tweet AS your authenticated account. Irreversible: the tweet is permanently removed. You can only delete tweets your authenticated account authored. Provide the tweet id or url. Requires write capability behind your key.",
347
- shape: { ...TWEET_REF, ...ACCOUNT },
369
+ shape: { ...TWEET_REF, ...ACCOUNT, ...INLINE },
348
370
  },
349
371
  // ── Writes: engagement (favorite / retweet / bookmark) + inverses ──────────
350
372
  {
@@ -354,7 +376,7 @@ export const TOOLS = [
354
376
  write: true,
355
377
  description:
356
378
  "Like (favorite) a tweet AS your authenticated account. Provide the tweet id or url. Requires write capability behind your key. Reverse with twitter_unfavorite_tweet.",
357
- shape: { ...TWEET_REF, ...ACCOUNT },
379
+ shape: { ...TWEET_REF, ...ACCOUNT, ...INLINE },
358
380
  },
359
381
  {
360
382
  name: "twitter_unfavorite_tweet",
@@ -364,7 +386,7 @@ export const TOOLS = [
364
386
  destructive: true,
365
387
  description:
366
388
  "Remove a like (unfavorite) from a tweet AS your authenticated account. Provide the tweet id or url. Requires write capability behind your key.",
367
- shape: { ...TWEET_REF, ...ACCOUNT },
389
+ shape: { ...TWEET_REF, ...ACCOUNT, ...INLINE },
368
390
  },
369
391
  {
370
392
  name: "twitter_retweet",
@@ -373,7 +395,7 @@ export const TOOLS = [
373
395
  write: true,
374
396
  description:
375
397
  "Retweet a tweet AS your authenticated account. Provide the tweet id or url. Requires write capability behind your key. Reverse with twitter_unretweet.",
376
- shape: { ...TWEET_REF, ...ACCOUNT },
398
+ shape: { ...TWEET_REF, ...ACCOUNT, ...INLINE },
377
399
  },
378
400
  {
379
401
  name: "twitter_unretweet",
@@ -383,7 +405,7 @@ export const TOOLS = [
383
405
  destructive: true,
384
406
  description:
385
407
  "Undo a retweet AS your authenticated account. Provide the tweet id or url. Requires write capability behind your key.",
386
- shape: { ...TWEET_REF, ...ACCOUNT },
408
+ shape: { ...TWEET_REF, ...ACCOUNT, ...INLINE },
387
409
  },
388
410
  {
389
411
  name: "twitter_bookmark_tweet",
@@ -392,7 +414,7 @@ export const TOOLS = [
392
414
  write: true,
393
415
  description:
394
416
  "Bookmark a tweet to YOUR authenticated account's private bookmarks. Provide the tweet id or url. Requires write capability behind your key. Reverse with twitter_unbookmark_tweet.",
395
- shape: { ...TWEET_REF, ...ACCOUNT },
417
+ shape: { ...TWEET_REF, ...ACCOUNT, ...INLINE },
396
418
  },
397
419
  {
398
420
  name: "twitter_unbookmark_tweet",
@@ -402,7 +424,7 @@ export const TOOLS = [
402
424
  destructive: true,
403
425
  description:
404
426
  "Remove a tweet from YOUR authenticated account's bookmarks. Provide the tweet id or url. Requires write capability behind your key.",
405
- shape: { ...TWEET_REF, ...ACCOUNT },
427
+ shape: { ...TWEET_REF, ...ACCOUNT, ...INLINE },
406
428
  },
407
429
  // ── Writes: follow graph ───────────────────────────────────────────────────
408
430
  {
@@ -416,7 +438,7 @@ export const TOOLS = [
416
438
  user_id: z.string().describe(
417
439
  "Numeric user id of the account to follow. Resolve a handle to a user_id first with twitter_user_info.",
418
440
  ),
419
- ...ACCOUNT,
441
+ ...ACCOUNT, ...INLINE,
420
442
  },
421
443
  },
422
444
  {
@@ -431,7 +453,7 @@ export const TOOLS = [
431
453
  user_id: z.string().describe(
432
454
  "Numeric user id of the account to unfollow.",
433
455
  ),
434
- ...ACCOUNT,
456
+ ...ACCOUNT, ...INLINE,
435
457
  },
436
458
  },
437
459
  ];