mcp-anac-crawler
Enables searching, inspecting, and downloading Italian public tender notices from the ANAC Pubblicità Legale platform, supporting offset and cursor pagination, reference data lookup, and safe third-party document retrieval.
README
mcp-anac-crawler
MCP stdio server for crawling the Italian ANAC Pubblicità Legale tender platform
(pubblicitalegale.anticorruzione.it), built to the requirements in
docs/req-web-crawel.
The finding that shaped the design
The requirements assume an HTML crawler ("analizzare pagina web"). The target is not an
HTML site: /bandi is an Angular single-page application. The served HTML is a 37 KB
shell containing no tender data at all — scraping it returns nothing.
All data is delivered by a JSON API that the SPA's own BackendService calls. That API is
undocumented, so the contract was reverse-engineered from the production bundle
(main.<hash>.js) and verified against live responses. See
endpoints.py for the full contract.
Two consequences that drive most of this codebase:
- Two incompatible pagination models coexist.
/avvisiis offset-based (SpringPage, with totals);/avvisi-full-textis cursor-based — page N+1 requires the token returned with page N. Conflating them silently returns page 0 forever. - Documents are not on ANAC's domain.
documenti_di_gara_linkpoints at arbitrary third-party contracting-authority portals, and never directly at a file — verified across a full day of notices, zero links end in.pdf. Reaching a PDF therefore needs an HTML hop, and every such fetch is an untrusted-URL fetch (SSRF surface).
Quick start
python -m venv .venv && .venv/Scripts/pip install -e ".[dev]"
.venv/Scripts/python -m anac_crawler # speaks MCP over stdio
Register with an MCP client:
{
"mcpServers": {
"anac-crawler": {
"command": "C:/path/to/.venv/Scripts/python.exe",
"args": ["-m", "anac_crawler"],
"env": { "ANAC_LOG_LEVEL": "INFO", "ANAC_RATELIMIT_REQUESTS_PER_SECOND": "4" }
}
}
}
Tools
| Tool | Purpose | Notes |
|---|---|---|
anac_reference_data |
Taxonomies, value bands, publication dates, categories, news | Call first to build valid filters |
anac_search_notices |
Search with offset pagination | Reports totals; use for page jumps |
anac_search_notices_full_text |
Full-text search with cursor pagination | Pass next_token from previous page |
anac_collect_notices |
Multi-page traversal in one call | Hard record/page ceilings |
anac_get_notice |
Single notice by idAvviso |
Flattened record |
anac_get_notice_history |
Revision chronology | Upstream 404 → empty list |
anac_inspect_page |
Fetch an HTML landing page, discover PDF links | Untrusted third-party content |
anac_download_document |
Download a document | Not read-only; writes to disk, egress-guarded |
anac_health |
Breaker state, cache stats, latency percentiles, schema-drift signal |
Typical flow: anac_reference_data → anac_search_notices → anac_inspect_page on a
documenti_di_gara_link → anac_download_document on a discovered PDF.
Requirements coverage
| Requirement | Where |
|---|---|
| Python | 3.11+, src/anac_crawler/ |
| stdio | mcp_server/server.py + stdout guard |
| HTTP GET/POST | http/client.py (request, post_json) |
| Cookies | Persistent JSON jar + Azure ARRAffinity re-scoping |
| Sessions | One pooled, keep-alive AsyncClient shared across all tool calls |
| Web protocol | HTTP/2, conditional requests, Retry-After, redirects, robots.txt |
| Page analysis | documents/html.py (selectolax) |
| Caching | http/cache.py — 2-tier, ETag revalidation, single-flight, stale-if-error |
| Navigation | Unified pagination + bounded traversal in repository.py |
| Targeted file access + PDF download | documents/fetcher.py |
Configuration
Everything is env-driven and validated at startup (ANAC_ prefix) — see
.env.example and config.py.
The knobs that matter most in production:
ANAC_RATELIMIT_REQUESTS_PER_SECOND=4 # politeness toward a public service
ANAC_RATELIMIT_MAX_CONCURRENCY=4
ANAC_DOWNLOAD_HOST_ALLOWLIST= # pin the egress surface (strongly recommended)
ANAC_DOWNLOAD_MAX_BYTES=67108864
ANAC_CA_BUNDLE= # for corporate TLS interception
ANAC_LOG_FORMAT=json # structured logs on stderr
TLS note
Document downloads default to the OS trust store (via truststore), not certifi.
This is functional, not cosmetic: many Italian PA portals serve an incomplete certificate
chain, which OpenSSL cannot resolve but the Windows/macOS verifiers can (via AIA).
Verified against a live portal — certifi fails, the OS store completes TLS 1.3. Details in
http/tls.py.
Verification
.venv/Scripts/python -m pytest -q # 241 tests, no network
.venv/Scripts/python -m mypy src # strict, clean
.venv/Scripts/python -m ruff check src tests scripts
.venv/Scripts/python scripts/smoke_live.py # live API contract check
.venv/Scripts/python scripts/smoke_stdio.py # real JSON-RPC handshake over stdio
smoke_stdio.py is the one that matters most: it spawns the server as a subprocess and
drives it exactly as a client would, which is the only way to catch a stray byte on stdout.
Further reading
- docs/ARCHITECTURE.md — layering, request lifecycle, design decisions
- docs/ENTERPRISE.md — what is production-ready, what is not, and the recommended roadmap with known limitations stated explicitly
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。