Format, validate and normalize international phone numbers, work out exactly how to dial one country from another, clean phone columns in CRM or contact exports, and find calling windows across time...
Phone numbers look simple and are not. A model working from memory will confidently strip a leading zero that should have stayed, guess a country from an area code that three countries share, or declare a number "valid" when all it did was count digits. Those mistakes are expensive: they land in CRMs, SMS sends and calendar invites, and nobody notices until a message doesn't arrive.
This skill routes that work to countrycalling.codes, a free, no-auth service whose answers carry their own provenance — every result says which numbering metadata release or which reviewed country guide produced it, and what it does not establish.
Everything below is mechanics. These are the parts that change the quality of an answer.
1. Get country context before parsing. A number starting + carries its own country.
A national-format number (020 7946 0123, (555) 123-4567) does not, and no amount of
inference fixes that. Pass numberCountryCode. If the user hasn't said, ask — or call the
operation anyway and surface the needs-context status with its requiredContext list,
which names the exact field that would resolve it. Guessing here is how a UK number
becomes a US number.
2. "Well-formed" is not "real". Every response carries a limitations array, and it
means what it says: this service checks structure against numbering plans. It does not
know whether a number is allocated, active, reachable, owned by the person you think, or
consented for contact. There is no carrier query, no HLR/HSS lookup, no MNP database.
When reporting back, say "matches the UK numbering plan", not "is a valid number" — the
second phrasing is what gets a user to send 4,000 messages into the void.
3. Ambiguity is an answer, not a failure. Statuses needs-context, ambiguous and
unsupported are normal, correct outcomes with reasonCodes explaining them. Report them
as findings. A skill that always produces a confident number is worse than one that says
"these 40 rows are unambiguous, these 3 need a country".
The service is free and needs no key. Two ways in — prefer MCP when it's connected, since the tools are typed and the agent gets structured results directly.
MCP (Streamable HTTP, no auth):
{
"mcpServers": {
"country-calling-codes": {
"type": "streamable-http",
"url": "https://www.countrycalling.codes/api/mcp"
}
}
}
REST, when MCP isn't wired up. Every MCP tool has an exact REST twin returning the identical payload, so nothing below changes:
curl -s https://www.countrycalling.codes/api/phone/analyze \
-H 'Content-Type: application/json' \
-d '{"phoneNumber":"020 7946 0123","numberCountryCode":"GB"}'
POST bodies must be uncompressed application/json and under 64 KiB. GET endpoints
support ETags, so cache capabilities and input-rule reads rather than refetching.
| The task | MCP tool | REST |
|---|---|---|
| Is this country covered? What are the limits? | get_phone_capabilities |
GET /api/capabilities |
| Parse / normalize one number to E.164 | analyze_phone_number |
POST /api/phone/analyze |
| Clean a column of numbers (≤100 at a time) | normalize_phone_numbers |
POST /api/phone/batch |
| Are these two numbers the same person? | compare_phone_numbers |
POST /api/phone/compare |
| Which country is this number from? | resolve_phone_region |
POST /api/phone/region |
| How do I dial X from Y? | get_dialing_instructions |
POST /api/dialing/instructions |
| Why was this number rejected? | diagnose_dialing_problem |
POST /api/phone/diagnose |
| Placeholder / hint text for a form field | get_phone_input_rules |
GET /api/countries/{code}/phone-input |
| When can these people all take a call? | find_calling_windows |
POST /api/calling-windows |
| Look up a country's calling code | lookup_country_calling_code |
GET /api/calling-codes?q=... |
| Light formatter (prefer analyze for evidence) | format_international_phone_number |
POST /api/phone/format |
Start with get_phone_capabilities when a task spans countries you're unsure about. It
reports, per country, whether coverage is reviewed-guide-and-metadata (full editorial
dialing guidance), numbering-metadata (structural checks only) or directory-only. That
one call prevents promising dialing instructions the service can't give.
Full parameter and response detail lives in references/api.md — read it before
constructing a batch, a comparison or a calling-window request, since those three have
shape constraints that are easy to get wrong.
Every operation returns the same envelope:
{
"meta": { "apiVersion": "1.4.0", "datasetVersion": "...", "processing": "ephemeral", "liveCarrierCheck": false },
"result": { "...": "the actual answer" },
"limitations": ["..."]
}
Inside result, the fields that carry the most weight:
status — analyzed, needs-context, ambiguous, unsupported, invalidcanonicalInternational — the E.164 string; this is what belongs in a databasenationalDisplay / internationalDisplay — what belongs in front of a humancountryCandidates — more than one entry means a shared numbering plan (+1, +7,
+44), so the country is genuinely undetermined without more contextreasonCodes and requiredContext — why it stopped and what would unblock itevidenceIds → evidence[] — publisher, title, URL and review date. Cite these when a
user asks "says who?"; add "detail": "full" to expand them inline.Cleaning a contact export. Don't loop one number at a time — batch. Give every row a
stable id so results map back, keep batches ≤100, and set findDuplicates: true when
deduplication matters. Rows that fail come back as individual errors rather than killing
the batch; rows that are the same number in different formats come back grouped, with
extensionsDiffer flagged so you never silently merge +1...x201 into +1...x202.
For anything past a couple of hundred rows, run scripts/normalize_csv.py — it chunks,
retries, preserves the original column and writes the E.164 result alongside it, so a
human can diff before committing. Reinventing that loop each time wastes turns and tends
to drop the error rows.
Answering "how do I call X from Y". Pass origin and destination as separate fields and
let the service assemble the dial string; never concatenate an exit prefix yourself. It
handles the cases that trip people up: mobile handsets use + rather than an exit prefix,
+1-to-+1 is a shared-plan route and not an international one, and domestic landline
routing can depend on locality — in which case it deliberately returns guidance with a
display pattern instead of asserting a dial string it can't stand behind. Reviewed
dialing guidance exists for AE, AU, BR, CN, DE, FR, GB, IN, IT, JP, SG, US and ZA; other
pairs return unsupported, which is the honest answer.
Scheduling across time zones. find_calling_windows takes explicit IANA zones
(Europe/London, not GMT or UTC+1), ISO weekdays where Monday is 1, and a date range
of 1–31 days for up to 8 participants. It walks real UTC minutes, so overnight shifts and
DST transitions come out right. It does not know about public holidays — say so when
presenting results rather than letting a user book over Christmas.
Lead with the answer, then the caveat — not the reverse. "All 47 rows normalized; 3 need a country before they can be resolved" is useful. A paragraph of disclaimers before the numbers is not.
Two things are worth carrying into the final message every time, because they change what
the user does next: that formatting doesn't establish reachability, and which rows the
service declined to guess on. Everything else in limitations can stay in the tool output.
Phone numbers are personal data. The service processes them without storing them, but the surrounding conversation and any files written are a different matter — don't paste a full contact list into a summary when a count and the exceptions will do.