Cómo construí un MCP para tramites.gub.uy

Hay una pregunta que aparece todo el tiempo cuando uno trabaja con modelos de lenguaje: ¿cómo hacemos para que respondan con información útil, verificable y actualizada, sin depender de lo que "recuerdan"? Para trámites del Estado uruguayo, esa pregunta es especialmente importante. Esta es la solución que armé.

Un trámite puede cambiar de requisitos, costos, oficinas, canales de atención o enlaces oficiales. Si un asistente responde desde memoria, puede sonar convincente y estar mal. Para este tipo de casos, el camino razonable es conectar el modelo a una fuente de datos concreta.

Con esa idea armé MCP Trámites GUB.UY, un servidor MCP open source para consultar trámites publicados en la guía oficial de trámites de Uruguay. El código está en GitHub: github.com/fabdelgado/mcp-tramites-gub-uy.

Hay un detalle personal en este proyecto. En 2015 implementé la norma UNIT 1215 de Accesibilidad Web en el portal tramites.gub.uy desde AGESIC. Diez años después, este MCP permite a agentes de IA operar ese mismo portal. Mismo trámite, distinta capa.

Qué es

Es un servidor escrito en Go que descarga el dump XML mensual publicado por AGESIC en catalogodatos.gub.uy, lo importa a SQLite y expone herramientas MCP para que un LLM pueda buscar y consultar trámites usando datos reales.

La fuente es el dataset abierto AGESIC — Guía de trámites. La API CKAN usada es:

https://catalogodatos.gub.uy/api/3/action/package_show?id=agesic-guia-de-tramites

Los datos son de AGESIC y están publicados bajo licencia Creative Commons Attribution 4.0 International (CC BY 4.0). El código del proyecto está bajo licencia MIT.

Por qué MCP

Model Context Protocol permite exponer herramientas y recursos a clientes compatibles, como Claude Desktop, Claude Code u otros entornos que puedan hablar el protocolo. En lugar de pedirle al modelo que "se acuerde" cómo se renueva un pasaporte, el modelo puede llamar una tool:

search_uruguay_government_procedures(query, organismo?, limit?)

Y después pedir el detalle:

get_uruguay_government_procedure(id)

Cada respuesta incluye:

  • url_oficial
  • last_updated_at
  • atribución a AGESIC
  • licencia de los datos

Eso permite que el asistente responda con más cuidado, cite el enlace oficial y advierta sobre la frescura de la información.

Qué expone el servidor

El MCP server corre sobre transporte HTTP Streamable en /mcp.

Las tools disponibles son:

  • search_uruguay_government_procedures(query, organismo?, limit?)
  • get_uruguay_government_procedure(id)
  • list_uruguay_government_organismos()
  • find_uruguay_procedure_offices(procedure_id, departamento?)

También expone un resource:

tramite://{id}

La idea es que el LLM pueda buscar, ampliar contexto y traer el trámite completo cuando lo necesite.

Arquitectura

El proyecto tiene una arquitectura simple:

CKAN API → XML mensual → ingest streaming → SQLite → FTS5 / embeddings → MCP HTTP

Stack principal:

  • Go
  • Gin para HTTP routing
  • SQLite con modernc.org/sqlite
  • FTS5 para búsqueda léxica
  • OpenAI embeddings para búsqueda semántica
  • mcp-go para el servidor MCP
  • Docker Compose para deploy
  • Volumen persistente para la base SQLite

El ingest descarga el XML real, lo parsea en streaming con encoding/xml, calcula un content_hash por trámite y hace upsert solo cuando detecta cambios.

Si un trámite cambia, se resetea su embedding para regenerarlo. Si un trámite desaparece del XML actual, se marca con soft delete.

Búsqueda

El proyecto implementa tres modos:

FTS5

Búsqueda léxica con SQLite FTS5, tokenizer unicode61 remove_diacritics 2.

Vector search

Embeddings en memoria con similitud coseno.

Hybrid search

Combinación de FTS + vector usando Reciprocal Rank Fusion.

En el servidor MCP, la búsqueda usa FTS5 por defecto. Si la base tiene embeddings cargados y está configurada OPENAI_API_KEY, intenta búsqueda híbrida. Si no puede, cae a FTS5 y lo deja declarado en la respuesta.

Docker y Dokploy

El proyecto está pensado para correr en Docker de forma simple.

El primer arranque puede cargar la base automáticamente:

BOOTSTRAP_INGEST=true

Si la tabla de trámites está vacía, el entrypoint corre:

/app/ingest --if-empty

Después levanta el server. En reinicios posteriores, si la base ya tiene trámites, saltea el ingest y arranca rápido. La base vive en un volumen persistente:

/data/tramites.db

Esto lo hace cómodo para Dokploy: deployás el compose, exponés el puerto, configurás el dominio HTTPS y el endpoint MCP queda disponible en:

https://tu-dominio.com/mcp

Claude Desktop

Para usarlo como custom connector remoto en Claude Desktop o Claude.ai, el endpoint tiene que ser público y accesible por HTTPS. Ejemplo: https://tramites.example.com/mcp.

En Claude Desktop / Claude.ai:

Settings → Customize → Connectors → Add custom connector

Y se agrega como connector web.

Para desarrollo local con Claude Code:

claude mcp add --transport http tramites-gub-uy http://localhost:8080/mcp

Hardening mínimo

Además del flujo principal, el servidor incluye:

  • Rate limit en memoria de 60 requests/min por IP.
  • Logs JSON estructurados.
  • Logs específicos de búsquedas MCP con event=mcp_query.
  • Healthcheck HTTP.
  • Dockerfile multi-stage.
  • GitHub Actions con go build, go test, go vet y staticcheck.
  • Cron de ejemplo para refrescar datos el día 5 de cada mes.

No es una plataforma enorme. Es una pieza chica, mantenible y desplegable.

Ejemplo de uso

Buscar trámites por pasaporte:

curl -sS -X POST https://tramites.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_uruguay_government_procedures","arguments":{"query":"pasaporte","limit":3}}}'

Obtener un trámite por ID:

curl -sS -X POST https://tramites.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_uruguay_government_procedure","arguments":{"id":"606"}}}'

Leerlo como resource MCP:

tramite://606

Algunas decisiones

Quise mantener el proyecto deliberadamente sobrio.

SQLite alcanza perfectamente para este volumen de datos. FTS5 resuelve muy bien la búsqueda textual. Para búsqueda semántica, cargar los embeddings en memoria es más simple que meter una base vectorial adicional, y para unos miles de trámites funciona sin drama.

También preferí que el contenedor arranque con una DB funcional en el primer deploy. En un proyecto como este, la experiencia de despliegue importa: si alguien lo corre en Dokploy, debería poder levantar el servicio y empezar a consultar datos sin tener que ejecutar cinco comandos manuales.

Próximos pasos posibles

Algunas mejoras naturales:

  • Endpoint administrativo para recargar el índice vectorial después de un ingest.
  • Métricas Prometheus.
  • Autenticación opcional para deployments públicos.
  • Panel web simple para explorar trámites.
  • Caché de respuestas MCP frecuentes.
  • Job mensual integrado en el propio contenedor.

Pero la base ya está: datos abiertos, ingest reproducible, búsqueda, MCP y deploy.

Preguntas frecuentes

¿Por qué construir un MCP para trámites en lugar de un chatbot?

Un chatbot vive en un sitio específico y depende de su base de conocimiento. Un MCP es una capa de datos: cualquier cliente compatible (Claude Desktop, Claude Code, ChatGPT con conectores) puede usarlo sin reescribir nada. Y la fuente es oficial: el dataset abierto de AGESIC en catalogodatos.gub.uy.

¿Por qué Go en lugar de Python?

Tres razones: deploy más simple (binario único, sin gestión de dependencias en producción), consumo de memoria bajo para el tamaño del proyecto, y modernc.org/sqlite permite usar SQLite sin CGO, lo que simplifica los builds Docker multi-arquitectura.

¿Es seguro que un agente de IA opere trámites del Estado?

Esta primera versión es read-only: solo consulta información pública del dataset abierto de AGESIC. No inicia trámites ni transmite datos personales. Para versiones operativas con sistemas internos de organismos, aplica todo lo que sé de mis 4 años en AGESIC y mi Máster en Ciberseguridad en curso: autenticación fuerte, autorización granular, trazabilidad completa.

¿Mi organismo o empresa puede pedir un MCP a medida?

Sí. La arquitectura del proyecto (Go + SQLite + FTS5 + MCP HTTP Streamable) es replicable para cualquier dataset interno o API existente. Hago esto como servicio: ver "Desarrollo de agentes y MCPs".

¿Dónde está el código?

En GitHub: github.com/fabdelgado/mcp-tramites-gub-uy. Si trabajás con MCP, datos abiertos o asistentes aplicados a servicios públicos, este tipo de integración es un buen patrón: menos magia, más fuentes verificables.


¿Tu organismo o empresa necesita integrar IA con sistemas existentes? Construyo MCPs y agentes a medida con la misma filosofía: chico, mantenible, desplegable, con fuentes verificables. Ver servicios · Conversemos un proyecto. Para más contexto: IA en sector público uruguayo: estado y oportunidades.