feat: Twitter/X integration (read-only free tier)

- New client: src/clients/twitter.ts
- Tools: twitter_search_tweets, twitter_get_user_profile, twitter_get_user_tweets, twitter_create_tweet
- REST endpoints: GET /api/twitter/search, /api/twitter/user, /api/twitter/tweets, POST /api/twitter/tweet
- Multi-account env var: TWITTER_{ACCOUNT}_BEARER_TOKEN
- twitter_create_tweet returns clear error about paid tier requirement

Total tools: 36
This commit is contained in:
Garfield
2026-05-05 22:11:19 -04:00
parent 136bc257d1
commit 59501f11f1
5 changed files with 394 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ import { getProfile as getLinkedInProfile, createPost as createLinkedInPost, sea
import { getMe as getTelegramMe, sendMessage as sendTelegramMessage, sendPhoto as sendTelegramPhoto, getUpdates as getTelegramUpdates, getChat as getTelegramChat } from './clients/telegram.js';
import { getMe as getDiscordMe, getGuilds, getChannels, sendMessage as sendDiscordMessage, getMessages as getDiscordMessages } from './clients/discord.js';
import { getProfile as getInstagramProfile, getMedia as getInstagramMedia, createPost as createInstagramPost } from './clients/instagram.js';
import { searchTweets, getUserProfile, getUserTweets, createTweet } from './clients/twitter.js';
const ACCOUNT_PARAM = {
account: {
@@ -459,6 +460,62 @@ export const tools: Tool[] = [
},
},
},
// ── Twitter/X tools ──────────────────────────────────────────
{
name: 'twitter_search_tweets',
description:
'Search recent tweets on Twitter/X. Use when the user wants to find tweets about a topic, hashtag, or keyword.',
inputSchema: {
type: 'object',
required: ['query'],
properties: {
query: { type: 'string', description: 'Search query (keyword, hashtag, or phrase)' },
max_results: { type: 'number', description: 'Max tweets to return (default: 10, max: 100)' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
{
name: 'twitter_get_user_profile',
description:
'Get a Twitter/X user profile. Use when the user wants stats, bio, or follower count for a specific account.',
inputSchema: {
type: 'object',
required: ['username'],
properties: {
username: { type: 'string', description: 'Twitter username (without @)' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
{
name: 'twitter_get_user_tweets',
description:
'Get recent tweets from a specific Twitter/X user. Use to read someones timeline.',
inputSchema: {
type: 'object',
required: ['username'],
properties: {
username: { type: 'string', description: 'Twitter username (without @)' },
max_results: { type: 'number', description: 'Max tweets (default: 10, max: 100)' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
{
name: 'twitter_create_tweet',
description:
'Post a tweet on Twitter/X. [REQUIRES PAID TIER] The free API tier is read-only. Upgrade required to post.',
inputSchema: {
type: 'object',
required: ['text'],
properties: {
text: { type: 'string', description: 'Tweet text (max 280 chars)' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
];
function acct(args: Record<string, unknown>): Account {
@@ -689,6 +746,37 @@ export async function handleToolCall(
});
break;
// ── Twitter/X ───────────────────────────────────────────────
case 'twitter_search_tweets':
result = await searchTweets({
query: args.query as string,
max_results: (args.max_results as number) ?? 10,
account: args.account as string | undefined,
});
break;
case 'twitter_get_user_profile':
result = await getUserProfile({
username: args.username as string,
account: args.account as string | undefined,
});
break;
case 'twitter_get_user_tweets':
result = await getUserTweets({
username: args.username as string,
max_results: (args.max_results as number) ?? 10,
account: args.account as string | undefined,
});
break;
case 'twitter_create_tweet':
result = await createTweet({
text: args.text as string,
account: args.account as string | undefined,
});
break;
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
case 'yahoo_get_profile':
result = await getProfile('yahoo');