twittertools-mcp 1.0.2 → 1.1.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/README.md +4 -0
- package/package.json +1 -4
- package/server.js +84 -13
package/README.md
CHANGED
|
@@ -20,6 +20,10 @@ API, which resolves public posts through X's free syndication endpoint. The othe
|
|
|
20
20
|
three run entirely offline. Media is **not** proxied: tweet results carry the
|
|
21
21
|
direct `pbs.twimg.com` / `video.twimg.com` URLs, which any client can fetch on its own.
|
|
22
22
|
|
|
23
|
+
The package has **zero dependencies**: the MCP protocol layer is hand-rolled
|
|
24
|
+
(line-delimited JSON-RPC over stdio), so the entire supply-chain surface is the
|
|
25
|
+
couple hundred lines in this directory — verifiable against Socket or any scanner.
|
|
26
|
+
|
|
23
27
|
## Install
|
|
24
28
|
|
|
25
29
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twittertools-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
@@ -33,8 +33,5 @@
|
|
|
33
33
|
"bugs": "https://github.com/steley/twittertools/issues",
|
|
34
34
|
"scripts": {
|
|
35
35
|
"start": "node server.js"
|
|
36
|
-
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
39
36
|
}
|
|
40
37
|
}
|
package/server.js
CHANGED
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
* direct pbs.twimg.com / video.twimg.com URLs, which any client can fetch.
|
|
13
13
|
*
|
|
14
14
|
* Set TWITTERTOOLS_API_BASE to point at a self-hosted instance.
|
|
15
|
+
*
|
|
16
|
+
* The MCP protocol layer below is hand-rolled on purpose: the official SDK
|
|
17
|
+
* pulls an HTTP-server dependency tree (express, hono, ajv, cross-spawn, …)
|
|
18
|
+
* into a stdio-only package. A tools-only server needs exactly four verbs,
|
|
19
|
+
* implemented against the line-delimited JSON-RPC 2.0 framing of the MCP
|
|
20
|
+
* stdio transport — pinned by the vitest suite in test/.
|
|
15
21
|
*/
|
|
16
22
|
|
|
17
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
-
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
20
23
|
import {
|
|
21
24
|
countTweet,
|
|
22
25
|
splitThread,
|
|
@@ -26,7 +29,7 @@ import {
|
|
|
26
29
|
} from "./xrules.js";
|
|
27
30
|
|
|
28
31
|
const NAME = "twittertools";
|
|
29
|
-
const VERSION = "1.0
|
|
32
|
+
const VERSION = "1.1.0"; // keep in sync with package.json
|
|
30
33
|
const API_BASE = (process.env.TWITTERTOOLS_API_BASE || "https://twittertools.com").replace(/\/+$/, "");
|
|
31
34
|
const ATTRIBUTION = "\n\nvia twittertools.com";
|
|
32
35
|
const TEXT_INPUT_MAX = 100_000; // generous, but caps local work per call
|
|
@@ -154,7 +157,7 @@ async function callTool(name, args) {
|
|
|
154
157
|
case "get_thread": {
|
|
155
158
|
const data = await callApi("thread", { url: strArg(args, "url_or_id") });
|
|
156
159
|
if (!data?.tweets) throw new Error("twittertools API returned no thread");
|
|
157
|
-
const head = data
|
|
160
|
+
const head = data.partial
|
|
158
161
|
? `Note: this thread result is PARTIAL (${data.reason || "incomplete"}) — posts may be missing.\n\n`
|
|
159
162
|
: "";
|
|
160
163
|
return textResult(head + JSON.stringify({ count: data.tweets.length, tweets: data.tweets }, null, 2) + ATTRIBUTION);
|
|
@@ -190,17 +193,85 @@ async function callTool(name, args) {
|
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
|
|
196
|
+
// --------------------------------------------------------------------------- //
|
|
197
|
+
// Line-delimited JSON-RPC 2.0 over stdio (MCP stdio transport) //
|
|
198
|
+
// --------------------------------------------------------------------------- //
|
|
199
|
+
|
|
200
|
+
function send(msg) {
|
|
201
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
202
|
+
}
|
|
194
203
|
|
|
195
|
-
|
|
204
|
+
function sendResult(id, result) {
|
|
205
|
+
send({ jsonrpc: "2.0", id, result });
|
|
206
|
+
}
|
|
196
207
|
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
function sendError(id, code, message) {
|
|
209
|
+
send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function dispatch(msg) {
|
|
213
|
+
const { method, id, params } = msg;
|
|
214
|
+
const isRequest = "id" in msg;
|
|
199
215
|
try {
|
|
200
|
-
|
|
216
|
+
switch (method) {
|
|
217
|
+
case "initialize":
|
|
218
|
+
// echo the client's requested version: every current client accepts
|
|
219
|
+
// its own, and we make no use of newer protocol features
|
|
220
|
+
sendResult(id, {
|
|
221
|
+
protocolVersion: params?.protocolVersion || "2024-11-05",
|
|
222
|
+
capabilities: { tools: {} },
|
|
223
|
+
serverInfo: { name: NAME, version: VERSION },
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
case "notifications/initialized":
|
|
227
|
+
return; // notification — no response
|
|
228
|
+
case "tools/list":
|
|
229
|
+
sendResult(id, { tools: TOOLS });
|
|
230
|
+
return;
|
|
231
|
+
case "tools/call":
|
|
232
|
+
// tool execution failures are tool results (isError), not protocol
|
|
233
|
+
// errors — matches how every client renders a failed tool call
|
|
234
|
+
try {
|
|
235
|
+
sendResult(id, await callTool(params?.name, params?.arguments));
|
|
236
|
+
} catch (e) {
|
|
237
|
+
sendResult(id, textResult(`Error: ${e?.message || String(e)}`, { error: true }));
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
case "ping":
|
|
241
|
+
sendResult(id, {});
|
|
242
|
+
return;
|
|
243
|
+
default:
|
|
244
|
+
// unknown notifications are silently ignored per JSON-RPC 2.0
|
|
245
|
+
if (isRequest) sendError(id, -32601, `Method not found: ${method}`);
|
|
246
|
+
}
|
|
201
247
|
} catch (e) {
|
|
202
|
-
|
|
248
|
+
if (isRequest) sendError(id, -32603, `Internal error: ${e?.message || String(e)}`);
|
|
203
249
|
}
|
|
204
|
-
}
|
|
250
|
+
}
|
|
205
251
|
|
|
206
|
-
|
|
252
|
+
let buffer = "";
|
|
253
|
+
process.stdin.setEncoding("utf8");
|
|
254
|
+
process.stdin.on("data", (chunk) => {
|
|
255
|
+
buffer += chunk;
|
|
256
|
+
let i;
|
|
257
|
+
while ((i = buffer.indexOf("\n")) >= 0) {
|
|
258
|
+
const line = buffer.slice(0, i).trim();
|
|
259
|
+
buffer = buffer.slice(i + 1);
|
|
260
|
+
if (!line) continue;
|
|
261
|
+
let msg;
|
|
262
|
+
try {
|
|
263
|
+
msg = JSON.parse(line);
|
|
264
|
+
} catch {
|
|
265
|
+
sendError(null, -32700, "Parse error");
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (!msg || typeof msg !== "object" || typeof msg.method !== "string") {
|
|
269
|
+
if (msg && typeof msg === "object" && "id" in msg) {
|
|
270
|
+
sendError(msg.id, -32600, "Invalid Request");
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
dispatch(msg);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
process.stdin.on("end", () => process.exit(0));
|