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

@@ -769,6 +769,56 @@ app.post('/api/instagram/post', requireAuth, async (req, res) => {
}
});
// ── Twitter/X REST endpoints ────────────────────────────────────
app.get('/api/twitter/search', requireAuth, async (req, res) => {
const query = req.query.query as string | undefined;
const max_results = req.query.max_results ? parseInt(req.query.max_results as string, 10) : undefined;
const account = req.query.account as string | undefined;
if (!query) { res.status(400).json({ error: 'query is required' }); return; }
try {
const result = await handleToolCall('twitter_search_tweets', { query, max_results, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.get('/api/twitter/user', requireAuth, async (req, res) => {
const username = req.query.username as string | undefined;
const account = req.query.account as string | undefined;
if (!username) { res.status(400).json({ error: 'username is required' }); return; }
try {
const result = await handleToolCall('twitter_get_user_profile', { username, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.get('/api/twitter/tweets', requireAuth, async (req, res) => {
const username = req.query.username as string | undefined;
const max_results = req.query.max_results ? parseInt(req.query.max_results as string, 10) : undefined;
const account = req.query.account as string | undefined;
if (!username) { res.status(400).json({ error: 'username is required' }); return; }
try {
const result = await handleToolCall('twitter_get_user_tweets', { username, max_results, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.post('/api/twitter/tweet', requireAuth, async (req, res) => {
const { text, account } = req.body as Record<string, unknown>;
if (!text) { res.status(400).json({ error: 'text is required' }); return; }
try {
const result = await handleToolCall('twitter_create_tweet', { text, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.post('/api/pilot-request', async (req, res) => {
const origin = req.get('origin');
if (origin && !SQUAREMCP_ALLOWED_ORIGINS.has(origin)) {