🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

footnote-mcp

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

footnote-mcp

MCP server for source-grounded web research: search, structured-data extraction, evidence verification, and browser automation.

pipPyPI
Version
0.2.3
Weekly downloads
1.1K
Maintainers
1

footnote-mcp

An MCP server for source-grounded web research. It searches the web, fetches and extracts pages, pulls structured data out of tables/files/APIs, and — the part that sets it apart — verifies that a claim is actually supported by its source instead of trusting a snippet. 45 tools over stdio MCP, driven by any MCP client (Claude Desktop, Cursor) or by the companion Scholiast research agent.

The design priority is trustworthiness over convenience: search snippets are treated as discovery only, every fetched page is cached with provenance, and claims are checked against the source text before they count. It also degrades gracefully — with no API keys and no config it still works (scraped search + an automatic headless-browser fallback + an offline verification heuristic); keys and env vars only make it better.

Quick start

From PyPI (Python ≥ 3.10):

pip install footnote-mcp
python -m playwright install chromium   # the headless browser used by the fetch fallback
footnote-mcp                            # start the server (speaks MCP over stdio)

Or from source:

python3 -m venv .venv && source .venv/bin/activate
pip install -e .                        # installs the `footnote-mcp` console script + deps
python -m playwright install chromium   # the headless browser used by the fetch fallback
footnote-mcp                            # start the server (speaks MCP over stdio)

footnote-mcp now waits for an MCP client on stdio. Point a client at it by dropping this into its MCP settings (Claude Desktop: claude_desktop_config.json; Cursor: ~/.cursor/mcp.json):

{
  "mcpServers": {
    "footnote": { "command": "footnote-mcp" }
  }
}

Hosted MCP endpoint (Render)

This repository also includes an authenticated Streamable HTTP deployment. The stdio command above remains the recommended local option; the hosted endpoint is for MCP clients that support remote servers.

  • Push this repository to GitHub, then create a Render Blueprint from it. Render reads render.yaml, builds the included Dockerfile (including Chromium and Tesseract), and exposes the health check at /healthz.
  • Choose a unique service name, then set FOOTNOTE_MCP_PUBLIC_URL in Render to its exact public origin, for example https://my-footnote-mcp.onrender.com. FOOTNOTE_MCP_API_KEY is generated by the Blueprint; it is the owner key. Keep it secret and use it only for administration/testing.
  • Connect an MCP client to https://my-footnote-mcp.onrender.com/mcp with:
Authorization: Bearer <FOOTNOTE_MCP_API_KEY>

The API key is required: a public, unauthenticated research and browser server would let strangers consume its outbound traffic and call its tools. The service also validates its configured public host and browser Origin to protect the MCP endpoint from DNS rebinding.

Render Free is suitable for demos, not production: it sleeps after 15 minutes of inactivity, cold starts take about a minute, and both outbound bandwidth and instance hours are limited. The filesystem is ephemeral, so do not rely on the source cache or browser profile persisting across restarts.

Giving access to other people

Never distribute the owner key. Create a separate random key for each person:

python -m footnote_mcp.keygen

Then update FOOTNOTE_MCP_API_KEYS in Render's Environment settings and redeploy. Its value is JSON, where each user has a key and a personal requests-per-minute limit:

{
  "alice": {"key": "fn_alice_key_here", "rpm": 20},
  "bob": {"key": "fn_bob_key_here", "rpm": 10}
}

Give each person only their own value. To revoke access, remove that user from the JSON and redeploy; the other keys keep working. Limits are held in memory, which is appropriate for this one-instance Free service and reset on restart.

No API keys are required to start — search falls back to scraping Bing + DuckDuckGo. Add keys later under "env" (see Search backends). Pass --headed to watch the browser tier work.

Optional runtime variables are documented in .env.example. Copy it to .env for local shells, or paste selected variables into your MCP client config:

{
  "mcpServers": {
    "footnote": {
      "command": "footnote-mcp",
      "env": {
        "TAVILY_API_KEY": "..."
      }
    }
  }
}

To run without installing, straight from the source tree:

PYTHONPATH=src python -m footnote_mcp

Verifying claims — the differentiator

The reason to use this over a plain search tool is evidence_entailment and friends: they tell a claim a source supports from one it does not. benchmarks/run_benchmark.py measures that on a labeled set of claim/source pairs (and demos corroborate_claim and locate_claim_span):

python benchmarks/run_benchmark.py                    # offline heuristic (deterministic)
python benchmarks/run_benchmark.py --backend ollama   # LLM judge (needs ollama)

Offline-heuristic result on the labeled set (benchmarks/REPORT.md):

SetnAccuracyUnsupported-claim catch ratePrecision on "supported"
Data domain (numeric + factual)15100%100%100%
Overall (incl. semantic)1883%78%80%

On its design domain — numeric and factual data claims — the offline heuristic never blesses an unsupported claim and never misses one. Its blind spot is purely-semantic negation/paraphrase; for those, evidence_entailment with backend="ollama" (a local LLM judge) closes the gap. Run the --backend ollama line above to score that path on your own machine.

Tools

Discovery and reading (12 tools)
ToolDescription
web_searchConfigured SearXNG first, then keyed providers, then scraped Bing + DuckDuckGo. Snippets are discovery only.
web_search_recentSearch restricted to a recency window (day/week/month/year).
web_deep_searchAutomatically route across web/papers/encyclopedia/GitHub/archive sources, then fetch, extract, rerank, and return source context.
web_readFetch one URL, extract text, classify source quality, persist cache metadata.
papers_searchSearch Crossref and arXiv through one normalized, zero-key paper contract.
encyclopedia_searchSearch Wikipedia/Wikidata entities or run read-only Wikidata SPARQL.
github_searchSearch public repositories, issues, code, or commits; authentication is optional.
archive_searchFind URL captures through Wayback Machine and Common Crawl, optionally extracting archived text.
web_archive_fetchFind the closest Wayback Machine snapshot for a dead/changed URL.
web_fetch_authenticatedFetch a page that needs cookies or custom headers.
web_crawlBreadth-first crawl from a start URL, on-host by default (≤ 50 pages).
generate_search_queriesGenerate operator queries (site:, filetype:csv, API/data-table variants).
Structured data (9 tools)
ToolDescription
web_extract_tablesParse HTML tables into columns/rows with source-URL provenance.
web_detect_downloadsDetect linked CSV/TSV/XLS/XLSX/PDF/JSON/XML files.
web_parse_fileDownload and parse CSV/TSV/XLS/XLSX/PDF/JSON.
web_fetch_jsonFetch direct API/JSON endpoints into parsed JSON.
check_date_completenessValidate required date coverage (day/week/month).
resolve_unitsDetect currencies, currency pairs, measurement units.
validate_unit_rowsReject rows with incompatible units or currency pairs.
reconcile_time_seriesAlign series on a key, compute deltas, flag missing keys/outliers.
export_datasetWrite consolidated rows to a csv/xlsx/json file.
Source quality and verification (8 tools)
ToolDescription
classify_sourceClassify official / aggregator / blog / forum / interactive / blocked / error.
evidence_entailmentStrict claim-vs-source checker: heuristic, auto, ollama, optional local_nli.
corroborate_claimTriangulate a claim across excerpts (corroborated / conflicting / single_source / …).
locate_claim_spanLocate supporting sentence(s) with char offsets and a containment score.
source_cache_get / source_cache_putInspect and write persistent source-cache entries.
build_research_debug_reportCompact report of queries, URLs, source quality, verification gaps.
startup_health_checkCheck parser, OCR, browser, and cache dependencies.
Controlled extraction recipes (6 tools)

When generic parsers fail, synthesize a sandboxed parser:

ToolDescription
tool_spec_proposePropose a task-specific extraction recipe spec.
tool_code_generateGenerate a starter extract(source_text, input_payload) recipe.
tool_code_validateValidate recipe code against a static safety allowlist.
tool_code_run_sandboxedRun validated code in a limited subprocess (JSON output only).
tool_promoteSave a validated recipe as reusable memory (no server edit).
recipe_registryManage promoted recipes: list / get / run / delete.
Browser fallback (10 tools)

A controlled Chromium session for JS-heavy or interactive pages:

ToolDescription
web_navigate · web_snapshot · web_click · web_type · web_extract · web_scrollDrive a page via stable element refs.
browser_set_date_range · browser_extract_tables · browser_extract_tables_for_date_rangeSet a date range, submit, extract visible tables.
web_screenshotSave a PNG and optionally OCR text locked inside the image.

Search backends

web_search routes through a provider layer. A configured zero-key SearXNG instance is tried first, followed by keyed providers and finally scraped Bing + DuckDuckGo. Results are normalized to one shape regardless of backend.

ProviderEnv varsNotes
SearXNGFOOTNOTE_SEARXNG_URL (or SEARXNG_URL)Zero-key JSON API; instance must enable JSON output.
TavilyTAVILY_API_KEYLLM-oriented search API.
BraveBRAVE_API_KEYIndependent web index.
GoogleGOOGLE_API_KEY + GOOGLE_CSE_IDProgrammable Search (Custom Search JSON API).
Bing + DuckDuckGononeDefault fallback; scraped, no key.

auto (default) tries configured providers in order SearXNG → Tavily → Brave → Google, then scrapes. Force one with the provider argument (searxng/tavily/brave/google/scrape).

Specialized zero-key discovery

The public MCP surface is organized by user intent rather than by HTTP API:

Intent toolBackendsRouting notes
papers_searchCrossref + arXivsource=auto queries both; force either backend when needed.
encyclopedia_searchWikipedia + WikidataEntity search by default; optional read-only SPARQL for structured facts.
github_searchGitHub REST searchPublic zero-key requests work at GitHub's unauthenticated rate limit; GITHUB_TOKEN is optional.
archive_searchWayback + Common CrawlAccepts a URL/host pattern. fetch_text=true attempts archived-content extraction.

All four return title, url, snippet, published, authors, source, and source_type where those fields apply. web_deep_search accepts an optional sources array (web, papers, encyclopedia, github, archive). With an empty array it always uses general web discovery and adds specialized sources when the query signals their intent.

Semantic reranking. Pass semantic: true to web_search to reorder by meaning rather than keyword overlap: it over-fetches, embeds query and results with a local ollama model, and sorts by cosine similarity (each result gains semantic_score). Best-effort — if ollama is unavailable the original order is returned. Model: FOOTNOTE_EMBED_MODEL (default bge-m3).

Fetching & anti-bot ladder

web_read fetches through an escalation ladder (scraper.py): the cheapest method runs first and escalates only when a result looks blocked or empty. A block/quality detector decides when to escalate; a per-domain rate limiter, circuit breaker, and negative cache keep it polite. The tier used and the full attempt trace come back in fetch_tier / scrape_tiers.

TierMethodEnabled by
1HTTP (curl_cffi TLS impersonation)always
2HTTP through a rotating proxyFOOTNOTE_PROXIES set
3Headless Chromium (runs JavaScript)FOOTNOTE_BROWSER_FALLBACK=1 (default on)
4Chromium through a proxyproxies + browser
5Hosted scrape API (Firecrawl / ScrapingBee)FOOTNOTE_SCRAPE_API set

With nothing configured it is the plain HTTP path plus an automatic browser fallback for JavaScript-rendered pages.

Env varDefaultPurpose
FOOTNOTE_BROWSER_FALLBACK1Escalate blocked/JS pages to headless Chromium.
FOOTNOTE_PROXIES(none)Comma-separated proxy URLs; sticky per domain with health tracking.
FOOTNOTE_SCRAPE_API(none)firecrawl or scrapingbee (needs the matching API key).
FOOTNOTE_DOMAIN_RPS / _BURST3 / 5Per-domain rate limit (token bucket).
FOOTNOTE_BREAKER_THRESHOLD / _COOLDOWN5 / 120Per-domain circuit breaker.
FOOTNOTE_NEGCACHE_TTL300Seconds to remember a blocked URL.
FOOTNOTE_THIN_CONTENT_CHARS200Below this extracted length, a script-heavy page counts as a JS shell.

Runtime data

~/.footnote-mcp/source_cache/        # persistent page cache (with provenance)
~/.footnote-mcp/research_memory.json # persistent research memory

Override the cache location with FOOTNOTE_SOURCE_CACHE=/path/to/cache footnote-mcp.

check_date_completeness supports the calendars calendar, business_day, crypto_24_7, forex_weekday, us_business_day, and ru_business_day (pass explicit holidays for source-specific ones; the us_/ru_ variants use the optional holidays package).

Other install paths

Docker bundles Chromium and tesseract — nothing else to install:

docker build -t footnote-mcp .
docker run -i --rm footnote-mcp        # the client launches this; see MCP config below

Published images are available from GitHub Container Registry:

docker run -i --rm ghcr.io/kazkozdev/footnote-mcp:0.2.3
docker run -i --rm ghcr.io/kazkozdev/footnote-mcp:latest
{
  "mcpServers": {
    "footnote": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "ghcr.io/kazkozdev/footnote-mcp:latest"]
    }
  }
}

pipx / uvx (isolated install of the entry point):

pipx install /path/to/footnote-mcp          # or: pipx install git+<repo-url>
uvx --from /path/to/footnote-mcp footnote-mcp   # ad-hoc, no install

OCR. PDF/image OCR uses pytesseract + the system tesseract binary (brew install tesseract on macOS). Local NLI backend for evidence_entailment backend="local_nli": pip install -r requirements-nli.txt (model via FOOTNOTE_NLI_MODEL). Either way, startup_health_check reports what is actually available. Runtime dependency ranges are declared in pyproject.toml and mirrored in requirements.txt.

Tests

pip install -r requirements-dev.txt
python -m pytest -q          # offline unit + smoke tests; no network or keys needed

tests/test_mcp_smoke.py launches the server over real MCP stdio and exercises the tools end to end against a local HTTP fixture; the rest are offline unit tests of the parsers, fetch ladder, search providers, and dispatch. The live search test is opt-in:

RUN_LIVE_WEB_TESTS=1 python -m pytest -m live

CI runs the same suite (.github/workflows/tests.yml).

License

MIT — see LICENSE.

Keywords

mcp

FAQs

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts