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

245
README.md
View file

@ -9,7 +9,7 @@ FastMCP. Streaming and webhook endpoints are excluded.
- An X Developer Platform app (to get tokens) - An X Developer Platform app (to get tokens)
- Optional: an xAI API key if you want to run the Grok test client - 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: 1. Create a virtual environment and install dependencies:
- `python -m venv .venv` - `python -m venv .venv`
@ -17,97 +17,218 @@ FastMCP. Streaming and webhook endpoints are excluded.
- `pip install -r requirements.txt` - `pip install -r requirements.txt`
2. Create your local `.env`: 2. Create your local `.env`:
- `cp env.example .env` - `cp env.example .env`
- Fill in the OAuth1 section (consumer key/secret and callback settings). - Required values (do not skip):
3. Run the server: - `X_OAUTH_CONSUMER_KEY`
- `python server.py` - `X_OAUTH_CONSUMER_SECRET`
- `X_BEARER_TOKEN` (required for this setup; keep it set even if using OAuth1)
The server starts at `http://127.0.0.1:8000` by default. - OAuth1 callback (defaults are fine):
The MCP endpoint is `http://127.0.0.1:8000/mcp`. - `X_OAUTH_CALLBACK_HOST` (default `127.0.0.1`)
- `X_OAUTH_CALLBACK_PORT` (default `8976`)
## Environment variables - `X_OAUTH_CALLBACK_PATH` (default `/oauth/callback`)
- `X_OAUTH_CALLBACK_TIMEOUT` (default `300`)
Required (OAuth1 user context): - Server settings (optional):
- `TWITTER_CONSUMER_KEY` - `X_API_BASE_URL` (default `https://api.x.com`)
- `TWITTER_CONSUMER_SECRET` - `X_API_TIMEOUT` (default `30`)
- `X_OAUTH_CALLBACK_HOST` (default `127.0.0.1`) - `MCP_HOST` (default `127.0.0.1`)
- `X_OAUTH_CALLBACK_PORT` (default `8976`) - `MCP_PORT` (default `8000`)
- `X_OAUTH_CALLBACK_PATH` (default `/oauth/callback`) - `X_API_DEBUG` (default `1`)
- `X_OAUTH_CALLBACK_TIMEOUT` (default `300`) - Tool filtering (optional, comma-separated):
- `X_API_TOOL_ALLOWLIST`
Optional auth fallback: - Optional Grok test client:
- `X_BEARER_TOKEN` (OAuth2 bearer token) - `XAI_API_KEY`
- `XAI_MODEL` (default `grok-4-1-fast`)
Optional server config: - `MCP_SERVER_URL` (default `http://127.0.0.1:8000/mcp`)
- `MCP_HOST` (default `127.0.0.1`) - Optional OAuth2 token generation:
- `MCP_PORT` (default `8000`) - `CLIENT_ID`
- `X_API_BASE_URL` (default `https://api.x.com`) - `CLIENT_SECRET`
- `X_API_TIMEOUT` (default `30`) - `X_OAUTH_ACCESS_TOKEN`
- `X_API_DEBUG` (default `1`) - `X_OAUTH_ACCESS_TOKEN_SECRET` (optional)
- `FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER` - Optional OAuth1 debug output:
- `X_OAUTH_PRINT_TOKENS`
Tool filtering (comma-separated): - `X_OAUTH_PRINT_AUTH_HEADER`
- `X_API_TOOL_TAGS` 3. Register the callback URL in your X Developer App:
- `X_API_TOOL_ALLOWLIST`
- `X_API_TOOL_DENYLIST`
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:
``` ```
http://<X_OAUTH_CALLBACK_HOST>:<X_OAUTH_CALLBACK_PORT><X_OAUTH_CALLBACK_PATH> http://<X_OAUTH_CALLBACK_HOST>:<X_OAUTH_CALLBACK_PORT><X_OAUTH_CALLBACK_PATH>
``` ```
Example: Example (defaults):
``` ```
http://127.0.0.1:8976/oauth/callback http://127.0.0.1:8976/oauth/callback
``` ```
When you start the server, it will open a browser tab for consent and wait 4. Start the server:
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:
``` ```
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) ## Generate an OAuth2 user token (optional)
If you want a user-context OAuth2 token:
1. Add `CLIENT_ID` and `CLIENT_SECRET` to your `.env`. 1. Add `CLIENT_ID` and `CLIENT_SECRET` to your `.env`.
2. Update `redirect_uri` in `generate_authtoken.py` to match your app settings. 2. Update `redirect_uri` in `generate_authtoken.py` to match your app settings.
3. Run `python generate_authtoken.py` and follow the prompts. 3. Run `python generate_authtoken.py` and follow the prompts.
4. Copy the printed access token into `.env` as `X_OAUTH_ACCESS_TOKEN`. 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) ## Run the Grok MCP test client (optional)
1. Set `XAI_API_KEY` in `.env`. 1. Set `XAI_API_KEY` in `.env`.
2. Make sure your MCP server is running locally (or set `MCP_SERVER_URL`). 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 3. If Grok is not running on your machine, use ngrok to expose your local MCP
local server and point `MCP_SERVER_URL` to the public ngrok URL. 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`. 4. Run `python test_grok_mcp.py`.
## Notes ## Notes

View file

@ -1,10 +1,9 @@
# X API auth (fallback, optional) # Required auth
X_OAUTH_ACCESS_TOKEN= X_OAUTH_CONSUMER_KEY=
X_OAUTH_CONSUMER_SECRET=
X_BEARER_TOKEN= X_BEARER_TOKEN=
# OAuth1 startup (required) # OAuth1 callback
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
X_OAUTH_CALLBACK_HOST=127.0.0.1 X_OAUTH_CALLBACK_HOST=127.0.0.1
X_OAUTH_CALLBACK_PORT=8976 X_OAUTH_CALLBACK_PORT=8976
X_OAUTH_CALLBACK_PATH=/oauth/callback X_OAUTH_CALLBACK_PATH=/oauth/callback
@ -19,19 +18,20 @@ X_API_DEBUG=1
MCP_HOST=127.0.0.1 MCP_HOST=127.0.0.1
MCP_PORT=8000 MCP_PORT=8000
# FastMCP parser flag (optional)
FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=
# Tool filtering (optional, comma-separated) # Tool filtering (optional, comma-separated)
X_API_TOOL_TAGS=
X_API_TOOL_ALLOWLIST= X_API_TOOL_ALLOWLIST=
X_API_TOOL_DENYLIST=
# Local Grok client (optional) # Optional Grok test client
XAI_API_KEY= XAI_API_KEY=
XAI_MODEL=grok-4-1-fast 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) # OAuth2 token generation (optional)
CLIENT_ID= CLIENT_ID=
CLIENT_SECRET= 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]: def run_oauth1_flow() -> tuple[str, str]:
consumer_key = os.getenv("TWITTER_CONSUMER_KEY") consumer_key = os.getenv("X_OAUTH_CONSUMER_KEY")
consumer_secret = os.getenv("TWITTER_CONSUMER_SECRET") consumer_secret = os.getenv("X_OAUTH_CONSUMER_SECRET")
if not consumer_key or not consumer_secret: if not consumer_key or not consumer_secret:
raise RuntimeError( 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") 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: def build_oauth1_client() -> OAuth1Client:
consumer_key = os.getenv("TWITTER_CONSUMER_KEY") consumer_key = os.getenv("X_OAUTH_CONSUMER_KEY")
consumer_secret = os.getenv("TWITTER_CONSUMER_SECRET") consumer_secret = os.getenv("X_OAUTH_CONSUMER_SECRET")
if not consumer_key or not consumer_secret: if not consumer_key or not consumer_secret:
raise RuntimeError( 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() 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) LOGGER.info("OAuth1 access token: %s", access_token)
return OAuth1Client( return OAuth1Client(
client_key=consumer_key, 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: def create_mcp() -> FastMCP:
load_env() load_env()
debug_enabled = setup_logging() debug_enabled = setup_logging()
@ -336,6 +353,9 @@ def create_mcp() -> FastMCP:
timeout = float(os.getenv("X_API_TIMEOUT", "30")) timeout = float(os.getenv("X_API_TIMEOUT", "30"))
oauth1_client = build_oauth1_client() 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() spec = load_openapi_spec()
filtered_spec = filter_openapi_spec(spec) filtered_spec = filter_openapi_spec(spec)
@ -390,6 +410,12 @@ def create_mcp() -> FastMCP:
) )
request.url = httpx.URL(signed_url) request.url = httpx.URL(signed_url)
request.headers.update(signed_headers) 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: async def log_request(request: httpx.Request) -> None:
if not debug_enabled: if not debug_enabled:

View file

@ -42,7 +42,7 @@ def main() -> None:
) )
chat.append( 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") print("Starting chat stream...\n")