{"slug": "i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-data", "title": "I Stopped Scraping Business Directories and Built an MCP Server on Official Registry Data", "summary": "A developer replaced fragile web scraping of business directories with an MCP server built on official open data from Romania's ONRC registry, covering 4.2 million firms. The server exposes three tools—lookup_business, extract_contacts, and a third—that decode registry codes and enrich results, enabling AI agents to query company data reliably. The project demonstrates a pattern for leveraging CKAN-backed government portals as clean data sources.", "body_md": "For a long time I built B2B lead-generation tools the way everyone does: scrape a directory, fight the anti-bot, re-scrape when the layout changes, pray the data is current. Then I found out the Romanian government publishes the *entire* company registry as open data — 4.2 million firms, refreshed monthly, with legal status, activity codes and directors. No scraping required.\n\nThis post is the story of that switch: how I turned an official open-data snapshot into a self-hosted MCP server with three tools that actually do work for an AI agent.\n\nIf you want to build a list of Romanian companies — say, every SRL in Cluj with a CAEN code for software services — you have three options:\n\nOption three is the one nobody seems to talk about.\n\nThe ONRC open-data programme publishes monthly CSV snapshots:\n\nThat's the whole registry, structured, and licensed for reuse. No login, no API key, no rate limit, no anti-bot. The fragility is gone: the government keeps the source fresh, and I just re-download the monthly snapshot.\n\nThe catch is that \"open data\" is not \"clean data\". Those CSVs are enormous (the firms file alone is ~690 MB), use `^`\n\nas a delimiter, carry a BOM, encode Romanian diacritics, and store statuses and activities as **codes** that mean nothing without the nomenclator tables. That's the real engineering work.\n\nThe pattern I landed on works for any CKAN-backed government portal:\n\n`GET {portal}/api/3/action/package_search`\n\nfinds the ONRC organization; `package_show`\n\nreturns the resource file URLs.`curl -r 0-1200`\n\n) reads the CSV header before committing to a download.`csv.reader`\n\n+ `executemany`\n\nin batches of 5,000 rows, with `errors=\"replace\"`\n\nfor diacritics.`LEFT JOIN`\n\nat query time to decode status and CAEN codes.The whole thing lives in a small Python package — a loader script, a nomenclator loader, and a FastMCP server. The MCP server (Streamable HTTP) runs on a homelab box and exposes three tools.\n\n`lookup_business`\n\n— the registry search\nSearch by name or CUI (tax ID). Digits hit the exact CUI index; anything else is a case-insensitive `LIKE`\n\non the name. Each result is enriched with decoded CAEN activities, directors, and status — the nomenclator joins are what make it useful. A real call:\n\n```\nlookup_business(\"Dedeman\")\n{\n  \"query\": \"Dedeman\",\n  \"total\": 2,\n  \"results\": [\n    {\n      \"companyName\": \"DEDEMAN SRL\",\n      \"cui\": \"2816464\",\n      \"registrationCode\": \"J1992002621040\",\n      \"registrationDate\": \"05/11/1992\",\n      \"legalForm\": \"SRL\",\n      \"euid\": \"ROONRC.J1992002621040\",\n      \"address\": \"Municipiul Bacău, Bacău, Str. ALEXEI TOLSTOI, 8, 600093\",\n      \"county\": \"Bacău\",\n      \"website\": \"www.dedeman.ro\",\n      \"caenActivities\": [\n        { \"code\": \"0125\", \"activity\": \"Cultivarea altor pomi fructiferi, a arbuștilor fructiferi, căpșunilor și a nuciferelor\" },\n        { \"code\": \"1610\", \"activity\": \"Tăierea și rindeluirea lemnului\" }\n      ],\n      \"directors\": [\"PAVAL I. DRAGOS\", \"BRINZEA S. STEFAN\"],\n      \"status\": [{ \"code\": \"1048\", \"name\": \"funcțiune\" }],\n      \"source\": \"onrc\"\n    }\n  ]\n}\n```\n\nEverything is decoded: `1048`\n\nis \"funcțiune\" (active), the CAEN codes come back as readable activities. An agent can ask \"what does this company actually do?\" and get a straight answer.\n\n`extract_contacts`\n\n— find the humans\nOnce you know a company exists, you need the contact points. This tool crawls the company website (bounded to a few pages, prioritising contact/about pages) and extracts emails, phone numbers and social profiles. It filters aggressively — no image files, no `example.com`\n\nplaceholders, no `noreply@`\n\n— and matches emails against the site's own domain to cut the noise:\n\n```\nextract_contacts(\"https://www.bitdefender.ro\")\n{\n  \"url\": \"https://www.bitdefender.ro\",\n  \"domain\": \"bitdefender.ro\",\n  \"pagesCrawled\": 3,\n  \"emails\": null,\n  \"phones\": null,\n  \"facebook\": \"https://www.facebook.com/bitdefender\",\n  \"twitter\": \"https://twitter.com/bitdefender\",\n  \"instagram\": \"https://www.instagram.com/bitdefender\",\n  \"linkedin\": \"https://www.linkedin.com/company/bitdefender\",\n  \"youtube\": \"https://www.youtube.com/c/Bitdefender\",\n  \"socialLinks\": [\n    \"https://www.facebook.com/bitdefender\",\n    \"https://www.twitter.com/bitdefender\",\n    \"https://www.instagram.com/bitdefender\",\n    \"https://www.linkedin.com/company/bitdefender\",\n    \"https://www.youtube.com/c/Bitdefender\"\n  ],\n  \"error\": null\n}\n```\n\nHonest limitations: corporate homepages often carry no public email (hence `emails: null`\n\nhere), and obfuscated emails (Cloudflare's `data-cfemail`\n\n, `name [at] domain [dot] com`\n\n) need decoders. The tool handles both, but you learn to expect gaps on big corporate sites — the SMB sites are where the gold is.\n\n`lookup_domain`\n\n— verify before you call\nEmail validation is a lead-gen step most people skip. This tool wraps WHOIS + DNS + SPF/DMARC so an agent can check a domain before adding it to a list — is it registered, who owns it, does it even have mail?\n\n```\nlookup_domain(\"dedeman.ro\")\n{\n  \"domain\": \"dedeman.ro\",\n  \"whois\": {\n    \"registrar\": \"ICI - Registrar\",\n    \"creationDate\": \"2001-04-23\",\n    \"nameServers\": [\"ns1.dedeman.ro\", \"ns2.orange.ro\"]\n  },\n  \"dns\": {\n    \"A\": [\"52.16.150.45\"],\n    \"MX\": [\"5 mx.dedeman.ro.\"]\n  },\n  \"security\": {\n    \"hasSPF\": true,\n    \"spf\": \"v=spf1 mx ip4:91.216.225.16/32 ... -all\",\n    \"hasDMARC\": true\n  }\n}\n```\n\nThat single call tells you the domain is 24 years old, points at a real mail server, and has both SPF and DMARC — a company that takes email seriously. For the ones that fail, you've just saved a bounced email.\n\n`1048`\n\ninto \"funcțiune\". Budget real time for them — they're in a `COD_INMATRICULARE`\n\n) while everyone searches by CUI (tax ID). Get that mapping right or every lookup \"silently fails\".`.CSV`\n\n-suffixed resource names, `errors=\"replace\"`\n\nfor diacritics. A range request to read the header first saves hours.`lookup_business(\"Dedeman\")`\n\nand reason about the result. MCP is the right seam between \"registry data\" and \"agent capability\".I'm packaging the same pipeline as an Apify actor so it can run on demand (Ro Business Data MCP, coming soon to [my Apify account](https://apify.com/darknezz)) — but the whole pattern is portable to any CKAN portal. Poland, France, Germany and most EU states publish similar company registries. If your lead-gen data is scraped from a directory, check whether your government already publishes the real thing. It probably does.\n\nWhile you're here, these might be worth a read:", "url": "https://wpnews.pro/news/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-data", "canonical_source": "https://dev.to/darksider4all_afa2428f63d0/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-registry-data-4caf", "published_at": "2026-08-13 17:05:25+00:00", "updated_at": "2026-08-13 17:19:27.317175+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-infrastructure"], "entities": ["ONRC", "Romania", "FastMCP", "CKAN", "Dedeman", "Bitdefender"], "alternates": {"html": "https://wpnews.pro/news/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-data", "markdown": "https://wpnews.pro/news/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-data.md", "text": "https://wpnews.pro/news/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-data.txt", "jsonld": "https://wpnews.pro/news/i-stopped-scraping-business-directories-and-built-an-mcp-server-on-official-data.jsonld"}}