From 028a62f918def95b1ac98a26a5e48cc9817baba3 Mon Sep 17 00:00:00 2001 From: Vardhan Agnihotri Date: Fri, 6 Feb 2026 10:35:21 -0800 Subject: [PATCH] updated readme, env var names --- README.md | 245 +++++++++++++++++++++++++++++++++++------------ env.example | 24 ++--- server.py | 38 ++++++-- test_grok_mcp.py | 2 +- 4 files changed, 228 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index bf4b1d8..f0c9906 100644 --- a/README.md +++ b/README.md @@ -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` -- `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`) -- `X_API_BASE_URL` (default `https://api.x.com`) -- `X_API_TIMEOUT` (default `30`) -- `X_API_DEBUG` (default `1`) -- `FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER` - -Tool filtering (comma-separated): -- `X_API_TOOL_TAGS` -- `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: + - 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`) + - 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`) + - Tool filtering (optional, comma-separated): + - `X_API_TOOL_ALLOWLIST` + - 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`) + - 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://: ``` -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://.ngrok-free.dev/mcp`. 4. Run `python test_grok_mcp.py`. ## Notes diff --git a/env.example b/env.example index 76d61a7..bb16fa8 100644 --- a/env.example +++ b/env.example @@ -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= diff --git a/server.py b/server.py index 70f4506..bec2a2e 100644 --- a/server.py +++ b/server.py @@ -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: diff --git a/test_grok_mcp.py b/test_grok_mcp.py index 71d9734..13f460d 100644 --- a/test_grok_mcp.py +++ b/test_grok_mcp.py @@ -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")