Releases: AKzar1el/mcp-geo
Releases · AKzar1el/mcp-geo
Release list
v0.2.1 — Connect-flow security gate, accurate citation matching, CI + unit tests
A security + accuracy pass ahead of wider distribution.
Added
- Optional
CONNECT_SECRETsecret. When set, the browser step of the OAuth connect flow shows a one-field form and only completes when the secret matches. Without it the OSS build keeps its auto-completing single-dev-user flow — which means anyone who knows the worker URL can connect an MCP client and callrefresh_brand(spending your engine API credits). Recommended for every deployment whose URL is shared anywhere. - Per-brand
aliasesandexclude_terms(migrations/0005_brand_alias_exclude.sql,Brandinsrc/db.ts,SeedBrandInputinsrc/seed.ts). Aliases are extra terms that always count as a mention; exclude terms suppress the bare-word match on the brand name and domain root — so "Monday" the brand stops matching "monday" the weekday — while the full domain and aliases still count.extractCitationsnow matches through amentionsTermSetterm set; its exported signature is unchanged. Apply 0005 to add thealiases_json+exclude_terms_jsoncolumns; existing brands read as empty arrays and behave exactly as before. SECURITY.md— security model, trust boundaries, and private vulnerability reporting.- GitHub Actions CI (
.github/workflows/ci.yml):tsc --noEmit+ unit tests on every push and PR. - Unit test suite (
npm run test:unit, Node test runner + tsx):tests/matching.test.tscovers brand/competitor mention matching (excluded bare words, full-domain matches that survive exclusion, root terms not matching inside a larger word);tests/unit/citations.test.tsandtests/unit/scoring.test.tscover citation extraction (extractCitations,hostMatchesDomain) and score aggregation (computeOverallScore). These are the pure functions every score flows through; the smoke suite still covers the deployed Worker end-to-end. migrations/README.md— how to apply migrations and why files must never be renumbered.- README: Claude Code connect instructions (
claude mcp add --transport http), architecture diagram, security section.
Fixed
- Word-boundary brand/competitor matching in
src/openai.ts.mentionsTermusedhaystack.includes(root), a raw substring test: a brand whose domain root is a common word (monday.com→ "monday",notion.so→ "notion") false-positived on the everyday word, and any term matched inside a larger word ("motion" inside "promotional"). Matching now uses Unicode-aware boundary checks —(?<![\p{L}\p{N}])…(?![\p{L}\p{N}])— for the brand name and the domain root, while the full domain is still accepted as a high-confidence substring. The exportedextractCitationssignature is unchanged. (The homograph case — a bare root term that is itself a common word, e.g. "monday"/"notion" — is handled via the new per-brandexclude_terms.) - Linked-citation checks require the exact brand domain or a subdomain of it. Previously a substring check meant
notacme.comcounted as a link toacme.com. Applies toextractCitations, the Perplexity and AI Overviews engine-citation merge, andget_citations'cited_urlselection — all through the new sharedhostMatchesDomainhelper. - NUL → unit-separator hash delimiter in
hashPrompt(src/openai.ts). The field join used two literal NUL (\x00) bytes, which made git classify the whole file as binary and refuse to render its diffs. It now joins with the ASCII unit separator through a namedHASH_FIELD_SEP = '\x1f'constant. Hash outputs change, so existingshared_prompt_cacherows simply miss and expire on their TTL — no cache clear or migration required. get_visibility_historymatchescheck_visibility's stance on partial runs. Runs interrupted mid-flight (stuck atin_progress) now count toward history viaCOALESCE(completed_at, started_at), and runs with zero ok rows are excluded entirely instead of charting as a fake score of 0.
Changed
- Docs (
README.md,SETUP.md) now recommend OpenAI + Anthropic (Claude) as the starting engine pair instead of the Gemini free tier. The Gemini free tier 429s for brands with more than ~5 prompts (excluding it from scoring), and Google AI Overviews frequently returnsNO_AI_OVERVIEW(scored as a zero), so the cheapest documented path produced misleading first-run data. Gemini and SerpAPI stay documented as opt-in engines; engine availability is unchanged and remains key-driven (getAvailableEnginesuntouched). SEED_SECRETandCONNECT_SECRETcomparisons are constant-time.- MCP server version string now tracks the package version (was stuck at an older value).
- Runtime dependencies (
@cloudflare/workers-oauth-provider,@modelcontextprotocol/sdk,agents,zod) moved fromdevDependenciestodependencies— wrangler bundles either way, but the manifest now tells the truth.
v0.2.0 — Per-engine fan-out, service binding, status column
A correctness + architecture pass driven by an end-to-end debugging session on the production fork. Every fix is described in terms of what the prior version got wrong.
Added
migrations/0004_response_status.sql:status(ok/failed/skipped) anderror_messagecolumns onprompt_responses, plus an index onstatus. Failed engine calls used to writeraw_response='ERROR: ...'rows that downstream scoring treated as real zero-mention hits. They're now explicitstatus='failed'rows that aggregates exclude.POST /admin/run-engine— single-engine handler./admin/run-liveself-fetches into it once per engine so each engine runs in its own worker invocation with its own free-plan 50-subrequest budget. Idempotent on(run_id, engine).POST /admin/cleanup-failed-runs— one-shot deletion of legacy polluted rows that were written before the status column existed. Idempotent, optionalbrand_idfilter.bulkCacheGethelper insrc/db.ts— one D1 read for N prompt hashes instead of N separate reads.persistEngineRunhelper insrc/db.ts— one batched D1 write at the end of an engine run instead of per-prompt inserts + cache puts + per-chunk run updates.EnginePromptResultinterface — engines collect their per-prompt results in memory and flush in one batch.runEngineInProcessinsrc/engines.ts— the in-process dispatcher used by/admin/run-engine.Fetcherservice binding (env.SELF) inEnvandEnginesEnv, plus"services"block inwrangler.example.jsonc. The per-engine fan-out goes throughenv.SELF.fetch()because a public-URL fetch back to your ownworkers.devhostname trips Cloudflare's "Worker called itself" guard (error 1042) and never lands.SELF_URLenv var inwrangler.example.jsonc— the cron path needs to know the worker's canonical URL since it has no inbound request to derive an origin from.
Changed
- Each engine client (
src/openai.ts,anthropic.ts,perplexity.ts,gemini.ts,ai-overviews.ts) now uses the bulk pattern: hash all prompts up front,bulkCacheGetonce, run LLM fetches in CONCURRENCY=5 chunks, collectEnginePromptResult[], flush viapersistEngineRun. Per-invocation subrequest count drops from ~89 to ~26. getLatestCompletedRunanchors onEXISTS(prompt_responses with status='ok')instead ofruns.status='completed'. Partially-finished runs still surface their data instead of silently disappearing from MCP tool output.check_visibility,compare_competitors, andget_content_gapsuserun.completed_at ?? run.started_atso they accept in-progress runs.get_visibility_historyandget_citationsSQL queries filterpr.status = 'ok'in the join/where clause so failed/skipped rows can't pollute aggregates./admin/run-engineINSERT OR IGNOREs its runs row before calling the engine — D1's cross-region replication may not have caught up with/admin/run-live'sINSERT INTO runsby the time the self-fetch lands, and a missing parent row would crash the engine's batched insert withFOREIGN KEY constraint failed.runEnginesaccepts an optionalrequestargument and falls back tonew URL(request.url).originwhenSELF_URLisn't set, so HTTP-triggered code paths work even without the env var.
Fixed
- Subrequest-cap row loss: pre-fix, every engine deterministically wrote 10 rows out of 20 because chunk 3 hit the 50-subrequest cap. Now writes complete in one batch under the cap.
- Silent row drops from failed Gemini / AI Overviews calls: pre-fix the catch block's
insertPromptResponse(failed)was itself a D1 subrequest that could fail, leaving zero rows for the prompt. The bulk batch holds failure results in memory and writes them in the same batch as the successes. - Visibility tools dropping engines whose runs got stuck at
in_progress: relaxedgetLatestCompletedRunsurfaces them.
v0.1.1 — Manual install path; setup.sh removed
- Removed
scripts/setup.sh— the bash script wasn't reliable across platforms (Git Bash on Windows in particular couldn't pipe stdin to wrangler's interactive prompts). Replaced with a fully manual, copy-pasteable walkthrough in SETUP.md as the canonical install path. - SETUP.md is now self-contained — every wrangler command, every interactive prompt response, and the most common failure modes are documented inline.
v0.1.0 — Initial public release
Initial public release.
Added
- 5-engine support: ChatGPT (
gpt-4o-mini), Claude (claude-haiku-4-5), Perplexity (sonar), Gemini (gemini-2.5-flash-lite), and Google AI Overviews (via SerpAPI). - 6 MCP tools:
check_visibility,get_visibility_history,compare_competitors,get_citations,get_content_gaps,refresh_brand. - Opt-in engine availability — engines are enabled only when their API key is present.
- D1-backed storage: brands, prompts, runs, prompt_responses, shared_prompt_cache.
- Cloudflare Cron Trigger (every 6h) that respects per-brand
refresh_frequency(daily/weekly). - Admin routes:
/admin/seed,/admin/run-live,/admin/run-batch-submit,/admin/run-batch-collect,/admin/generate-prompts,/admin/trigger-cron-test. All gated bySEED_SECRET. - Smoke test suite at
tests/smoke.test.mjs(Node built-in test runner; no external deps). - Claude-Haiku-powered prompt generator and content-gap analyzer with deterministic fallback recommendations.