updated readme, env var names

This commit is contained in:
Vardhan Agnihotri 2026-02-06 10:35:21 -08:00
parent f4e5a2d03b
commit 028a62f918
4 changed files with 228 additions and 81 deletions

223
README.md
View file

@ -9,7 +9,7 @@ FastMCP. Streaming and webhook endpoints are excluded.
- An X Developer Platform app (to get tokens)
- Optional: an xAI API key if you want to run the Grok test client
## Quick start (local server)
## Setup (local)
1. Create a virtual environment and install dependencies:
- `python -m venv .venv`
@ -17,97 +17,218 @@ FastMCP. Streaming and webhook endpoints are excluded.
- `pip install -r requirements.txt`
2. Create your local `.env`:
- `cp env.example .env`
- Fill in the OAuth1 section (consumer key/secret and callback settings).
3. Run the server:
- `python server.py`
The server starts at `http://127.0.0.1:8000` by default.
The MCP endpoint is `http://127.0.0.1:8000/mcp`.
## Environment variables
Required (OAuth1 user context):
- `TWITTER_CONSUMER_KEY`
- `TWITTER_CONSUMER_SECRET`
- Required values (do not skip):
- `X_OAUTH_CONSUMER_KEY`
- `X_OAUTH_CONSUMER_SECRET`
- `X_BEARER_TOKEN` (required for this setup; keep it set even if using OAuth1)
- OAuth1 callback (defaults are fine):
- `X_OAUTH_CALLBACK_HOST` (default `127.0.0.1`)
- `X_OAUTH_CALLBACK_PORT` (default `8976`)
- `X_OAUTH_CALLBACK_PATH` (default `/oauth/callback`)
- `X_OAUTH_CALLBACK_TIMEOUT` (default `300`)
Optional auth fallback:
- `X_BEARER_TOKEN` (OAuth2 bearer token)
Optional server config:
- `MCP_HOST` (default `127.0.0.1`)
- `MCP_PORT` (default `8000`)
- Server settings (optional):
- `X_API_BASE_URL` (default `https://api.x.com`)
- `X_API_TIMEOUT` (default `30`)
- `MCP_HOST` (default `127.0.0.1`)
- `MCP_PORT` (default `8000`)
- `X_API_DEBUG` (default `1`)
- `FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER`
Tool filtering (comma-separated):
- `X_API_TOOL_TAGS`
- Tool filtering (optional, comma-separated):
- `X_API_TOOL_ALLOWLIST`
- `X_API_TOOL_DENYLIST`
Optional Grok test client:
- Optional Grok test client:
- `XAI_API_KEY`
- `XAI_MODEL` (default `grok-4-1-fast`)
- `MCP_SERVER_URL` (default `http://127.0.0.1:8000/mcp`)
## Auth flow (OAuth1 on startup)
The server runs an OAuth1 browser flow on startup and uses the resulting
access token to sign every request. You must register a callback URL in your
X Developer App that matches:
- Optional OAuth2 token generation:
- `CLIENT_ID`
- `CLIENT_SECRET`
- `X_OAUTH_ACCESS_TOKEN`
- `X_OAUTH_ACCESS_TOKEN_SECRET` (optional)
- Optional OAuth1 debug output:
- `X_OAUTH_PRINT_TOKENS`
- `X_OAUTH_PRINT_AUTH_HEADER`
3. Register the callback URL in your X Developer App:
```
http://<X_OAUTH_CALLBACK_HOST>:<X_OAUTH_CALLBACK_PORT><X_OAUTH_CALLBACK_PATH>
```
Example:
Example (defaults):
```
http://127.0.0.1:8976/oauth/callback
```
When you start the server, it will open a browser tab for consent and wait
for the callback. Tokens are kept in memory only for the lifetime of the
server process.
## Tool whitelisting
If you want to limit the tool list (smaller context window, fewer tools),
use `X_API_TOOL_ALLOWLIST` or `X_API_TOOL_TAGS` in `.env`.
Example allowlist:
4. Start the server:
```
X_API_TOOL_ALLOWLIST=getUsersByUsername,createDirectMessagesByParticipantId
python server.py
```
Example tags:
The MCP endpoint is `http://127.0.0.1:8000/mcp` by default.
5. Connect an MCP client:
- Local client: point it to `http://127.0.0.1:8000/mcp`.
- Remote client: tunnel your local server (e.g., ngrok) and use the public URL.
## Whitelisting tools
Use `X_API_TOOL_ALLOWLIST` to load a small, explicit set of tools:
```
X_API_TOOL_TAGS=users,dm
X_API_TOOL_ALLOWLIST=getUsersByUsername,createPosts,searchPostsRecent
```
Allowlist and tags are applied at startup when the OpenAPI spec is loaded.
Whitelisting is applied at startup when the OpenAPI spec is loaded, so restart
the server after changes. See the full tool list below before building your
allowlist.
## OAuth1 flow (startup behavior)
On startup, the server opens a browser for OAuth1 consent and waits for the
callback. Tokens are kept in memory only for the lifetime of the server
process. Set `X_OAUTH_PRINT_TOKENS=1` to print tokens, or
`X_OAUTH_PRINT_AUTH_HEADER=1` to print request headers.
## Available tool calls (allowlist-ready)
Below is the full list of tool calls you can whitelist via
`X_API_TOOL_ALLOWLIST`. Copy any of these into your `.env` allowlist.
- `addListsMember`
- `addUserPublicKey`
- `appendMediaUpload`
- `blockUsersDms`
- `createCommunityNotes`
- `createComplianceJobs`
- `createDirectMessagesByConversationId`
- `createDirectMessagesByParticipantId`
- `createDirectMessagesConversation`
- `createLists`
- `createMediaMetadata`
- `createMediaSubtitles`
- `createPosts`
- `createUsersBookmark`
- `deleteActivitySubscription`
- `deleteAllConnections`
- `deleteCommunityNotes`
- `deleteConnectionsByEndpoint`
- `deleteConnectionsByUuids`
- `deleteDirectMessagesEvents`
- `deleteLists`
- `deleteMediaSubtitles`
- `deletePosts`
- `deleteUsersBookmark`
- `evaluateCommunityNotes`
- `finalizeMediaUpload`
- `followList`
- `followUser`
- `getAccountActivitySubscriptionCount`
- `getActivitySubscriptions`
- `getChatConversation`
- `getChatConversations`
- `getCommunitiesById`
- `getComplianceJobs`
- `getComplianceJobsById`
- `getConnectionHistory`
- `getDirectMessagesEvents`
- `getDirectMessagesEventsByConversationId`
- `getDirectMessagesEventsById`
- `getDirectMessagesEventsByParticipantId`
- `getInsights28Hr`
- `getInsightsHistorical`
- `getListsById`
- `getListsFollowers`
- `getListsMembers`
- `getListsPosts`
- `getMarketplaceHandleAvailability`
- `getMediaAnalytics`
- `getMediaByMediaKey`
- `getMediaByMediaKeys`
- `getMediaUploadStatus`
- `getNews`
- `getOpenApiSpec`
- `getPostsAnalytics`
- `getPostsById`
- `getPostsByIds`
- `getPostsCountsAll`
- `getPostsCountsRecent`
- `getPostsLikingUsers`
- `getPostsQuotedPosts`
- `getPostsRepostedBy`
- `getPostsReposts`
- `getSpacesBuyers`
- `getSpacesByCreatorIds`
- `getSpacesById`
- `getSpacesByIds`
- `getSpacesPosts`
- `getTrendsByWoeid`
- `getTrendsPersonalizedTrends`
- `getUsage`
- `getUserPublicKeys`
- `getUsersAffiliates`
- `getUsersBlocking`
- `getUsersBookmarkFolders`
- `getUsersBookmarks`
- `getUsersBookmarksByFolderId`
- `getUsersById`
- `getUsersByIds`
- `getUsersByUsername`
- `getUsersByUsernames`
- `getUsersFollowedLists`
- `getUsersFollowers`
- `getUsersFollowing`
- `getUsersLikedPosts`
- `getUsersListMemberships`
- `getUsersMe`
- `getUsersMentions`
- `getUsersMuting`
- `getUsersOwnedLists`
- `getUsersPinnedLists`
- `getUsersPosts`
- `getUsersRepostsOfMe`
- `getUsersTimeline`
- `hidePostsReply`
- `initializeMediaUpload`
- `likePost`
- `mediaUpload`
- `muteUser`
- `pinList`
- `removeListsMemberByUserId`
- `repostPost`
- `searchCommunities`
- `searchCommunityNotesWritten`
- `searchEligiblePosts`
- `searchNews`
- `searchPostsAll`
- `searchPostsRecent`
- `searchSpaces`
- `searchUsers`
- `sendChatMessage`
- `unblockUsersDms`
- `unfollowList`
- `unfollowUser`
- `unlikePost`
- `unmuteUser`
- `unpinList`
- `unrepostPost`
- `updateActivitySubscription`
- `updateLists`
## Generate an OAuth2 user token (optional)
If you want a user-context OAuth2 token:
1. Add `CLIENT_ID` and `CLIENT_SECRET` to your `.env`.
2. Update `redirect_uri` in `generate_authtoken.py` to match your app settings.
3. Run `python generate_authtoken.py` and follow the prompts.
4. Copy the printed access token into `.env` as `X_OAUTH_ACCESS_TOKEN`.
If your flow returns a secret, store it as `X_OAUTH_ACCESS_TOKEN_SECRET`.
## Run the Grok MCP test client (optional)
1. Set `XAI_API_KEY` in `.env`.
2. Make sure your MCP server is running locally (or set `MCP_SERVER_URL`).
3. If Grok cannot reach `http://127.0.0.1:8000/mcp`, use ngrok to tunnel your
local server and point `MCP_SERVER_URL` to the public ngrok URL.
3. If Grok is not running on your machine, use ngrok to expose your local MCP
server and set `MCP_SERVER_URL` to the public HTTPS URL that ends with `/mcp`.
Example flow: `ngrok http 8000` then `MCP_SERVER_URL=https://<id>.ngrok-free.dev/mcp`.
4. Run `python test_grok_mcp.py`.
## Notes

View file

@ -1,10 +1,9 @@
# X API auth (fallback, optional)
X_OAUTH_ACCESS_TOKEN=
# Required auth
X_OAUTH_CONSUMER_KEY=
X_OAUTH_CONSUMER_SECRET=
X_BEARER_TOKEN=
# OAuth1 startup (required)
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
# OAuth1 callback
X_OAUTH_CALLBACK_HOST=127.0.0.1
X_OAUTH_CALLBACK_PORT=8976
X_OAUTH_CALLBACK_PATH=/oauth/callback
@ -19,19 +18,20 @@ X_API_DEBUG=1
MCP_HOST=127.0.0.1
MCP_PORT=8000
# FastMCP parser flag (optional)
FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=
# Tool filtering (optional, comma-separated)
X_API_TOOL_TAGS=
X_API_TOOL_ALLOWLIST=
X_API_TOOL_DENYLIST=
# Local Grok client (optional)
# Optional Grok test client
XAI_API_KEY=
XAI_MODEL=grok-4-1-fast
MCP_SERVER_URL=[your ngrok url]/mcp
MCP_SERVER_URL=http://127.0.0.1:8000/mcp
# OAuth2 token generation (optional)
CLIENT_ID=
CLIENT_SECRET=
X_OAUTH_ACCESS_TOKEN=
X_OAUTH_ACCESS_TOKEN_SECRET=
# OAuth1 debug output (optional)
X_OAUTH_PRINT_TOKENS=
X_OAUTH_PRINT_AUTH_HEADER=

View file

@ -153,11 +153,11 @@ def _wait_for_callback(
def run_oauth1_flow() -> tuple[str, str]:
consumer_key = os.getenv("TWITTER_CONSUMER_KEY")
consumer_secret = os.getenv("TWITTER_CONSUMER_SECRET")
consumer_key = os.getenv("X_OAUTH_CONSUMER_KEY")
consumer_secret = os.getenv("X_OAUTH_CONSUMER_SECRET")
if not consumer_key or not consumer_secret:
raise RuntimeError(
"Missing TWITTER_CONSUMER_KEY or TWITTER_CONSUMER_SECRET for OAuth1 flow."
"Missing X_OAUTH_CONSUMER_KEY or X_OAUTH_CONSUMER_SECRET for OAuth1 flow."
)
callback_host = os.getenv("X_OAUTH_CALLBACK_HOST", "127.0.0.1")
@ -308,13 +308,16 @@ def get_auth_headers(oauth_token: str | None = None) -> dict:
def build_oauth1_client() -> OAuth1Client:
consumer_key = os.getenv("TWITTER_CONSUMER_KEY")
consumer_secret = os.getenv("TWITTER_CONSUMER_SECRET")
consumer_key = os.getenv("X_OAUTH_CONSUMER_KEY")
consumer_secret = os.getenv("X_OAUTH_CONSUMER_SECRET")
if not consumer_key or not consumer_secret:
raise RuntimeError(
"Missing TWITTER_CONSUMER_KEY or TWITTER_CONSUMER_SECRET for OAuth1 signing."
"Missing X_OAUTH_CONSUMER_KEY or X_OAUTH_CONSUMER_SECRET for OAuth1 signing."
)
access_token, access_secret = run_oauth1_flow()
if is_truthy(os.getenv("X_OAUTH_PRINT_TOKENS", "0")):
print("OAuth1 access token:", access_token)
print("OAuth1 access token secret:", access_secret)
LOGGER.info("OAuth1 access token: %s", access_token)
return OAuth1Client(
client_key=consumer_key,
@ -325,6 +328,20 @@ def build_oauth1_client() -> OAuth1Client:
)
def print_oauth1_header_probe(oauth1_client: OAuth1Client, base_url: str) -> None:
probe_url = f"{base_url}/2/users/me"
_, signed_headers, _ = oauth1_client.sign(
probe_url,
http_method="GET",
headers={},
)
auth_header = signed_headers.get("Authorization")
if auth_header:
print("OAuth1 Authorization header (sample GET /2/users/me):", auth_header)
else:
print("OAuth1 Authorization header missing from signed probe request.")
def create_mcp() -> FastMCP:
load_env()
debug_enabled = setup_logging()
@ -336,6 +353,9 @@ def create_mcp() -> FastMCP:
timeout = float(os.getenv("X_API_TIMEOUT", "30"))
oauth1_client = build_oauth1_client()
print_oauth_header = is_truthy(os.getenv("X_OAUTH_PRINT_AUTH_HEADER", "0"))
if print_oauth_header:
print_oauth1_header_probe(oauth1_client, base_url)
spec = load_openapi_spec()
filtered_spec = filter_openapi_spec(spec)
@ -390,6 +410,12 @@ def create_mcp() -> FastMCP:
)
request.url = httpx.URL(signed_url)
request.headers.update(signed_headers)
if print_oauth_header:
auth_header = signed_headers.get("Authorization")
if auth_header:
print("OAuth1 Authorization header:", auth_header)
else:
print("OAuth1 Authorization header missing from signed request.")
async def log_request(request: httpx.Request) -> None:
if not debug_enabled:

View file

@ -42,7 +42,7 @@ def main() -> None:
)
chat.append(
user('dm @taycaldwell the message "bot.grokcommand sent this message with xMCP". If we receive an error please explicity inform us what the error is. This incldues trace ids.')
user('create a post saying xmcp test')
)
print("Starting chat stream...\n")