Announcing CIMD support for MCP Client registration
Learn more

X MCP vs X API for AI Agents (2026)

Saif Ali Shaik
Founding Developer Advocate

TL;DR

  • The hosted X MCP server covers search, post and user lookup, trends, news, bookmarks, and Articles. Creating a Post is not in the tool set. Neither are DMs, likes, reposts, follows, media upload, Lists, Spaces, or the X Activity event surface. All of those are X API v2 only.
  • X MCP auth runs through xurl mcp, a local bridge that performs an OAuth 2.0 PKCE login in a browser and caches the token in ~/.xurl on the machine running the agent. There is no Dynamic Client Registration and api.x.com/mcp does not advertise MCP OAuth discovery. A static app-only Bearer token works against the URL directly, but it is read-only, has no user context, and does not auto-refresh.
  • That bridge is the disqualifier for multi-tenant B2B agents. A per-machine token cache cannot express "fifty customers, fifty X accounts, one deployment." The X API with server-side per-user OAuth 2.0 tokens can.
  • Both paths bill identically because both hit the same metered endpoints under your own developer app. Post reads cost $0.005 per resource, user reads $0.010, a Post create $0.015, and a Post create containing a URL $0.200. A link-posting agent has a cost model, not just a rate limit.
  • Scalekit's X connector ships 80 prebuilt tools over per-user OAuth 2.0, stores and refreshes the tokens outside your agent runtime, and can serve the same scoped tool set over MCP through a Virtual MCP server. The MCP vs API decision stops changing your auth infrastructure.

Your agent needs to read and write X. Since June 30, 2026 there are two first-party ways to do it: the hosted MCP server at api.x.com/mcp, and the X API v2 you have probably already integrated against. They look interchangeable in a demo and are not interchangeable in production. The capability surfaces differ, the auth models differ in a way that decides whether multi-tenant agents are even possible, and the metering is identical in a way that will surprise you. Here is how to pick.

What X MCP and X API actually are

These are two different products from the same team, built for two different consumers. One is aimed at a developer sitting in front of an IDE. The other is the platform surface your backend has always called. Knowing which is which saves an architecture rewrite later.

The hosted X MCP server

X exposes a Streamable HTTP MCP server at https://api.x.com/mcp, speaking protocol revision 2025-06-18 and identifying itself as xmcp. Its published capability groups are Posts, Search, Users, Bookmarks, News and Trends, and Articles. In practice that means full-archive post search, user and news search, resolving the current user, reading a user's posts, timeline and mentions, listing and managing bookmarks and bookmark folders, pulling trends for a WOEID, and drafting or publishing Articles.

How agents reach the hosted server

You do not point your MCP client at that URL for user-context work. X requires your own developer app, offers no Dynamic Client Registration, and does not advertise native MCP OAuth discovery on the endpoint. Instead you run the open-source xurl bridge locally over stdio; it owns the app identity, performs a one-time browser login, and injects a fresh Bearer token on every call. Full setup lives in X's MCP documentation.

The X API v2 surface

The X API v2 is the complete platform: Posts, Users, Direct Messages, Spaces, Lists, Likes, Trends, Media, Communities, Community Notes, News, and Compliance, plus Filtered Stream, the X Activity event API, and webhooks for real-time delivery. Volume Streams, Likes Streams, Powerstream, post analytics at scale, and Account Activity sit behind an Enterprise plan.

Authentication accepts three models, documented on X's authentication overview: OAuth 2.0 Authorization Code Flow with PKCE for user context, OAuth 1.0a User Context, and an OAuth 2.0 app-only Bearer Token for public read access. Per-user delegation and app-level service patterns both work, and you choose per workload.

The docs MCP server

X also hosts a second MCP server at docs.x.com/mcp exposing two tools, search_x and get_page_x, for searching and reading the X API documentation. This is a build-time convenience for your coding agent, not a runtime surface for your product agent. Do not count it as capability coverage.

Comparing them where it matters for agents

The interesting differences are not in the tool names. They are in what is missing, who holds the credential, and what a single tool call costs. Each of the four dimensions below changes a different part of your architecture.

What your agent can actually do

The hosted MCP is built around retrieval. It is good at the thing X uniquely owns, which is the live and historical public conversation, and it stops well short of the write surface a social agent typically needs.

Capability
X MCP (hosted)
X API v2
Recent and full-archive post search
Yes
Yes
User lookup, user posts, timeline, mentions
Yes
Yes
Trends by WOEID and news stories
Yes
Yes
Bookmarks read, add, remove, folders
Yes
Yes
Draft and publish Articles
Yes
Yes
Create or delete a Post
No
Yes
Like, repost, follow, mute, block
No
Yes
Direct Messages
No
Yes
Media upload
No
Yes
Lists, Spaces, Communities, Community Notes
No
Yes
Real-time events: X Activity, webhooks, Filtered Stream
No
Yes
Compliance jobs, usage metering, post analytics
No
Yes

Where the MCP ceiling sits

The most consequential gap is post creation. A brand-monitoring agent that finds mentions and drafts a reply can read everything it needs over MCP and then cannot send the reply. You end up running the API path anyway for the last step, which means you now maintain both.

The second gap is events. Anything that reacts rather than polls — whether a mention monitor, a follower-change trigger, or a Post create feed for a tracked account — lives in the X Activity API and webhooks. There is no event subscription surface on the MCP server, so a polling loop is your only option, and on X a polling loop is a metered cost line.

The auth path each one puts you on

This is where the X comparison diverges from every other tool in this series. Most hosted MCP servers are OAuth-only and hand you a per-user session. X does the opposite in both directions: it supports a static app-only token, and it delegates user-context OAuth to a process running on your machine. Understanding why static credentials break in production helps clarify why this matters so much for agent architectures.

Auth model
X MCP (hosted)
X API v2
OAuth 2.0 Authorization Code with PKCE
Yes, through the local xurl bridge
Yes
OAuth 2.0 app-only Bearer
Yes, direct URL, read only, no refresh
Yes
OAuth 1.0a user context
No
Yes
Dynamic Client Registration
No
Not applicable
Server-side per-user token storage
No
Yours to build or buy

The simple route is genuinely simple: paste an app-only Bearer token into an Authorization header on a remote MCP client and read public data. The tradeoff is stated plainly in X's own docs. No auto-refresh, and no user context, which means the agent cannot act as anyone.

How the xurl bridge actually works

The full route runs npx -y @xdevplatform/xurl mcp https://api.x.com/mcp as a stdio child process. On first run with no cached token it opens a browser, holds the MCP handshake open until the login completes, then caches and auto-refreshes the token in ~/.xurl. X recommends a startup timeout of at least 300 seconds for exactly this reason.

Why that breaks multi-tenant agents

Read the bridge as an architecture statement. The credential lives on a filesystem, scoped to a machine, tied to whichever X account happened to be logged into that browser. There is no server-side store, no tenant namespace, and no revocation primitive beyond deleting a file. For a single developer in Cursor that is fine. For a B2B product where every customer connects their own X account, it is not a configuration gap; it is the wrong shape. This is the same fundamental problem explored in how tool calling auth changes when you move from single-tenant to multi-tenant.

What you own in production

On the MCP path, X owns hosting, the tool schemas, and the endpoint normalization. You own the bridge process lifecycle, the token cache on every host that runs an agent, the CLIENT_ID and CLIENT_SECRET that the bridge needs in its environment, and the startup latency of a browser login that no headless job can complete on its own.

On the API path you own more and control more: endpoint selection, pagination, retries, and the full token lifecycle. In exchange you get a versioned, documented contract. MCP tool schemas change when X updates the hosted server, and there is no version header to pin. For a deterministic pipeline where an unexpected schema change is an incident, that distinction decides the question by itself.

What every call costs you

X moved to pay-per-usage pricing with no subscription. Reads are charged per resource returned and writes per request, and both paths consume the same credits because MCP tool calls resolve to the same X API endpoints under your own developer app.

Operation
Unit cost
Post read
$0.005 per resource
User read
$0.010 per resource
Owned reads (your own posts, bookmarks, followers)
$0.001 per resource
Post create
$0.015 per request
Post create containing a URL
$0.200 per request
Trends
$0.010 per request

The two numbers that change your design

Pay-per-usage plans cap at two million post reads per monthly billing cycle, above which you need an Enterprise plan. And a Post containing a link costs more than thirteen times a Post without one, so an agent that shares links has a unit economics problem long before it has a rate limit problem.

Resources are deduplicated within a 24-hour UTC window, which makes caching worth building. Rate limits are separate and orthogonal: 15-minute windows, per-endpoint, applied per user for user tokens and per app for Bearer tokens, as described in X's rate limit documentation.

When to use X MCP

The hosted server earns its place when a human is present and the work is retrieval.

  • You are working in Cursor, Claude Desktop, VS Code, or Grok Build and want the live X graph available to you personally while you build or research
  • The job is social listening or trend analysis: full-archive search, user search, news search, and trends by WOEID are X-proprietary data an agent cannot get cleanly anywhere else
  • You want public read access with zero credential plumbing and a static app-only Bearer token is acceptable
  • You are validating whether an X-connected agent is worth building before writing integration code

When to use the X API

The API is the answer whenever the agent has to act, react, or serve more than one person.

  • The agent creates Posts, replies, DMs, likes, reposts, or uploads media, none of which the hosted MCP exposes
  • The agent runs headless on a schedule, where no browser exists to complete the bridge login
  • The agent reacts to events through the X Activity API, webhooks, or Filtered Stream rather than polling
  • You are building multi-tenant B2B, where each customer authorizes their own X account and revocation must be scoped to one customer
  • You need Lists, Spaces, Communities, Community Notes, compliance jobs, or usage metering

The credential problem that exists on both paths

Strip away the tool schemas and both paths converge on the same unsolved problem. Every user who connects X to your agent produces one X credential, and neither X MCP nor the X API stores, rotates, or revokes it for you.

N users, N X credentials

Fifty customers means fifty OAuth grants, fifty refresh cycles, and fifty revocation events you have to detect. The MCP bridge caches tokens on disk with no tenant boundary. The API hands you a token payload and wishes you luck. In both cases the token must be encrypted at rest, isolated per tenant, and invalidated the moment a user disconnects the app from their X settings, which they can do at any time without telling you. Your agent finds out through a 401 in the middle of a run, if you were watching for it. The challenge of secure token management for AI agents at scale applies to every credential your agent holds, not just X tokens.

Where Scalekit fits

Scalekit's X connector runs the OAuth 2.0 flow, holds the per-user tokens in a vault outside your agent runtime, refreshes them automatically, and resolves the right credential at call time. The same infrastructure serves both paths, so choosing MCP or the raw API stops being an auth decision.

Building X agents with Scalekit

Scalekit does not ship a separate X MCP connector, and it does not need to. The X connector is API-backed with 80 prebuilt tools, and MCP delivery is a presentation layer you turn on with a Virtual MCP server. That is the useful arrangement: full API coverage underneath, MCP transport when a client wants it.

Configure the connection once

In the Scalekit dashboard, go to AgentKit, then Connections, then Create Connection, and pick Twitter. Copy the Redirect URI it gives you into your X app under User authentication settings, set App permissions to Read and Write, then paste your OAuth 2.0 Client ID and Client Secret back into Scalekit and select scopes. For a posting agent that is typically tweet.read, tweet.write, users.read, like.write, and offline.access for refresh tokens.

The connection_name you use in code must match the connection name configured in the Scalekit dashboard exactly. This is the single most common integration error, and it fails in a way that looks like a missing tool rather than a config mistake.

pip install scalekit-sdk-python langchain langchain-openai npm install @scalekit-sdk/node @anthropic-ai/sdk

Authorize a user

Before any tool call, confirm the user has an active connected account and send them through consent if they do not. This is the only browser step in the whole flow, and unlike the xurl bridge it happens in your product, on your schedule.

import os from scalekit import ScalekitClient scalekit = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit.actions CONNECTION_NAME = "twitter" # must match AgentKit > Connections exactly IDENTIFIER = "user_123" # your app's stable ID for this user account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=IDENTIFIER, ) if account.connected_account.status != "ACTIVE": magic_link = actions.get_authorization_link( connection_name=CONNECTION_NAME, identifier=IDENTIFIER, ) print("Send the user to:", magic_link.link)

Scoped tool calling with LangChain in Python

The agent should not receive all 80 X tools. It receives the tools this user's connected account is authorized to call, filtered down to what this agent role actually needs. Eighty tools at roughly 200 tokens each is 16,000 tokens spent before the model does any work, and it measurably degrades tool selection. Scoping to four tools fixes both problems at once. This pattern is central to how LangChain tool calling works and where it stops without proper credential management underneath.

from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, ToolMessage tools = actions.langchain.get_tools( identifier=IDENTIFIER, connection_names=[CONNECTION_NAME], tool_names=[ "twitter_user_me", "twitter_recent_search", "twitter_recent_tweet_counts", "twitter_post_create", ], page_size=100, ) tool_map = {tool.name: tool for tool in tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [HumanMessage( "Find mentions of our product from the last 24 hours, " "count them by day, and draft one reply post." )] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for call in response.tool_calls: result = tool_map[call["name"]].invoke(call["args"]) messages.append( ToolMessage(content=str(result), tool_call_id=call["id"]) )

Scoped tool calling with the Claude SDK in TypeScript

The same model in TypeScript, using listScopedTools to build the tool array and executeTool to run the loop. Note that listScopedTools returns pagination metadata, which is why it is the right call for building a tool surface rather than a framework helper.

import { ScalekitClient } from "@scalekit-sdk/node"; import Anthropic from "@anthropic-ai/sdk"; const sk = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const anthropic = new Anthropic(); const CONNECTION_NAME = "twitter"; // must match AgentKit > Connections exactly const IDENTIFIER = "user_123"; const { tools } = await sk.tools.listScopedTools(IDENTIFIER, { filter: { connectionNames: [CONNECTION_NAME], toolNames: [ "twitter_user_me", "twitter_recent_search", "twitter_post_create", ], }, pageSize: 100, }); type ToolDefinition = { name: string; description: string; input_schema: Record; }; const claudeTools = tools.map((scoped) => { const def = scoped.tool!.definition as unknown as ToolDefinition; return { name: def.name, description: def.description, input_schema: def.input_schema as Anthropic.Tool.InputSchema, }; }); const messages: Anthropic.MessageParam[] = [ { role: "user", content: "Summarise mentions of our product from the last 24 hours." }, ]; while (true) { const response = await anthropic.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, tools: claudeTools, messages, }); messages.push({ role: "assistant", content: response.content }); if (response.stop_reason !== "tool_use") { console.log(response.content); break; } const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type !== "tool_use") continue; const result = await sk.actions.executeTool({ toolName: block.name, toolInput: block.input as Record, identifier: IDENTIFIER, connector: CONNECTION_NAME, }); toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result.data), }); } messages.push({ role: "user", content: toolResults }); }

Dropping to the raw X API when no tool fits

X ships endpoints faster than any tool catalog tracks them. When you need one that has no prebuilt tool yet, the proxy path sends a raw request with the user's token injected server-side, so you never handle the credential to reach an uncovered endpoint.

response = actions.request( connection_name=CONNECTION_NAME, identifier=IDENTIFIER, path="/2/tweets/search/recent", method="GET", query_params={ "query": "from:xdevelopers -is:retweet", "max_results": 10, "tweet.fields": "created_at,public_metrics", }, ) print(response.json())

Serving the same tools over MCP

If your consumer is an MCP client rather than your own agent loop, a Virtual MCP server gives you a static endpoint that declares exactly which tools are exposed and mints a short-lived, per-user token before each run. One server definition serves every tenant, which is precisely what the xurl bridge cannot do.

from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping # Setup: once per agent role, not once per user vmcp = actions.mcp.create_config( name="x-brand-monitor", description="Read-mostly X agent for brand monitoring and replies", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name=CONNECTION_NAME, tools=[ "twitter_user_me", "twitter_recent_search", "twitter_recent_tweet_counts", "twitter_post_create", ], ) ], ) config_id = vmcp.config.id mcp_server_url = vmcp.config.mcp_server_url

Runtime is two calls: confirm the user's connections are still live, then mint a token scoped to that user. Never reuse a token across runs or across users.

from datetime import timedelta accounts = actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier=IDENTIFIER, include_auth_link=True, ) for account in accounts.connected_accounts: if account.connected_account_status.upper() != "ACTIVE": raise RuntimeError( f"{account.connection_name} needs auth: {account.authentication_link}" ) session = actions.mcp.create_session_token( mcp_config_id=config_id, identifier=IDENTIFIER, expiry=timedelta(minutes=30), ) mcp_server = { "url": mcp_server_url, "headers": {"Authorization": f"Bearer {session.token}"}, }

Auth logs and downstream tool call observability

The hosted MCP path produces one audit trail, and it belongs to X: your developer app made a call. It cannot tell you which of your users triggered it, because from X's side there is one app and one cached token.

Per-user connected accounts change that. Every execute_tool call returns an execution_id, and Auth Logs record the authorization events, token lifecycle events, and tool calls tied to the connected account that authorized them, with 90 days of history on the X connector. When a compliance reviewer asks who posted from the brand account at 2am, the answer is a query, not a three-week investigation. This kind of per-action traceability is what separates functional agents from production-grade agent tool observability.

Which one to build against

If a human is present and the job is reading X, the hosted MCP server is the faster path and the xurl bridge is a reasonable amount of setup. If your agent posts, replies, sends DMs, reacts to events, runs on a schedule, or serves more than one customer, build against the X API v2 with per-user OAuth 2.0 tokens. The hosted server's local token cache and read-heavy tool set are architectural facts, not configuration you can tune around.

Most production X agents will run the API path and expose a scoped subset over MCP for the interactive surface. Both ends need the same thing underneath: one credential per user, stored outside the agent, refreshed before it expires, revocable per tenant, and logged on every call.

Talk to other X agent builders

Bring your X agent architecture questions to the Scalekit Slack community, or talk to an engineer if you need help now.

Browse the Scalekit X connector docs and the X connector page.

No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
zero upfront cost.

Every feature unlocked. No hidden fees.