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.
@@ -0,0 +1,2 @@
1
+ * text=auto eol=lf
2
+ tests/fixtures/** -text
@@ -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,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ *.egg-info/
@@ -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
+ [![tests](https://github.com/manusabbath-arch/mcp-uruguay/actions/workflows/tests.yml/badge.svg)](https://github.com/manusabbath-arch/mcp-uruguay/actions/workflows/tests.yml)
49
+ [![PyPI](https://img.shields.io/pypi/v/mcp-uruguay)](https://pypi.org/project/mcp-uruguay/)
50
+ [![Licencia: MIT](https://img.shields.io/badge/licencia-MIT-blue)](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
+ [![tests](https://github.com/manusabbath-arch/mcp-uruguay/actions/workflows/tests.yml/badge.svg)](https://github.com/manusabbath-arch/mcp-uruguay/actions/workflows/tests.yml)
6
+ [![PyPI](https://img.shields.io/pypi/v/mcp-uruguay)](https://pypi.org/project/mcp-uruguay/)
7
+ [![Licencia: MIT](https://img.shields.io/badge/licencia-MIT-blue)](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,3 @@
1
+ """mcp-uruguay — servidor MCP con datos públicos de Uruguay y la región, sobre `uydatos`."""
2
+
3
+ __version__ = "0.2.1"
@@ -0,0 +1,3 @@
1
+ from mcp_uruguay.servidor import main
2
+
3
+ main()
@@ -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 ""