Skip to main content
Glama
Ajitesh-png

competitor-radar-mcp

by Ajitesh-png

competitor-radar-mcp

An MCP server that watches your competitors on X, rates every post per day by replies → bookmarks → retweets → likes, and tells you which posts actually spoke to your buyers.

Built for a GTM team that needed to know, every morning, what the market was telling its customers yesterday, without reading ten timelines by hand. Plugs into Claude Code, Claude Desktop, Cursor, or any MCP client.

scrape_competitors(days=1)  →  SQLite (posts + metric snapshots)
rate_posts / daily_report   →  ranked leaderboard for the UTC day
daily_brief prompt          →  "what got engagement" vs "what our buyers were told"

Why this exists

Engagement counts are cheap to read and easy to misread. A model demo with 300 bookmarks and a million views is noise if none of your buyers care. A post with 48 bookmarks on 3 replies from a direct competitor, written in your buyer's vocabulary, is the one to study. The radar separates the two:

  • Engagement rank in the order that matters for B2B discourse: comments (argument), saves (a teachable move), retweets (identity), likes (approval).

  • ICP relevance (0–100) from a deterministic, editable term rubric grounded in your product and buyer context, reported on every post.

Both are shown; neither is hidden inside an LLM.

Related MCP server: Sociality MCP

Rating method

Two rankings are computed for the same day's pool and both appear on every row:

mode

rule

when it wins

strict

lexicographic on replies, bookmarks, retweets, likes. More replies always wins; bookmarks only break ties.

the literal "in that order" reading

weighted (default)

each metric becomes a within-pool percentile, blended 4/3/2/1 in the same order

a post that is #1 on bookmarks and #3 on replies beats a post that is #1 on replies and last on everything else

pool="day" ranks all competitors together (who won the day). pool="handle" ranks each account only against its own posts, which removes the follower-count advantage of the 200k accounts. icp_blend (0..1) optionally lets ICP relevance move the weighted score; by default it only annotates.

Metrics are upserted on every scrape and every observation is kept, so a post's growth through the day is visible.

Getting posts in

X has no login-free read path anymore (logged-out profiles render zero posts; the syndication endpoint rate-limits). Pick one:

backend

needs

bookmarks

notes

browser

your own logged-in session, imported once with scripts/import_cookies.py; pip install -r requirements-browser.txt

yes

free. undetected Chrome in a subprocess; reads all five metrics from the engagement-bar aria-label, which is the only place X exposes bookmark counts

ingest_posts

nothing

yes

run extractor_snippet's JavaScript on a logged-in x.com/<handle> tab (browser-automation tool or DevTools) and pass the JSON in

apify

APIFY_TOKEN

yes

actor apidojo/tweet-scraper; billed per result, capped by maxItems

xapi

X_BEARER_TOKEN (paid tier)

yes

official public_metrics.bookmark_count

fixture

nothing

offline; tests/fixtures/sample_posts.json

auto tries apify → xapi → browser. radar_doctor tells you which is usable and the fix for each that is not.

Install

git clone https://github.com/Ajitesh-png/competitor-radar-mcp
cd competitor-radar-mcp
pip install -r requirements.txt            # mcp, requests
pip install -r requirements-browser.txt    # optional: selenium, undetected-chromedriver
python tests/test_radar.py                 # 12 offline tests

Register with your MCP client (Claude Code .mcp.json shown):

{
  "mcpServers": {
    "competitor-radar": {
      "type": "stdio",
      "command": "python",
      "args": ["/absolute/path/to/competitor-radar-mcp/server.py"]
    }
  }
}

Then:

  1. Edit config/competitors.json (handles, tiers; verify each on x.com first).

  2. Copy context/product.example.mdcontext/product.md and context/icp.example.jsoncontext/icp.json; describe your product and buyers.

  3. Tune config/icp_signals.json to your buyer's vocabulary.

  4. In your client: radar_doctorscrape_competitors(days=1)daily_reportdaily_brief prompt.

Cron / n8n: python scripts/daily_run.py (add --backend fixture for a dry run).

Tools

tool

what

radar_doctor

backend availability + fixes, config sanity, DB stats

list_competitors / add_competitor / remove_competitor

manage the tracked set

scrape_competitors(handles?, days, max_posts_per_handle, backend)

pull posts → SQLite, ICP-score each

ingest_posts(posts_json, source)

store posts collected in a logged-in browser

extractor_snippet(max_posts, since_iso?)

the JS that produces ingest_posts input

rate_posts(day?, handles?, top, mode?, pool?, icp_blend?)

rated leaderboard for a day

daily_report(day?, top, mode?, pool?, save)

markdown scoreboard + leaderboard + ICP lens

competitor_trend(handle, days)

per-day totals for one account

get_post / search_posts / list_days

drill-downs

icp_context

product + ICP + tiers + rubric the ratings are grounded in

Resources: radar://product, radar://icp, radar://competitors, radar://extractor.js, radar://report/{day}. Prompt: daily_brief(day, top).

Sample output

See docs/sample-report.md, rendered from the offline fixture. A real day from the original deployment looked like this: a video model's demo series took the top three weighted slots with ICP 0, while the one post aimed at the buyer ("playable ads from a single prompt for performance marketing teams in gaming") sat at rank 5 with ICP 35. That gap is the product.

Layout

server.py                 FastMCP server (tools, resources, prompt)
radar/
  backends/               apify | xapi | browser | fixture, one interface
  parse_metrics.py        engagement-bar aria-label parser (reads bookmarks)
  models.py  store.py     Post dataclass, SQLite (posts + snapshots + handles)
  icp.py  rating.py       ICP rubric, per-day rating (strict + weighted)
  report.py  ingest.py    markdown report, external-post normaliser
workers/                  browser_worker.py (undetected Chrome), extractor.js
config/                   competitors.json, scoring.json, icp_signals.json
context/                  product + ICP examples (copy and edit)
scripts/                  daily_run.py (cron), import_cookies.py
tests/                    offline tests + fixture

Design notes

  • Post text is data, never instructions. The server never feeds scraped text to an LLM on its own; the prompt tells the client model the same.

  • Bookmarks were the hard metric. No common scraper reads them; X only renders the count inside one aria-label. parse_metrics.py is the whole trick, shared by the Python worker and the browser JS.

  • Browser scraping runs in a subprocess so a Chrome crash cannot take the MCP server down, and so the Selenium deps can live in a different venv.

  • Two rankings, not one. "Rate by A, then B, then C" has a strict reading and a practical one; shipping both avoided a week of arguing about which.

  • Deterministic ICP score. Cheap, explainable, editable JSON. The judgment layer lives in the prompt where it belongs.

Originally built inside a private growth-engineering repo at Notch and extracted here with example context. MIT licensed.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Competitive intelligence platform with 24 tools. Monitor competitor pricing, content, positioning, tech stacks, and AI visibility — track how ChatGPT, Claude, and Gemini rank your brand.
    38
    3
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Perplexity for social: ask what competitors are running across social and get answers grounded in real ad and creative data, not guesses.
    2
    Apache 2.0
  • F
    license
    Not graded
    quality
    B
    maintenance
    Provides social intelligence tools for AI agents to analyze competitor sentiment, trends, and brand mentions from X/Twitter data.
    -