mcp-uruguay 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_uruguay-0.2.1/.gitattributes +2 -0
- mcp_uruguay-0.2.1/.github/workflows/publicar.yml +38 -0
- mcp_uruguay-0.2.1/.github/workflows/tests.yml +42 -0
- mcp_uruguay-0.2.1/.gitignore +8 -0
- mcp_uruguay-0.2.1/CHANGELOG.md +21 -0
- mcp_uruguay-0.2.1/LICENSE +21 -0
- mcp_uruguay-0.2.1/PKG-INFO +160 -0
- mcp_uruguay-0.2.1/README.md +117 -0
- mcp_uruguay-0.2.1/pyproject.toml +49 -0
- mcp_uruguay-0.2.1/server.json +21 -0
- mcp_uruguay-0.2.1/src/mcp_uruguay/__init__.py +3 -0
- mcp_uruguay-0.2.1/src/mcp_uruguay/__main__.py +3 -0
- mcp_uruguay-0.2.1/src/mcp_uruguay/cache.py +108 -0
- mcp_uruguay-0.2.1/src/mcp_uruguay/herramientas.py +335 -0
- mcp_uruguay-0.2.1/src/mcp_uruguay/servidor.py +157 -0
- mcp_uruguay-0.2.1/tests/conftest.py +58 -0
- mcp_uruguay-0.2.1/tests/test_cache.py +93 -0
- mcp_uruguay-0.2.1/tests/test_herramientas.py +147 -0
- mcp_uruguay-0.2.1/tests/test_red.py +48 -0
- mcp_uruguay-0.2.1/tests/test_servidor.py +54 -0
- mcp_uruguay-0.2.1/uv.lock +1115 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: publicar
|
|
2
|
+
|
|
3
|
+
# Se dispara al publicar una Release en GitHub. PyPI y el registro de MCP confían en este
|
|
4
|
+
# workflow por OIDC: no hay tokens ni contraseñas guardadas en el repo.
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publicar:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
18
|
+
- name: Etiqueta, pyproject y server.json dicen la misma versión
|
|
19
|
+
run: |
|
|
20
|
+
version=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
|
|
21
|
+
test "v$version" = "${GITHUB_REF_NAME}" || { echo "etiqueta ${GITHUB_REF_NAME} != pyproject $version"; exit 1; }
|
|
22
|
+
test "$(jq -r .version server.json)" = "$version" || { echo "server.json .version != $version"; exit 1; }
|
|
23
|
+
test "$(jq -r '.packages[0].version' server.json)" = "$version" || { echo "server.json packages[0].version != $version"; exit 1; }
|
|
24
|
+
- name: Tests
|
|
25
|
+
run: uv run --python 3.13 --with pytest --with-editable . pytest -q
|
|
26
|
+
- name: Construir
|
|
27
|
+
run: uv build && uvx twine check dist/*
|
|
28
|
+
- name: Publicar en PyPI
|
|
29
|
+
run: uv publish --trusted-publishing always
|
|
30
|
+
- name: Publicar en el registro de MCP
|
|
31
|
+
run: |
|
|
32
|
+
curl -sL -o mcp-publisher.tar.gz https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_linux_amd64.tar.gz
|
|
33
|
+
echo "$(curl -sL https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/registry_1.8.1_checksums.txt | grep ' mcp-publisher_linux_amd64.tar.gz$' | cut -d' ' -f1) mcp-publisher.tar.gz" | sha256sum -c -
|
|
34
|
+
tar xzf mcp-publisher.tar.gz mcp-publisher
|
|
35
|
+
# PyPI tarda un poco en exponer una versión recién subida; el registro la verifica ahí.
|
|
36
|
+
for i in 1 2 3 4 5 6; do curl -sf "https://pypi.org/pypi/mcp-uruguay/${GITHUB_REF_NAME#v}/json" >/dev/null && break; sleep 20; done
|
|
37
|
+
./mcp-publisher login github-oidc
|
|
38
|
+
./mcp-publisher publish
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
# Una vez por semana contra las fuentes reales: avisa si alguna cambió de forma.
|
|
9
|
+
- cron: "0 9 * * 1"
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
tests:
|
|
17
|
+
if: github.event_name != 'schedule'
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
os: [ubuntu-latest]
|
|
23
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
24
|
+
include:
|
|
25
|
+
- os: windows-latest
|
|
26
|
+
python: "3.13"
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v7
|
|
29
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
30
|
+
- name: Lint
|
|
31
|
+
run: uvx ruff check src tests && uvx ruff format --check src tests
|
|
32
|
+
- name: Tests sin red
|
|
33
|
+
run: uv run --python ${{ matrix.python }} --with pytest --with-editable . pytest -q
|
|
34
|
+
|
|
35
|
+
fuentes-reales:
|
|
36
|
+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
41
|
+
- name: Tests contra las fuentes reales
|
|
42
|
+
run: uv run --python 3.13 --with pytest --with-editable . pytest -q -m red
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Cambios
|
|
2
|
+
|
|
3
|
+
## 0.2.1 — 2026-10-09
|
|
4
|
+
|
|
5
|
+
- Publicado en PyPI (`uvx mcp-uruguay`) y en el registro oficial de MCP como
|
|
6
|
+
`io.github.manusabbath-arch/mcp-uruguay`, con trusted publishing al crear una Release.
|
|
7
|
+
- Depende de `uydatos` desde PyPI (`>=0.2.1,<0.3`) en vez de una etiqueta de GitHub.
|
|
8
|
+
- Insignias y un resumen en inglés en el README.
|
|
9
|
+
|
|
10
|
+
## 0.2.0 — 2026-10-09
|
|
11
|
+
|
|
12
|
+
- **Nueva herramienta: `geocodificar_direccion`**, sobre `uydatos.ideuy` (IDE.uy). Devuelve la
|
|
13
|
+
calidad del resultado y no da coordenada cuando la API devuelve otra calle o el centro de la
|
|
14
|
+
localidad.
|
|
15
|
+
- Pasa a `uydatos` v0.2.0.
|
|
16
|
+
- CI: tests en Python 3.10 a 3.13 y Windows, y una corrida semanal contra las fuentes reales.
|
|
17
|
+
|
|
18
|
+
## 0.1.0 — 2026-10-09
|
|
19
|
+
|
|
20
|
+
Primera versión: 12 herramientas de solo lectura (BCU, pesos constantes, INDEC, IBGE, ANCAP,
|
|
21
|
+
INIA, Salto Grande, RUT y cédula, semanas ISO y catálogo de datos abiertos).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manu Martinez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mcp-uruguay
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Servidor MCP con datos públicos de Uruguay y la región: cotizaciones del BCU, IPC de Argentina y Brasil, combustibles de ANCAP, clima de INIA, estaciones de Salto Grande, geocodificación con IDE.uy, RUT y cédula.
|
|
5
|
+
Project-URL: Repository, https://github.com/manusabbath-arch/mcp-uruguay
|
|
6
|
+
Project-URL: Changelog, https://github.com/manusabbath-arch/mcp-uruguay/blob/main/CHANGELOG.md
|
|
7
|
+
Author: Manu Martinez
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Manu Martinez
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: bcu,datos-abiertos,mcp,mcp-server,open-data,uruguay
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Natural Language :: Spanish
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
37
|
+
Requires-Python: >=3.10
|
|
38
|
+
Requires-Dist: mcp<3,>=2.3
|
|
39
|
+
Requires-Dist: uydatos<0.3,>=0.2.1
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# mcp-uruguay
|
|
45
|
+
|
|
46
|
+
<!-- mcp-name: io.github.manusabbath-arch/mcp-uruguay -->
|
|
47
|
+
|
|
48
|
+
[](https://github.com/manusabbath-arch/mcp-uruguay/actions/workflows/tests.yml)
|
|
49
|
+
[](https://pypi.org/project/mcp-uruguay/)
|
|
50
|
+
[](LICENSE)
|
|
51
|
+
|
|
52
|
+
> **In English:** an MCP server that gives AI agents Uruguayan and regional public data straight
|
|
53
|
+
> from the primary sources: Central Bank of Uruguay exchange rates and inflation-adjusted pesos,
|
|
54
|
+
> Argentina's CPI, Brazil's IPCA, ANCAP fuel prices, INIA weather in Salto, Uruguay River stations,
|
|
55
|
+
> IDE.uy geocoding and RUT/ID validation. Read-only, and every answer carries its source and
|
|
56
|
+
> retrieval time.
|
|
57
|
+
|
|
58
|
+
Servidor [MCP](https://modelcontextprotocol.io) con **datos públicos de Uruguay y la región**,
|
|
59
|
+
leídos de la fuente primaria. Le da a cualquier agente de IA (Claude, o cualquier cliente MCP)
|
|
60
|
+
cotizaciones del BCU, pesos constantes, IPC de Argentina y Brasil, combustibles de ANCAP, clima
|
|
61
|
+
de Salto, estaciones del río Uruguay y validación de RUT y cédula.
|
|
62
|
+
|
|
63
|
+
Está construido sobre [uydatos](https://github.com/manusabbath-arch/uy-datos-abiertos), así que
|
|
64
|
+
hereda su regla: **rechazar en vez de arreglar**. Un agente que recibe un error con motivo puede
|
|
65
|
+
decirlo; uno que recibe un número plausible pero falso lo repite.
|
|
66
|
+
|
|
67
|
+
## Herramientas
|
|
68
|
+
|
|
69
|
+
| Herramienta | Qué responde | Fuente |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `cotizacion` | Dólar, UI, peso argentino o real de un día; si es fin de semana, dice qué día hábil usó | BCU |
|
|
72
|
+
| `cotizacion_mensual` | Promedio mensual, con los días publicados y el mes en curso marcado como parcial | BCU |
|
|
73
|
+
| `pesos_constantes` | Lleva montos de distintos meses a pesos de un mes base, con la Unidad Indexada | BCU |
|
|
74
|
+
| `ipc_argentina` | IPC nacional, base dic-2016 = 100 | INDEC |
|
|
75
|
+
| `ipca_brasil` | IPCA número-índice, base dic-1993 = 100 | IBGE |
|
|
76
|
+
| `combustible_ancap` | Precio máximo al público de cada combustible, por mes | ANCAP |
|
|
77
|
+
| `clima_salto` | Lluvia y temperaturas diarias desde 1970, con la anomalía contra la normal | INIA |
|
|
78
|
+
| `clima_tendencia` | Tendencia de la temperatura media anual, en °C por década | INIA |
|
|
79
|
+
| `estaciones_cercanas` | Estaciones hidrometeorológicas de la cuenca del río Uruguay cerca de un punto | CTM Salto Grande |
|
|
80
|
+
| `geocodificar_direccion` | Coordenadas de una dirección o esquina, con su calidad; descarta otra calle o el centro de la localidad | IDE.uy |
|
|
81
|
+
| `validar_documento` | Dígito verificador de RUT o cédula | sin red |
|
|
82
|
+
| `semana` | Semana ISO (`W41`) de una fecha, con su lunes y su domingo | sin red |
|
|
83
|
+
| `recursos_catalogo` | Archivos vigentes de un dataset de catalogodatos.gub.uy | Catálogo de datos abiertos |
|
|
84
|
+
|
|
85
|
+
**Todas son de solo lectura**, y así lo declaran al cliente.
|
|
86
|
+
|
|
87
|
+
Cada respuesta trae dos campos para citar el dato:
|
|
88
|
+
|
|
89
|
+
- `fuente`: quién lo publica.
|
|
90
|
+
- `bajado`: cuándo se obtuvo la copia que se usó.
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"moneda": "USD",
|
|
95
|
+
"fecha_pedida": "2026-10-04",
|
|
96
|
+
"fecha_cotizacion": "2026-10-02",
|
|
97
|
+
"valor_en_pesos": 40.342,
|
|
98
|
+
"nota": "Se usó la cotización del 2026-10-02, último día publicado antes del 2026-10-04. ...",
|
|
99
|
+
"fuente": "Banco Central del Uruguay, web service de cotizaciones (cotizaciones.bcu.gub.uy)",
|
|
100
|
+
"bajado": "2026-10-09T20:45"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Instalación
|
|
105
|
+
|
|
106
|
+
Requiere [uv](https://docs.astral.sh/uv/). No hace falta clonar nada.
|
|
107
|
+
|
|
108
|
+
**Claude Code**
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
claude mcp add uruguay -- uvx mcp-uruguay
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Claude Desktop y otros clientes** (`claude_desktop_config.json` o el equivalente de tu cliente)
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"mcpServers": {
|
|
119
|
+
"uruguay": {
|
|
120
|
+
"command": "uvx",
|
|
121
|
+
"args": ["mcp-uruguay"]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Después, preguntale al agente cosas como:
|
|
128
|
+
|
|
129
|
+
- *"¿Cuánto valía el dólar el 4 de octubre de 2026?"*
|
|
130
|
+
- *"Vendí 1.200.000 pesos en septiembre de 2025 y 1.450.000 en septiembre de 2026. ¿Cuánto crecí en términos reales?"*
|
|
131
|
+
- *"¿Hizo más calor de lo normal en Salto en enero de 2026?"*
|
|
132
|
+
- *"¿Cuál es la estación meteorológica más cercana a 18 de Julio y Ejido, en Montevideo?"*
|
|
133
|
+
|
|
134
|
+
## Caché
|
|
135
|
+
|
|
136
|
+
Lo que se baja queda en `~/.cache/mcp-uruguay`, o en la carpeta que indique la variable
|
|
137
|
+
`MCP_URUGUAY_CACHE`.
|
|
138
|
+
|
|
139
|
+
- **Lo cerrado no se vuelve a pedir:** un mes ya terminado del BCU no cambia.
|
|
140
|
+
- **Lo vivo vence:** el mes en curso a las 3 horas; el IPC, el IPCA, ANCAP y las estaciones al
|
|
141
|
+
día; el clima a la semana.
|
|
142
|
+
- **Una descarga ilegible nunca reemplaza a una copia buena.**
|
|
143
|
+
|
|
144
|
+
La primera consulta de clima baja la serie completa de INIA (unos 57 archivos) y tarda alrededor
|
|
145
|
+
de un minuto. Las siguientes son instantáneas.
|
|
146
|
+
|
|
147
|
+
## Desarrollo
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
git clone https://github.com/manusabbath-arch/mcp-uruguay && cd mcp-uruguay
|
|
151
|
+
uv run --with pytest --with-editable . pytest # sin red
|
|
152
|
+
uv run --with pytest --with-editable . pytest -m red # contra las fuentes reales
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Los tests del servidor conectan un cliente MCP real en el mismo proceso. Verifican que las 13
|
|
156
|
+
herramientas existan, que todas sean de solo lectura y que un error vuelva con su motivo.
|
|
157
|
+
|
|
158
|
+
## Licencia
|
|
159
|
+
|
|
160
|
+
MIT. Usalo, adaptalo y compartilo; solo mantené el aviso de autoría.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# mcp-uruguay
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.manusabbath-arch/mcp-uruguay -->
|
|
4
|
+
|
|
5
|
+
[](https://github.com/manusabbath-arch/mcp-uruguay/actions/workflows/tests.yml)
|
|
6
|
+
[](https://pypi.org/project/mcp-uruguay/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
> **In English:** an MCP server that gives AI agents Uruguayan and regional public data straight
|
|
10
|
+
> from the primary sources: Central Bank of Uruguay exchange rates and inflation-adjusted pesos,
|
|
11
|
+
> Argentina's CPI, Brazil's IPCA, ANCAP fuel prices, INIA weather in Salto, Uruguay River stations,
|
|
12
|
+
> IDE.uy geocoding and RUT/ID validation. Read-only, and every answer carries its source and
|
|
13
|
+
> retrieval time.
|
|
14
|
+
|
|
15
|
+
Servidor [MCP](https://modelcontextprotocol.io) con **datos públicos de Uruguay y la región**,
|
|
16
|
+
leídos de la fuente primaria. Le da a cualquier agente de IA (Claude, o cualquier cliente MCP)
|
|
17
|
+
cotizaciones del BCU, pesos constantes, IPC de Argentina y Brasil, combustibles de ANCAP, clima
|
|
18
|
+
de Salto, estaciones del río Uruguay y validación de RUT y cédula.
|
|
19
|
+
|
|
20
|
+
Está construido sobre [uydatos](https://github.com/manusabbath-arch/uy-datos-abiertos), así que
|
|
21
|
+
hereda su regla: **rechazar en vez de arreglar**. Un agente que recibe un error con motivo puede
|
|
22
|
+
decirlo; uno que recibe un número plausible pero falso lo repite.
|
|
23
|
+
|
|
24
|
+
## Herramientas
|
|
25
|
+
|
|
26
|
+
| Herramienta | Qué responde | Fuente |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `cotizacion` | Dólar, UI, peso argentino o real de un día; si es fin de semana, dice qué día hábil usó | BCU |
|
|
29
|
+
| `cotizacion_mensual` | Promedio mensual, con los días publicados y el mes en curso marcado como parcial | BCU |
|
|
30
|
+
| `pesos_constantes` | Lleva montos de distintos meses a pesos de un mes base, con la Unidad Indexada | BCU |
|
|
31
|
+
| `ipc_argentina` | IPC nacional, base dic-2016 = 100 | INDEC |
|
|
32
|
+
| `ipca_brasil` | IPCA número-índice, base dic-1993 = 100 | IBGE |
|
|
33
|
+
| `combustible_ancap` | Precio máximo al público de cada combustible, por mes | ANCAP |
|
|
34
|
+
| `clima_salto` | Lluvia y temperaturas diarias desde 1970, con la anomalía contra la normal | INIA |
|
|
35
|
+
| `clima_tendencia` | Tendencia de la temperatura media anual, en °C por década | INIA |
|
|
36
|
+
| `estaciones_cercanas` | Estaciones hidrometeorológicas de la cuenca del río Uruguay cerca de un punto | CTM Salto Grande |
|
|
37
|
+
| `geocodificar_direccion` | Coordenadas de una dirección o esquina, con su calidad; descarta otra calle o el centro de la localidad | IDE.uy |
|
|
38
|
+
| `validar_documento` | Dígito verificador de RUT o cédula | sin red |
|
|
39
|
+
| `semana` | Semana ISO (`W41`) de una fecha, con su lunes y su domingo | sin red |
|
|
40
|
+
| `recursos_catalogo` | Archivos vigentes de un dataset de catalogodatos.gub.uy | Catálogo de datos abiertos |
|
|
41
|
+
|
|
42
|
+
**Todas son de solo lectura**, y así lo declaran al cliente.
|
|
43
|
+
|
|
44
|
+
Cada respuesta trae dos campos para citar el dato:
|
|
45
|
+
|
|
46
|
+
- `fuente`: quién lo publica.
|
|
47
|
+
- `bajado`: cuándo se obtuvo la copia que se usó.
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"moneda": "USD",
|
|
52
|
+
"fecha_pedida": "2026-10-04",
|
|
53
|
+
"fecha_cotizacion": "2026-10-02",
|
|
54
|
+
"valor_en_pesos": 40.342,
|
|
55
|
+
"nota": "Se usó la cotización del 2026-10-02, último día publicado antes del 2026-10-04. ...",
|
|
56
|
+
"fuente": "Banco Central del Uruguay, web service de cotizaciones (cotizaciones.bcu.gub.uy)",
|
|
57
|
+
"bajado": "2026-10-09T20:45"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Instalación
|
|
62
|
+
|
|
63
|
+
Requiere [uv](https://docs.astral.sh/uv/). No hace falta clonar nada.
|
|
64
|
+
|
|
65
|
+
**Claude Code**
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
claude mcp add uruguay -- uvx mcp-uruguay
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Claude Desktop y otros clientes** (`claude_desktop_config.json` o el equivalente de tu cliente)
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"uruguay": {
|
|
77
|
+
"command": "uvx",
|
|
78
|
+
"args": ["mcp-uruguay"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Después, preguntale al agente cosas como:
|
|
85
|
+
|
|
86
|
+
- *"¿Cuánto valía el dólar el 4 de octubre de 2026?"*
|
|
87
|
+
- *"Vendí 1.200.000 pesos en septiembre de 2025 y 1.450.000 en septiembre de 2026. ¿Cuánto crecí en términos reales?"*
|
|
88
|
+
- *"¿Hizo más calor de lo normal en Salto en enero de 2026?"*
|
|
89
|
+
- *"¿Cuál es la estación meteorológica más cercana a 18 de Julio y Ejido, en Montevideo?"*
|
|
90
|
+
|
|
91
|
+
## Caché
|
|
92
|
+
|
|
93
|
+
Lo que se baja queda en `~/.cache/mcp-uruguay`, o en la carpeta que indique la variable
|
|
94
|
+
`MCP_URUGUAY_CACHE`.
|
|
95
|
+
|
|
96
|
+
- **Lo cerrado no se vuelve a pedir:** un mes ya terminado del BCU no cambia.
|
|
97
|
+
- **Lo vivo vence:** el mes en curso a las 3 horas; el IPC, el IPCA, ANCAP y las estaciones al
|
|
98
|
+
día; el clima a la semana.
|
|
99
|
+
- **Una descarga ilegible nunca reemplaza a una copia buena.**
|
|
100
|
+
|
|
101
|
+
La primera consulta de clima baja la serie completa de INIA (unos 57 archivos) y tarda alrededor
|
|
102
|
+
de un minuto. Las siguientes son instantáneas.
|
|
103
|
+
|
|
104
|
+
## Desarrollo
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
git clone https://github.com/manusabbath-arch/mcp-uruguay && cd mcp-uruguay
|
|
108
|
+
uv run --with pytest --with-editable . pytest # sin red
|
|
109
|
+
uv run --with pytest --with-editable . pytest -m red # contra las fuentes reales
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Los tests del servidor conectan un cliente MCP real en el mismo proceso. Verifican que las 13
|
|
113
|
+
herramientas existan, que todas sean de solo lectura y que un error vuelva con su motivo.
|
|
114
|
+
|
|
115
|
+
## Licencia
|
|
116
|
+
|
|
117
|
+
MIT. Usalo, adaptalo y compartilo; solo mantené el aviso de autoría.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-uruguay"
|
|
3
|
+
version = "0.2.1"
|
|
4
|
+
description = "Servidor MCP con datos públicos de Uruguay y la región: cotizaciones del BCU, IPC de Argentina y Brasil, combustibles de ANCAP, clima de INIA, estaciones de Salto Grande, geocodificación con IDE.uy, RUT y cédula."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
authors = [{ name = "Manu Martinez" }]
|
|
9
|
+
keywords = ["mcp", "mcp-server", "uruguay", "datos-abiertos", "open-data", "bcu"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Natural Language :: Spanish",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"mcp>=2.3,<3",
|
|
20
|
+
"uydatos>=0.2.1,<0.3",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
test = ["pytest>=8"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
mcp-uruguay = "mcp_uruguay.servidor:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/manusabbath-arch/mcp-uruguay"
|
|
31
|
+
Changelog = "https://github.com/manusabbath-arch/mcp-uruguay/blob/main/CHANGELOG.md"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/mcp_uruguay"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
addopts = "-m 'not red'"
|
|
43
|
+
markers = ["red: sale a internet contra las fuentes reales; se corre con `pytest -m red`"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 120
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "F", "I", "W"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.manusabbath-arch/mcp-uruguay",
|
|
4
|
+
"title": "Uruguay: datos públicos",
|
|
5
|
+
"description": "Uruguayan public data from primary sources: BCU rates, CPI, fuel, weather, IDE.uy geocoding, RUT.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/manusabbath-arch/mcp-uruguay",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.2.1",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "pypi",
|
|
14
|
+
"identifier": "mcp-uruguay",
|
|
15
|
+
"version": "0.2.1",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Caché en disco de lo que se baja de cada fuente.
|
|
2
|
+
|
|
3
|
+
Lo bajado se guarda tal cual vino, así cada respuesta puede decir **cuándo** se obtuvo el dato.
|
|
4
|
+
Dos reglas:
|
|
5
|
+
|
|
6
|
+
- **Lo cerrado no se vuelve a pedir.** Un mes ya terminado del BCU no cambia.
|
|
7
|
+
- **Lo vivo vence.** El mes en curso, la serie del IPC o el CSV de ANCAP se refrescan cuando el
|
|
8
|
+
archivo es más viejo que su `ttl`.
|
|
9
|
+
|
|
10
|
+
Una descarga que no se puede leer **no queda en la caché**: si quedara, envenenaría todas las
|
|
11
|
+
consultas siguientes con el mismo error.
|
|
12
|
+
|
|
13
|
+
Directorio: la variable `MCP_URUGUAY_CACHE`, o `~/.cache/mcp-uruguay`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
20
|
+
from datetime import date, datetime, timedelta
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Callable, TypeVar
|
|
23
|
+
|
|
24
|
+
from uydatos import bcu
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
HORA = 3600
|
|
29
|
+
DIA = 24 * HORA
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def directorio() -> Path:
|
|
33
|
+
base = os.environ.get("MCP_URUGUAY_CACHE")
|
|
34
|
+
d = Path(base) if base else Path.home() / ".cache" / "mcp-uruguay"
|
|
35
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
return d
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def sello(ruta: Path) -> str:
|
|
40
|
+
"""Cuándo se bajó el archivo, en hora local, para citarlo junto al dato."""
|
|
41
|
+
return datetime.fromtimestamp(ruta.stat().st_mtime).isoformat(timespec="minutes")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def vencido(ruta: Path, ttl: float) -> bool:
|
|
45
|
+
return not ruta.exists() or time.time() - ruta.stat().st_mtime > ttl
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def archivo(ruta: Path, bajar: Callable[[Path], object], leer: Callable[[Path], T], ttl: float) -> tuple[T, Path]:
|
|
49
|
+
"""Devuelve `(leer(ruta), ruta)`, bajando antes si falta o venció.
|
|
50
|
+
|
|
51
|
+
Si la descarga nueva no se puede leer, se descarta y se vuelve a la copia anterior si la
|
|
52
|
+
había. Sin copia anterior, el error sube.
|
|
53
|
+
"""
|
|
54
|
+
ruta.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
if vencido(ruta, ttl):
|
|
56
|
+
nueva = ruta.with_name(ruta.name + ".nuevo")
|
|
57
|
+
try:
|
|
58
|
+
bajar(nueva)
|
|
59
|
+
dato = leer(nueva)
|
|
60
|
+
except Exception:
|
|
61
|
+
nueva.unlink(missing_ok=True)
|
|
62
|
+
if ruta.exists():
|
|
63
|
+
return leer(ruta), ruta
|
|
64
|
+
raise
|
|
65
|
+
nueva.replace(ruta)
|
|
66
|
+
return dato, ruta
|
|
67
|
+
return leer(ruta), ruta
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _meses(desde: date, hasta: date) -> list[date]:
|
|
71
|
+
out, d = [], desde.replace(day=1)
|
|
72
|
+
while d <= hasta:
|
|
73
|
+
out.append(d)
|
|
74
|
+
d = (d + timedelta(days=32)).replace(day=1)
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def bcu_serie(moneda: int, desde: date, hasta: date, hoy: date | None = None) -> tuple[dict[date, float], str]:
|
|
79
|
+
"""La serie diaria de una moneda entre dos fechas, bajando solo los meses que faltan.
|
|
80
|
+
|
|
81
|
+
El mes en curso se vuelve a pedir si el archivo tiene más de 3 horas. Un mes en curso sin
|
|
82
|
+
publicaciones todavía (el 1.º de enero, por ejemplo) no es un error: aporta cero días.
|
|
83
|
+
"""
|
|
84
|
+
hoy = hoy or date.today()
|
|
85
|
+
if desde > hoy:
|
|
86
|
+
raise ValueError(f"{desde} es posterior a hoy ({hoy}): el BCU no publica el futuro")
|
|
87
|
+
hasta = min(hasta, hoy)
|
|
88
|
+
carpeta = directorio() / "bcu"
|
|
89
|
+
carpeta.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
serie: dict[date, float] = {}
|
|
91
|
+
sellos = []
|
|
92
|
+
for mes in _meses(desde, hasta):
|
|
93
|
+
ruta = carpeta / f"{moneda}_{mes:%Y-%m}.xml"
|
|
94
|
+
en_curso = (mes.year, mes.month) == (hoy.year, hoy.month)
|
|
95
|
+
if ruta.exists() and en_curso and vencido(ruta, 3 * HORA):
|
|
96
|
+
ruta.unlink()
|
|
97
|
+
if not ruta.exists():
|
|
98
|
+
bcu.descargar(moneda, f"{mes:%Y-%m}", f"{mes:%Y-%m}", carpeta)
|
|
99
|
+
try:
|
|
100
|
+
serie.update(bcu.leer_crudo(ruta, moneda))
|
|
101
|
+
except bcu.BcuInvalido as e:
|
|
102
|
+
if en_curso and "sin datos" in str(e):
|
|
103
|
+
sellos.append(sello(ruta))
|
|
104
|
+
continue
|
|
105
|
+
ruta.unlink(missing_ok=True) # una respuesta mala no se queda en la caché
|
|
106
|
+
raise
|
|
107
|
+
sellos.append(sello(ruta))
|
|
108
|
+
return dict(sorted(serie.items())), min(sellos) if sellos else ""
|