Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getTwitterUserIdFromUsername,
ReplyTweet,
} from "./tools/twitter.js";
import { getGetXAPIBackendStatus } from "./tools/getxapi.js";
import {
createAndPostTwitterThreadSchema,
createTwitterpostSchema,
Expand Down Expand Up @@ -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) => {
Expand Down
109 changes: 109 additions & 0 deletions src/tools/getxapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
type GetXAPIRecord = Record<string, unknown>;

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<string, string | number | boolean | undefined>,
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<string, string | number | boolean | undefined>
): Promise<unknown> => {
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<GetXAPIRecord[]> => {
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",
};
};