Releases: SohrabZ/x-browser-mcp
Release list
v0.0.9
Install
go install github.com/SohrabZ/x-browser-mcp@latestAdded
read_mentionsreads the posts that mention you, andread_notificationsreads
the rest: likes, follows, reposts and X's own recommendations. Both are also
GET /api/v1/mentionsandGET /api/v1/notifications, andread_x_urlresolves
either notification URL.- Notifications are their own shape rather than posts, because the page is not a
timeline of them: on a real account one cell in eighteen held a post. A
notification carries the words X wrote, the accounts it names, and when — and
keeps X's aggregation, so a cell saying two accounts liked one post stays one
notification with two actors. There is no post id or URL, because a cell links
only to the accounts involved.kindis read from X's own words, so it is empty
under a different interface language and for the cells X renders as nothing but a
name and a time. AGENTS.mdrecords what this project has cost to learn: the build gates, the
design decisions that are not inferable from the code, and the gotchas — X
ignoring synthesized mouse events on its engagement controls, a reply composer
discarding text typed before it is focused, rod's idle wait being a minimum
rather than a maximum.CLAUDE.mdimports it rather than repeating it.
Fixed
- A failure told a caller the message of the error it arrived wrapped in rather
than the one that was recognised, and a wrapper is where the internals are: a
profile already in use carries the path of the lock that says so. The MCP tools
handed that to a model, into the same context as untrusted post text; the HTTP
write path did the same. Both now report the recognised message and nothing else,
and an unrecognised failure says only that there was one, with the detail going
to the log. - A link
read_x_urlcould not read — not an x.com URL, no post id in it, a search
with no query — was reported as a server fault instead of as the caller's
mistake. - Liking, reposting or bookmarking a deleted or private post was reported as a
server fault. It now says the post is not there, which is an answer rather than
something to retry. - The message quoting a rejected URL back is bounded. It is the caller's own input,
but one reader of these messages is a model, and an unbounded message is a way to
fill a context window with a single bad request.
v0.0.8
Install
go install github.com/SohrabZ/x-browser-mcp@latestAdded
-allowed-hostnames a furtherHostthe server answers to. Only a bind
beyond loopback that is reached by hostname needs it, now that every request has
to address the server by name; a loopback deployment never does.
Changed
- A read that fails says what failed. Anything that was not "login required" or
"rate limited" collapsed tointernal error, so a list id that does not exist
read as a server fault. Now400for a request that was wrong,404for a
post, account or list X had nothing on,503while a login or a write holds the
profile, and504for a read that ran out of time. Only a fault you can do
nothing about is still hidden. - Reaching the server by a hostname other than
localhostneeds that name passed
to-allowed-host, including on a bind beyond loopback. See below.
Fixed
- A page you visit in an ordinary browser could reach the loopback server. A
domain the attacker controls, re-resolved to127.0.0.1after the page has
loaded, gave it a route to the port, and the read tools would have answered.
Every request is now checked before it reaches a handler,/mcpincluded: the
Hostheader has to name this server, and anyOriginis refused because it
means a browser is calling. This applies to every bind — rebinding works against
a LAN address too, and exposing the session to a network is not exposing it to
every website. - A read that broke was reported as a read that found nothing. A failure while
scraping or scrolling ended the collection loop, and an empty result then read as
"the account or list may not exist" — sending the caller after a post that was
probably there, and hiding the fault that stopped it being found. - A failure reported the error it arrived wrapped in rather than the one that was
recognised, so a profile already in use answered with the path of the lock that
said so. - The MCP server told clients a version that was written down, and had been three
releases stale. It is read from the build.
v0.0.7
Install
go install github.com/SohrabZ/x-browser-mcp@latestAdded
unbookmark_postremoves a post from the bookmarks. Bookmarking was one-way:
the only way back out was clicking the control by hand, and calling
bookmark_postagain does not do it, because the guard that stops a second
save from silently removing one sees the bookmark and returns.- The write actions are served over the plain HTTP API as well as MCP:
POST /api/v1/post,/reply,/like,/repost,/bookmarkand
/unbookmark. They take the same confirmation token, and like the MCP tools
they are not registered at all without-allow-writes— a route that is not
there cannot be reached by anything that gets hold of the port.
Changed
- A write over HTTP reports why it failed rather than a generic message, since
"like did not stick" is the answer. The status says whose problem it is:403
refused,400malformed,412sign-in needed,429write budget spent, and
502attempted but not applied by X. - Two write failure messages said X still showed the action as available, when
all that had been checked was that the applied state never appeared.
v0.0.6
Install
go install github.com/SohrabZ/x-browser-mcp@latestAdded
-write-timeout(default2m) bounds one write. Writes shared
-fetch-timeoutbefore, which is sized for a read: a write starts its own
browser, presses a control, waits for X's request and reloads the post to
confirm it, so a read's budget could cut the confirmation off and report a
timeout rather than a verdict.TESTING.mdrecords how to check that a write actually happened, and why the
check has to be indirect.
Changed
- An engagement action costs a request wait and a page reload — roughly 3-4s
more. The alternative is what it replaces: a tool that reportsokand
changes nothing.
Fixed
- A write reported that the click succeeded, not that the action happened. X
applies engagement over the network and swaps the control before its request
completes, so a click the page accepted and never sent looked identical to one
that worked. The request is waited on, and the post reloaded afterwards to
confirm the action survived a round trip. - Anything that disturbed the page while that request was in flight cancelled
it — tearing the browser down, navigating, or reloading in order to check. A
check that read the page too eagerly caused the failure it was looking for. - Synthesized mouse events do nothing on X's engagement controls. A like
dispatched through CDP was accepted by the page, raised no error, and made no
request. Those controls are pressed with a DOM click. - A reply never reached the composer. The box stays collapsed until focused and
text entered into an unfocused one is discarded, so the box looked filled, the
submit button stayed disabled, and the click landed on a control that could
not be pressed. It is focused first, and the submit control waited on. - An action could be applied to the wrong post. A permalink renders the post's
ancestors, its replies and any quoted post alongside it, each with a like,
repost and bookmark row of its own, and the checks matched the whole document
— so one reply you had already liked was enough for a like to report success
without touching the post asked for. - Bookmark and repost had no confirmation selector, so they could only ever
report the click. Both have one. - Closing a write browser raised Chrome's "Leave site?" prompt. The page-level
close runsbeforeunloadhooks and X registers one on its composer, so
teardown blocked on a dialog nobody was there to answer, holding the profile
until the shutdown wait gave up. Tabs are discarded at the target level. - Each read leaked a page for the life of the process, because discarding a tab
at the target level skips the cleanup the page-level close does. - A post id of anything but digits could not match a status link, so it fell
back to the first post on the page rather than failing. It is rejected before
a browser is opened.
v0.0.5
Install
go install github.com/SohrabZ/x-browser-mcp@latestAdded
- Reads share one warm browser instead of launching and quitting Chrome for
every request. Median uncached read: 3.58s → 2.02s. -browser-idle(default3m) sets how long an unused browser stays warm.
0restores a browser per read.
Changed
- Reads queue while a sign-in window or a write holds the profile, rather than
failing with a profile-in-use error.
Fixed
- A successful write now clears the read cache. Posting or liking was invisible
to any read made in the previous five minutes. - A Chrome that was killed rather than quit left a lock behind that failed every
later read until the server was restarted. A lock whose owning process is gone
is now recognised as stale.
v0.0.4
Install
go install github.com/SohrabZ/x-browser-mcp@latestAdded
- Reads X Articles — long-form posts carry a
titleand their body as text.
These render nothing undertweetText, so they previously came back empty. - Pacing is configurable:
-read-interval,-read-window,-read-max,
-write-interval,-write-jitter,-write-window,-write-max.
Changed
- Read budget raised to 30 per 10 minutes, 3s apart. Reads are not engagement,
and a limiter that ordinary use trips is not a safeguard. Cached reads still
cost nothing. - Writes are paced like a person rather than an agent: at least 45s apart plus
a random extra delay, and at most 6 an hour. Engagement arriving at a fixed
cadence is a signature on its own.
v0.0.3
Install
go install github.com/SohrabZ/x-browser-mcp@v0.0.3Added
read_x_url— pass any x.com link and it resolves to the right surface: a
post reads its thread, a profile reads that account, and list, bookmarks,
home and search URLs read their timelines. Accepts links with the?s=
tracking parameter.- Posts now carry their attached images, with alt text where X provides it.
TESTING.md— automated gates plus the manual browser, agent and
write-gating passes.SECURITY.md— what the server exposes and how to report issues.
Fixed
- Image-only posts were scraped and then discarded, because post validation
required non-empty text. A visual self-thread of ten image replies read as a
root post with no replies at all.
v0.0.2
Browser-backed MCP server for X. Reads and posts from your own logged-in
Chrome profile — no X API key, no developer account, no per-request billing.
Install
go install github.com/SohrabZ/x-browser-mcp@v0.0.2
x-browser-mcp
curl -X POST http://127.0.0.1:18110/api/v1/login/startSign in once in the window that opens, then fully quit it.
Read tools
read_x_url · check_login_status · start_login · read_home_timeline ·
search_x · read_user_posts · read_thread · read_bookmarks · read_list
read_x_url takes any x.com link and resolves it — a post reads its thread, a
profile reads that account, and list, bookmarks, home and search URLs read their
timelines. It accepts what people actually copy, including the ?s= tracking
parameter on share links.
Write tools
post_to_x · reply_to_post · like_post · repost_post · bookmark_post
Disabled unless started with -allow-writes. When disabled they are not
registered at all, so a connected model cannot see or call them. When enabled,
each requires a confirmation token printed to the operator's terminal at
startup — the read tools pull attacker-authored post text into the same context
that can act on your account, and text scraped from a web page cannot supply a
token it has never seen. Writes also get their own tighter budget, an
append-only audit log, and nothing destructive: no delete, unfollow, block or DM.
Notes
- Binds to
127.0.0.1by default. The API is unauthenticated; keep it there. - Session state lives in
~/.x-browser-mcp/at mode 0700. - Reads are paced (15s apart, 8 per 10 minutes) to avoid tripping X.
- On macOS, do not run this from a
launchdagent: Chrome cannot reach the
login Keychain there and will destroy the saved session on every run.
Requires Go 1.25+ and Google Chrome.