@twitterapis/mcp 0.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/LICENSE +21 -0
- package/README.md +88 -0
- package/package.json +44 -0
- package/src/index.js +95 -0
- package/src/tools.js +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 twitterapis.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @twitterapis/mcp
|
|
2
|
+
|
|
3
|
+
Official **Model Context Protocol** server for [twitterapis.com](https://www.twitterapis.com) — the Twitter / X **read** API as native tools for Claude, Cursor, and any MCP client.
|
|
4
|
+
|
|
5
|
+
Ask your agent to *"find the latest tweets about AI agents"* or *"pull @openai's followers"* and it calls the API directly. Every tool maps to a REST endpoint at `https://api.twitterapis.com`; the server holds no state and forwards your API key per call.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
No install needed — run it on demand with `npx`. You only need an API key (free `$0.50` in credits, no card): **https://www.twitterapis.com/signup**.
|
|
10
|
+
|
|
11
|
+
### Claude Desktop
|
|
12
|
+
|
|
13
|
+
Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"twitterapis": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["-y", "@twitterapis/mcp@latest"],
|
|
21
|
+
"env": { "TWITTERAPIS_KEY": "YOUR_API_KEY" }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Cursor
|
|
28
|
+
|
|
29
|
+
`~/.cursor/mcp.json` (or Settings → MCP → Add):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"twitterapis": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "@twitterapis/mcp@latest"],
|
|
37
|
+
"env": { "TWITTERAPIS_KEY": "YOUR_API_KEY" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Restart the client and the `twitter_*` tools appear.
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
| Env var | Required | Default | Purpose |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `TWITTERAPIS_KEY` | ✅ | — | Your API key from the dashboard |
|
|
50
|
+
| `TWITTERAPIS_BASE_URL` | | `https://api.twitterapis.com` | Override the API host |
|
|
51
|
+
| `TWITTERAPIS_TIMEOUT_MS` | | `30000` | Per-request timeout |
|
|
52
|
+
|
|
53
|
+
## Tools
|
|
54
|
+
|
|
55
|
+
All read-only. User endpoints take `username` **or** `user_id`; tweet endpoints take `id` **or** `url`; list endpoints page with `count` + `cursor`.
|
|
56
|
+
|
|
57
|
+
| Tool | What it does |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `twitter_advanced_search` | Search tweets with X operators (`from:`, `min_faves:`, `filter:`, …) |
|
|
60
|
+
| `twitter_user_search` | Search accounts by name/keyword |
|
|
61
|
+
| `twitter_user_info` | Full profile by handle |
|
|
62
|
+
| `twitter_user_info_by_id` | Full profile by numeric id |
|
|
63
|
+
| `twitter_user_tweets` | A user's recent tweets (no replies) |
|
|
64
|
+
| `twitter_user_tweets_and_replies` | A user's full timeline (tweets + replies) |
|
|
65
|
+
| `twitter_user_followers` | Accounts that follow a user |
|
|
66
|
+
| `twitter_user_following` | Accounts a user follows |
|
|
67
|
+
| `twitter_user_verified_followers` | A user's verified followers |
|
|
68
|
+
| `twitter_user_media` | Media a user has posted |
|
|
69
|
+
| `twitter_user_mentions` | Recent tweets mentioning a user |
|
|
70
|
+
| `twitter_tweet_detail` | One tweet's full detail |
|
|
71
|
+
| `twitter_tweet_replies` | Replies to a tweet |
|
|
72
|
+
| `twitter_tweet_thread` | A tweet's self-thread |
|
|
73
|
+
| `twitter_tweet_retweeters` | Accounts that retweeted a tweet |
|
|
74
|
+
| `twitter_list_members` | Members of a List |
|
|
75
|
+
|
|
76
|
+
## Pricing
|
|
77
|
+
|
|
78
|
+
Calls are billed to your twitterapis.com account at the standard read rate (`$0.0008`/call); your first `$0.50` is free. See [pricing](https://www.twitterapis.com/pricing).
|
|
79
|
+
|
|
80
|
+
## Links
|
|
81
|
+
|
|
82
|
+
- Docs — https://docs.twitterapis.com
|
|
83
|
+
- Dashboard / keys — https://www.twitterapis.com/dashboard
|
|
84
|
+
- REST API (no MCP needed) — https://api.twitterapis.com
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@twitterapis/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official MCP server for twitterapis.com — the Twitter/X read API (search, users, followers, tweets, threads, lists) as native tools for Claude, Cursor, and any MCP client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"twitterapis-mcp": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node src/index.js",
|
|
20
|
+
"check": "node --check src/index.js && node --check src/tools.js",
|
|
21
|
+
"test": "node test/tools.test.mjs && node test/smoke.mjs"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
25
|
+
"zod": "^3.23.8 || ^4.0.0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"twitter",
|
|
31
|
+
"x",
|
|
32
|
+
"twitter-api",
|
|
33
|
+
"x-api",
|
|
34
|
+
"claude",
|
|
35
|
+
"cursor",
|
|
36
|
+
"twitterapis"
|
|
37
|
+
],
|
|
38
|
+
"author": "twitterapis.com",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"homepage": "https://www.twitterapis.com/mcp",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://www.twitterapis.com/contact"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @twitterapis/mcp — official MCP server for twitterapis.com
|
|
3
|
+
//
|
|
4
|
+
// Exposes the Twitter / X READ API as native MCP tools (search, users,
|
|
5
|
+
// followers/following, tweets, threads, lists, mentions) for Claude, Cursor,
|
|
6
|
+
// and any MCP client. Each tool is a thin, typed wrapper over a REST endpoint
|
|
7
|
+
// at https://api.twitterapis.com — the server holds no state and forwards your
|
|
8
|
+
// API key on every call. The tool catalog lives in ./tools.js.
|
|
9
|
+
//
|
|
10
|
+
// Config (env):
|
|
11
|
+
// TWITTERAPIS_KEY required — your key from https://www.twitterapis.com/signup
|
|
12
|
+
// TWITTERAPIS_BASE_URL optional — defaults to https://api.twitterapis.com
|
|
13
|
+
// TWITTERAPIS_TIMEOUT_MS optional — per-request timeout (default 30000)
|
|
14
|
+
//
|
|
15
|
+
// Run: npx -y @twitterapis/mcp@latest (stdio transport)
|
|
16
|
+
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { TOOLS, buildQuery } from "./tools.js";
|
|
20
|
+
|
|
21
|
+
const API_KEY = process.env.TWITTERAPIS_KEY;
|
|
22
|
+
const BASE_URL = (
|
|
23
|
+
process.env.TWITTERAPIS_BASE_URL || "https://api.twitterapis.com"
|
|
24
|
+
).replace(/\/+$/, "");
|
|
25
|
+
const REQUEST_TIMEOUT_MS = Number(process.env.TWITTERAPIS_TIMEOUT_MS || 30000);
|
|
26
|
+
|
|
27
|
+
if (!API_KEY) {
|
|
28
|
+
console.error(
|
|
29
|
+
"[twitterapis-mcp] Missing TWITTERAPIS_KEY. Get a key at https://www.twitterapis.com/signup and set it in your MCP client config.",
|
|
30
|
+
);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── REST call ────────────────────────────────────────────────────────────────
|
|
35
|
+
async function callEndpoint(path, args) {
|
|
36
|
+
const q = buildQuery(args);
|
|
37
|
+
const url = `${BASE_URL}${path}${q ? `?${q}` : ""}`;
|
|
38
|
+
|
|
39
|
+
const ctrl = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
method: "GET",
|
|
44
|
+
headers: {
|
|
45
|
+
// The API accepts either header; send both for maximum compatibility.
|
|
46
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
47
|
+
"x-api-key": API_KEY,
|
|
48
|
+
accept: "application/json",
|
|
49
|
+
"user-agent": "twitterapis-mcp/0.1.0",
|
|
50
|
+
},
|
|
51
|
+
signal: ctrl.signal,
|
|
52
|
+
});
|
|
53
|
+
const body = await res.text();
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const hint =
|
|
56
|
+
res.status === 401
|
|
57
|
+
? " (check TWITTERAPIS_KEY)"
|
|
58
|
+
: res.status === 402
|
|
59
|
+
? " (insufficient credits — top up at https://www.twitterapis.com/dashboard)"
|
|
60
|
+
: res.status === 429
|
|
61
|
+
? " (rate limited — retry shortly)"
|
|
62
|
+
: "";
|
|
63
|
+
return { isError: true, content: [{ type: "text", text: `HTTP ${res.status}${hint}: ${body.slice(0, 1200)}` }] };
|
|
64
|
+
}
|
|
65
|
+
return { content: [{ type: "text", text: body }] };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const msg = err?.name === "AbortError" ? `timed out after ${REQUEST_TIMEOUT_MS}ms` : err?.message || String(err);
|
|
68
|
+
return { isError: true, content: [{ type: "text", text: `Request failed: ${msg}` }] };
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── MCP server ───────────────────────────────────────────────────────────────
|
|
75
|
+
const server = new McpServer({ name: "twitterapis", version: "0.1.0" });
|
|
76
|
+
|
|
77
|
+
for (const tool of TOOLS) {
|
|
78
|
+
server.registerTool(
|
|
79
|
+
tool.name,
|
|
80
|
+
{ description: tool.description, inputSchema: tool.shape },
|
|
81
|
+
async (args) => callEndpoint(tool.path, args),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
const transport = new StdioServerTransport();
|
|
87
|
+
await server.connect(transport);
|
|
88
|
+
// Logs go to stderr so they never corrupt the stdio JSON-RPC stream.
|
|
89
|
+
console.error(`[twitterapis-mcp] ready · ${TOOLS.length} tools · base ${BASE_URL}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main().catch((err) => {
|
|
93
|
+
console.error("[twitterapis-mcp] fatal:", err);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Tool catalog + pure query-builder for @twitterapis/mcp.
|
|
2
|
+
// Kept separate from the server wiring (index.js) so it can be unit-tested
|
|
3
|
+
// without spawning the stdio transport.
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
// ── Shared Zod input-schema fragments ───────────────────────────────────────
|
|
7
|
+
const PAGINATION = {
|
|
8
|
+
count: z.number().int().positive().optional().describe("Max items to return for this page (endpoint default applies if omitted)."),
|
|
9
|
+
cursor: z.string().optional().describe("Pagination cursor returned by a previous call to fetch the next page."),
|
|
10
|
+
};
|
|
11
|
+
const USER_REF = {
|
|
12
|
+
username: z.string().optional().describe('Twitter/X handle, without the leading @ (e.g. "elonmusk"). Provide this OR user_id.'),
|
|
13
|
+
user_id: z.string().optional().describe("Numeric user id. Provide this OR username."),
|
|
14
|
+
};
|
|
15
|
+
const TWEET_REF = {
|
|
16
|
+
id: z.string().optional().describe('Tweet id (e.g. "1789..."). Provide this OR url.'),
|
|
17
|
+
url: z.string().optional().describe("Full tweet URL (https://x.com/<user>/status/<id>). Provide this OR id."),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ── Read-only tool catalog. Tool arg names map 1:1 to endpoint query params. ─
|
|
21
|
+
export const TOOLS = [
|
|
22
|
+
{ name: "twitter_advanced_search", path: "/twitter/tweet/advanced_search",
|
|
23
|
+
description: "Search recent tweets with X's advanced-search syntax (operators like from:, to:, since:, until:, min_faves:, filter:). Returns matching tweets with author, metrics, and a cursor for paging.",
|
|
24
|
+
shape: { query: z.string().describe('Search query, e.g. "AI agents min_faves:100" or "from:openai".'), product: z.enum(["Top", "Latest", "Media", "People"]).optional().describe('Result tab. "Latest" for reverse-chron, "Top" for ranked (default).'), ...PAGINATION } },
|
|
25
|
+
{ name: "twitter_user_search", path: "/twitter/user/search",
|
|
26
|
+
description: "Search for users/accounts by name or keyword. Returns matching profiles with a paging cursor.",
|
|
27
|
+
shape: { query: z.string().describe("Name or keyword to search accounts for."), ...PAGINATION } },
|
|
28
|
+
{ name: "twitter_user_info", path: "/twitter/user/info",
|
|
29
|
+
description: "Get a user's full profile by handle: bio, follower/following counts, verification, location, created date, pinned tweet.",
|
|
30
|
+
shape: { username: z.string().describe("Twitter/X handle without the leading @.") } },
|
|
31
|
+
{ name: "twitter_user_info_by_id", path: "/twitter/user/info_by_id",
|
|
32
|
+
description: "Get a user's full profile by numeric user id (use when you have the id but not the handle).",
|
|
33
|
+
shape: { user_id: z.string().describe("Numeric user id.") } },
|
|
34
|
+
{ name: "twitter_user_tweets", path: "/twitter/user/tweets",
|
|
35
|
+
description: "Get a user's recent original tweets (no replies). Cursored.",
|
|
36
|
+
shape: { ...USER_REF, ...PAGINATION } },
|
|
37
|
+
{ name: "twitter_user_tweets_and_replies", path: "/twitter/user/tweets_and_replies",
|
|
38
|
+
description: "Get a user's recent tweets AND replies (their full timeline). Cursored.",
|
|
39
|
+
shape: { ...USER_REF, ...PAGINATION } },
|
|
40
|
+
{ name: "twitter_user_followers", path: "/twitter/user/followers",
|
|
41
|
+
description: "List the accounts that follow a user. Cursored.",
|
|
42
|
+
shape: { ...USER_REF, ...PAGINATION } },
|
|
43
|
+
{ name: "twitter_user_following", path: "/twitter/user/following",
|
|
44
|
+
description: "List the accounts a user follows. Cursored.",
|
|
45
|
+
shape: { ...USER_REF, ...PAGINATION } },
|
|
46
|
+
{ name: "twitter_user_verified_followers", path: "/twitter/user/verified_followers",
|
|
47
|
+
description: "List a user's verified (blue/business) followers. Cursored.",
|
|
48
|
+
shape: { ...USER_REF, ...PAGINATION } },
|
|
49
|
+
{ name: "twitter_user_media", path: "/twitter/user/media",
|
|
50
|
+
description: "Get the media (images/videos) a user has posted. Cursored.",
|
|
51
|
+
shape: { ...USER_REF, ...PAGINATION } },
|
|
52
|
+
{ name: "twitter_user_mentions", path: "/twitter/user/mentions",
|
|
53
|
+
description: "Get recent tweets that mention a user (implemented as a to:<username> search). Cursored.",
|
|
54
|
+
shape: { username: z.string().describe("Twitter/X handle without the leading @."), ...PAGINATION } },
|
|
55
|
+
{ name: "twitter_tweet_detail", path: "/twitter/tweet/detail",
|
|
56
|
+
description: "Get a single tweet's full detail: text, author, metrics, media, and quoted/replied context.",
|
|
57
|
+
shape: { ...TWEET_REF } },
|
|
58
|
+
{ name: "twitter_tweet_replies", path: "/twitter/tweet/replies",
|
|
59
|
+
description: "Get the replies to a tweet. Cursored.",
|
|
60
|
+
shape: { ...TWEET_REF, ...PAGINATION } },
|
|
61
|
+
{ name: "twitter_tweet_thread", path: "/twitter/tweet/thread",
|
|
62
|
+
description: "Get the full self-thread for a tweet (the author's connected chain). Cursored.",
|
|
63
|
+
shape: { ...TWEET_REF, ...PAGINATION } },
|
|
64
|
+
{ name: "twitter_tweet_retweeters", path: "/twitter/tweet/retweeters",
|
|
65
|
+
description: "List the accounts that retweeted a tweet. Cursored.",
|
|
66
|
+
shape: { ...TWEET_REF, ...PAGINATION } },
|
|
67
|
+
{ name: "twitter_list_members", path: "/twitter/list/members",
|
|
68
|
+
description: "List the members of a Twitter/X List by list id. Cursored.",
|
|
69
|
+
shape: { list_id: z.string().describe("Numeric List id."), ...PAGINATION } },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// Pure query-string builder: drops undefined/null/empty values, URL-encodes the rest.
|
|
73
|
+
export function buildQuery(args) {
|
|
74
|
+
const qs = new URLSearchParams();
|
|
75
|
+
for (const [k, v] of Object.entries(args || {})) {
|
|
76
|
+
if (v !== undefined && v !== null && String(v).length > 0) qs.set(k, String(v));
|
|
77
|
+
}
|
|
78
|
+
return qs.toString();
|
|
79
|
+
}
|