Skip to content

CodeAlanDebug/hsm-mcp

Repository files navigation

hsm-mcp

Remote MCP server that mirrors the IND public register of recognised sponsors (Work) — the list of Dutch organisations that can sponsor work / highly-skilled-migrant residence permits — and makes it queryable by AI assistants.

IND publishes the register as a monthly-updated HTML page with no API. This Worker scrapes it daily (write-on-change), stores it in KV with monthly snapshots, and serves two MCP tools over Streamable HTTP:

  • search_sponsors — company name or 8-digit KvK number in, ranked candidate matches out (exact / base-name / substring / fuzzy). Deliberately returns candidates, not a yes/no verdict: the register lists registered legal names, queries use trade names, and the calling LLM is the right judge of whether "Adyen N.V." is the "Adyen" you meant. See docs/adr/0001.
  • get_register_status — register date, scrape health, staleness flag.

Runs entirely on the Cloudflare free tier: Workers + KV + SQLite-backed Durable Objects (McpAgent) + cron + Email Routing for scrape-failure alerts.

Use

Connect the server (no auth; rate limited to 30 req/min per IP):

Claude Code

claude mcp add --transport http ind-sponsors https://hsm.codealan.com/mcp

claude.ai / Claude Desktop — Settings → Connectors → Add custom connector → https://hsm.codealan.com/mcp

Any MCP client (Cursor, etc.)

{ "mcpServers": { "ind-sponsors": { "url": "https://hsm.codealan.com/mcp" } } }

Then just ask in plain language — the assistant calls the tools itself:

  • "Is Adyen a recognised sponsor for highly skilled migrants?"
  • "Check these five companies from my shortlist against the IND register."
  • "KvK 56317441 — who is this and can they sponsor?"
  • "How fresh is the sponsor data?" (hits get_register_status)

Reading the answers: tools return ranked candidates, never a yes/no verdict. A base_name/exact_name match at score ≥0.95 is a solid yes; a KvK-number match is identity-grade; an empty result is not a "no" — the register lists registered legal names while companies go by trade names, so try the legal name (often ends in B.V./N.V.) or the KvK number. Always confirm against the official register before acting on it.

Architecture

flowchart LR
    client["AI client<br/>(Claude, any MCP client)"]
    ind["ind.nl Work register<br/>(HTML page, updated ~monthly)"]
    inbox["Owner's inbox"]

    subgraph gh["GitHub Actions"]
        ci["CI: push to main<br/>test → deploy"]
        health["Health check (daily)<br/>GET /health, 503 = stale"]
    end

    subgraph cf["Cloudflare (free plan)"]
        worker["Worker: routing +<br/>30 req/min per-IP rate limit"]
        mcpdo["HsmMcp Durable Object<br/>McpAgent tools:<br/>search_sponsors · get_register_status<br/>(cached fuzzy-match index)"]
        scrdo["Scraper Durable Object<br/>parse + sanity gates<br/>(DO dodges 10 ms Worker CPU cap)"]
        kv[("KV<br/>register:current<br/>snapshot:YYYY-MM-DD<br/>status")]
        cron["Cron 06:17 UTC daily"]
        mail["Email Routing"]
    end

    client -- "MCP (streamable HTTP) /mcp" --> worker --> mcpdo
    mcpdo -- "read + cache per version" --> kv
    cron --> scrdo
    worker -- "POST /admin/scrape (bearer)" --> scrdo
    scrdo -- "fetch HTML" --> ind
    scrdo -- "write-on-change<br/>+ monthly snapshot" --> kv
    scrdo -- "3 consecutive failures" --> mail --> inbox
    health -- "probe" --> worker
    ci -- "wrangler deploy" --> worker
Loading

Key design points, in one breath: the scraper polls daily but writes only when IND's own "last updated" date changes; sanity gates (row count, KvK format, date parse) mean a broken scrape can never replace good data with garbage; searches run against an in-memory index rebuilt only when the register version changes; and every snapshot is kept forever so a future diff feature can answer "when did company X appear?".

Path What happens
Query client → /mcpHsmMcp DO → ranked candidates from cached index
Refresh cron (or POST /admin/scrape) → Scraper DO → gates → KV
Monitoring GitHub Action probes /health daily; 503 fails the run → GitHub emails the owner
Deploy push to main → tests → wrangler deploy (config from repo secrets)

Setup, file-by-file architecture notes, and deploy steps: see CLAUDE.md.

Unofficial project; data © IND, republished for easier checking. Always verify with the official register before acting on it.

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors