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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/server.js +16 -5
  3. package/xrules.js +10 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twittertools-mcp",
3
- "version": "1.2.0",
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.0"; // keep in sync with package.json
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
- // echo the client's requested version: every current client accepts
242
- // its own, and we make no use of newer protocol features
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: params?.protocolVersion || "2024-11-05",
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 text. */
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
- const idMatch = text.match(/(\d{15,25})/);
232
- if (idMatch) return { id: idMatch[1], screenName: null };
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
  }