twittertools-mcp 1.2.0 → 1.2.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/package.json +1 -1
- package/server.js +16 -5
- 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.1",
|
|
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.1"; // 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
|
|
@@ -202,6 +207,7 @@ async function callTool(name, args) {
|
|
|
202
207
|
JSON.stringify(
|
|
203
208
|
{
|
|
204
209
|
id: parsed.id,
|
|
210
|
+
source: parsed.source,
|
|
205
211
|
screenName: parsed.screenName,
|
|
206
212
|
createdAt: Number.isNaN(created.getTime()) ? null : created.toISOString(),
|
|
207
213
|
permalink: permalinkFor(parsed.id, parsed.screenName),
|
|
@@ -237,15 +243,20 @@ async function dispatch(msg) {
|
|
|
237
243
|
const isRequest = "id" in msg;
|
|
238
244
|
try {
|
|
239
245
|
switch (method) {
|
|
240
|
-
case "initialize":
|
|
241
|
-
//
|
|
242
|
-
//
|
|
246
|
+
case "initialize": {
|
|
247
|
+
// Answer with a version we actually implement: echoing whatever the
|
|
248
|
+
// client asked for would claim support for versions we've never seen.
|
|
249
|
+
// A client that can't accept our answer disconnects, per spec.
|
|
250
|
+
const requested = params?.protocolVersion;
|
|
243
251
|
sendResult(id, {
|
|
244
|
-
protocolVersion:
|
|
252
|
+
protocolVersion: SUPPORTED_VERSIONS.includes(requested)
|
|
253
|
+
? requested
|
|
254
|
+
: SUPPORTED_VERSIONS[0],
|
|
245
255
|
capabilities: { tools: {} },
|
|
246
256
|
serverInfo: { name: NAME, version: VERSION },
|
|
247
257
|
});
|
|
248
258
|
return;
|
|
259
|
+
}
|
|
249
260
|
case "notifications/initialized":
|
|
250
261
|
return; // notification — no response
|
|
251
262
|
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
|
}
|