twittertools-mcp 1.2.0 → 1.2.2
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/package.json +1 -1
- package/server.js +23 -9
- package/xrules.js +10 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twittertools-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"mcpName": "io.github.steley/twittertools-mcp",
|
|
5
5
|
"description": "MCP server for twittertools.com — fetch X (Twitter) posts and threads, count and split posts with X's real rules. No login, no tracking.",
|
|
6
6
|
"license": "MIT",
|
package/server.js
CHANGED
|
@@ -29,7 +29,12 @@ import {
|
|
|
29
29
|
} from "./xrules.js";
|
|
30
30
|
|
|
31
31
|
const NAME = "twittertools";
|
|
32
|
-
const VERSION = "1.2.
|
|
32
|
+
const VERSION = "1.2.2"; // keep in sync with package.json
|
|
33
|
+
// Protocol versions this server can honestly claim: across these revisions the
|
|
34
|
+
// four verbs used here (initialize, tools/list, tools/call, ping) kept their
|
|
35
|
+
// shapes, and the additions (icons, tasks, elicitation, …) are all optional
|
|
36
|
+
// features a tools-only stdio server never touches.
|
|
37
|
+
const SUPPORTED_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
33
38
|
const API_BASE = (process.env.TWITTERTOOLS_API_BASE || "https://twittertools.com").replace(/\/+$/, "");
|
|
34
39
|
const VIA = "https://twittertools.com";
|
|
35
40
|
const TEXT_INPUT_MAX = 100_000; // generous, but caps local work per call
|
|
@@ -51,15 +56,18 @@ const TOOLS = [
|
|
|
51
56
|
description:
|
|
52
57
|
"Fetch one public X (Twitter) post by URL or ID: text, author, engagement counts, " +
|
|
53
58
|
"and media with direct CDN URLs (photos, or MP4 video variants sorted by quality). " +
|
|
54
|
-
"
|
|
59
|
+
"Use for a specific post the user links or cites — not for searching X, timelines, " +
|
|
60
|
+
"bookmarks, or DMs. Deleted, protected, or age-restricted posts are not available " +
|
|
61
|
+
"without an X login.",
|
|
55
62
|
inputSchema: urlOrIdSchema,
|
|
56
63
|
},
|
|
57
64
|
{
|
|
58
65
|
name: "get_thread",
|
|
59
66
|
description:
|
|
60
|
-
"Unroll a public X thread into its posts in order.
|
|
61
|
-
"and prefer pasting the thread's LAST
|
|
62
|
-
"be partial; the response says so
|
|
67
|
+
"Unroll a public X thread into its posts in order. For a single post, use get_tweet " +
|
|
68
|
+
"instead. Expensive upstream — call sparingly, and prefer pasting the thread's LAST " +
|
|
69
|
+
"post, which returns the whole chain. Results may be partial; the response says so " +
|
|
70
|
+
"when they are.",
|
|
63
71
|
inputSchema: urlOrIdSchema,
|
|
64
72
|
},
|
|
65
73
|
{
|
|
@@ -202,6 +210,7 @@ async function callTool(name, args) {
|
|
|
202
210
|
JSON.stringify(
|
|
203
211
|
{
|
|
204
212
|
id: parsed.id,
|
|
213
|
+
source: parsed.source,
|
|
205
214
|
screenName: parsed.screenName,
|
|
206
215
|
createdAt: Number.isNaN(created.getTime()) ? null : created.toISOString(),
|
|
207
216
|
permalink: permalinkFor(parsed.id, parsed.screenName),
|
|
@@ -237,15 +246,20 @@ async function dispatch(msg) {
|
|
|
237
246
|
const isRequest = "id" in msg;
|
|
238
247
|
try {
|
|
239
248
|
switch (method) {
|
|
240
|
-
case "initialize":
|
|
241
|
-
//
|
|
242
|
-
//
|
|
249
|
+
case "initialize": {
|
|
250
|
+
// Answer with a version we actually implement: echoing whatever the
|
|
251
|
+
// client asked for would claim support for versions we've never seen.
|
|
252
|
+
// A client that can't accept our answer disconnects, per spec.
|
|
253
|
+
const requested = params?.protocolVersion;
|
|
243
254
|
sendResult(id, {
|
|
244
|
-
protocolVersion:
|
|
255
|
+
protocolVersion: SUPPORTED_VERSIONS.includes(requested)
|
|
256
|
+
? requested
|
|
257
|
+
: SUPPORTED_VERSIONS[0],
|
|
245
258
|
capabilities: { tools: {} },
|
|
246
259
|
serverInfo: { name: NAME, version: VERSION },
|
|
247
260
|
});
|
|
248
261
|
return;
|
|
262
|
+
}
|
|
249
263
|
case "notifications/initialized":
|
|
250
264
|
return; // notification — no response
|
|
251
265
|
case "tools/list":
|
package/xrules.js
CHANGED
|
@@ -217,19 +217,23 @@ const STATUS_RE =
|
|
|
217
217
|
/(?:^|[^A-Za-z0-9-])(?:https?:\/\/)?(?:www\.|mobile\.)?(?:x|twitter)\.com\/([A-Za-z0-9_]{1,15})\/status(?:es)?\/(\d{5,25})/i;
|
|
218
218
|
const BARE_ID_RE = /^\d{5,25}$/;
|
|
219
219
|
|
|
220
|
-
/** Extract a status id (and screen name when present) from a URL, bare ID, or free
|
|
220
|
+
/** Extract a status id (and screen name when present) from a URL, bare ID, or free
|
|
221
|
+
* text. `source` records which form matched — the one field the standalone port
|
|
222
|
+
* adds beyond the TypeScript original. */
|
|
221
223
|
export function parseTweetInput(input) {
|
|
222
224
|
const text = (input ?? '').trim();
|
|
223
225
|
if (!text) return null;
|
|
224
226
|
|
|
225
227
|
const m = text.match(STATUS_RE);
|
|
226
|
-
if (m) return { id: m[2], screenName: m[1].toLowerCase() === 'i' ? null : m[1] };
|
|
228
|
+
if (m) return { id: m[2], screenName: m[1].toLowerCase() === 'i' ? null : m[1], source: 'url' };
|
|
227
229
|
|
|
228
|
-
if (BARE_ID_RE.test(text)) return { id: text, screenName: null };
|
|
230
|
+
if (BARE_ID_RE.test(text)) return { id: text, screenName: null, source: 'bare_id' };
|
|
229
231
|
|
|
230
|
-
// A bare ID hidden inside longer text
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
// A bare ID hidden inside longer text. Both edges are digit-guarded so a
|
|
233
|
+
// longer digit run (order number, hash) yields nothing instead of a
|
|
234
|
+
// silently clipped pseudo-ID.
|
|
235
|
+
const idMatch = text.match(/(?<!\d)(\d{15,25})(?!\d)/);
|
|
236
|
+
if (idMatch) return { id: idMatch[1], screenName: null, source: 'text' };
|
|
233
237
|
|
|
234
238
|
return null;
|
|
235
239
|
}
|