DEV Community

Martin Havel
Martin Havel

Posted on

Building MCP servers for a country that isn't in the dataset (Czech gov APIs)

Abstract

In December 2025, Anthropic donated the Model Context Protocol to the Linux Foundation. MCP is now a vendor-neutral standard — OpenAI, Block, and dozens of IDEs ship with it. The registry has ~300 servers. Most of them are for English-first ecosystems: GitHub, Slack, Linear, Notion.

Nobody had written an MCP server for Czech government or business data. So I spent an evening doing it. Two servers live, published to npm and the official registry. This is what I learned about adapting MCP to a locale the tooling wasn't designed for.

TL;DR


1. The context problem

If you ask Claude about GitHub, it knows what GitHub is. It ships with an MCP server for it. Same for Postgres, Puppeteer, Stripe, Slack.

If you ask Claude about ARES (the Czech Business Register), it doesn't know. It's gov data for 10M people. The API is public and documented. But there's no MCP adapter. So every time you want your Czech tax assistant to pre-fill a company address from an IČO, you have to either:

  1. Write a one-off REST wrapper in your agent code.
  2. Tell Claude "call GET https://ares.gov.cz/... with these params, then parse the JSON."

Neither scales past one agent. MCP solves this for English-speaking SaaS — it should solve it for everyone else too.

2. The Czech Business ID gotcha (MOD11)

Every Czech company has an IČO — 8-digit identifier. But not every 8-digit number is valid. The last digit is a MOD11 checksum:

export function isValidIco(input: string | number): boolean {
  const ico = String(input).padStart(8, '0');
  const digits = ico.split('').map(Number);
  let sum = 0;
  for (let i = 0; i < 7; i++) sum += digits[i] * (8 - i);
  const rem = sum % 11;
  const expected = rem === 0 ? 1 : rem === 1 ? 0 : 11 - rem;
  return expected === digits[7];
}
Enter fullscreen mode Exit fullscreen mode

This is the kind of knowledge that's obvious if you've built Czech software and invisible otherwise. An MCP server that accepts IČO input must validate before hitting the upstream API — otherwise every typo wastes a round-trip and risks getting your origin IP blacklisted.

This is reason #1 an MCP wrapper has real value over "tell the LLM the REST path": it encodes domain knowledge the LLM won't know.

3. The ARES API that lies

ARES has an OpenAPI spec at https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/v3/api-docs. If you follow it to build a client, your requests will return 404.

The actual endpoints are at a different path than the spec claims. For example:

  • Docs say: GET /v3/ekonomicky-subjekt/{ico}
  • Reality: GET /ekonomicke-subjekty/{ico} (no /v3/, plural noun)

It took an hour of DevTools staring to figure out. I put the correct paths in the MCP server and cached the gotcha in repo docs. That's reason #2 — downstream consumers don't re-discover the mismatch.

4. DNS authentication for the MCP registry

The MCP registry (registry.modelcontextprotocol.io) supports four auth methods: GitHub interactive, GitHub OIDC, DNS proof, HTTP proof. For a custom namespace like dev.cz-agents/* (reverse-DNS, so the domain name claims ownership), DNS is cleanest.

How it works:

  1. Generate Ed25519 keypair: openssl genpkey -algorithm ED25519 -out private.pem
  2. Add TXT record at the root domain (not _mcp. as some docs suggest):
   cz-agents.dev.  TXT  "v=MCPv1; k=ed25519; p=<base64 public key>"
Enter fullscreen mode Exit fullscreen mode
  1. mcp-publisher login dns --domain cz-agents.dev --private-key <hex> — the CLI signs a challenge, the registry fetches the TXT and verifies.
  2. mcp-publisher publish packages/ares/server.json

Two things caught me off-guard:

  • The public key must be base64, not hex (documentation was ambiguous).
  • The registry's DNS resolver has a negative cache of ~60 seconds. If you tried to log in before adding the TXT, you'll get no such host for a minute even after the record propagates.

5. Rate limiting against yourself

The ARES servers run on infrastructure paid for by Czech taxpayers. Hammering them from an AI agent is rude and eventually gets your IP blacklisted.

I built two layers:

  • Application rate limit per client IP (60 req/min, token bucket in memory), reading CF-Connecting-IP when behind Cloudflare. Returns 429 + Retry-After header.
  • Response cache — ARES company data changes maybe monthly. I cache by IČO for 1 hour. For a batch of 100 agents asking about the same invoice, that's 1 upstream call instead of 100.

If you're building an MCP server over a public API, these aren't optional. Your origin IP is a shared resource.

6. The scope problem

ARES is well-documented. But once you step beyond it, Czech gov APIs range from "poorly documented but functional" to "only SOAP, service has been down for 18 months" to "the data is behind an Angular SPA with no backend API".

I spent 15 minutes probing ISIR (insolvency register) and found: REST endpoints return 404, SOAP is unreachable, the "open data portal" is a React app that fetches its own JSON via undocumented URLs. I dropped it from v1.

Lesson for other locales: start with the APIs that have OpenAPI specs and no auth. Business registries, currency rates, postal codes. Save the complicated ones for v2 when you have community pressure and can involve users in testing.

7. What's live

  • @czagents/ares — 9 tools: lookup_by_ico, search_companies, search_by_address, search_by_nace, get_statutaries, validate_dic, check_vat_payer, get_bank_accounts, get_history.
  • @czagents/cnb — 3 tools: get_rates, convert, get_rate.
  • Streamable HTTP at https://ares.cz-agents.dev/mcp and https://cnb.cz-agents.dev/mcp.
  • Rate-limited, cached, MIT licensed.

8. Why bother — the real answer

I build two Czech SaaS products: Kniha Jízd (mileage logbook) and Šiml (tax assistant). Both need ARES lookups daily — "fill in the address of this company by its IČO" was hundreds of lines of custom integration code per product.

Now it's one config line in claude_desktop_config.json or a three-line fetch from my own SDK. The open-source version is a side-effect of building my own tooling for my own products. That's the best reason to open source: you were going to write it anyway.

If you're building software for a non-English-first locale, I'd love to see a /fr-agents, /de-agents, /jp-agents drop in the coming months. The pattern is portable.


Questions, feedback, PRs welcome at github.com/martinhavel/cz-agents-mcp. If your country has similar public registries and you want a hand designing the namespace, ping me.

Top comments (0)