Skip to content

Latest commit

 

History

109 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

📮 Sepomex

The open REST and MCP API for Mexico's postal codes

Every Mexican zip code, state, municipality, city and settlement — one bundled SQLite database, no API key, no rate limit.

Release Build codecov Ruby Rails MCP License

Quick start · Hosted instance · Querying the API · MCP server · Development · Architecture


Why Sepomex?

Looking up a Mexican postal code usually means scraping the official Correos de México site or wrangling a clunky Excel export. Sepomex turns that same official dataset into a clean, paginated JSON API you can query by zip code, state, city, municipality or colony — and, as of v1.0.0, exposes it to AI agents through the Model Context Protocol as well.

The whole catalog (~154k settlements) ships inside the app as a SQLite database, so there's nothing external to provision: clone, load, run.

✨ Features

  • 🔎 Search that fits the question — query zip codes by zip_code, state, city, colony, or any combination, accent- and case-insensitive.
  • 🧭 Four resources — zip codes (settlements), states, municipalities and cities, each with show and paginated index endpoints.
  • 📄 Predictable pagination — every list response carries a meta.pagination block plus Link / X-Total-* headers.
  • 🤖 MCP server built in — the same data as agentic tools over Streamable HTTP (/mcp) and stdio (bin/mcp).
  • 🗃️ Zero external dependencies — bundled SQLite dataset; no Postgres, no Redis, no API keys.
  • 🌐 CORS-friendly & read-only — safe to call straight from the browser.
  • 🐳 Dockerized — multi-stage build, one-command dev environment, CI that ships images to Docker Hub.

No key, no quota, no tracking. Sepomex is a read-only public API over public data. Host your own in minutes — the dataset travels with the code.

🌐 Hosted instance

Sepomex is designed to be self-hosted — the whole catalog ships with the code, so standing up your own copy is a few commands. But if you just want to try it (or wire up a quick prototype), a free public instance runs at:

https://sepomex.kurenn.dev
curl "https://sepomex.kurenn.dev/api/v1/zip_codes?zip_code=64000"

This is a best-effort community instance — a single small server, rate-limited to ~300 requests per 5 minutes per IP, with no uptime or availability guarantee. For production traffic, or anything you depend on, run your own (see Development); it's the same image this instance runs.

🚀 Quick start

All endpoints live under the /api/v1 path — prefix it with your host (the examples below use a local http://localhost:3000):

/api/v1

Grab every settlement for a postal code:

curl "http://localhost:3000/api/v1/zip_codes?zip_code=64000"
{
  "zip_codes": [
    {
      "id": 1,
      "d_codigo": "64000",
      "d_asenta": "Centro",
      "d_tipo_asenta": "Colonia",
      "d_mnpio": "Monterrey",
      "d_estado": "Nuevo León",
      "d_cp": "64000",
      "...": "..."
    }
  ],
  "meta": { "pagination": { "per_page": 15, "total_pages": 1, "total_objects": 1, "links": { "first": "", "last": "" } } }
}

Prefer to run it yourself? Jump to Development. A health probe lives at GET /up.

🧭 Querying the API

Interactive docs: a Swagger UI is served at /api-docs, and the raw OpenAPI 3 spec at /api-docs/v1/swagger.yaml.

Four resources, all under /api/v1:

Resource Endpoint Notes
Zip codes GET /zip_codes Searchable by zip_code, state, city, colony
States GET /states · GET /states/:id GET /states/:id/municipalities for its municipalities
Municipalities GET /municipalities · GET /municipalities/:id
Cities GET /cities · GET /cities/:id

Searching zip codes

Mix and match any of the search parameters — matching is partial and accent-insensitive:

# by postal code
curl "http://localhost:3000/api/v1/zip_codes?zip_code=67173"

# by city / municipality
curl "http://localhost:3000/api/v1/zip_codes?city=monterrey"

# by state
curl "http://localhost:3000/api/v1/zip_codes?state=nuevo%20leon"

# by colony (settlement)
curl "http://localhost:3000/api/v1/zip_codes?colony=del%20valle"

# all together
curl "http://localhost:3000/api/v1/zip_codes?state=nuevo%20leon&city=guadalupe&colony=del%20valle"
Example zip code response
{
  "zip_codes": [
    {
      "id": 1,
      "d_codigo": "01000",
      "d_asenta": "San Ángel",
      "d_tipo_asenta": "Colonia",
      "d_mnpio": "Álvaro Obregón",
      "d_estado": "Ciudad de México",
      "d_ciudad": "Ciudad de México",
      "d_cp": "01001",
      "c_estado": "09",
      "c_oficina": "01001",
      "c_cp": null,
      "c_tipo_asenta": "09",
      "c_mnpio": "010",
      "id_asenta_cpcons": "0001",
      "d_zona": "Urbano",
      "c_cve_ciudad": "01"
    }
  ],
  "meta": {
    "pagination": {
      "per_page": 15,
      "total_pages": 9728,
      "total_objects": 145906,
      "links": {
        "first": "/api/v1/zip_codes?page=1",
        "last": "/api/v1/zip_codes?page=9728",
        "next": "/api/v1/zip_codes?page=2"
      }
    }
  }
}

States, municipalities & cities

curl "http://localhost:3000/api/v1/states"
curl "http://localhost:3000/api/v1/states/1"
curl "http://localhost:3000/api/v1/states/1/municipalities"
curl "http://localhost:3000/api/v1/municipalities/1"
curl "http://localhost:3000/api/v1/cities/1"
Example state & municipality responses
// GET /api/v1/states/1
{ "state": { "id": 1, "name": "Ciudad de México", "cities_count": 16 } }

// GET /api/v1/states/1/municipalities
{
  "municipalities": [
    { "id": 1, "name": "Álvaro Obregón", "municipality_key": "010", "zip_code": "01001", "state_id": 1 },
    { "id": 16, "name": "Xochimilco", "municipality_key": "013", "zip_code": "16001", "state_id": 1 }
  ]
}

Pagination

Every index response is paginated — 15 per page by default, up to 200 via per_page (larger values fall back to 15). Combine per_page with page:

curl "http://localhost:3000/api/v1/zip_codes?per_page=200&page=2"

Each response includes a meta.pagination block:

"meta": {
  "pagination": {
    "per_page": 15,
    "total_pages": 9728,
    "total_objects": 145906,
    "links": {
      "first": "/api/v1/zip_codes?page=1",
      "last": "/api/v1/zip_codes?page=9728",
      "prev": "/api/v1/zip_codes?page=1",
      "next": "/api/v1/zip_codes?page=3"
    }
  }
}

The same numbers are also returned as Link, X-Total-Pages and X-Total-Count response headers.

🤖 MCP server

Sepomex ships a Model Context Protocol server so AI agents (Claude Desktop, Claude Code, Cursor, …) can query the catalog as tools. Every tool reuses the exact models and serializers behind the REST API, so both surfaces stay in sync.

Tool Description Arguments
lookup_zip_code All settlements for one exact CP zip_code (required)
search_zip_codes Filter settlements zip_code, state, city, colony, limit
list_states The 32 states
state_municipalities Municipalities of a state state_id (required)
search_cities Cities by name query, limit

Each tool returns a human-readable summary and structuredContent (the same fields as the REST serializers).

Streamable HTTP — mounted at /mcp, alongside the REST API:

curl -X POST http://localhost:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"lookup_zip_code","arguments":{"zip_code":"64000"}}}'

stdiobin/mcp serves the protocol over stdin/stdout against the local database. Add it to your client config:

{
  "mcpServers": {
    "sepomex": { "command": "/absolute/path/to/sepomex/bin/mcp" }
  }
}

By default bin/mcp uses the development database; set RAILS_ENV=production to serve the bundled production catalog.

🛠️ Development

The dataset lives in a bundled SQLite database, so there's no external service to run. Set up locally with rbenv/ruby or with Docker.

Local (rbenv / ruby)

git clone git@github.com:IcaliaLabs/sepomex.git
cd sepomex
bundle install
bin/rails db:prepare        # create the SQLite database + load the schema
bin/rake data:loadev        # import the ~154k settlements from the bundled CSV
bin/rails server            # http://localhost:3000

Docker

docker compose run --rm development bash   # shell in the dev container
bin/rails db:prepare && bin/rake data:loadev
exit
docker compose up                          # http://localhost:3000

Running specs

bundle exec rspec                 # locally
docker compose run --rm tests     # in the container (as CI does)

Refreshing the data

The bundled dataset tracks the official SEPOMEX export. One command downloads the latest export and regenerates the CSV:

rake data:refresh                       # download the export + rebuild lib/sepomex_db.csv
bin/rails db:reset && rake data:loadev  # rebuild the local DB from it

data:refresh performs the export site's form postback for you (via FetchSepomexExport), so there's no file to download by hand. Already have a CPdescarga.xml? Import it directly instead:

rake "data:import_xml[/path/to/CPdescarga.xml]"   # regenerate lib/sepomex_db.csv

Both stream the ~67 MB XML (it never loads fully into memory) and write the pipe-delimited CSV the loader consumes. A scheduled data-refresh workflow runs data:refresh monthly and opens a PR whenever the data changes.

🧱 Architecture

app/
├── controllers/
│   ├── api/v1/                     # REST endpoints (zip_codes, states, municipalities, cities)
│   ├── concerns/paginatable.rb     # meta.pagination + Link / X-Total-* contract
│   └── mcp_controller.rb           # MCP over Streamable HTTP  →  POST /mcp
├── mcp/sepomex_mcp/                # MCP server + the 5 tools
├── models/                         # ZipCode, State, Municipality, City, FtsZipCode
├── serializers/                    # Active Model Serializers (:json adapter)
└── services/                       # CSV loader + XML→CSV importer
bin/mcp                             # MCP over stdio
lib/sepomex_db.csv                  # bundled dataset (~154k rows)

Stack: Ruby 3.4 · Rails 8.1 (API-only) · SQLite · Puma · Active Model Serializers · mcp

🔁 How it ships

Pushing to main runs the GitHub Actions pipeline (.github/workflows): it builds the Docker testing image, runs the spec suite, then builds and pushes the release image to Docker Hub (icalia/sepomex). The production image bakes the SQLite catalog at build time and can be deployed to any container host; set a SECRET_KEY_BASE in that environment (it isn't baked into the image).

🗺️ Roadmap

  • Optional bearer-token gating for the MCP endpoint.
  • Deeper agentic tooling (specialist agents / plugins).

📓 Changelog

Notable changes are recorded in CHANGELOG.md.

🤝 Contributing

Pull requests are welcome — please branch off main and open one against a separate branch. This project follows the Contributor Covenant.

📄 License

Code and documentation © 2013–2026 Icalia Labs, released under the MIT License.

About

A REST API for the SEPOMEX database

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

103 stars

Watchers

21 watching

Forks

Releases

Packages

Used by

Contributors

Languages