diff --git a/README.md b/README.md index 08ed0ff..19ab53d 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,17 @@ TWITTER_EMAIL="YOUR_TWITTER_EMAIL" You need to configure Twitter authentication by creating a `.env` file or directly adding the variables to your environment. +### Optional GetXAPI Read Backend + +For read-only tools, you can set a GetXAPI API key. When configured, search-based reads can route through GetXAPI's advanced_search endpoint with no X developer account required. + +```sh +GETXAPI_API_KEY="YOUR_GETXAPI_API_KEY" +GETXAPI_BASE_URL="https://api.getxapi.com" +``` + +Write tools continue to use the existing Twitter credential or API modes. + ### Configuration via Twitter API 1. Create a Developer Account: diff --git a/src/index.ts b/src/index.ts index ed91462..bdae752 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { getTwitterUserIdFromUsername, ReplyTweet, } from "./tools/twitter.js"; +import { getGetXAPIBackendStatus } from "./tools/getxapi.js"; import { createAndPostTwitterThreadSchema, createTwitterpostSchema, @@ -113,6 +114,12 @@ export const registerTools = (TwitterToolRegistry: TwitterTool[]) => { description: "Get current account profile data", execute: getOwnTwitterAccountInfo, }); + + TwitterToolRegistry.push({ + name: "get_getxapi_backend_status", + description: "Get optional GetXAPI read backend status without exposing secrets", + execute: getGetXAPIBackendStatus as any, + }); }; export const RegisterToolInServer = async (twitter: TwitterManager) => { diff --git a/src/tools/getxapi.ts b/src/tools/getxapi.ts new file mode 100644 index 0000000..dd2b819 --- /dev/null +++ b/src/tools/getxapi.ts @@ -0,0 +1,109 @@ +type GetXAPIRecord = Record; + +const DEFAULT_GETXAPI_BASE_URL = "https://api.getxapi.com"; + +export const configuredGetXAPIApiKey = ( + env: NodeJS.ProcessEnv = process.env +): string | undefined => env.GETXAPI_API_KEY || env.GETXAPI_KEY; + +export const configuredGetXAPIBaseUrl = ( + env: NodeJS.ProcessEnv = process.env +): string => { + const baseUrl = env.GETXAPI_BASE_URL || DEFAULT_GETXAPI_BASE_URL; + return baseUrl.replace(/\/+$/, ""); +}; + +export const hasGetXAPIBackend = (): boolean => + Boolean(configuredGetXAPIApiKey()); + +export const buildGetXAPIUrl = ( + path: string, + query: Record, + env: NodeJS.ProcessEnv = process.env +): string => { + const url = new URL(path, `${configuredGetXAPIBaseUrl(env)}/`); + for (const [key, value] of Object.entries(query)) { + if (value !== undefined && value !== "") { + url.searchParams.set(key, String(value)); + } + } + return url.toString(); +}; + +const requestGetXAPIJson = async ( + path: string, + query: Record +): Promise => { + const apiKey = configuredGetXAPIApiKey(); + if (!apiKey) { + throw new Error("Set GETXAPI_API_KEY to use GetXAPI reads"); + } + + const response = await fetch(buildGetXAPIUrl(path, query), { + headers: { + Accept: "application/json", + Authorization: `Bearer ${apiKey}`, + "User-Agent": "mcp_twitter-getxapi/1.0", + }, + }); + + if (!response.ok) { + throw new Error(`GetXAPI read backend returned HTTP ${response.status}`); + } + + return response.json(); +}; + +export const extractGetXAPITweets = (payload: unknown): GetXAPIRecord[] => { + if (Array.isArray(payload)) { + return payload.filter( + (item): item is GetXAPIRecord => typeof item === "object" && item !== null + ); + } + if (!payload || typeof payload !== "object") { + return []; + } + + const record = payload as GetXAPIRecord; + for (const key of ["tweets", "results", "items"]) { + const value = record[key]; + if (Array.isArray(value)) { + return value.filter( + (item): item is GetXAPIRecord => typeof item === "object" && item !== null + ); + } + } + + const data = record.data; + if (Array.isArray(data)) { + return data.filter( + (item): item is GetXAPIRecord => typeof item === "object" && item !== null + ); + } + if (data && typeof data === "object") { + return extractGetXAPITweets(data); + } + + return []; +}; + +export const searchGetXAPITweets = async ( + query: string, + limit = 25 +): Promise => { + const payload = await requestGetXAPIJson("/twitter/tweet/advanced_search", { + q: query, + limit, + }); + return extractGetXAPITweets(payload); +}; + +export const getGetXAPIBackendStatus = async () => { + return { + status: "ok", + configured: hasGetXAPIBackend(), + base_url: configuredGetXAPIBaseUrl(), + endpoint: "/twitter/tweet/advanced_search", + auth: "Bearer", + }; +};