Summary
Two bugs in src/mcp/ cause most scrape tools (x_search_tweets, x_get_hashtag, x_brand_monitor, x_monitor_keyword, x_crypto_analyze, …) to silently return empty results when XACTIONS_SESSION_COOKIE is set the way the README instructs.
Hit these on nirholas/XActions main at commit f30e59781e728234606be600738dc7df2839341e (package version 3.1.0, also current npm latest). Happy to open a PR — the two diffs are below.
Bug 1 — ensureBrowser() never auto-logs-in the singleton browser
Symptom. With XACTIONS_SESSION_COOKIE correctly set via env (or via the MCP server's env block in a Claude Code --mcp-config), calling x_search_tweets / x_get_hashtag / x_brand_monitor / x_crypto_analyze / ~15 other scrape tools returns [] or tweetCount: 0. No error, no log, no rate-limit message.
Repro.
export XACTIONS_SESSION_COOKIE="<valid_auth_token>"
npx -y xactions-mcp
# then call, via any MCP client:
# x_search_tweets { "query": "bitcoin", "limit": 10 }
# → []
Calling x_get_profile elonmusk in the same session works (it doesn't require login), which makes the bug look like "search is broken" or "cookie expired" rather than an auth-wiring issue.
Root cause. src/mcp/local-tools.js:ensureBrowser() launches a shared Puppeteer browser and reuses it across tool calls, but never calls loginWithCookie:
async function ensureBrowser() {
if (!browser || !browser.isConnected()) {
if (browser) { try { await browser.close(); } catch {} }
browser = await createBrowser();
page = await createPage(browser);
// ← nothing consumes SESSION_COOKIE here
}
return { browser, page };
}
server.js:49 reads SESSION_COOKIE = process.env.XACTIONS_SESSION_COOKIE and some handlers (x_export_account, algorithm builder, spaces agent, streaming pollers) do call loginWithCookie(page, SESSION_COOKIE) automatically. The shared ensureBrowser() path is the one that doesn't, so search/hashtag/crypto_analyze/brand_monitor/etc. all run against a logged-out singleton page that lands on the x.com login wall; document.querySelectorAll('article[data-testid="tweet"]') returns zero elements; searchTweets() returns [].
Workaround: call the x_login tool manually once at the start of every session. Not documented as required and the tools don't surface any "you are not authenticated" signal, so users currently go down a rabbit hole assuming the cookie is wrong or the scraper is broken.
Proposed fix (src/mcp/local-tools.js):
browser = await createBrowser();
page = await createPage(browser);
+ // Auto-login the singleton browser with the env-var cookie so scrape
+ // tools (x_search_tweets, x_get_hashtag, x_brand_monitor,
+ // x_monitor_keyword, x_crypto_analyze, ...) work without requiring a
+ // separate x_login call each session.
+ const envCookie = process.env.XACTIONS_SESSION_COOKIE;
+ if (envCookie) {
+ try {
+ await loginWithCookie(page, envCookie);
+ } catch (e) {
+ console.error('[xactions] auto-login failed:', e.message);
+ }
+ }
}
return { browser, page };
}
loginWithCookie is already imported at the top of local-tools.js, so this is a 12-line addition with no new dependencies.
Bug 2 — x_crypto_analyze reads the wrong shape from x_search_tweets
Symptom. Even after x_login has been called manually to work around Bug 1, x_crypto_analyze always returns tweetCount: 0 with an all-zero sentiment.
Root cause. src/mcp/server.js:2700:
case 'x_crypto_analyze': {
const searchResults = await localTools.x_search_tweets?.({ query: args.query, limit: args.limit || 100 });
const tweets = searchResults?.tweets || []; // ← bug
...
}
x_search_tweets returns a plain array of tweet objects, not { tweets: [...] }. searchResults.tweets is always undefined, so tweets is always [], and every subsequent metric is 0.
Proposed fix (src/mcp/server.js):
case 'x_crypto_analyze': {
const searchResults = await localTools.x_search_tweets?.({ query: args.query, limit: args.limit || 100 });
- const tweets = searchResults?.tweets || [];
+ // x_search_tweets returns a plain array, not {tweets:[...]}. Accept
+ // both shapes so tweetCount is populated.
+ const tweets = Array.isArray(searchResults) ? searchResults : (searchResults?.tweets || []);
Verification
Applied both patches locally against a fresh clone of main, ran the MCP with XACTIONS_SESSION_COOKIE set, made zero manual x_login calls:
x_search_tweets { "query": "bitcoin", "limit": 5 }
→ 5 tweets, newest timestamped seconds before the call
(@AkasoRose25841, @MithebolRaghu, @InvestorsPOV, @Baerental, @ChainBitX)
x_crypto_analyze { "query": "BTC", "timeframe": "24h", "limit": 80 }
→ {
"query": "BTC",
"tweetCount": 80,
"sentiment": { "bullish": 28, "bearish": 11, "neutral": 61, "score": 16 },
"topInfluencers": [ { "username": "nehalzzzz1", "engagement": "060" } ]
}
Both numbers were 0/[] before the patches.
Minor observation (not part of the proposed fix)
The x_crypto_analyze influencer filter requires engagement > 50 on (likes + retweets), but x_search_tweets returns "likes": "0" for most live-feed tweets because X reports zero engagement for tweets only seconds old. As a result, topInfluencers is usually a one-item array or empty on short timeframes. Might be worth either lowering the threshold, pulling from f=top for the influencer pass, or weighting by impressions instead. Happy to include in a follow-up PR if there's interest — keeping this issue focused on the two correctness bugs.
Environment
xactions 3.1.0 (latest on npm and on main)
- Node.js 22.22.0 on Linux x86_64
- Puppeteer adapter (default)
- Invoked as an MCP stdio server with
XACTIONS_SESSION_COOKIE set in the process env at spawn time
Thanks for the project — the tool surface is genuinely impressive, and once past these two bugs the crypto-sentiment stack is in great shape for what we're building on top.
Summary
Two bugs in
src/mcp/cause most scrape tools (x_search_tweets,x_get_hashtag,x_brand_monitor,x_monitor_keyword,x_crypto_analyze, …) to silently return empty results whenXACTIONS_SESSION_COOKIEis set the way the README instructs.Hit these on
nirholas/XActionsmainat commitf30e59781e728234606be600738dc7df2839341e(package version3.1.0, also current npmlatest). Happy to open a PR — the two diffs are below.Bug 1 —
ensureBrowser()never auto-logs-in the singleton browserSymptom. With
XACTIONS_SESSION_COOKIEcorrectly set via env (or via the MCP server'senvblock in a Claude Code--mcp-config), callingx_search_tweets/x_get_hashtag/x_brand_monitor/x_crypto_analyze/ ~15 other scrape tools returns[]ortweetCount: 0. No error, no log, no rate-limit message.Repro.
Calling
x_get_profile elonmuskin the same session works (it doesn't require login), which makes the bug look like "search is broken" or "cookie expired" rather than an auth-wiring issue.Root cause.
src/mcp/local-tools.js:ensureBrowser()launches a shared Puppeteer browser and reuses it across tool calls, but never callsloginWithCookie:server.js:49readsSESSION_COOKIE = process.env.XACTIONS_SESSION_COOKIEand some handlers (x_export_account, algorithm builder, spaces agent, streaming pollers) do callloginWithCookie(page, SESSION_COOKIE)automatically. The sharedensureBrowser()path is the one that doesn't, so search/hashtag/crypto_analyze/brand_monitor/etc. all run against a logged-out singleton page that lands on the x.com login wall;document.querySelectorAll('article[data-testid="tweet"]')returns zero elements;searchTweets()returns[].Workaround: call the
x_logintool manually once at the start of every session. Not documented as required and the tools don't surface any "you are not authenticated" signal, so users currently go down a rabbit hole assuming the cookie is wrong or the scraper is broken.Proposed fix (
src/mcp/local-tools.js):browser = await createBrowser(); page = await createPage(browser); + // Auto-login the singleton browser with the env-var cookie so scrape + // tools (x_search_tweets, x_get_hashtag, x_brand_monitor, + // x_monitor_keyword, x_crypto_analyze, ...) work without requiring a + // separate x_login call each session. + const envCookie = process.env.XACTIONS_SESSION_COOKIE; + if (envCookie) { + try { + await loginWithCookie(page, envCookie); + } catch (e) { + console.error('[xactions] auto-login failed:', e.message); + } + } } return { browser, page }; }loginWithCookieis already imported at the top oflocal-tools.js, so this is a 12-line addition with no new dependencies.Bug 2 —
x_crypto_analyzereads the wrong shape fromx_search_tweetsSymptom. Even after
x_loginhas been called manually to work around Bug 1,x_crypto_analyzealways returnstweetCount: 0with an all-zero sentiment.Root cause.
src/mcp/server.js:2700:x_search_tweetsreturns a plain array of tweet objects, not{ tweets: [...] }.searchResults.tweetsis alwaysundefined, sotweetsis always[], and every subsequent metric is0.Proposed fix (
src/mcp/server.js):case 'x_crypto_analyze': { const searchResults = await localTools.x_search_tweets?.({ query: args.query, limit: args.limit || 100 }); - const tweets = searchResults?.tweets || []; + // x_search_tweets returns a plain array, not {tweets:[...]}. Accept + // both shapes so tweetCount is populated. + const tweets = Array.isArray(searchResults) ? searchResults : (searchResults?.tweets || []);Verification
Applied both patches locally against a fresh clone of
main, ran the MCP withXACTIONS_SESSION_COOKIEset, made zero manualx_logincalls:Both numbers were
0/[]before the patches.Minor observation (not part of the proposed fix)
The
x_crypto_analyzeinfluencer filter requiresengagement > 50on(likes + retweets), butx_search_tweetsreturns"likes": "0"for most live-feed tweets because X reports zero engagement for tweets only seconds old. As a result,topInfluencersis usually a one-item array or empty on short timeframes. Might be worth either lowering the threshold, pulling fromf=topfor the influencer pass, or weighting by impressions instead. Happy to include in a follow-up PR if there's interest — keeping this issue focused on the two correctness bugs.Environment
xactions3.1.0 (latest on npm and onmain)XACTIONS_SESSION_COOKIEset in the process env at spawn timeThanks for the project — the tool surface is genuinely impressive, and once past these two bugs the crypto-sentiment stack is in great shape for what we're building on top.