@twitterapis/mcp 0.11.0 → 0.12.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/CHANGELOG.md +45 -0
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/tools.js +57 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 (2026-09-13)
|
|
4
|
+
|
|
5
|
+
- **Two new tools, 107 -> 109 (65 reads, 44 writes): `twitter_update_avatar` and
|
|
6
|
+
`twitter_update_banner`.** Both endpoints had been serving behind a feature flag
|
|
7
|
+
on the API and documented on no surface, because their upstream host was an
|
|
8
|
+
INFERENCE from the sibling profile write rather than an observation. Both have
|
|
9
|
+
now been called end to end against a live account, both success envelopes were
|
|
10
|
+
captured, and each write was confirmed applied by reading the profile back
|
|
11
|
+
through the GraphQL user lookup. The flag is deleted from the API entirely.
|
|
12
|
+
- Each takes ONE field of base64-encoded image bytes (`image` / `banner`): not a
|
|
13
|
+
URL, not multipart, and NOT a `media_id` from `twitter_media_upload`. Both tool
|
|
14
|
+
descriptions say so in the first two sentences, because a model choosing between
|
|
15
|
+
these and the media-upload tool on the word "upload" alone will get it wrong.
|
|
16
|
+
- Both descriptions also carry the two things a schema cannot: there is NO UNDO
|
|
17
|
+
and the vendor keeps no history, so read and save the current image URL first;
|
|
18
|
+
and confirm a banner write by reading `cover_picture` back specifically, since
|
|
19
|
+
at least one vendor endpoint reports that field as empty for accounts that
|
|
20
|
+
plainly have one.
|
|
21
|
+
- Both are declared `jsonBody`, and here the API's "json-only" classification is
|
|
22
|
+
EXACT rather than conservative: the shared handler reads the JSON body and has
|
|
23
|
+
no query fallback at all, so a query string genuinely cannot work.
|
|
24
|
+
|
|
25
|
+
## 0.11.1 (2026-09-13)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- `twitter_update_profile`: an empty string CLEARS a field. The tool description said
|
|
29
|
+
the opposite, and 0.11.0 shipped that to npm. It was true of the API when written
|
|
30
|
+
and stopped being true when the handler was fixed the same day.
|
|
31
|
+
|
|
32
|
+
### Notes
|
|
33
|
+
- A PATCH, not a minor: the catalog is unchanged at 107 tools, 65 reads and 42 writes.
|
|
34
|
+
Only description text moves, which `scripts/prepublish-version-class.mjs` confirms.
|
|
35
|
+
- The claim is now verified the only way that works on this endpoint: by sending a
|
|
36
|
+
value that CHANGES and reading the profile back. The previous check used a
|
|
37
|
+
byte-for-byte no-op, which is the one test that cannot fail, since the profile looks
|
|
38
|
+
identical whether the write applied or was silently dropped.
|
|
39
|
+
- Why it matters that this was wrong rather than merely vague: the old text told a
|
|
40
|
+
model that `description: ""` was a harmless no-op. After the handler fix, that exact
|
|
41
|
+
call blanks the field. A description that is confidently wrong about a destructive
|
|
42
|
+
operation is worse than one that says nothing.
|
|
43
|
+
- Separately, writes to this endpoint are sometimes accepted upstream and sometimes
|
|
44
|
+
refused, and only some refusals are classified honestly. That is a defect under
|
|
45
|
+
repair on the API side, not part of this tool's contract, so it is deliberately not
|
|
46
|
+
written into the description.
|
|
47
|
+
|
|
3
48
|
## 0.11.0 (2026-09-13)
|
|
4
49
|
|
|
5
50
|
### 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
|
-
|
|
94
|
+
109 tools: 65 reads and 44 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. Four of the reads are free account lookups (`twitter_account_me`, `twitter_account_payments`, `twitter_feedback_get`, `twitter_feedback_list`); the 14 monitoring tools and `twitter_feedback_send` 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** and **feedback** tools (see below) are the 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
|
|
|
@@ -170,6 +170,8 @@ Public reads (search, profiles, tweets, followers, likes) work with just your AP
|
|
|
170
170
|
| `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 |
|
|
171
171
|
| `twitter_media_upload` | Upload a base64 image, returns a `media_id` for `twitter_create_tweet` |
|
|
172
172
|
| `twitter_update_profile` | Change your own name, bio, location or link. A PARTIAL update: omitted fields keep their values, an empty string clears one |
|
|
173
|
+
| `twitter_update_avatar` | Replace your own profile picture. Takes base64 image bytes in `image`, not a URL and not a `media_id`. No undo, and X keeps no history |
|
|
174
|
+
| `twitter_update_banner` | Replace your own header image. Takes base64 image bytes in `banner`, not a URL and not a `media_id`. No undo, and X keeps no history |
|
|
173
175
|
|
|
174
176
|
### Drafts and scheduled posts _(require a linked X session)_
|
|
175
177
|
|
|
@@ -327,7 +329,7 @@ count: 50
|
|
|
327
329
|
|
|
328
330
|
## Pricing
|
|
329
331
|
|
|
330
|
-
Calls are billed to your twitterapis.com account. Most endpoints are $0.0008/call: nearly every read (search, profiles, tweets, followers, likes) plus the simple write actions (like, retweet, bookmark, follow and their undos, delete). 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, sending a DM (`twitter_dm_send`), and DM reads (`twitter_dm_list`, `twitter_dm_conversation`) at $0.0016/call, full tweet history (`twitter_user_tweets_complete`) at $0.0024/call, a full tweet thread (`twitter_tweet_thread`) and a Grok answer (`twitter_grok_chat`) at $0.004/call, and the article-editing writes (`twitter_article_create`, `twitter_article_update_title`, `twitter_article_update_cover_media`, `twitter_article_update_content`, `twitter_article_publish`, `twitter_article_unpublish`) at $0.0016. The compose surface is $0.0016 for `twitter_draft_create`, `twitter_draft_edit`, `twitter_scheduled_create` and, note, BOTH LIST READS (`twitter_draft_list`, `twitter_scheduled_list`), which are the two reads that are not at the read rate; `twitter_draft_delete` and `twitter_scheduled_delete` are $0.0008/call, while `twitter_article_get`, `twitter_article_list` and `twitter_article_delete` stay at the standard $0.0008/call. `twitter_update_profile`,
|
|
332
|
+
Calls are billed to your twitterapis.com account. Most endpoints are $0.0008/call: nearly every read (search, profiles, tweets, followers, likes) plus the simple write actions (like, retweet, bookmark, follow and their undos, delete). 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, sending a DM (`twitter_dm_send`), and DM reads (`twitter_dm_list`, `twitter_dm_conversation`) at $0.0016/call, full tweet history (`twitter_user_tweets_complete`) at $0.0024/call, a full tweet thread (`twitter_tweet_thread`) and a Grok answer (`twitter_grok_chat`) at $0.004/call, and the article-editing writes (`twitter_article_create`, `twitter_article_update_title`, `twitter_article_update_cover_media`, `twitter_article_update_content`, `twitter_article_publish`, `twitter_article_unpublish`) at $0.0016. The compose surface is $0.0016 for `twitter_draft_create`, `twitter_draft_edit`, `twitter_scheduled_create` and, note, BOTH LIST READS (`twitter_draft_list`, `twitter_scheduled_list`), which are the two reads that are not at the read rate; `twitter_draft_delete` and `twitter_scheduled_delete` are $0.0008/call, while `twitter_article_get`, `twitter_article_list` and `twitter_article_delete` stay at the standard $0.0008/call. The three profile writes (`twitter_update_profile`, `twitter_update_avatar`, `twitter_update_banner`) are $0.0016/call each. Your first $0.50 is free. See [twitterapis.com/pricing](https://www.twitterapis.com/pricing).
|
|
331
333
|
|
|
332
334
|
## Links
|
|
333
335
|
|
|
@@ -340,7 +342,7 @@ Calls are billed to your twitterapis.com account. Most endpoints are $0.0008/cal
|
|
|
340
342
|
|
|
341
343
|
**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.
|
|
342
344
|
|
|
343
|
-
**Is it read-only?** No. 65 read tools work with just your API key;
|
|
345
|
+
**Is it read-only?** No. 65 read tools work with just your API key; 44 write actions (post, save or schedule a post, like, retweet, follow, DM, media upload, profile name/bio/avatar/banner updates, List create/add member/remove member, article create/edit/publish/delete, monitor/webhook create/update/delete, feedback send) act as a linked X account or per-call inline credentials, except monitor/webhook CRUD and feedback, which are account administration and need only your API key.
|
|
344
346
|
|
|
345
347
|
**Which clients are supported?** Claude Desktop, Cursor, Windsurf, and VS Code (Copilot agent mode), or any Model Context Protocol client.
|
|
346
348
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twitterapis/mcp",
|
|
3
3
|
"mcpName": "io.github.TwitterAPIs/twitterapis-mcp",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
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.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/src/tools.js
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
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:
|
|
11
|
+
// Catalog: 109 tools (65 reads, 44 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
|
|
15
15
|
// write actions, reads its params from the query string), except the per-call
|
|
16
|
-
// inline credentials, which travel as x-* request headers, the
|
|
16
|
+
// inline credentials, which travel as x-* request headers, the 14
|
|
17
17
|
// jsonBody tools, whose fields travel in a JSON request body, and any arg listed
|
|
18
18
|
// in pathParams, which is substituted into the URL path (e.g. {id}) instead. A
|
|
19
19
|
// tool with `method: "POST"` or `method: "DELETE"` is a write that acts on
|
|
@@ -1168,16 +1168,16 @@ export const TOOLS = [
|
|
|
1168
1168
|
write: true,
|
|
1169
1169
|
jsonBody: true,
|
|
1170
1170
|
description:
|
|
1171
|
-
"Change the display name, bio, location or link on your authenticated account's own X profile. This is a PARTIAL update: send only the fields you want to change and everything you omit keeps its current value, so passing just a name will NOT wipe the bio. An empty string
|
|
1171
|
+
"Change the display name, bio, location or link on your authenticated account's own X profile. This is a PARTIAL update: send only the fields you want to change and everything you omit keeps its current value, so passing just a name will NOT wipe the bio. An empty string CLEARS that field, which is different from omitting it: \"\" blanks the value, an absent key leaves it alone. At least one of name, description, location or url is required, and a request whose only value is an empty string is a valid clear rather than an empty request. It writes a real profile and takes effect immediately with no undo, so read the current values with twitter_user_info first if you may need to restore them. Requires an authenticated session behind your key. Returns ok and updated_fields, which echoes the field names you SENT rather than a diff against the previous profile.",
|
|
1172
1172
|
shape: {
|
|
1173
1173
|
name: z.string().optional().describe(
|
|
1174
1174
|
"Optional. New display name, up to 50 characters. Omit to leave it unchanged.",
|
|
1175
1175
|
),
|
|
1176
1176
|
description: z.string().optional().describe(
|
|
1177
|
-
"Optional. New bio.
|
|
1177
|
+
"Optional. New bio. Send an EMPTY STRING to clear it; OMIT the field to leave it alone. Those are different.",
|
|
1178
1178
|
),
|
|
1179
1179
|
location: z.string().optional().describe(
|
|
1180
|
-
"Optional. New location text. Same rule: an empty string
|
|
1180
|
+
"Optional. New location text. Same rule: an empty string clears it, omitting the field leaves it alone.",
|
|
1181
1181
|
),
|
|
1182
1182
|
url: z.string().optional().describe(
|
|
1183
1183
|
"Optional. New profile link.",
|
|
@@ -1196,6 +1196,58 @@ export const TOOLS = [
|
|
|
1196
1196
|
),
|
|
1197
1197
|
},
|
|
1198
1198
|
},
|
|
1199
|
+
{
|
|
1200
|
+
name: "twitter_update_avatar",
|
|
1201
|
+
path: "/twitter/user/update_avatar",
|
|
1202
|
+
method: "POST",
|
|
1203
|
+
write: true,
|
|
1204
|
+
jsonBody: true,
|
|
1205
|
+
description:
|
|
1206
|
+
"Replace the profile picture on your authenticated account's own X profile. Takes ONE field, image, holding base64-encoded image bytes: not a URL, not multipart, and not a media_id from twitter_media_upload. It writes a real profile and takes effect immediately with NO UNDO, and X keeps no history of the previous picture, so if the old image might be wanted back, read profile_image_url with twitter_user_info and save that file BEFORE calling this. Returns ok. To confirm it applied, read the account back with twitter_user_info: X mints a new media id for every accepted upload, so profile_image_url changes even when the image is byte-identical to the one already in place. Requires an authenticated session behind your key.",
|
|
1207
|
+
shape: {
|
|
1208
|
+
image: z.string().describe(
|
|
1209
|
+
"REQUIRED. Base64-encoded image bytes. Not a URL, not multipart, and not a media_id. banner and data are accepted as aliases for this same field.",
|
|
1210
|
+
),
|
|
1211
|
+
auth_token: z.string().optional().describe(
|
|
1212
|
+
"Optional. The account's auth_token cookie, to act AS that account for this call (must be paired with ct0). Travels out of band: as the x-auth-token request header on most tools, or inside the JSON request body on the tools that take one. Never a query parameter, so it never reaches a URL or an access log.",
|
|
1213
|
+
),
|
|
1214
|
+
ct0: z.string().optional().describe(
|
|
1215
|
+
"Optional. The account's ct0 cookie, paired with auth_token. Same transport as auth_token: the x-ct0 request header, or the JSON body on a body-taking tool. Never a query parameter.",
|
|
1216
|
+
),
|
|
1217
|
+
proxy_url: z.string().optional().describe(
|
|
1218
|
+
"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 request header, or in the JSON body on a body-taking tool.",
|
|
1219
|
+
),
|
|
1220
|
+
user_agent: z.string().optional().describe(
|
|
1221
|
+
"Optional. User-Agent string to send for this session. Sent as the x-user-agent request header, or in the JSON body on a body-taking tool.",
|
|
1222
|
+
),
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
name: "twitter_update_banner",
|
|
1227
|
+
path: "/twitter/user/update_banner",
|
|
1228
|
+
method: "POST",
|
|
1229
|
+
write: true,
|
|
1230
|
+
jsonBody: true,
|
|
1231
|
+
description:
|
|
1232
|
+
"Replace the wide header image on your authenticated account's own X profile. Takes ONE field, banner, holding base64-encoded image bytes: not a URL, not multipart, and not a media_id from twitter_media_upload. X renders the header as a wide strip, so a 3:1 image fills it without cropping. It writes a real profile and takes effect immediately with NO UNDO, and X keeps no history of the previous banner, so if the old image might be wanted back, read cover_picture with twitter_user_info and save that file BEFORE calling this. Returns ok. To confirm it applied, read the account back with twitter_user_info and check cover_picture specifically: at least one of X's own endpoints reports that field as empty for accounts that plainly have a banner, so an empty answer from anywhere else is not evidence the account has none. Requires an authenticated session behind your key.",
|
|
1233
|
+
shape: {
|
|
1234
|
+
banner: z.string().describe(
|
|
1235
|
+
"REQUIRED. Base64-encoded image bytes. Not a URL, not multipart, and not a media_id. image and data are accepted as aliases for this same field.",
|
|
1236
|
+
),
|
|
1237
|
+
auth_token: z.string().optional().describe(
|
|
1238
|
+
"Optional. The account's auth_token cookie, to act AS that account for this call (must be paired with ct0). Travels out of band: as the x-auth-token request header on most tools, or inside the JSON request body on the tools that take one. Never a query parameter, so it never reaches a URL or an access log.",
|
|
1239
|
+
),
|
|
1240
|
+
ct0: z.string().optional().describe(
|
|
1241
|
+
"Optional. The account's ct0 cookie, paired with auth_token. Same transport as auth_token: the x-ct0 request header, or the JSON body on a body-taking tool. Never a query parameter.",
|
|
1242
|
+
),
|
|
1243
|
+
proxy_url: z.string().optional().describe(
|
|
1244
|
+
"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 request header, or in the JSON body on a body-taking tool.",
|
|
1245
|
+
),
|
|
1246
|
+
user_agent: z.string().optional().describe(
|
|
1247
|
+
"Optional. User-Agent string to send for this session. Sent as the x-user-agent request header, or in the JSON body on a body-taking tool.",
|
|
1248
|
+
),
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1199
1251
|
{
|
|
1200
1252
|
name: "twitter_draft_create",
|
|
1201
1253
|
path: "/twitter/draft/create",
|