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. It also now hosts
a self-hosted intake-form system (/forms/*) that replaces Typeform for
HMP's lead-capture flows — see "Self-hosted intake forms" below. Seven MCP
tools today:
post_to_x— milestone 1, fully proven except a real credentialed test post.upload_video— needs a video already hosted somewhere (no video generation exists here). Defaults toprivacy_status: private— must be set explicitly topublicorunlisted.update_video_metadata— edit an existing video's title/description/tags. Fetches and preserves whatever fields you don't pass (YouTube's API replaces the whole metadata set per request, not a per-field patch — this tool does the fetch-and-merge for you). No draft/confirm gate; it edits something already live.get_upload_status— live processing-status check against YouTube (not a replay of the upload log), with the exact failure/rejection reason surfaced if there is one.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. YouTube Community Tab posts are not built — see "What's not
built, and why" below. Scheduling and a general get_post_status
across all platforms are not built. See PLATFORM_EXPANSION.md for
exactly what's needed to get each platform live.
What's not built, and why
post_community_update (YouTube Community Tab posts) was requested but
isn't built. I don't have confident knowledge that YouTube's Data API v3
has a public, documented endpoint for creating Community posts — they've
historically been a YouTube Studio-only feature, and I'm not aware of a
communityPosts.insert (or equivalent) in the standard, generally-available
API surface. Building against a guessed endpoint would compile fine and
fail every single call in production — worse than not building it, since
it would look done when it isn't. If there's a real, current, documented
endpoint for this (Google ships new API surface fairly often, and this
could have changed), point me at the docs and I'll build it properly. Also
worth knowing: any unofficial way to do this (reverse-engineered
internal APIs some third-party tools use) would be a ToS risk to the
channel and inherently unstable — not something to build against even if
it technically works today.
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.
Related MCP server: Remote MCP Server (Authless)
Self-hosted intake forms
/forms/:formId is a small, generic form-rendering and submission system —
one Worker, no external form vendor. It exists because Make.com's
Typeform-triggered email steps for HMP's Credit Analysis flow had repeated,
unconfirmed delivery issues; this gives Damon a system he controls end to
end, where "did submission X actually process" is answerable with a direct
D1 query instead of squinting at Make's execution history.
What's built (milestone 1): one form, credit-analysis-intake.
Deliberately not a general migration of every HMP form — see "First
milestone" in the spec this was built from.
GET /forms/credit-analysis-intake— serves a real HTML form, styled with HMP's obsidian/gold branding (src/formRender.ts), unauthenticated (a real visitor has no bearer token).POST /forms/credit-analysis-intake— validates the submission against the form's field definitions (configs/forms/credit-analysis-intake.json, loaded viasrc/formConfigs.ts), then:Calls the real Anthropic Messages API (
src/anthropicApi.ts) with the exact prompt text ported verbatim from the Make scenario this replaces ("20-Page Credit Analysis - Auto-Deliver + Log (Claude)", scenario ID 5761132) — confirmed via a live API call to that scenario, not from memory, so the AI-writing quality doesn't regress. Usesclaude-opus-5(this environment's real current model ID), not Make's internal labelclaude-opus-4-8.Emails the generated report to the client via Resend (
src/formEmail.ts).Sends a best-effort internal notification to
FORM_ADMIN_EMAIL(mirrors the Make scenario's second Gmail step) — its failure is logged but doesn't block the client-facing result, since the thing the client actually needed (their report) already happened.Logs exactly one row per submission to D1's
form_submissionstable (migrations/0002_form_submissions.sql) — real status, the stage the pipeline reached, and the exact error text on failure. Same no-silent-failures guardrail aspost_log.
Public, but not unprotected: /forms/* is carved out of the
MCP_AUTH_TOKEN gate in src/index.ts on purpose (see the comment there),
and instead has its own per-IP fixed-window rate limiter
(src/formRateLimit.ts, 5 submissions per 10 minutes per IP, backed by the
FORM_RATE_LIMIT KV namespace) plus server-side field validation
(required/optional, type, max length) that runs before any submission
reaches the rate-limit-consuming path or gets a D1 row.
Adding a second form (e.g. the real-estate lead intake, deferred per
the "don't migrate everything at once" instruction) means: a new JSON file
under configs/forms/, registering it in FORMS in src/formConfigs.ts,
a prompt-builder + entry in GENERATORS in src/formSubmit.ts — the
render/validate/rate-limit/log plumbing is already generic.
Setup, once secrets exist:
npx wrangler kv namespace create FORM_RATE_LIMIT
# paste the returned id into wrangler.toml, replacing REPLACE_WITH_REAL_KV_NAMESPACE_ID
npm run db:migrate:remote # picks up migrations/0002_form_submissions.sql too
npx wrangler secret put ANTHROPIC_API_KEY # a real Claude API key — NOT the OPENAI_API_KEY above
npx wrangler secret put RESEND_API_KEY
npx wrangler secret put FORM_FROM_EMAIL # must be a Resend-verified sending address
npx wrangler secret put FORM_ADMIN_EMAIL # optional; where "submission delivered" notices goUntil these are set, GET /forms/credit-analysis-intake still renders
correctly and POST still validates input — it just returns a clear
"not configured yet" response instead of attempting generation, and writes
nothing to form_submissions in that case (no attempt was actually made).
Query submissions directly any time with:
npx wrangler d1 execute x_mcp_db --remote --command "SELECT id, created_at, status, stage, client_name, error_message FROM form_submissions ORDER BY created_at DESC LIMIT 20;"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.
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
# KV namespace (backs the /forms/* rate limiter — see "Self-hosted intake
# forms" above):
npx wrangler kv namespace create FORM_RATE_LIMIT
# paste the returned id into wrangler.toml, replacing
# REPLACE_WITH_REAL_KV_NAMESPACE_ID
# 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=...
# Optional — self-hosted intake forms (/forms/*). Omit and the form still
# renders and validates; it just returns "not configured yet" on submit:
ANTHROPIC_API_KEY=...
RESEND_API_KEY=...
FORM_FROM_EMAIL=...
FORM_ADMIN_EMAIL=...Note: wrangler dev --local simulates KV/D1 with a real id placeholder
fine, but the FORM_RATE_LIMIT KV namespace binding still needs to exist
in wrangler.toml (the placeholder id is enough locally).
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.upload_video,post_to_instagram,post_to_facebook:tools/listcorrectly exposes all seven 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.upload_video's draft preview confirmed theprivacy_status: privatedefault is actually taking effect (not just documented) — passing noprivacy_statusat all shows "Privacy: private" in the returned preview, and tags are correctly threaded through.update_video_metadata,get_upload_status: callingupdate_video_metadatawith no fields at all is correctly rejected before any credential check or API call ("Nothing to update"); with a real field but no YouTube credentials configured, it returns the same "not configured yet" message as the others, not a crash from the fetch-then-merge logic running against undefined credentials.get_upload_statusbehaves the same way with no credentials configured.Type-checks clean end to end.
/forms/credit-analysis-intake, viawrangler dev --local+ curl, against the real remaining pieces, not stubs: unauthenticatedGETcorrectly renders the branded HTML form (no bearer token needed, confirming the/forms/*carve-out insrc/index.tsactually works);GETfor an unknown form ID correctly 404s; aPOSTmissing required fields or with a malformed email is correctly rejected (400) with per-field errors and no D1 row written; a fully validPOSTwith noANTHROPIC_API_KEYset correctly returns "not configured yet" (503) and still writes no D1 row (no attempt was actually made, so none is logged); the per-IP rate limiter correctly starts returning 429 after 5 submission attempts in the same window (counting failed/rejected attempts too, which is intentional — validation-failure spam is still spam); and with a real but intentionally invalidANTHROPIC_API_KEYset, a full submission made a genuine network round-trip toapi.anthropic.com(this sandbox's network policy did not block that host, unlikeapi.twitter.com/api.openai.com), got a real401 invalid x-api-keyresponse back, and that exact error text landed correctly in theform_submissionsD1 row (verified by querying the table directly afterward) withstatus = 'failure'andstage = 'generation'. The client-email and admin-email stages (src/formEmail.ts) are structurally identical to the already-proven Resend pattern from the home-care-intake-bot project, but weren't exercised against a real Resend key here, since generation has to succeed first to reach them.
What's not verified: an actual successful post against any of the social
platform APIs (X, YouTube, Instagram, Facebook), an actual successful
OpenAI generation for generate_post, or an actual successful
/forms/credit-analysis-intake submission all the way through to a
delivered email — all of that needs real credentials Damon holds. That's
the real milestone-1 test for both halves of this server: once secrets are
set and this is deployed, one real post through Claude and one real form
submission that lands in an inbox is what proves each 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
);
CREATE TABLE form_submissions (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
form_id TEXT NOT NULL,
client_name TEXT NOT NULL,
client_email TEXT NOT NULL,
fields_json TEXT NOT NULL, -- full validated submission, as JSON
status TEXT NOT NULL, -- 'success' | 'failure'
stage TEXT NOT NULL, -- 'generation' | 'client_email' | 'admin_email' | 'complete'
generated_report TEXT, -- the AI-written report, if generation succeeded
error_message TEXT, -- the exact error, if status = 'failure'
raw_anthropic_response TEXT,
raw_email_response TEXT
);Query them directly any time with:
npx wrangler d1 execute x_mcp_db --remote --command "SELECT * FROM post_log ORDER BY created_at DESC LIMIT 20;"
npx wrangler d1 execute x_mcp_db --remote --command "SELECT id, created_at, status, stage, client_name, error_message FROM form_submissions 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.
Related MCP Servers
- AlicenseBqualityDmaintenanceAn MCP server that allows Claude to create, manage and publish X/Twitter posts directly through the chat interface.Last updated561MIT
- Flicense-qualityCmaintenanceA deployable MCP server on Cloudflare Workers that doesn't require authentication, allowing connection from the Cloudflare AI Playground or Claude Desktop to use custom tools via the Model Context Protocol.Last updated
- Flicense-quality-maintenanceA deployable MCP server on Cloudflare Workers that enables tool integration without authentication requirements, allowing connection from AI Playground or Claude Desktop.Last updated
- Flicense-qualityBmaintenanceAn MCP server that enables Claude to deploy full-stack web apps to Cloudflare, including databases, authentication, and file storage, directly through natural language.Last updated
Related MCP Connectors
Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
Connect any AI agent to 11+ social platforms: schedule, publish & track posts via hosted MCP.
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