MCP con Python: permite que una IA consulte tu inventario
Inicia sesión para descargarAprende a construir StockBridge, un servidor MCP con Python que permite a una inteligencia artificial consultar productos, precios y existencias desde un inventario JSON. Descubre cómo funcionan las herramientas MCP, cómo probarlas …
Contenido del tutorial ⌄
- ¿Y si una IA consultara tu inventario en lugar de adivinar?
- La IA consultó el inventario y obtuvo esta respuesta
- Construiremos cinco piezas, paso a paso
- Un laboratorio pequeño y reproducible
- El protocolo que conecta una IA con tus herramientas
- Prepara tu entorno de desarrollo
- Una sola dependencia para comenzar
- Un inventario sencillo, pero estructurado
- ¿Qué información podrá consultar el servidor?
- Convirtamos nuestro inventario en herramientas para una IA
- Cómo valida StockBridge los datos antes de utilizarlos
- 1. Importaciones
- 2. Estructuras de respuesta
- 3. Carga del inventario
- 4. Validaciones y errores
- Dos funciones Python que un asistente puede ejecutar
- ¿Qué hace la última línea de server.py?
- ¿Cuándo se lee el inventario?
- Que funcione una vez no significa que funcione siempre
- Diez verificaciones que protegen nuestro inventario
- Las instrucciones que hacen posibles estas comprobaciones
- Ahora comprobamos el protocolo, no solamente las funciones
- 1. Ejecutar un proceso independiente
- 2. Abrir la sesión con el servidor
- 3. Descubrir herramientas
- 4. Invocar herramientas
- 5. Comprobar un error real
- Ejecutemos las once pruebas
01 · El problema
¿Y si una IA consultara tu inventario en lugar de adivinar?
Imagina que administras una tienda. Necesitas saber qué productos se están agotando, pero no quieres revisar cada referencia manualmente.
La diferencia está en consultar datos mediante una herramienta, no en inventar una respuesta convincente. Para demostrarlo crearemos StockBridge: un servidor MCP local que conecta un asistente con un inventario JSON ficticio.
Así viaja la consulta02 · Una prueba real
✓ Herramienta MCP ejecutadaLa IA consultó el inventario y obtuvo esta respuesta
En Visual Studio Code, GitHub Copilot invocó
productos_por_agotarse con
limite=5.
El resultado coincidió con nuestro archivo de ejemplo:
| Producto | Unidades | Precio (COP) |
|---|---|---|
| Miel 250 g | 0 · Agotado | $18.000 |
| Café en grano 500 g | 2 | $28.000 |
| Café molido 500 g | 3 | $24.000 |
Ran productos_por_agotarse stockbridge (MCP Server)
Usamos la condición stock < limite;
por eso un producto con cinco unidades no aparece.
Los precios y productos son ficticios.
03 · Qué aprenderás
Construiremos cinco piezas, paso a paso
El objetivo no es crear otra tienda: es aprender cómo una IA descubre herramientas, les envía argumentos y recibe información comprobable.
- 01 Inventario JSON Cinco artículos con identificador, precio en COP y existencias.
-
02
Búsqueda de productos
buscar_producto(nombre)encuentra coincidencias parciales. -
03
Alerta de existencias
productos_por_agotarse(limite=5)detecta artículos por debajo del umbral. -
04
Servidor MCP
Un proceso Python local que utiliza
stdio, sin API web. - 05 Validación con IA Pruebas automáticas, MCP Inspector y ejecución real desde un asistente.
04 · Antes de programar
Un laboratorio pequeño y reproducible
El proyecto usa datos ficticios y funciona localmente. No modificará existencias ni dependerá de una tienda real.
¿Necesito pagar una API de IA?
No para construir el servidor ni para probar sus herramientas con MCP Inspector. Una prueba conversacional requiere un cliente compatible y acceso a un modelo.
Versión demostrativa 0.1.0. La validación original se realizó en Windows con Python 3.13.15 y once pruebas locales.
05 · Comprender MCP
El protocolo que conecta una IA con tus herramientas
Un modelo de lenguaje puede interpretar preguntas y generar respuestas, pero no conoce automáticamente el contenido actualizado de los archivos de tu equipo. Para consultar esos datos necesita una forma de acceder a herramientas externas.
Model Context Protocol (MCP) establece una forma estandarizada para que una aplicación de IA descubra herramientas disponibles, les envíe argumentos y reciba sus resultados.
El modelo determina si necesita consultar una herramienta para responder.
Descubre las herramientas, envía las llamadas y recibe las respuestas del servidor.
StockBridge recibe una solicitud y ejecuta la función correspondiente.
Nuestro archivo JSON contiene los productos, las existencias y sus precios.
En StockBridge utilizaremos el transporte
stdio: el cliente inicia un proceso
local y se comunica con él mediante
la entrada y salida estándar.
06 · Preparación
Prepara tu entorno de desarrollo
Para construir nuestro servidor necesitaremos Python 3.11 o superior, un editor como Visual Studio Code y una terminal. Los siguientes comandos están preparados para Windows y PowerShell.
Crearemos una carpeta llamada
stockbridge-mcp y, dentro de ella,
un entorno virtual .venv.
Esto permite instalar las dependencias
sin mezclarlas con las de otros proyectos.
Al completar esta sección tendrás tres archivos fundamentales:
- requirements.txt: versión del SDK que utilizaremos.
- inventario.json: información ficticia de cinco productos.
- server.py: las funciones y el servidor MCP.
Más adelante añadiremos los archivos de pruebas y la configuración del cliente MCP.
mkdir stockbridge-mcp
cd stockbridge-mcp
py -m venv .venv
07 · Dependencias
Una sola dependencia para comenzar
StockBridge utiliza el SDK de MCP para Python. Esta biblioteca proporciona los componentes necesarios para declarar herramientas y atender solicitudes de un cliente compatible.
Crearemos requirements.txt
en la raíz del proyecto.
Fijaremos la versión 2.3.0
para que el ejercicio corresponda
con la implementación que hemos probado.
Fijar una versión reduce diferencias entre instalaciones, aunque no garantiza que todas las plataformas tengan exactamente el mismo entorno.
mcp==2.3.0
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe -m pip check
08 · Modelo de datos
Un inventario sencillo, pero estructurado
Nuestro servidor necesita una fuente de información reproducible. Utilizaremos JSON: un formato que permite almacenar objetos mediante pares de claves y valores.
Cada producto tendrá exactamente cuatro propiedades:
Código único para reconocer el producto.
Texto utilizado durante las búsquedas.
Valor entero expresado en pesos colombianos.
Cantidad disponible de unidades.
[
{
"id": "P001",
"nombre": "Café molido 500 g",
"precio_cop": 24000,
"stock": 3
},
{
"id": "P002",
"nombre": "Miel 250 g",
"precio_cop": 18000,
"stock": 0
},
{
"id": "P003",
"nombre": "Chocolate 250 g",
"precio_cop": 12000,
"stock": 5
},
{
"id": "P004",
"nombre": "Panela 500 g",
"precio_cop": 6000,
"stock": 12
},
{
"id": "P005",
"nombre": "Café en grano 500 g",
"precio_cop": 28000,
"stock": 2
}
]
09 · Leer nuestros datos
¿Qué información podrá consultar el servidor?
El archivo contiene una lista de cinco objetos. Cada objeto representa un producto con su nombre, su precio y la cantidad disponible.
Por ejemplo, el producto P002
tiene stock=0; está agotado.
El producto P003 tiene
stock=5, mientras que
P004 tiene stock=12.
Nuestra herramienta buscará productos cuya cantidad disponible sea estrictamente menor que el límite. Si el límite es cinco, devolverá tres productos: miel, café en grano y café molido.
10 · El servidor MCP
Convirtamos nuestro inventario en herramientas para una IA
Hasta ahora tenemos datos, pero todavía no existe
una forma de consultarlos mediante MCP.
Esa será la responsabilidad de
server.py.
El archivo realizará cuatro tareas:
- Cargar y validar el inventario desde JSON.
- Definir estructuras para los productos y las respuestas.
- Registrar dos herramientas que el cliente MCP podrá descubrir.
-
Iniciar un servidor local
utilizando el transporte
stdio.
Crea un archivo llamado server.py
junto a inventario.json.
Copia el código completo del siguiente bloque.
Después analizaremos cada una de sus partes.
"""StockBridge: inventario de ejemplo consultable mediante MCP local."""
import json
from pathlib import Path
from typing import TypedDict
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
class Producto(TypedDict):
id: str
nombre: str
precio_cop: int
stock: int
class Busqueda(TypedDict):
productos: list[Producto]
total: int
class BajoStock(Busqueda):
limite: int
def cargar_inventario(ruta: Path | None = None) -> list[Producto]:
"""Carga y valida el JSON antes de atender cualquier consulta."""
ruta = ruta or Path(__file__).resolve().with_name("inventario.json")
try:
datos = json.loads(ruta.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(f"No se pudo cargar {ruta}: {exc}") from exc
if not isinstance(datos, list):
raise ValueError("inventario.json debe contener una lista de productos")
ids: set[str] = set()
for numero, producto in enumerate(datos, start=1):
if not isinstance(producto, dict) or set(producto) != {"id", "nombre", "precio_cop", "stock"}:
raise ValueError(f"Producto {numero}: se esperan id, nombre, precio_cop y stock")
if (not isinstance(producto["id"], str) or not producto["id"].strip()
or producto["id"] in ids
or not isinstance(producto["nombre"], str) or not producto["nombre"].strip()
or type(producto["precio_cop"]) is not int or producto["precio_cop"] < 0
or type(producto["stock"]) is not int or producto["stock"] < 0):
raise ValueError(f"Producto {numero}: id/nombre inválido, id duplicado o precio/stock inválido")
ids.add(producto["id"])
return datos
INVENTARIO = cargar_inventario()
mcp = MCPServer("StockBridge", version="0.1.0")
@mcp.tool()
def buscar_producto(nombre: str) -> Busqueda:
"""Busca productos por parte del nombre, sin distinguir mayúsculas; no modifica datos."""
if not isinstance(nombre, str) or not nombre.strip():
raise ToolError("nombre debe ser un texto no vacío")
encontrados = [p for p in INVENTARIO if nombre.strip().casefold() in p["nombre"].casefold()]
return {"productos": encontrados, "total": len(encontrados)}
@mcp.tool()
def productos_por_agotarse(limite: int = 5) -> BajoStock:
"""Lista productos con stock estrictamente menor al límite, incluyendo agotados."""
if type(limite) is not int or limite < 0:
raise ToolError("limite debe ser un entero mayor o igual que cero")
encontrados = sorted((p for p in INVENTARIO if p["stock"] < limite),
key=lambda p: (p["stock"], p["id"]))
return {"productos": encontrados, "total": len(encontrados), "limite": limite}
if __name__ == "__main__":
mcp.run(transport="stdio")
11 · Comprender Python
Cómo valida StockBridge los datos antes de utilizarlos
1. Importaciones
json permite convertir el contenido
del archivo JSON en objetos Python.
Path facilita localizar archivos
y TypedDict describe la estructura
esperada de nuestros diccionarios.
MCPServer proporciona el servidor MCP
y ToolError permite señalar
argumentos inválidos durante una llamada
a una herramienta.
2. Estructuras de respuesta
Producto describe un diccionario
con cuatro campos:
id, nombre,
precio_cop y stock.
Busqueda representa una respuesta
con la lista productos
y el contador total.
BajoStock reutiliza esa estructura
y agrega limite.
3. Carga del inventario
cargar_inventario() construye la ruta
utilizando __file__. Así encuentra
inventario.json junto a
server.py, aunque ejecutemos
Python desde otra carpeta.
read_text(encoding="utf-8") lee
el archivo y json.loads()
interpreta su contenido. Si falta el archivo
o el JSON tiene errores de sintaxis,
se genera una excepción explicativa.
4. Validaciones y errores
Primero comprobamos que el documento sea
una lista. Después recorremos cada producto
con enumerate(), verificando
que sea un diccionario con las cuatro
propiedades esperadas.
El conjunto ids detecta
identificadores repetidos.
Los campos id y nombre
deben ser textos no vacíos.
El precio y las existencias deben ser
enteros no negativos.
La expresión type(valor) is int
es deliberada: en Python,
bool es una subclase de
int. Así evitamos aceptar
True como si fuera un precio
o una cantidad válida.
ValueError. No intenta
corregir silenciosamente datos incorrectos.
12 · Herramientas MCP
Dos funciones Python que un asistente puede ejecutar
Después de cargar el inventario,
inicializamos el servidor con
MCPServer("StockBridge", version="0.1.0").
Su nombre y versión identifican esta
implementación ante el cliente.
El decorador @mcp.tool()
registra una función como herramienta MCP.
Su nombre, descripción, argumentos y anotaciones
de tipos ayudan al cliente a descubrir
cómo utilizarla.
- 01 buscar_producto(nombre) Recibe una cadena de texto y devuelve los productos cuyo nombre contiene la expresión buscada.
- 02 strip() y casefold() strip elimina espacios exteriores; casefold permite comparar sin diferenciar mayúsculas y minúsculas.
- 03 productos_por_agotarse() Recibe un límite entero, que por defecto es cinco, y filtra productos con existencias inferiores a ese valor.
- 04 sorted() y lambda Ordena primero por cantidad disponible. Si dos productos tienen el mismo stock, usa su identificador como segundo criterio.
- 05 Resultados estructurados Las herramientas devuelven diccionarios con productos, total y, cuando corresponde, el límite utilizado en la consulta.
13 · Ejecutar MCP
¿Qué hace la última línea de server.py?
La condición if __name__ == "__main__"
permite distinguir entre ejecutar el archivo
directamente e importarlo desde otro módulo.
Cuando ejecutamos server.py
como programa principal, la instrucción
mcp.run(transport="stdio")
inicia la comunicación MCP mediante
entrada y salida estándar.
El servidor recibe solicitudes enviadas por el cliente MCP.
El servidor entrega las respuestas del protocolo al cliente.
¿Cuándo se lee el inventario?
La instrucción INVENTARIO =
cargar_inventario() se ejecuta
cuando se importa o inicia el módulo.
Por tanto, si editas el archivo JSON
mientras el servidor sigue abierto,
deberás reiniciar el proceso para
cargar esos cambios.
Hasta este punto hemos construido el servidor y explicado todo el código de server.py. Ahora podemos comprobar sus funciones antes de conectarlas a un cliente MCP.
.\.venv\Scripts\python.exe -c "from server import buscar_producto, productos_por_agotarse; print(buscar_producto('CAFÉ')); print(productos_por_agotarse(5))"
14 · Validación del proyecto
Que funcione una vez no significa que funcione siempre
Nuestra primera prueba mostró dos resultados correctos. Pero todavía quedan preguntas importantes: ¿qué ocurre si el usuario escribe espacios?, ¿si el archivo JSON contiene datos inválidos?, ¿si el límite de existencias es negativo?
Para responderlas utilizaremos pruebas automatizadas. Una prueba ejecuta una operación, compara su resultado con lo esperado y señala cualquier diferencia.
Verificaremos búsquedas, filtros, validaciones y protección de datos.
Iniciaremos un servidor real y comprobaremos sus herramientas mediante un cliente MCP.
Utilizaremos unittest,
que viene incluido con Python.
No necesitamos instalar pytest
ni añadir nuevas dependencias al proyecto.
"""Pruebas funcionales del contrato de StockBridge."""
import json
import os
import tempfile
import unittest
from pathlib import Path
from mcp.server.mcpserver.exceptions import ToolError
import server
class PruebasInventario(unittest.TestCase):
def test_busqueda_simple(self):
self.assertEqual(
[p["id"] for p in server.buscar_producto("miel")["productos"]],
["P002"]
)
def test_mayusculas_y_espacios(self):
resultado = server.buscar_producto(" CAFÉ ")
self.assertEqual(
[p["id"] for p in resultado["productos"]],
["P001", "P005"]
)
self.assertEqual(resultado["total"], 2)
def test_sin_coincidencias(self):
self.assertEqual(
server.buscar_producto("arroz"),
{"productos": [], "total": 0}
)
def test_busqueda_vacia(self):
for valor in ("", " "):
with self.subTest(valor=valor), self.assertRaisesRegex(
ToolError, "no vacío"
):
server.buscar_producto(valor)
def test_umbral_exclusivo_y_ordenado(self):
resultado = server.productos_por_agotarse(5)
self.assertEqual(
[p["id"] for p in resultado["productos"]],
["P002", "P005", "P001"]
)
self.assertEqual(
(resultado["total"], resultado["limite"]),
(3, 5)
)
self.assertNotIn(
"P003",
[p["id"] for p in resultado["productos"]]
)
def test_umbral_por_defecto_y_cero(self):
self.assertEqual(
server.productos_por_agotarse(),
server.productos_por_agotarse(5)
)
self.assertEqual(
server.productos_por_agotarse(0),
{"productos": [], "total": 0, "limite": 0}
)
def test_limite_invalido(self):
for valor in (-1, 2.5, True, False, "5"):
with self.subTest(valor=valor), self.assertRaisesRegex(
ToolError, "entero"
):
server.productos_por_agotarse(valor)
def test_no_se_modifica_inventario(self):
ruta = Path(server.__file__).with_name("inventario.json")
antes = ruta.read_bytes()
server.buscar_producto("café")
server.productos_por_agotarse(5)
self.assertEqual(ruta.read_bytes(), antes)
def test_carga_desde_otra_carpeta(self):
actual = Path.cwd()
with tempfile.TemporaryDirectory() as carpeta:
try:
os.chdir(carpeta)
self.assertEqual(
len(server.cargar_inventario()),
5
)
finally:
os.chdir(actual)
def test_datos_invalidos(self):
casos = [
"{",
"{}",
'[{"id":"P001"}]',
json.dumps([
{
"id": "P001",
"nombre": "Miel",
"precio_cop": 1,
"stock": -1
}
]),
json.dumps([
{
"id": "P001",
"nombre": "Miel",
"precio_cop": True,
"stock": 0
}
]),
json.dumps([
{
"id": "P001",
"nombre": "Miel",
"precio_cop": 1,
"stock": 0
}
] * 2)
]
with tempfile.TemporaryDirectory() as carpeta:
ruta = Path(carpeta) / "inventario.json"
with self.assertRaisesRegex(
ValueError, "No se pudo cargar"
):
server.cargar_inventario(ruta)
for contenido in casos:
with self.subTest(contenido=contenido):
with self.assertRaises(ValueError):
ruta.write_text(
contenido,
encoding="utf-8"
)
server.cargar_inventario(ruta)
if __name__ == "__main__":
unittest.main()
15 · Comprender las pruebas
Diez verificaciones que protegen nuestro inventario
El archivo test_server.py utiliza
unittest.TestCase.
Cada método cuyo nombre comienza con
test_ representa un caso
que Python puede ejecutar automáticamente.
Revisemos qué comprueba cada uno:
- Búsqueda simple: consultar "miel" debe devolver P002.
- Mayúsculas y espacios: " CAFÉ " debe encontrar los dos cafés.
- Sin coincidencias: una búsqueda inexistente devuelve una lista vacía y total cero.
-
Búsqueda vacía:
una cadena vacía o formada por espacios
debe producir
ToolError. - Umbral y orden: con límite cinco aparecen P002, P005 y P001, en ese orden. P003 queda excluido porque tiene exactamente cinco unidades.
- Valores por defecto: omitir el límite equivale a usar cinco. Un límite de cero devuelve una lista vacía.
- Límites inválidos: se rechazan números negativos, decimales, booleanos y cadenas.
- Solo lectura: después de consultar las herramientas, el contenido del JSON permanece idéntico.
- Rutas independientes: el servidor encuentra el inventario aunque cambiemos de directorio.
- Datos inválidos: el servidor rechaza JSON mal formado, estructuras incompletas, existencias negativas, precios booleanos e identificadores duplicados.
Las instrucciones que hacen posibles estas comprobaciones
assertEqual() compara el valor
obtenido con el resultado esperado.
assertNotIn() comprueba
que un elemento esté ausente.
assertRaisesRegex()
verifica que ocurra una excepción
y que su mensaje contenga
una expresión determinada.
subTest() permite evaluar
varios valores dentro de una prueba
e identificar cuál falló.
TemporaryDirectory()
crea una carpeta temporal para probar
datos incorrectos sin modificar
el inventario original.
Finalmente, read_bytes()
permite comparar el contenido exacto
del archivo antes y después
de ejecutar las consultas.
Estas pruebas demuestran que las funciones se comportan según nuestras reglas. Sin embargo, todavía no verifican que un cliente MCP pueda descubrirlas y ejecutarlas mediante el protocolo.
"""Integración real: lanza server.py como subprocess y habla MCP por stdio."""
import sys
import unittest
from pathlib import Path
from mcp import Client, StdioServerParameters
class PruebasMCP(unittest.IsolatedAsyncioTestCase):
async def test_descubrimiento_y_consultas_stdio(self):
servidor = Path(__file__).resolve().with_name("server.py")
proceso = StdioServerParameters(
command=sys.executable,
args=[str(servidor)]
)
async with Client(proceso) as cliente:
herramientas = await cliente.list_tools()
self.assertEqual(
{t.name for t in herramientas.tools},
{"buscar_producto", "productos_por_agotarse"}
)
for herramienta in herramientas.tools:
self.assertIsNotNone(herramienta.input_schema)
self.assertIsNotNone(herramienta.output_schema)
bajos = await cliente.call_tool(
"productos_por_agotarse",
{"limite": 5}
)
self.assertFalse(bajos.is_error)
self.assertEqual(
[
p["id"]
for p in bajos.structured_content["productos"]
],
["P002", "P005", "P001"]
)
self.assertEqual(
bajos.structured_content["total"],
3
)
buscados = await cliente.call_tool(
"buscar_producto",
{"nombre": " CAFÉ "}
)
self.assertFalse(buscados.is_error)
self.assertEqual(
[
p["id"]
for p in buscados.structured_content["productos"]
],
["P001", "P005"]
)
error = await cliente.call_tool(
"buscar_producto",
{"nombre": " "}
)
self.assertTrue(error.is_error)
self.assertIn(
"no vacío",
str(error.content)
)
if __name__ == "__main__":
unittest.main()
16 · Prueba de integración
Ahora comprobamos el protocolo, no solamente las funciones
En las pruebas anteriores llamábamos
directamente a las funciones Python.
Ahora haremos algo diferente:
iniciaremos server.py
como un proceso independiente
y nos comunicaremos con él
mediante un cliente MCP.
1. Ejecutar un proceso independiente
StdioServerParameters
describe cómo iniciar el servidor.
Utilizamos sys.executable
para ejecutar el mismo intérprete
Python que está corriendo las pruebas.
Esto evita depender de una ruta fija del equipo y permite usar el entorno virtual correspondiente.
2. Abrir la sesión con el servidor
async with Client(proceso)
establece la conexión con el servidor.
El cliente se encarga de iniciar
el proceso y gestionar el intercambio
de mensajes MCP.
La prueba utiliza
IsolatedAsyncioTestCase
porque la comunicación con el servidor
se realiza mediante operaciones asíncronas.
Por eso aparecen async
y await.
3. Descubrir herramientas
cliente.list_tools()
solicita al servidor su catálogo.
Verificamos que incluya exactamente
buscar_producto y
productos_por_agotarse.
También comprobamos que cada herramienta publique esquemas de entrada y salida. Estos esquemas describen los argumentos que acepta y la estructura de sus resultados.
4. Invocar herramientas
cliente.call_tool()
realiza una llamada a una herramienta
utilizando su nombre y un diccionario
con los argumentos.
El campo is_error
indica si la ejecución devolvió un error.
structured_content
contiene los datos estructurados
que podemos validar.
Comprobamos que el filtro de existencias devuelva P002, P005 y P001 y que la búsqueda de café devuelva P001 y P005.
5. Comprobar un error real
Finalmente, enviamos una búsqueda formada únicamente por espacios. El servidor debe devolver una respuesta de error, sin detener inesperadamente toda la prueba.
17 · Verificación automatizada
Ejecutemos las once pruebas
Guarda test_server.py
y test_protocol.py
en la misma carpeta que
server.py.
Abre PowerShell dentro de
stockbridge-mcp
y ejecuta el siguiente comando.
Utilizaremos el Python del
entorno virtual creado al inicio.
Comentarios y valoraciones
No hay comentarios aún. ¡Sé el primero en opinar!