@twitterapis/mcp 0.9.3 → 0.9.5
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 +19 -0
- package/README.md +4 -2
- package/icon.png +0 -0
- package/package.json +3 -2
- package/src/index.js +24 -1
- package/src/tools.js +36 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.5 (2026-08-31)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`twitter_monitor_webhook_redrive`, replay the deliveries you missed while your endpoint was down.** A delivery is dead-lettered after it fails all 8 attempts across 21 minutes, so an outage longer than that window loses those events outright. This re-queues them with a full retry budget, oldest first, and is free per call. Bounded by default so a recovered endpoint is not flooded: `max_age_hours` defaults to 24 (1 to 168) and `limit` to 100 (1 to 1000). Returns `requeued` and `skipped_permanent`; a delivery that died for a permanent reason (a 410 Gone, a deleted webhook, or a URL egress refused) is not replayed, because it would fail the same way and spend the budget again. Replayed events carry the same signature and payload as the original, so make your handler idempotent on the event id if a duplicate would matter to you. This takes the catalog to 96 tools, 61 reads and 35 writes, which is exact parity with the API's own endpoint count.
|
|
8
|
+
- **`include_replies` on `twitter_monitor_create` and `twitter_monitor_update`.** The parameter was added upstream and neither tool exposed it, so a caller could not turn replies off through the MCP at all. `true` delivers the account's replies as well as its own posts, which is the default and what every monitor has always done; `false` holds replies back. It must be a real boolean: the string `"false"` and the number `0` are rejected with a 400 rather than coerced, because coercing them would quietly give you the opposite of what you typed, and the wrong answer here is invisible since it looks exactly like the account not having posted. The generator is fail-closed on an unexposed spec param and refused to build until both were declared, which is how this surfaced.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **The redrive tool would have 400'd on every call without `jsonBody: true`.** Its handler reads `max_age_hours` and `limit` from the body only, so the args would have gone out as a query string. Caught by `body-mode-parity`, which reads the backend's own generated route manifest rather than trusting this repo's view of it. That manifest was itself stale on the backend's main branch (the route landed without regenerating it), so the failure surfaced here first and was fixed upstream in twitterapis-backend#408 before this release.
|
|
13
|
+
|
|
14
|
+
## 0.9.4 (2026-08-19)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A malformed `TWITTERAPIS_TIMEOUT_MS` silently timed out every single tool call.** `Number(process.env.TWITTERAPIS_TIMEOUT_MS || 30000)` had no validation: a non-numeric value (`"30000ms"`, `"60,000"`, a stray comma or unit) parses to `NaN`, and Node's `setTimeout` clamps a `NaN` delay to about 1ms, so the abort controller fired before any real request could complete. Every tool call failed with `Request failed: timed out after NaNms`, which reads as a live API outage rather than the config typo it actually is. An explicit `0` or negative value had the same effect with no typo required at all. The value is now validated as a finite, positive number before use, falls back to the documented 30000ms default otherwise, and logs a clear warning to stderr naming the bad value instead of silently breaking every call.
|
|
19
|
+
- **8 tool args declared `optional: true` in `scripts/tools.overrides.mjs`, a key the generator never reads** (`with_listeners` / `with_replays` on `twitter_spaces_info`, `message` / `messages` / `conversation_id` / `mode` / `image_count` on `twitter_grok_chat`, `media_category` on `twitter_article_update_cover_media`). The render logic only ever checks `a.required`, so `optional: true` was silently a no-op; each of these 8 args happened to render as optional anyway only because the vendored spec's own `required` flag for that param already defaulted to false. A future spec refresh flipping one of those defaults would have silently made the arg required with no warning from any gate. Renamed to `required: false`, the property the generator actually reads. `src/tools.js` is byte-identical before and after (`catalog-identity` confirms), so this closes a live gap without changing today's behavior.
|
|
20
|
+
- **The generator now fails the build on any unrecognized key in a `tools.overrides.mjs` arg entry** (`scripts/gen-tools.mjs`), so the class of bug above can't recur silently. Red-tested: reintroducing `optional: true` on a synthetic arg makes `npm run build:check` fail with `sets unrecognized key "optional" ... did you mean "required: false"?`.
|
|
21
|
+
|
|
3
22
|
## 0.9.3 (2026-08-18)
|
|
4
23
|
|
|
5
24
|
PR #36 (3 new tools + openapi-parity fix) merged after 0.9.2 had already been
|
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
|
+
96 tools: 61 reads and 35 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
|
|
|
@@ -204,6 +204,7 @@ Watch an X account for new posts and get them pushed to your own HTTPS endpoint,
|
|
|
204
204
|
| `twitter_monitor_webhook_list` | List every webhook registered on your account |
|
|
205
205
|
| `twitter_monitor_webhook_delete` | Soft-delete a webhook by id (irreversible from the caller's side) |
|
|
206
206
|
| `twitter_monitor_webhook_test` | Send one signed test event to a webhook right now, synchronously |
|
|
207
|
+
| `twitter_monitor_webhook_redrive` | Replay deliveries that dead-lettered while your endpoint was down, oldest first |
|
|
207
208
|
|
|
208
209
|
### Session setup
|
|
209
210
|
|
|
@@ -212,6 +213,7 @@ Link an X account to your key once, so the account-only reads and write actions
|
|
|
212
213
|
| Tool | What it does |
|
|
213
214
|
|---|---|
|
|
214
215
|
| `twitter_customer_session` | Register your x.com session cookies (`auth_token` + `ct0`) against your key |
|
|
216
|
+
| `twitter_customer_session_status` | Read back the registered session without changing it: resolved account, live/dead status, timestamps, and which egress tier a write would use. Never returns the cookies. Free |
|
|
215
217
|
| `twitter_customer_session_delete` | Revoke that stored session, deleting your `auth_token` + `ct0` from the service. Idempotent and free |
|
|
216
218
|
| `twitter_user_login` | Log in with `username` + `password` (+ `totp_secret` for 2FA); stores the session against your key. Returns a confirmation, never the cookies |
|
|
217
219
|
|
|
@@ -295,7 +297,7 @@ Calls are billed to your twitterapis.com account. Almost every endpoint is $0.00
|
|
|
295
297
|
|
|
296
298
|
**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.
|
|
297
299
|
|
|
298
|
-
**Is it read-only?** No.
|
|
300
|
+
**Is it read-only?** No. 61 read tools work with just your API key; 35 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.
|
|
299
301
|
|
|
300
302
|
**Which clients are supported?** Claude Desktop, Cursor, Windsurf, and VS Code (Copilot agent mode), or any Model Context Protocol client.
|
|
301
303
|
|
package/icon.png
ADDED
|
Binary file
|
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.9.
|
|
4
|
+
"version": "0.9.5",
|
|
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",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"src",
|
|
17
17
|
"README.md",
|
|
18
18
|
"LICENSE",
|
|
19
|
-
"CHANGELOG.md"
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"icon.png"
|
|
20
21
|
],
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=18"
|
package/src/index.js
CHANGED
|
@@ -30,7 +30,30 @@ const API_KEY = process.env.TWITTERAPIS_KEY;
|
|
|
30
30
|
const BASE_URL = (
|
|
31
31
|
process.env.TWITTERAPIS_BASE_URL || "https://api.twitterapis.com"
|
|
32
32
|
).replace(/\/+$/, "");
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
35
|
+
// A malformed TWITTERAPIS_TIMEOUT_MS (non-numeric, or <= 0) used to reach
|
|
36
|
+
// setTimeout() unvalidated. Number("30000ms") and Number("60,000") are both
|
|
37
|
+
// NaN, and Node clamps a NaN or sub-1 delay to ~1ms (verified directly:
|
|
38
|
+
// `setTimeout(fn, NaN)` fires in under 1ms), so every tool call aborted
|
|
39
|
+
// almost immediately with "Request failed: timed out after NaNms" -- which
|
|
40
|
+
// reads as a live outage, not the config typo it actually is. An explicit 0
|
|
41
|
+
// or negative value has the same effect with no typo needed at all. Fall
|
|
42
|
+
// back to the documented default on anything that is not a finite, positive
|
|
43
|
+
// number, and say so loudly rather than silently eating every call.
|
|
44
|
+
let REQUEST_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
|
|
45
|
+
const rawTimeoutEnv = process.env.TWITTERAPIS_TIMEOUT_MS;
|
|
46
|
+
if (rawTimeoutEnv) {
|
|
47
|
+
const parsed = Number(rawTimeoutEnv);
|
|
48
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
49
|
+
REQUEST_TIMEOUT_MS = parsed;
|
|
50
|
+
} else {
|
|
51
|
+
console.error(
|
|
52
|
+
`[twitterapis-mcp] TWITTERAPIS_TIMEOUT_MS="${rawTimeoutEnv}" is not a positive number; ` +
|
|
53
|
+
`falling back to the default ${DEFAULT_TIMEOUT_MS}ms instead of timing out every call immediately.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
34
57
|
|
|
35
58
|
// Lazy validation, not exit-on-boot: an MCP registry scanner (Smithery, Glama,
|
|
36
59
|
// the official registry, Claude Connectors) connects the stdio transport with
|
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: 96 tools (61 reads, 35 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 10
|
|
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
|
|
@@ -1421,6 +1421,13 @@ export const TOOLS = [
|
|
|
1421
1421
|
),
|
|
1422
1422
|
},
|
|
1423
1423
|
},
|
|
1424
|
+
{
|
|
1425
|
+
name: "twitter_customer_session_status",
|
|
1426
|
+
path: "/twitter/customer/session/status",
|
|
1427
|
+
description:
|
|
1428
|
+
"Read back the X account session you registered with twitter_customer_session, without changing it. Returns registered (false if you never registered one), the resolved username and twitter_user_id the session actually maps to, status ('ok', or 'dead' once X has rejected the cookies), created_at, updated_at, last_used_at, and an egress block: source (one of session, sticky_residential, pool_residential, direct), customer_proxy_in_use (true when the proxy_url you registered is the one your writes leave from), and a note explaining that tier. Never returns auth_token, ct0, or any proxy URL. Use it to answer 'am I posting as the account I think I am', 'has my session expired', and 'is the proxy I supplied actually being used' without opening a support ticket. Free, and scoped to your own API key by construction: it takes no account identifier of any kind, so it cannot read another key's session.",
|
|
1429
|
+
shape: {},
|
|
1430
|
+
},
|
|
1424
1431
|
{
|
|
1425
1432
|
name: "twitter_customer_session_delete",
|
|
1426
1433
|
path: "/twitter/customer/session/delete",
|
|
@@ -1780,6 +1787,9 @@ export const TOOLS = [
|
|
|
1780
1787
|
webhook_ids: z.string().optional().describe(
|
|
1781
1788
|
"Optional. Comma-separated webhook id(s) from twitter_monitor_webhook_create to restrict this monitor's deliveries to. Omit to deliver to every active webhook on the account (the default).",
|
|
1782
1789
|
),
|
|
1790
|
+
include_replies: z.string().optional().describe(
|
|
1791
|
+
"Optional boolean. true delivers the account's replies as well as its own posts, which is the default and what every monitor has always done; false holds replies back and delivers only the account's own posts. Must be a real boolean: the string \"false\" and the number 0 are rejected with a 400 rather than coerced, because coercing them would quietly give you the opposite of what you typed, and the wrong answer here is invisible since it looks exactly like the account not having posted.",
|
|
1792
|
+
),
|
|
1783
1793
|
domain_filter: z.string().optional().describe(
|
|
1784
1794
|
"Optional. A bare hostname ('example.com') or a full URL ('https://example.com/blog') to restrict delivery to only the new posts that link to that host or a subdomain of it (e.g. 'example.com' matches both example.com and blog.example.com). Normalized server-side: lowercased, scheme/path/query/fragment/leading www./trailing :port stripped. Omit for no filter, the default (deliver every new post). Rejected with a 400 if what remains after normalization is not a valid hostname shape. A post with no matching link is filtered out of delivery, never silently dropped: it still advances the monitor's cursor and counts toward the account's tweets_domain_filtered health metric.",
|
|
1785
1795
|
),
|
|
@@ -1814,6 +1824,9 @@ export const TOOLS = [
|
|
|
1814
1824
|
domain_filter: z.string().nullable().optional().describe(
|
|
1815
1825
|
"Optional. A bare hostname or full URL to restrict delivery to, same shape and normalization as twitter_monitor_create's domain_filter. Pass an empty string (or null) to clear an existing filter back to 'deliver every new post'. Omit entirely to leave the current filter unchanged. Rejected with a 400 if a non-empty value does not normalize to a valid hostname.",
|
|
1816
1826
|
),
|
|
1827
|
+
include_replies: z.string().optional().describe(
|
|
1828
|
+
"Optional boolean. true delivers the account's replies as well as its own posts, false holds replies back and delivers only its own posts. Omit the field entirely to leave it unchanged. Same boolean-only validation as twitter_monitor_create: a non-boolean is a 400 rather than a coercion.",
|
|
1829
|
+
),
|
|
1817
1830
|
},
|
|
1818
1831
|
},
|
|
1819
1832
|
{
|
|
@@ -1947,6 +1960,27 @@ export const TOOLS = [
|
|
|
1947
1960
|
),
|
|
1948
1961
|
},
|
|
1949
1962
|
},
|
|
1963
|
+
{
|
|
1964
|
+
name: "twitter_monitor_webhook_redrive",
|
|
1965
|
+
path: "/twitter/webhook/{id}/redrive",
|
|
1966
|
+
method: "POST",
|
|
1967
|
+
write: true,
|
|
1968
|
+
jsonBody: true,
|
|
1969
|
+
pathParams: ["id"],
|
|
1970
|
+
description:
|
|
1971
|
+
"Replay deliveries that dead-lettered while your endpoint was down. A delivery is dead-lettered after it fails all 8 attempts across 21 minutes, so an outage longer than that window loses those events; this re-queues them with a full retry budget, oldest first. Bounded by default so a recovered endpoint is not flooded: max_age_hours defaults to 24 and limit to 100. Returns requeued and skipped_permanent. A delivery that died for a permanent reason, a 410 Gone, a deleted webhook, or a URL egress refused, is not replayed, because it would fail the same way and spend the budget again. Replayed events carry the same signature and payload as the original, so make your handler idempotent on the event id if a duplicate would matter. Returns 409 if the webhook is disabled, which happens after your endpoint answers 410 Gone: re-register it first. Free per call.",
|
|
1972
|
+
shape: {
|
|
1973
|
+
id: z.string().describe(
|
|
1974
|
+
"The webhook's id, from twitter_monitor_webhook_create or twitter_monitor_webhook_list.",
|
|
1975
|
+
),
|
|
1976
|
+
max_age_hours: z.number().int().optional().describe(
|
|
1977
|
+
"Optional. How far back to look for dead-lettered deliveries, 1 to 168 hours. Defaults to 24.",
|
|
1978
|
+
),
|
|
1979
|
+
limit: z.number().int().optional().describe(
|
|
1980
|
+
"Optional. Most deliveries to replay in one call, 1 to 1000, oldest first. Defaults to 100.",
|
|
1981
|
+
),
|
|
1982
|
+
},
|
|
1983
|
+
},
|
|
1950
1984
|
];
|
|
1951
1985
|
|
|
1952
1986
|
// The query-string builder and the path-param substitution helper are
|