mcp-inegi 0.1.0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Inaki Sobera (nanisadw3)
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,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-inegi
3
+ Version: 0.1.0
4
+ Summary: Servidor MCP para consultar indicadores oficiales del INEGI: población, inflación (INPC), empleo, PIB e IGAE, a nivel nacional, estatal y municipal
5
+ Author-email: Inaki Sobera <inakisobera8@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/itzamna-mx/mcp-inegi
8
+ Project-URL: Repository, https://github.com/itzamna-mx/mcp-inegi
9
+ Project-URL: Issues, https://github.com/itzamna-mx/mcp-inegi/issues
10
+ Keywords: mcp,mcp-server,inegi,mexico,datos-abiertos,inflacion,inpc,pib,censo,ai-agents,claude-desktop,itzamna
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: mcp>=1.2.0
25
+ Requires-Dist: httpx>=0.27.0
26
+ Requires-Dist: pydantic>=2.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.1; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # mcp-inegi
35
+
36
+ Servidor MCP para que cualquier asistente de IA (Claude, ChatGPT, Gemini, Cursor o un modelo local)
37
+ consulte **indicadores oficiales del INEGI** y conteste citando la fuente: población, inflación,
38
+ empleo, PIB e IGAE, a nivel nacional, estatal y, para datos censales, municipal.
39
+
40
+ Forma parte de [Itzamná](https://inakisobera.me/itzamna/), una familia de servidores MCP de código
41
+ abierto sobre los datos públicos de México. Proyecto independiente, sin afiliación con el INEGI.
42
+
43
+ ## Qué puedes preguntarle
44
+
45
+ - *¿Cuál fue la inflación anual de septiembre?* → 3.45 % (INPC, indicador 910406, periodo 2026/09)
46
+ - *¿Cuánta gente vive en Naucalpan?* → 834,434 personas (Censo 2020, municipio 15057)
47
+ - *¿Qué estados tienen más población desocupada?* → compara las 32 entidades con la ENOE
48
+ - *¿Cómo va el PIB?* → variación anual, PIB real en millones de pesos de 2018 e IGAE mensual
49
+
50
+ Cada respuesta trae el ID del indicador, el periodo, la unidad, la fuente oficial y la fecha de
51
+ última actualización, para que cualquiera la pueda verificar.
52
+
53
+ ## Herramientas
54
+
55
+ | Herramienta | Qué hace |
56
+ |---|---|
57
+ | `indicadores_clave` | Catálogo de indicadores verificados, con su ID y cobertura geográfica |
58
+ | `consultar_indicador` | Último valor o serie de un indicador para el país, un estado o un municipio |
59
+ | `comparar_areas` | El mismo indicador en varios estados o municipios, ordenado de mayor a menor |
60
+ | `buscar_municipio` | Clave de un municipio o alcaldía por nombre, sin importar acentos |
61
+ | `metadatos_indicador` | Nombre oficial, unidad, frecuencia, fuente y periodo cubierto |
62
+
63
+ `consultar_indicador` acepta las claves del catálogo (`INFLACION_ANUAL`, `POBLACION_TOTAL`,
64
+ `TASA_DESOCUPACION`, `PIB_REAL`, `IGAE`…) o cualquier ID numérico del Banco de Indicadores.
65
+ Las áreas se pueden dar por nombre o por clave: estados (`Jalisco`, `CDMX`, `14`) y los 2,478
66
+ municipios y alcaldías (`Naucalpan`, `Juárez, Chihuahua`, `15057`). Si un nombre existe en varios
67
+ estados, como Benito Juárez, el servidor devuelve los candidatos en lugar de adivinar.
68
+
69
+ ## Instalación
70
+
71
+ 1. Pide tu token gratuito en el
72
+ [INEGI](https://www.inegi.org.mx/app/desarrolladores/generatoken/Usuarios/token_Verify).
73
+ 2. Verifica la conexión:
74
+
75
+ ```bash
76
+ export INEGI_TOKEN="tu_token"
77
+ uvx mcp-inegi --test
78
+ ```
79
+
80
+ 3. Agrégalo a tu cliente MCP. En Claude Desktop (`claude_desktop_config.json`):
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "inegi": {
86
+ "command": "uvx",
87
+ "args": ["mcp-inegi"],
88
+ "env": { "INEGI_TOKEN": "tu_token" }
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ En Claude Code:
95
+
96
+ ```bash
97
+ claude mcp add inegi -e INEGI_TOKEN=tu_token -- uvx mcp-inegi
98
+ ```
99
+
100
+ ## Servidor remoto
101
+
102
+ ```bash
103
+ mcp-inegi --transport streamable-http --host 127.0.0.1 --port 8000 --allowed-host tu.dominio
104
+ ```
105
+
106
+ Detrás de un proxy inverso, `--allowed-host` declara el nombre público para la protección contra
107
+ DNS rebinding.
108
+
109
+ ## Notas sobre la API del INEGI
110
+
111
+ Cosas que descubrimos al construirlo (octubre de 2026) y que el servidor ya resuelve por ti:
112
+
113
+ - El banco económico se llama `BIE-BISE`; con `BIE` a secas la API responde que no hay resultados.
114
+ - Se pide siempre la serie completa: con `reciente=true` la API falla en municipios cuando el
115
+ periodo nacional más nuevo (p. ej. 2025) no existe a ese nivel.
116
+ - El token va en la URL, así que el servidor silencia los logs de `httpx` para no filtrarlo.
117
+ - El catálogo de municipios viene incluido en el paquete (`scripts/generar_municipios.py` lo
118
+ regenera desde el Marco Geoestadístico).
119
+ - Las respuestas se guardan en caché una hora (`INEGI_CACHE_TTL`, en segundos).
120
+
121
+ ## Desarrollo
122
+
123
+ ```bash
124
+ uv venv && uv pip install -e ".[dev]"
125
+ pytest
126
+ ruff check .
127
+ ```
128
+
129
+ Licencia MIT.
@@ -0,0 +1,96 @@
1
+ # mcp-inegi
2
+
3
+ Servidor MCP para que cualquier asistente de IA (Claude, ChatGPT, Gemini, Cursor o un modelo local)
4
+ consulte **indicadores oficiales del INEGI** y conteste citando la fuente: población, inflación,
5
+ empleo, PIB e IGAE, a nivel nacional, estatal y, para datos censales, municipal.
6
+
7
+ Forma parte de [Itzamná](https://inakisobera.me/itzamna/), una familia de servidores MCP de código
8
+ abierto sobre los datos públicos de México. Proyecto independiente, sin afiliación con el INEGI.
9
+
10
+ ## Qué puedes preguntarle
11
+
12
+ - *¿Cuál fue la inflación anual de septiembre?* → 3.45 % (INPC, indicador 910406, periodo 2026/09)
13
+ - *¿Cuánta gente vive en Naucalpan?* → 834,434 personas (Censo 2020, municipio 15057)
14
+ - *¿Qué estados tienen más población desocupada?* → compara las 32 entidades con la ENOE
15
+ - *¿Cómo va el PIB?* → variación anual, PIB real en millones de pesos de 2018 e IGAE mensual
16
+
17
+ Cada respuesta trae el ID del indicador, el periodo, la unidad, la fuente oficial y la fecha de
18
+ última actualización, para que cualquiera la pueda verificar.
19
+
20
+ ## Herramientas
21
+
22
+ | Herramienta | Qué hace |
23
+ |---|---|
24
+ | `indicadores_clave` | Catálogo de indicadores verificados, con su ID y cobertura geográfica |
25
+ | `consultar_indicador` | Último valor o serie de un indicador para el país, un estado o un municipio |
26
+ | `comparar_areas` | El mismo indicador en varios estados o municipios, ordenado de mayor a menor |
27
+ | `buscar_municipio` | Clave de un municipio o alcaldía por nombre, sin importar acentos |
28
+ | `metadatos_indicador` | Nombre oficial, unidad, frecuencia, fuente y periodo cubierto |
29
+
30
+ `consultar_indicador` acepta las claves del catálogo (`INFLACION_ANUAL`, `POBLACION_TOTAL`,
31
+ `TASA_DESOCUPACION`, `PIB_REAL`, `IGAE`…) o cualquier ID numérico del Banco de Indicadores.
32
+ Las áreas se pueden dar por nombre o por clave: estados (`Jalisco`, `CDMX`, `14`) y los 2,478
33
+ municipios y alcaldías (`Naucalpan`, `Juárez, Chihuahua`, `15057`). Si un nombre existe en varios
34
+ estados, como Benito Juárez, el servidor devuelve los candidatos en lugar de adivinar.
35
+
36
+ ## Instalación
37
+
38
+ 1. Pide tu token gratuito en el
39
+ [INEGI](https://www.inegi.org.mx/app/desarrolladores/generatoken/Usuarios/token_Verify).
40
+ 2. Verifica la conexión:
41
+
42
+ ```bash
43
+ export INEGI_TOKEN="tu_token"
44
+ uvx mcp-inegi --test
45
+ ```
46
+
47
+ 3. Agrégalo a tu cliente MCP. En Claude Desktop (`claude_desktop_config.json`):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "inegi": {
53
+ "command": "uvx",
54
+ "args": ["mcp-inegi"],
55
+ "env": { "INEGI_TOKEN": "tu_token" }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ En Claude Code:
62
+
63
+ ```bash
64
+ claude mcp add inegi -e INEGI_TOKEN=tu_token -- uvx mcp-inegi
65
+ ```
66
+
67
+ ## Servidor remoto
68
+
69
+ ```bash
70
+ mcp-inegi --transport streamable-http --host 127.0.0.1 --port 8000 --allowed-host tu.dominio
71
+ ```
72
+
73
+ Detrás de un proxy inverso, `--allowed-host` declara el nombre público para la protección contra
74
+ DNS rebinding.
75
+
76
+ ## Notas sobre la API del INEGI
77
+
78
+ Cosas que descubrimos al construirlo (octubre de 2026) y que el servidor ya resuelve por ti:
79
+
80
+ - El banco económico se llama `BIE-BISE`; con `BIE` a secas la API responde que no hay resultados.
81
+ - Se pide siempre la serie completa: con `reciente=true` la API falla en municipios cuando el
82
+ periodo nacional más nuevo (p. ej. 2025) no existe a ese nivel.
83
+ - El token va en la URL, así que el servidor silencia los logs de `httpx` para no filtrarlo.
84
+ - El catálogo de municipios viene incluido en el paquete (`scripts/generar_municipios.py` lo
85
+ regenera desde el Marco Geoestadístico).
86
+ - Las respuestas se guardan en caché una hora (`INEGI_CACHE_TTL`, en segundos).
87
+
88
+ ## Desarrollo
89
+
90
+ ```bash
91
+ uv venv && uv pip install -e ".[dev]"
92
+ pytest
93
+ ruff check .
94
+ ```
95
+
96
+ Licencia MIT.
@@ -0,0 +1,5 @@
1
+ """mcp-inegi: servidor MCP para indicadores oficiales del INEGI."""
2
+
3
+ from .constants import PACKAGE_VERSION
4
+
5
+ __version__ = PACKAGE_VERSION
@@ -0,0 +1,170 @@
1
+ """Resolución de áreas geográficas: país, estados y municipios por nombre o por clave."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import unicodedata
7
+ from functools import lru_cache
8
+ from importlib.resources import files
9
+
10
+ from .constants import AREA_NACIONAL, ESTADOS
11
+
12
+ # Nombres comunes que no coinciden con el nombre oficial del estado.
13
+ _ALIAS_ESTADOS = {
14
+ "nacional": "00",
15
+ "mexico": "00",
16
+ "republica mexicana": "00",
17
+ "estados unidos mexicanos": "00",
18
+ "cdmx": "09",
19
+ "df": "09",
20
+ "distrito federal": "09",
21
+ "edomex": "15",
22
+ "estado de mexico": "15",
23
+ "coahuila": "05",
24
+ "michoacan": "16",
25
+ "veracruz": "30",
26
+ "queretaro de arteaga": "22",
27
+ "nl": "19",
28
+ "bc": "02",
29
+ "bcs": "03",
30
+ }
31
+
32
+ # Prefijos que la gente antepone al nombre del municipio.
33
+ _PREFIJOS = ("municipio de ", "municipio ", "alcaldia de ", "alcaldia ", "delegacion ")
34
+
35
+
36
+ class AreaAmbiguaError(ValueError):
37
+ """El nombre coincide con municipios de varios estados."""
38
+
39
+ def __init__(self, texto: str, candidatos: list[dict[str, str]]):
40
+ self.candidatos = candidatos
41
+ opciones = "; ".join(f"{c['nombre']}, {c['estado']} ({c['clave']})" for c in candidatos[:10])
42
+ extra = f" y {len(candidatos) - 10} más" if len(candidatos) > 10 else ""
43
+ super().__init__(
44
+ f"'{texto}' coincide con {len(candidatos)} municipios: {opciones}{extra}. "
45
+ "Indica el estado ('Benito Juárez, CDMX') o usa la clave de 5 dígitos."
46
+ )
47
+
48
+
49
+ def normalizar(texto: str) -> str:
50
+ """Minúsculas, sin acentos y con espacios simples."""
51
+ sin_acentos = unicodedata.normalize("NFD", texto)
52
+ sin_acentos = "".join(c for c in sin_acentos if unicodedata.category(c) != "Mn")
53
+ return " ".join(sin_acentos.lower().replace(".", " ").split())
54
+
55
+
56
+ _POR_NOMBRE = {normalizar(nombre): clave for clave, nombre in ESTADOS.items()}
57
+ _POR_NOMBRE.update(_ALIAS_ESTADOS)
58
+
59
+
60
+ @lru_cache(maxsize=1)
61
+ def municipios() -> dict[str, str]:
62
+ """Catálogo {clave de 5 dígitos: nombre oficial}, generado por scripts/generar_municipios.py."""
63
+ texto = files("mcp_inegi").joinpath("data/municipios.json").read_text(encoding="utf-8")
64
+ return json.loads(texto)
65
+
66
+
67
+ @lru_cache(maxsize=1)
68
+ def _municipios_normalizados() -> list[tuple[str, str]]:
69
+ return [(clave, normalizar(nombre)) for clave, nombre in municipios().items()]
70
+
71
+
72
+ def _estado(texto: str, *, en_contexto: bool = False) -> str | None:
73
+ """Clave de 2 dígitos de un estado por nombre o número, o None.
74
+
75
+ Con `en_contexto` ("Naucalpan, México") "México" es el estado, no el país.
76
+ """
77
+ t = normalizar(texto)
78
+ if t.isdigit() and len(t) <= 2 and t.zfill(2) in ESTADOS:
79
+ return t.zfill(2)
80
+ if en_contexto and t == "mexico":
81
+ return "15"
82
+ clave = _POR_NOMBRE.get(t)
83
+ return None if clave == AREA_NACIONAL and en_contexto else clave
84
+
85
+
86
+ def _ficha(clave: str) -> dict[str, str]:
87
+ return {"clave": clave, "nombre": municipios()[clave], "estado": ESTADOS[clave[:2]]}
88
+
89
+
90
+ def buscar_municipios(texto: str, estado: str | None = None) -> list[dict[str, str]]:
91
+ """Municipios que coinciden con `texto`, del mejor nivel de coincidencia disponible.
92
+
93
+ Niveles: nombre exacto > el nombre empieza con el texto ("naucalpan" -> "Naucalpan de
94
+ Juárez") > todas las palabras del texto aparecen en el nombre. Se devuelve solo el mejor
95
+ nivel con resultados, para que "Juárez" no arrastre a todos los "Benito Juárez".
96
+ """
97
+ q = normalizar(texto)
98
+ for prefijo in _PREFIJOS:
99
+ if q.startswith(prefijo):
100
+ q = q[len(prefijo):]
101
+ break
102
+ if not q:
103
+ return []
104
+
105
+ clave_estado = None
106
+ if estado:
107
+ clave_estado = _estado(estado, en_contexto=True)
108
+ if clave_estado is None:
109
+ raise ValueError(f"No reconozco el estado '{estado}'.")
110
+
111
+ universo = [
112
+ (clave, nombre)
113
+ for clave, nombre in _municipios_normalizados()
114
+ if clave_estado is None or clave.startswith(clave_estado)
115
+ ]
116
+ palabras = q.split()
117
+ niveles = (
118
+ lambda n: n == q,
119
+ lambda n: n.startswith(q + " "),
120
+ lambda n: all(p in n.split() for p in palabras),
121
+ )
122
+ for coincide in niveles:
123
+ encontrados = [clave for clave, nombre in universo if coincide(nombre)]
124
+ if encontrados:
125
+ return [_ficha(c) for c in encontrados]
126
+ return []
127
+
128
+
129
+ def resolver_area(area: str | None) -> str:
130
+ """Convierte un nombre o clave de área geográfica a la clave que espera la API.
131
+
132
+ Acepta "00"/"nacional", la clave de estado con o sin cero ("9" o "09"), el nombre del
133
+ estado ("Jalisco", "CDMX", "Edomex"), la clave de municipio de 5 dígitos ("15057") o su
134
+ nombre ("Naucalpan", "Benito Juárez, CDMX"). Si un nombre es a la vez estado y municipio
135
+ ("Aguascalientes", "Hidalgo") gana el estado; para el municipio, "Hidalgo, Michoacán".
136
+ "México" a secas se toma como el país; el estado es "Estado de México" o "15".
137
+ """
138
+ if area is None or not str(area).strip():
139
+ return AREA_NACIONAL
140
+ texto = str(area).strip()
141
+
142
+ if texto.isdigit():
143
+ if len(texto) <= 2:
144
+ clave = texto.zfill(2)
145
+ if clave == AREA_NACIONAL or clave in ESTADOS:
146
+ return clave
147
+ elif len(texto) == 5 and texto in municipios():
148
+ return texto
149
+ raise ValueError(
150
+ f"Clave de área '{texto}' no válida. Usa '00' (nacional), 2 dígitos para estado "
151
+ "(p. ej. '09') o 5 dígitos para municipio (estado + municipio, p. ej. '15057')."
152
+ )
153
+
154
+ if "," in texto:
155
+ nombre, _, estado = texto.rpartition(",")
156
+ candidatos = buscar_municipios(nombre, estado)
157
+ else:
158
+ clave = _POR_NOMBRE.get(normalizar(texto))
159
+ if clave is not None:
160
+ return clave
161
+ candidatos = buscar_municipios(texto)
162
+
163
+ if len(candidatos) == 1:
164
+ return candidatos[0]["clave"]
165
+ if candidatos:
166
+ raise AreaAmbiguaError(texto, candidatos)
167
+ raise ValueError(
168
+ f"No reconozco el área '{texto}'. Usa el nombre de un estado o municipio "
169
+ "('Jalisco', 'Naucalpan', 'Benito Juárez, CDMX') o su clave numérica."
170
+ )
@@ -0,0 +1,157 @@
1
+ """Interfaz de línea de comandos para mcp-inegi."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import os
8
+ import sys
9
+
10
+ from . import __version__
11
+ from .client import InegiAPIError, InegiClient
12
+ from .constants import INEGI_TOKEN_HELP_URL
13
+ from .server import create_server
14
+
15
+
16
+ def build_transport_security(allowed_hosts: list[str] | None):
17
+ """Arma la configuración anti DNS-rebinding para los transportes HTTP.
18
+
19
+ Detrás de un proxy inverso el header Host que llega es el nombre público,
20
+ que la librería no conoce: sin declararlo responde 421 a cada petición.
21
+ Devolver None deja que la librería aplique su default para loopback.
22
+ """
23
+ if not allowed_hosts:
24
+ return None
25
+
26
+ from mcp.server.transport_security import TransportSecuritySettings
27
+
28
+ hosts: list[str] = []
29
+ origins: list[str] = []
30
+ for nombre in allowed_hosts:
31
+ # El proxy puede o no incluir el puerto en el Host.
32
+ for entrada in (nombre, f"{nombre}:*"):
33
+ if entrada not in hosts:
34
+ hosts.append(entrada)
35
+ for entrada in (f"https://{nombre}", f"http://{nombre}"):
36
+ if entrada not in origins:
37
+ origins.append(entrada)
38
+
39
+ # Conservar loopback para que las pruebas locales sigan funcionando.
40
+ for entrada in ("127.0.0.1:*", "localhost:*", "[::1]:*"):
41
+ if entrada not in hosts:
42
+ hosts.append(entrada)
43
+ for entrada in ("http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"):
44
+ if entrada not in origins:
45
+ origins.append(entrada)
46
+
47
+ return TransportSecuritySettings(
48
+ enable_dns_rebinding_protection=True,
49
+ allowed_hosts=hosts,
50
+ allowed_origins=origins,
51
+ )
52
+
53
+
54
+ def test_connection(token: str | None = None) -> int:
55
+ """Prueba la conexión a la API del INEGI consultando la inflación anual más reciente."""
56
+ print("=" * 60)
57
+ print(f"mcp-inegi v{__version__} - Verificación de conexión a la API del INEGI")
58
+ print("=" * 60)
59
+
60
+ effective_token = token or os.environ.get("INEGI_TOKEN")
61
+ if not effective_token:
62
+ print("\n[AVISO] No se detectó la variable de entorno INEGI_TOKEN.")
63
+ print(f"Para obtener un token gratuito, visita: {INEGI_TOKEN_HELP_URL}\n")
64
+ print("Ejemplo de uso:")
65
+ print(' export INEGI_TOKEN="tu_token_aqui"')
66
+ print(" mcp-inegi\n")
67
+ return 1
68
+
69
+ client = InegiClient(token=effective_token)
70
+
71
+ async def _run():
72
+ print("Consultando inflación anual (INPC, indicador 910406)...")
73
+ try:
74
+ res = await client.consultar("INFLACION_ANUAL")
75
+ ultimo = res["ultimo"] or {}
76
+ print(f"-> Éxito: periodo {ultimo.get('periodo')}, inflación anual {ultimo.get('valor')} %")
77
+ return 0
78
+ except InegiAPIError as e:
79
+ print(f"[ERROR] Falló la consulta al INEGI: {e}")
80
+ return 1
81
+
82
+ return asyncio.run(_run())
83
+
84
+
85
+ def main() -> None:
86
+ """Punto de entrada principal para el comando mcp-inegi."""
87
+ parser = argparse.ArgumentParser(
88
+ prog="mcp-inegi",
89
+ description=(
90
+ "Servidor MCP comunitario para consultar indicadores oficiales del INEGI. "
91
+ "Parte de Itzamná; proyecto independiente, sin afiliación con el INEGI."
92
+ ),
93
+ )
94
+ parser.add_argument("--version", action="version", version=f"mcp-inegi {__version__}")
95
+ parser.add_argument(
96
+ "--token",
97
+ type=str,
98
+ default=None,
99
+ help="Token de la API de Indicadores del INEGI (o configurar variable INEGI_TOKEN)",
100
+ )
101
+ parser.add_argument(
102
+ "--test",
103
+ action="store_true",
104
+ help="Verifica la conectividad con la API del INEGI y muestra la inflación anual",
105
+ )
106
+ parser.add_argument(
107
+ "--transport",
108
+ choices=["stdio", "sse", "streamable-http"],
109
+ default="stdio",
110
+ help=(
111
+ "Tipo de transporte MCP (por defecto: stdio). "
112
+ "'streamable-http' sirve el endpoint remoto; 'sse' está deprecado."
113
+ ),
114
+ )
115
+ parser.add_argument(
116
+ "--host",
117
+ default="127.0.0.1",
118
+ help="Host de escucha para transportes HTTP (por defecto: 127.0.0.1)",
119
+ )
120
+ parser.add_argument(
121
+ "--allowed-host",
122
+ action="append",
123
+ default=[],
124
+ metavar="HOST",
125
+ help=(
126
+ "Nombre público aceptado en el header Host al correr detrás de un "
127
+ "proxy inverso. Repetible. Sin esto el servidor rechaza con 421."
128
+ ),
129
+ )
130
+ parser.add_argument(
131
+ "--port",
132
+ type=int,
133
+ default=8000,
134
+ help="Puerto de escucha para transportes HTTP (por defecto: 8000)",
135
+ )
136
+
137
+ args = parser.parse_args()
138
+
139
+ if args.test:
140
+ sys.exit(test_connection(args.token))
141
+
142
+ server = create_server(token=args.token)
143
+
144
+ if args.transport == "stdio":
145
+ server.run(transport="stdio")
146
+ else:
147
+ # Detrás de nginx esto se queda en loopback y el proxy publica el TLS.
148
+ server.run(
149
+ transport=args.transport,
150
+ host=args.host,
151
+ port=args.port,
152
+ transport_security=build_transport_security(args.allowed_host),
153
+ )
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()