twittertools-mcp 1.1.1 → 1.2.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/package.json +1 -1
- package/server.js +24 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twittertools-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
package/server.js
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "./xrules.js";
|
|
30
30
|
|
|
31
31
|
const NAME = "twittertools";
|
|
32
|
-
const VERSION = "1.
|
|
32
|
+
const VERSION = "1.2.0"; // keep in sync with package.json
|
|
33
33
|
const API_BASE = (process.env.TWITTERTOOLS_API_BASE || "https://twittertools.com").replace(/\/+$/, "");
|
|
34
34
|
const VIA = "https://twittertools.com";
|
|
35
35
|
const TEXT_INPUT_MAX = 100_000; // generous, but caps local work per call
|
|
@@ -147,17 +147,38 @@ async function callApi(endpoint, params) {
|
|
|
147
147
|
return body;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// A Snowflake id encodes the creation time, so an id decoding to the future
|
|
151
|
+
// cannot belong to any post — X's endpoint still withholds those like
|
|
152
|
+
// sensitive content. Fail fast (no upstream call) with the real cause instead
|
|
153
|
+
// of relaying a misleading login-wall message for what is likely a typo.
|
|
154
|
+
function impossibleIdError(input) {
|
|
155
|
+
const parsed = parseTweetInput(input);
|
|
156
|
+
if (!parsed) return null;
|
|
157
|
+
const created = snowflakeToDate(parsed.id);
|
|
158
|
+
if (Number.isNaN(created.getTime()) || created.getTime() <= Date.now() + 60_000) return null;
|
|
159
|
+
return textResult(
|
|
160
|
+
`Post not found — this ID encodes a posting date in the future (${created.toISOString()}), so no post can have it. Check the number for typos.`,
|
|
161
|
+
{ error: true }
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
150
165
|
async function callTool(name, args) {
|
|
151
166
|
switch (name) {
|
|
152
167
|
case "get_tweet": {
|
|
153
|
-
const
|
|
168
|
+
const input = strArg(args, "url_or_id");
|
|
169
|
+
const impossible = impossibleIdError(input);
|
|
170
|
+
if (impossible) return impossible;
|
|
171
|
+
const data = await callApi("tweet", { id: input });
|
|
154
172
|
if (!data?.tweet) throw new Error("twittertools API returned no tweet");
|
|
155
173
|
// attribution rides inside the JSON: clients that pretty-render tool
|
|
156
174
|
// results would strip a trailing signature line appended after it
|
|
157
175
|
return textResult(JSON.stringify({ tweet: data.tweet, via: VIA }, null, 2));
|
|
158
176
|
}
|
|
159
177
|
case "get_thread": {
|
|
160
|
-
const
|
|
178
|
+
const input = strArg(args, "url_or_id");
|
|
179
|
+
const impossible = impossibleIdError(input);
|
|
180
|
+
if (impossible) return impossible;
|
|
181
|
+
const data = await callApi("thread", { url: input });
|
|
161
182
|
if (!data?.tweets) throw new Error("twittertools API returned no thread");
|
|
162
183
|
const head = data.partial
|
|
163
184
|
? `Note: this thread result is PARTIAL (${data.reason || "incomplete"}) — posts may be missing.\n\n`
|