X MCP Server
Allows posting to Facebook Page feed (text, link, or image) with draft-and-confirm guardrails and logging.
Allows posting to Instagram (image or video/Reels) with draft-and-confirm guardrails and logging.
Provides content drafting for TikTok using brand voice knowledge base; posting is not built due to API restrictions.
Allows posting to YouTube (requires pre-hosted video) with draft-and-confirm guardrails and logging.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@X MCP ServerPost 'Just finished my morning run!' to X."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
X MCP Server
A self-hosted MCP (Model Context Protocol) server on Cloudflare Workers that lets Claude post directly to X, YouTube, Instagram, and Facebook, and draft on-brand content for all five platforms including TikTok. Five tools today:
post_to_x— milestone 1, fully proven except a real credentialed test post.post_to_youtube— needs a video already hosted somewhere (no video generation exists here).post_to_instagram— image or video/Reels.post_to_facebook— Page feed posts, text/link or image.generate_post— drafts content for all five platforms (including TikTok) using the brand voice KB. Drafting only, never posts.
Every posting tool uses the same draft/confirm gate and D1 logging
guardrails as post_to_x. TikTok posting is not built — its API
restricts unaudited apps to private posts, which doesn't serve the actual
goal. Scheduling, list_scheduled_posts, and get_post_status are not
built. See PLATFORM_EXPANSION.md for exactly what's needed to get each
platform live, and why TikTok is different from the other three.
Brand voice content is grounded in BRAND_VOICE.md — read that before
assuming generate_post's output is final; parts of it are my best
inference from existing product content, not something dictated directly,
and it's flagged as such inline.
Architecture
One Cloudflare Worker, three moving parts:
src/mcp-agent.ts— the actual MCP server: tool definitions using Anthropic's official@modelcontextprotocol/sdk(McpServer,server.tool()).Cloudflare's
agentspackage (McpAgent, fromagents/mcp) — the transport adapter. The official MCP SDK's built-in HTTP transport assumes a long-lived Node process; Workers don't have that, soagentswraps the official SDK in a Durable Object to hold session state across requests. You're still writing against the official SDK's API (McpServer,server.tool()) —agentsonly supplies the plumbing to make that work on Workers at all.src/xApi.ts+src/oauth1.ts— the X API v2 client, including a from-scratch OAuth 1.0a request signer (RFC 5849) implemented against Web Crypto, since there isn't a maintained OAuth1.0a signing library built for the Workers runtime.
Every post attempt (confirm=true) is logged to D1 (src/db.ts,
migrations/0001_init.sql) — timestamp, status, and the exact error
message if it failed. Nothing about a failed post is summarized or guessed;
the real API response text is what gets logged and returned.
Related MCP server: Remote MCP Server (Authless)
Why OAuth 1.0a, not OAuth 2.0
X still requires OAuth 1.0a user-context signing to post via the API, even though it offers OAuth 2.0 for other flows. OAuth 1.0a also has no token refresh step for a single personal account — the four credentials below are static once issued, which keeps this simple for a one-account server.
Security: this server is protected by a bearer token
Anyone who can reach /mcp unauthenticated could post to your X account —
so every request is checked against MCP_AUTH_TOKEN before it reaches any
MCP session or tool logic (src/index.ts, using a constant-time comparison).
This is proportionate for a personal, single-user server; if this is ever
shared with other people or used from clients you don't fully trust, that's
the point to move to a real OAuth consent flow
(@cloudflare/workers-oauth-provider, which agents also supports) instead
of a shared static secret.
Setup
1. X Developer App
developer.x.com → create/open a Project + App.
App → User authentication settings → enable OAuth 1.0a with Read and Write permission (not Read-only).
Generate API Key, API Key Secret, Access Token, Access Token Secret — regenerate the Access Token after setting Read+Write, or it'll carry stale read-only scope.
Confirm your API access tier actually allows posting. X's tiers have changed more than once; check your Developer Portal's current plan before assuming the free tier covers this.
2. Cloudflare resources (needs your account — I can't do this part)
npm install
npx wrangler login
# D1 database:
npx wrangler d1 create x_mcp_db
# paste the returned database_id into wrangler.toml, replacing
# REPLACE_WITH_REAL_D1_DATABASE_ID, then:
npm run db:migrate:remote
# Secrets (you'll be prompted to paste each value):
npx wrangler secret put X_API_KEY
npx wrangler secret put X_API_SECRET
npx wrangler secret put X_ACCESS_TOKEN
npx wrangler secret put X_ACCESS_TOKEN_SECRET
npx wrangler secret put MCP_AUTH_TOKEN # generate with: openssl rand -base64 32
npx wrangler secret put OPENAI_API_KEY # used by generate_post; can be a fresh key
npx wrangler deploy3. Point Claude at it
For Claude clients that support remote MCP servers, add this server using
your deployed Worker URL (https://x-mcp-server.YOUR-SUBDOMAIN.workers.dev/mcp)
with an Authorization: Bearer <MCP_AUTH_TOKEN> header. Exact config format
depends on which Claude client you're using (Claude Code, Claude Desktop,
claude.ai connectors) — let me know which one and I'll give you the precise
config snippet.
Local development
npm install
npx wrangler d1 migrations apply x_mcp_db --local
npx wrangler dev --local.dev.vars (gitignored, never commit):
X_API_KEY=...
X_API_SECRET=...
X_ACCESS_TOKEN=...
X_ACCESS_TOKEN_SECRET=...
MCP_AUTH_TOKEN=some-local-test-secret
OPENAI_API_KEY=...
# Optional — omit any of these and that platform's tool returns a clear
# "not configured yet" message instead of failing confusingly:
YT_CLIENT_ID=...
YT_CLIENT_SECRET=...
YT_REFRESH_TOKEN=...
META_PAGE_ACCESS_TOKEN=...
META_PAGE_ID=...
META_IG_USER_ID=...What's actually verified, and how
I don't have real X API credentials, so I couldn't fire a real post from here — but everything short of the live API call is verified directly, not assumed:
OAuth1.0a signing (
src/oauth1.ts): 16 property-based checks run against the real implementation — correct RFC3986 percent-encoding of the exact characters OAuth1.0a cares about (spaces,!'()*, reserved punctuation), deterministic output for identical inputs, the signature changing when any single input changes (consumer key/secret, token/token secret, nonce, timestamp, any parameter, the URL, the HTTP method), correctly sorted parameters regardless of insertion order, and correct HMAC-SHA1 output length. I deliberately didn't test against a from-memory external test vector (X's classic documented example) — a misremembered 300-character string would be more likely to send me chasing a false failure than catch a real bug.The whole MCP + logging pipeline, for real, via
wrangler devand raw JSON-RPC over curl: unauthenticated request correctly gets401;initializehandshake returns the right protocol version and session ID;tools/listcorrectly exposespost_to_x's schema; calling it withoutconfirmreturns a draft preview and writes nothing to the log; calling it withconfirm=trueagainst placeholder credentials attempts a real network call, and when that call failed (this sandbox's network policy blocksapi.twitter.comoutbound — a good stand-in for "the API call failed" here), the exact error text was returned to the caller and landed correctly in the D1 log with a timestamp, verified by querying the table directly afterward.generate_post, the same way:tools/listcorrectly exposes both tools; an invalid platform value is correctly rejected by schema validation with a clear error (never silently coerced); calling it with a placeholder OpenAI key attempts a real network call and surfaces the exact failure text back to the caller (isError: true), same pattern aspost_to_x.post_to_youtube,post_to_instagram,post_to_facebook:tools/listcorrectly exposes all five tools; each one's draft mode (noconfirm) correctly returns a preview with no API call and no log entry; each one'sconfirm=truepath, with no platform credentials set at all, correctly returns its specific "not configured yet — set X, Y, Z" message rather than crashing or attempting a call with undefined credentials — this is the fallback path every one of these three tools takes today, since none of their credentials exist yet, and it's exercised for real, not assumed.Type-checks clean end to end.
What's not verified: an actual successful post against any of these real platform APIs (X, YouTube, Instagram, Facebook) or an actual successful OpenAI generation, since all of that needs real credentials. That's the real milestone-1 test — once secrets are set and this is deployed, sending one real post through Claude is what proves this end-to-end.
Database schema
CREATE TABLE post_log (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
platform TEXT NOT NULL,
post_text TEXT NOT NULL,
media_url TEXT,
status TEXT NOT NULL, -- 'success' | 'failure'
external_post_id TEXT, -- the X post ID, on success
error_message TEXT, -- the exact error, on failure
raw_response TEXT -- full API response body
);Query it directly any time with:
npx wrangler d1 execute x_mcp_db --remote --command "SELECT * FROM post_log ORDER BY created_at DESC LIMIT 20;"(That's effectively get_post_status/list_scheduled_posts by hand, until
those tools are built for real.)
Known limitation: media upload
post_to_x accepts an optional media_url, uploaded via X's v1.1
simple (non-chunked) media endpoint — images under 5MB only. Video/GIF/
larger images need the chunked INIT/APPEND/FINALIZE flow, which isn't
implemented. This is also the less-exercised code path for milestone 1 (the
spec's one real test post is about proving text posting) — treat it as
needing its own dedicated test before relying on it for anything real.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/legacyarchitects1/x-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server