IA Intermedio

Crea tu primer servidor MCP con Python: herramientas para consultar documentos Markdown

Aprende a construir un servidor MCP con Python que permite listar, buscar y leer notas Markdown locales. Descubre cómo funcionan las herramientas MCP, cómo validar entradas y cómo comprobar el proyecto con …

Publicado: 02/11/2026 Por: Juan Felipe Orozco Cortés Duración: 2–3 horas Nivel: Intermedio
☰ Contenido del tutorial ⌄
Proyecto práctico · Python + MCP

Convierte tus notas Markdown en herramientas MCP

Imagina que una aplicación pudiera consultar tus apuntes locales usando funciones bien definidas, sin recibir permiso para modificar los archivos. En este tutorial construiremos exactamente eso con Python.

Desarrollaremos un servidor del Model Context Protocol (MCP) capaz de listar notas, buscar contenido y leer un documento por su nombre. Después comprobaremos cada operación con un cliente Python, pruebas automatizadas y MCP Inspector.

3 Herramientas de solo lectura
Python Servidor y cliente local
Sin claves No necesitas contratar una IA

La idea central: MCP no es un modelo de inteligencia artificial. Es un protocolo que permite a un cliente descubrir e invocar las herramientas que ofrece un servidor.

¿Qué construirás y qué aprenderás?

El resultado será un pequeño sistema de notas Markdown con tres herramientas MCP. Todas trabajan sobre una carpeta local controlada por el usuario.

01

listar_notas()

Devuelve los nombres de las notas Markdown permitidas.

02

buscar_notas(consulta)

Busca una palabra o expresión en el contenido de las notas.

03

leer_nota(nombre)

Lee una nota por nombre y rechaza rutas que no están permitidas.

Al finalizar podrás

  1. Explicar qué son un servidor, un cliente, un transporte y una herramienta MCP.
  2. Separar la lógica de lectura de archivos del código del protocolo.
  3. Validar nombres, limitar tamaños y gestionar errores esperados.
  4. Invocar herramientas MCP desde un cliente Python sin utilizar un LLM.
  5. Ejecutar pruebas con pytest y comprobar el servidor mediante MCP Inspector.

Objetivo práctico: entender por qué funciona cada componente, no solamente copiar un proyecto terminado.

Antes de empezar: conocimientos y herramientas

Este tutorial está pensado para quienes conocen los fundamentos de Python y quieren dar sus primeros pasos con MCP. Trabajaremos desde la terminal y utilizaremos una carpeta de notas de demostración.

A

Necesitas para programar

  • Python 3.11, 3.12 o 3.13.
  • Git y acceso a una terminal.
  • Conocimientos básicos de funciones, clases y entornos virtuales.
  • Conexión a Internet para instalar las dependencias.
B

Para las pruebas visuales

  • Node.js con npx.
  • uv para iniciar el entorno de MCP Inspector.
  • Un navegador moderno.
  • Windows PowerShell, Linux o macOS.

Bibliotecas que utilizaremos

Trabajaremos con mcp[cli]==2.3.0 y pytest==9.0.2. En la sección de instalación aprenderás a crear el entorno y a instalarlas desde los archivos de requisitos.

No necesitas una API de pago. El servidor y el cliente de demostración funcionan localmente, sin claves de un proveedor de IA.

En la siguiente sección veremos la arquitectura y el recorrido de una solicitud antes de empezar a escribir código.

Fundamentos · MCP

¿Qué es MCP y qué problema resuelve?

Un programa puede tener información útil y otro puede necesitarla. ¿Cómo conectarlos sin inventar una integración distinta para cada herramienta? Esa es la pregunta que nos ayuda a responder MCP.

Model Context Protocol (MCP) es un protocolo abierto para comunicar aplicaciones cliente con servidores que ofrecen capacidades, como consultar información o ejecutar herramientas. Define una manera común de descubrir esas capacidades, enviar solicitudes y recibir resultados.

En este proyecto, la información son archivos .md de una carpeta local y las capacidades son tres funciones Python: listar, buscar y leer notas. MCP nos permite presentar esas funciones a un cliente sin acoplarlo a los detalles internos del sistema de archivos.

¿Por qué utilizar un protocolo compartido?

Supón que tienes varias aplicaciones que necesitan consultar las mismas notas. Si cada una se comunica con el programa de una forma diferente, deberás mantener varias integraciones. MCP establece un contrato compartido entre clientes y servidores compatibles.

01

Descubrir

El cliente puede consultar qué herramientas expone el servidor y qué argumentos esperan.

02

Invocar

El cliente llama una herramienta por su nombre y proporciona argumentos estructurados.

03

Responder

El servidor ejecuta su lógica y devuelve un resultado o comunica un error.

MCP no es un modelo de inteligencia artificial

Un modelo de IA puede beneficiarse de herramientas MCP cuando una aplicación compatible se las proporciona, pero MCP no exige usar un LLM. Nuestro archivo cliente_demo.py llama las herramientas directamente desde Python, sin claves de API ni servicios de IA de pago.

Qué aprenderás a distinguir: MCP define la comunicación; el SDK de Python nos ayuda a implementarla; y notas_core.py contiene las reglas reales para acceder a los documentos. Son tres responsabilidades distintas.

Cliente, servidor, transporte y herramientas: quién hace qué

Para entender el funcionamiento de MCP conviene separar los participantes de la comunicación. En algunas aplicaciones hay un host (por ejemplo, una aplicación que integra clientes MCP); cada cliente mantiene la conexión con un servidor. En nuestro tutorial podemos trabajar directamente con un cliente Python sin construir una aplicación host completa.

01

Cliente MCP

Es el componente que establece la conexión, descubre herramientas y envía solicitudes. En el repositorio lo encontrarás en cliente_demo.py, mediante la clase Client.

02

Servidor MCP

Publica las capacidades disponibles y atiende las llamadas. En server.py lo construimos con MCPServer y registramos funciones mediante @server.tool.

03

Herramientas

Son operaciones concretas que el servidor ofrece al cliente. Las nuestras se llaman listar_notas, buscar_notas y leer_nota.

04

Transporte

Es el mecanismo que lleva los mensajes entre cliente y servidor. Este ejemplo permite ejecutarlos localmente sin red; al iniciar server.py como programa, el servidor utiliza stdio.

Dos maneras de probar nuestro mismo servidor

Cliente de demostración: conexión en memoria

cliente_demo.py importa mcp desde server.py y ejecuta Client(mcp). Así podemos invocar herramientas en el mismo entorno Python, sin iniciar manualmente otro proceso.

MCP Inspector: comunicación por STDIO

Con mcp dev server.py abriremos Inspector. El entorno de desarrollo puede iniciar el servidor como un proceso y comunicarse con él mediante la entrada y salida estándar.

¿Qué sucede al solicitar una búsqueda?

Pensemos en una llamada real: el cliente invoca buscar_notas con el argumento consulta="permisos".

  1. El cliente identifica la herramienta y envía el argumento consulta.
  2. El servidor MCP recibe la invocación y ejecuta su función registrada.
  3. La función delega la búsqueda en NotesStore.buscar_notas, dentro de notas_core.py.
  4. El núcleo valida el término y examina el contenido de las notas autorizadas.
  5. El resultado vuelve al cliente, que obtiene una lista con seguridad.md.

Un detalle de seguridad: declarar una herramienta como de solo lectura mediante read_only_hint=True informa de su intención, pero no impone permisos por sí mismo. La restricción efectiva depende de las operaciones que implementamos y de las validaciones de NotesStore.

Ahora que conocemos los componentes, el siguiente bloque mostrará su relación en un diagrama de arquitectura.

Arquitectura MCP: cliente Python en memoria o MCP Inspector por STDIO, servidor server.py, núcleo NotesStore en notas_core.py y notas Markdown locales.
Figura 1. Arquitectura de MCP Python Secure Notes. Dos clientes alternativos invocan las tres herramientas del servidor MCP; NotesStore realiza la validación y lectura de los archivos Markdown locales.

Del cliente a las notas: recorrido de una solicitud MCP

Ya conocemos los componentes y hemos visto su arquitectura en la figura 1. Ahora seguiremos una operación real del proyecto: buscar la palabra permisos dentro de nuestras notas Markdown.

Cliente Python → Servidor MCP → NotesStore → Archivos .md

¿Qué sucede exactamente?

  1. El cliente prepara la petición. cliente_demo.py llama a buscar_notas con el argumento {"consulta": "permisos"}.
  2. MCP identifica la herramienta. El servidor creado en server.py tiene registrada una función con ese nombre y recibe el argumento consulta.
  3. El servidor delega el trabajo. La función MCP llama a store.buscar_notas(consulta). La lógica de búsqueda no está implementada dentro del protocolo.
  4. NotesStore valida y busca. Comprueba el término, enumera los archivos permitidos y busca el texto en las notas que puede leer correctamente.
  5. El resultado regresa al cliente. La búsqueda encuentra seguridad.md, cuyo contenido incluye la palabra «permisos».

Idea clave: MCP comunica la petición y el resultado, pero la validación de los archivos pertenece a notas_core.py. Esta separación permite probar el núcleo sin iniciar un servidor MCP.

La llamada en el código real

En cliente_demo.py hay un bucle que invoca las tres herramientas. La segunda llamada es precisamente la búsqueda que acabamos de recorrer. El siguiente bloque es un fragmento auténtico de ese archivo.

Python
for nombre, argumentos in (
    ("listar_notas", {}),
    ("buscar_notas", {"consulta": "permisos"}),
    ("leer_nota", {"nombre": "python.md"}),
):
    result = await client.call_tool(nombre, argumentos)
    if result.is_error:
        raise RuntimeError(f"Error en {nombre}: {result.content}")
    print(f"{nombre}: {result.structured_content}")

¿Qué respuesta recibe el cliente?

Para nuestra búsqueda, el cliente obtiene una estructura equivalente a {"result": ["seguridad.md"]}. El elemento encontrado es el nombre de la nota, no una copia de toda la carpeta.

El ejemplo imprime los resultados mediante result.structured_content. En el fragmento anterior, la variable client ya está conectada dentro de un contexto asíncrono; estudiaremos esa parte cuando lleguemos a cliente_demo.py.

¿Y si algo sale mal? Por ejemplo, intentar leer ../secreto.md provoca un error de validación. NotesStore genera NotesError y server.py lo transforma en un error de herramienta MCP mediante ToolError. No se devuelve el archivo solicitado.

Nota: el cliente de demostración se conecta al servidor en memoria. MCP Inspector utiliza la alternativa STDIO. Ambos invocan las mismas herramientas, sin necesidad de un modelo de IA.

Cómo está organizado el proyecto

Antes de examinar las funciones línea por línea, debemos saber dónde vive cada responsabilidad. El repositorio separa el protocolo MCP, la lógica de lectura de notas y el cliente de demostración.

Esta división no es solamente una cuestión de orden: hace que el código resulte más sencillo de comprender, probar y mantener. A continuación vemos el árbol de archivos de la versión del proyecto utilizada en este tutorial.

Texto plano
mcp-python-secure-notes/
├── server.py
├── notas_core.py
├── cliente_demo.py
├── notas/
│   ├── python.md
│   └── seguridad.md
├── tests/
│   ├── test_notas_core.py
│   └── test_mcp.py
├── requirements.txt
├── requirements-dev.txt
├── README.md
├── LICENSE
├── .gitignore
└── .github/
    └── workflows/
        └── ci.yml

Responsabilidad de los archivos principales

01

notas_core.py

Implementa NotesStore: valida nombres y consultas, enumera notas y realiza lecturas limitadas de archivos Markdown. No necesita importar MCP.

02

server.py

Crea MCPServer, registra las tres herramientas y conecta sus llamadas con los métodos de NotesStore.

03

cliente_demo.py

Utiliza Client para conectarse en memoria, invocar las herramientas y mostrar sus resultados estructurados.

Archivos de apoyo: datos, dependencias y pruebas

Datos y dependencias

notas/ contiene los dos documentos de ejemplo. requirements.txt instala el SDK MCP y requirements-dev.txt añade pytest para el desarrollo.

Calidad y documentación

tests/ contiene pruebas del núcleo y del protocolo. .github/workflows/ci.yml automatiza su ejecución. README.md explica el uso y LICENSE contiene la licencia MIT.

El principio que debemos recordar: el servidor MCP no debería encargarse de verificar cada archivo por su cuenta. Esa responsabilidad vive en NotesStore; el servidor expone sus operaciones mediante un protocolo común.

¿Qué sigue?

Ya entendemos qué es MCP, cómo se comunican los componentes y cómo se organiza este proyecto. En la siguiente fase construiremos y analizaremos el núcleo notas_core.py, desde sus constantes hasta las operaciones de listado, búsqueda y lectura segura de notas.

Fase C · El núcleo Python

Dentro de notas_core.py: la lógica que consulta los documentos

MCP hace posible que un cliente invoque herramientas, pero la seguridad de los archivos no aparece por arte de magia: debemos programar las reglas que autorizan cada operación.

El módulo notas_core.py concentra esas reglas en la clase NotesStore. Está escrito con la biblioteca estándar de Python y no necesita importar el SDK MCP.

server.py → NotesStore → notas/*.md

Importaciones y límites de recursos

os sirve para inspeccionar y abrir archivos, re para validar los nombres, stat para reconocer archivos regulares y Path para manipular rutas. Las constantes definen límites explícitos antes de consultar datos locales.

Python
from __future__ import annotations

import os
import re
import stat
from pathlib import Path

MAX_BYTES = 64 * 1024
MAX_FILES = 100
MAX_RESULTS = 10
NAME_PATTERN = re.compile(r"[A-Za-z0-9_-]{1,60}\.md\Z", re.ASCII)
64 KiB

MAX_BYTES

Cada nota legible puede contener como máximo 65.536 bytes. No son necesariamente 65.536 caracteres: UTF-8 usa varios bytes para algunos símbolos.

100

MAX_FILES

Permite enumerar hasta cien archivos regulares con nombres admitidos; un archivo válido adicional provoca un error.

10

MAX_RESULTS

Una búsqueda devuelve, como máximo, diez nombres coincidentes. Para encontrarlos puede revisar más de diez notas.

Decisión de ingeniería: los límites no tienen por qué ser iguales en todos los sistemas. Aquí son valores pequeños adecuados para un ejemplo local; facilitan comprobar comportamientos predecibles.

Validar nombres con una expresión regular

Si un cliente pide leer un documento, ¿debemos aceptar una ruta como ../secreto.md? En este proyecto, no. La herramienta espera un nombre simple de archivo, sin directorios, terminado en .md.

Descomponer NAME_PATTERN

  • [A-Za-z0-9_-]: letras ASCII, números, guion y guion bajo.
  • {1,60}: entre uno y sesenta caracteres antes de la extensión.
  • \.md: punto literal y extensión minúscula .md.
  • \Z: final de la cadena; junto con fullmatch exige un nombre completo válido.
  • re.ASCII: expresa la intención de trabajar con un conjunto ASCII restringido.

Ejemplos admitidos

python.md, seguridad.md, tema_1.md y api-v2.md.

Ejemplos rechazados

../secreto.md, carpeta/nota.md, hola.txt, nota privada.md y éxito.md.

Dentro de leer_nota el programa utiliza isinstance(nombre, str) y NAME_PATTERN.fullmatch(nombre): si el valor no es texto o no coincide completamente con el patrón, genera un error antes de construir la ruta.

Alcance real: esta validación impide rutas arbitrarias suministradas como nombres, pero no sustituye un aislamiento completo frente a modificaciones concurrentes del directorio.

Python
NAME_PATTERN = re.compile(r"[A-Za-z0-9_-]{1,60}\.md\Z", re.ASCII)

NotesError: errores que el programa sabe explicar

Un nombre inválido, una nota inexistente o un documento demasiado grande son problemas que podemos prever. En lugar de permitir que el usuario reciba mensajes inesperados del sistema operativo, el núcleo emplea una excepción propia llamada NotesError.

La clase hereda de ValueError. No necesita métodos adicionales: su nombre permite distinguir los errores recuperables de consulta frente a problemas ajenos a la lógica de notas.

NotesStore detecta un problema → NotesError → server.py: ToolError

La conversión a ToolError ocurre en la capa MCP, que estudiaremos en la Fase D. Por eso notas_core.py puede seguir siendo independiente del protocolo.

Python
class NotesError(ValueError):
    """Error recuperable al consultar notas."""

Inicializar NotesStore sin fijar una carpeta en el código

NotesStore representa una colección de notas locales. Su constructor recibe root y lo transforma en Path. Así, el mismo código sirve para la carpeta notas/ y para los directorios temporales que usan las pruebas automatizadas.

Observa que __init__ no abre ni valida todavía la carpeta: únicamente guarda la ruta. La comprobación se hace al llamar a un método de lectura, búsqueda o listado.

Principio de diseño: recibir la dependencia como argumento facilita escribir pruebas sin tocar los documentos reales. En el repositorio, server.py construye el almacén con la carpeta local de notas.

Python
class NotesStore:
    """Acceso no recursivo a una carpeta local confiable."""

    def __init__(self, root: Path):
        self.root = Path(root)

Comprobar que la carpeta está disponible

Antes de enumerar o leer archivos, _check_root() evalúa la ruta recibida. is_symlink() comprueba si la entrada final es un enlace simbólico y is_dir() verifica que existe un directorio.

Si la carpeta falta o la entrada es un enlace simbólico, se lanza NotesError con el mensaje «La carpeta de notas no está disponible».

Una protección útil, con límites

  • Se rechaza una carpeta raíz que sea directamente un enlace simbólico.
  • No se presupone que una ruta inexistente debe crearse automáticamente.
  • No se eliminan todas las carreras entre comprobación y uso ni se validan todos los directorios antecesores contra cambios simultáneos.

Supuesto fundamental: la carpeta de notas debe ser confiable y permanecer bajo control del usuario. Esta implementación es un ejemplo local de solo lectura, no un sandbox para archivos que controla un atacante.

Python
def _check_root(self) -> None:
        if self.root.is_symlink() or not self.root.is_dir():
            raise NotesError("La carpeta de notas no está disponible")

Enumerar archivos con os.scandir

No toda entrada de una carpeta es una nota válida: puede ser un subdirectorio, un archivo con otra extensión o un enlace simbólico. La función privada _names() recorre las entradas y acepta únicamente archivos regulares con nombres que cumplen NAME_PATTERN.

Qué hace _names()

  1. Comprueba la carpeta con _check_root().
  2. Itera sus entradas con os.scandir(self.root), sin recorrer subdirectorios.
  3. Filtra nombres mediante NAME_PATTERN.fullmatch(entry.name).
  4. Usa entry.is_file(follow_symlinks=False) para no seguir enlaces simbólicos.
  5. Rechaza una entrada válida adicional cuando ya reunió cien notas permitidas.
  6. Devuelve sorted(names), una lista alfabética de nombres, no de rutas absolutas.

Un error del sistema operativo durante la enumeración se convierte en NotesError. Los archivos que no cumplen la política sencillamente se omiten.

Python
def _names(self) -> list[str]:
        self._check_root()
        try:
            names = []
            # scandir itera progresivamente; Path.iterdir puede materializar
            # toda la carpeta antes de aplicar MAX_FILES en algunas versiones.
            with os.scandir(self.root) as entries:
                for entry in entries:
                    if not NAME_PATTERN.fullmatch(entry.name):
                        continue
                    if entry.is_file(follow_symlinks=False):
                        if len(names) >= MAX_FILES:
                            raise NotesError(f"Hay más de {MAX_FILES} notas permitidas")
                        names.append(entry.name)
        except OSError as exc:
            raise NotesError("No se pudo enumerar las notas") from exc
        return sorted(names)

Por qué listar_notas() es tan corto

La API pública no necesita repetir los filtros: devuelve el resultado de _names(). Este método compartido también se utilizará en las búsquedas; eso evita implementar dos políticas diferentes para decidir qué nombres se aceptan.

Con los documentos de ejemplo: listar_notas() devuelve ['python.md', 'seguridad.md'].

El listado no abre cada documento, así que un archivo demasiado grande o con codificación incorrecta puede aparecer como nombre. Su contenido se validará al intentar leerlo.

Python
def listar_notas(self) -> list[str]:
        """No devuelve rutas completas ni archivos fuera de la carpeta."""
        return self._names()

Leer una nota de forma limitada con leer_nota()

Este es el método más importante del núcleo: toma un nombre proporcionado por el cliente y realiza una lectura real de archivos. No basta con validar la extensión; también hay que comprobar el tipo de entrada, el tamaño y la codificación.

El siguiente bloque reproduce el método completo de la versión verificada en GitHub. Aunque aparece sin las importaciones y la clase que lo rodean, el contenido corresponde exactamente a notas_core.py.

Python
def leer_nota(self, nombre: str) -> str:
        """Lee hasta 64 KiB de una nota regular y UTF-8."""
        if not isinstance(nombre, str) or not NAME_PATTERN.fullmatch(nombre):
            raise NotesError("Nombre inválido: letras, números, _ o - y termina en .md")
        self._check_root()
        path = self.root / nombre
        try:
            info = path.lstat()
            if not stat.S_ISREG(info.st_mode):
                raise NotesError("La nota no existe o no es un archivo regular")
            if info.st_size > MAX_BYTES:
                raise NotesError("La nota supera el límite de 64 KiB")
            flags = os.O_RDONLY | getattr(os, "O_BINARY", 0)
            flags |= getattr(os, "O_CLOEXEC", 0)
            flags |= getattr(os, "O_NOFOLLOW", 0)
            flags |= getattr(os, "O_NONBLOCK", 0)
            fd = os.open(path, flags)
            try:
                opened = os.fstat(fd)
                if not stat.S_ISREG(opened.st_mode):
                    raise NotesError("La nota no es un archivo regular")
                if opened.st_size > MAX_BYTES:
                    raise NotesError("La nota supera el límite de 64 KiB")
                with os.fdopen(fd, "rb") as stream:
                    fd = -1
                    data = stream.read(MAX_BYTES + 1)
            finally:
                if fd >= 0:
                    os.close(fd)
            if len(data) > MAX_BYTES:
                raise NotesError("La nota supera el límite de 64 KiB")
            return data.decode("utf-8").replace("\r\n", "\n")
        except FileNotFoundError as exc:
            raise NotesError("La nota no existe") from exc
        except UnicodeError as exc:
            raise NotesError("La nota no tiene codificación UTF-8 válida") from exc
        except OSError as exc:
            raise NotesError("No se pudo leer la nota autorizada") from exc

Explicación del método, paso a paso

  1. Entrada: nombre debe ser una cadena y cumplir el patrón. Esto rechaza rutas como ../secreto.md antes de consultar el archivo.
  2. Carpeta: _check_root() confirma que el directorio está disponible. Después se construye path = self.root / nombre.
  3. Inspección inicial: path.lstat() examina la entrada sin seguir su último enlace simbólico. stat.S_ISREG(info.st_mode) exige un archivo regular.
  4. Tamaño inicial: info.st_size impide intentar leer notas que ya superan 64 KiB.
  5. Apertura: os.O_RDONLY establece lectura solamente; las opciones O_BINARY, O_CLOEXEC, O_NOFOLLOW y O_NONBLOCK se usan cuando existen en el sistema operativo.
  6. Segunda comprobación: os.fstat(fd) inspecciona el archivo efectivamente abierto para verificar de nuevo tipo y tamaño.
  7. Lectura: read(MAX_BYTES + 1) lee como máximo 65.537 bytes. Ese byte adicional permite detectar un archivo que excedió el límite.
  8. Resultado: decode('utf-8') convierte bytes a texto y replace('\r\n', '\n') unifica los saltos de línea habituales de Windows.

Control de errores y liberación de archivos

El administrador de contexto with y el bloque finally evitan dejar abierto el descriptor. Los errores de archivo inexistente, codificación UTF-8 inválida y sistema operativo se transforman en NotesError con mensajes apropiados.

Caso válido

leer_nota('python.md') devuelve el texto del documento de demostración.

Caso rechazado

leer_nota('../secreto.md') produce NotesError: es una ruta, no un nombre permitido.

Límite de seguridad: algunas banderas, especialmente O_NOFOLLOW, dependen de la plataforma. Las comprobaciones reducen riesgos, pero no garantizan protección total frente a sustituciones concurrentes de archivos en un directorio no confiable.

Buscar contenido con buscar_notas()

La última operación recibe un término y devuelve los nombres de las notas cuyo texto lo contiene. No utiliza IA, índices semánticos ni expresiones regulares para buscar: realiza una comparación literal de subcadenas.

Qué debe aceptar una consulta

  • El valor debe ser una cadena de texto.
  • Los caracteres de control ASCII, como saltos de línea y tabulaciones, se rechazan incluso al principio o al final.
  • strip() elimina espacios exteriores válidos y casefold() facilita la comparación sin distinguir mayúsculas.
  • El término normalizado debe tener entre 2 y 80 caracteres.
Python
def buscar_notas(self, consulta: str) -> list[str]:
        """Busca texto literal en notas autorizadas; máximo diez resultados."""
        if not isinstance(consulta, str):
            raise NotesError("La búsqueda debe ser texto")
        # Validar antes de strip: un salto de línea al principio o al final
        # también es una entrada inválida, aunque strip lo eliminaría.
        if any(ord(ch) < 32 or ord(ch) == 127 for ch in consulta):
            raise NotesError("La búsqueda no admite caracteres de control")
        term = consulta.strip().casefold()
        if not 2 <= len(term) <= 80:
            raise NotesError("La búsqueda debe tener entre 2 y 80 caracteres")
        found = []
        for nombre in self._names():
            try:
                content = self.leer_nota(nombre)
            except NotesError:
                continue
            if term in content.casefold():
                found.append(nombre)
                if len(found) >= MAX_RESULTS:
                    break
        return found

Cómo encuentra y devuelve las coincidencias

El método obtiene primero la lista filtrada con self._names(). Para cada archivo llama a self.leer_nota(nombre), de modo que reutiliza las restricciones de lectura. Si una nota falla con NotesError, la búsqueda la omite y continúa.

Después comprueba term in content.casefold(). Cada coincidencia añade solamente el nombre del archivo a la lista found; cuando alcanza MAX_RESULTS, finaliza.

Consulta con resultado

buscar_notas('PERMISOS') devuelve ['seguridad.md'] con las notas del repositorio.

Consulta sin resultado

buscar_notas('rust') devuelve [] porque las notas de ejemplo no contienen ese término.

Qué significa buscar: encontrar permisos significa detectar esa secuencia en el texto. Este ejemplo no interpreta el significado ni responde preguntas sobre los documentos.

Resumen del núcleo: tres métodos, una política de acceso

Terminamos el estudio de notas_core.py. El módulo no sabe nada del transporte MCP; se limita a comprobar, enumerar, leer y buscar notas Markdown dentro de una carpeta confiable.

01

listar_notas()

Devuelve una lista ordenada de nombres de archivos regulares que cumplen la política de nombres.

02

leer_nota(nombre)

Valida nombre, tipo, tamaño y codificación antes de devolver el contenido.

03

buscar_notas(consulta)

Busca una subcadena normalizada y limita a diez los resultados devueltos.

Cómo sabemos que este comportamiento se comprueba

En tests/test_notas_core.py existen pruebas para nombres peligrosos, tamaños máximos, codificación UTF-8, CRLF de Windows, subdirectorios, límites de cantidad, búsquedas inválidas y ausencia de modificaciones en los documentos.

Las pruebas de enlaces simbólicos pueden marcarse como skipped en Windows cuando el sistema no permite crearlos; esto no equivale a que hayan pasado.

Lección de ingeniería: separar el núcleo de MCP facilita razonar sobre los datos, aplicar validaciones consistentes y probar comportamientos sin depender de la interfaz del servidor.

Advertencia: este es un ejemplo local de solo lectura, no un entorno de aislamiento completo ni un servicio preparado para exponerse públicamente a entradas y directorios no confiables.

Siguiente paso: conectar el núcleo con MCP

En la Fase D analizaremos server.py: cómo se construye MCPServer, cómo se registran las tres herramientas y cómo los errores del núcleo se traducen a respuestas de herramienta.

Fase D · Servidor MCP

Construir el servidor MCP: de funciones Python a herramientas

Ya sabemos cómo leer y buscar notas Markdown con NotesStore. Ahora necesitamos que un cliente pueda utilizar esas operaciones mediante MCP.

Esa es la responsabilidad de server.py: construir el servidor, registrar las herramientas disponibles y conectar cada solicitud con los métodos de notas_core.py.

No volveremos a implementar la búsqueda ni la lectura de archivos. Reutilizaremos la lógica que estudiamos anteriormente, manteniendo separadas las responsabilidades.

Cliente MCP → server.py → NotesStore

Las responsabilidades de nuestro servidor

01

Configurar

Crear el servidor MCP y establecer qué carpeta de notas consultará.

02

Exponer

Registrar tres funciones Python como herramientas que un cliente puede invocar.

03

Comunicar

Atender solicitudes, devolver resultados y comunicar errores de herramienta.

Principio de arquitectura: una herramienta MCP funciona como una interfaz para una capacidad del programa. La interfaz comunica qué se puede solicitar; NotesStore se encarga de realizar el trabajo.

Importaciones: conectar el SDK MCP con nuestro núcleo

Empezaremos leyendo las importaciones reales de server.py. Este proyecto utiliza el SDK de Python MCP, instalado mediante la dependencia mcp[cli]==2.3.0.

01

Biblioteca estándar

Path permite construir la ruta a la carpeta local de notas. __future__.annotations permite diferir la evaluación de las anotaciones de tipos.

02

SDK de MCP

MCPServer crea el servidor; ToolAnnotations describe propiedades de las herramientas; ToolError representa errores durante su ejecución.

Por último importamos NotesStore y NotesError desde notas_core.py. Esta dependencia va en una dirección: el servidor conoce al núcleo, pero el núcleo no necesita conocer MCP.

Python
from __future__ import annotations

from pathlib import Path
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations
from notas_core import NotesError, NotesStore

¿Dónde se encuentran las notas?

La constante NOTAS utiliza Path(__file__).resolve().parent para localizar el directorio donde se encuentra server.py. Después añade la carpeta notas.

Así, la ubicación predeterminada depende del archivo del proyecto y no del directorio desde el que abrimos la terminal.

¿Qué significa READ_ONLY?

ToolAnnotations permite aportar información sobre el comportamiento esperado de una herramienta.

RO

read_only_hint=True

Declara que las herramientas están diseñadas para consultar información sin modificarla.

LOC

open_world_hint=False

Describe la intención de trabajar con un entorno cerrado y limitado, en lugar de interactuar libremente con sistemas externos.

Importante: estas anotaciones son pistas informativas para los clientes. No activan automáticamente controles de acceso ni aíslan el sistema. Las restricciones efectivas dependen del código implementado y de los permisos del proceso.

Python
NOTAS = Path(__file__).resolve().parent / "notas"
READ_ONLY = ToolAnnotations(read_only_hint=True, open_world_hint=False)

Crear el servidor con create_server()

En lugar de construir el servidor directamente en muchas partes del programa, agrupamos su configuración en una función: create_server().

Recibe un argumento opcional, root, que señala la carpeta de notas. Si no especificamos uno, utiliza la ruta definida en NOTAS.

Dos objetos con responsabilidades diferentes

01

store = NotesStore(root)

Crea el objeto encargado de validar y consultar los archivos locales. Todavía no registra herramientas MCP.

02

server = MCPServer(...)

Crea el servidor y le asigna un nombre. Después registraremos las funciones que podrán invocar los clientes.

¿Por qué permitir cambiar root?

Este diseño facilita las pruebas automatizadas: podemos crear un servidor que consulte una carpeta temporal en lugar de utilizar los documentos reales del proyecto.

El argumento root es parte de la configuración Python, no un parámetro de las herramientas. Un cliente MCP no recibe una operación para cambiar libremente esta carpeta.

Concepto importante: separar la construcción del servidor de su ejecución permite comprobar sus herramientas sin iniciar un proceso independiente.

Python
def create_server(root: Path = NOTAS) -> MCPServer:
    """Configura la carpeta para tests sin permitir que un agente la cambie."""
    store = NotesStore(root)
    server = MCPServer("TuCodigoCotidiano - Notas seguras")

Los decoradores convierten funciones en herramientas MCP

Dentro de create_server() definimos tres funciones Python. Antes de cada una colocamos el decorador @server.tool(annotations=READ_ONLY).

Cuando se ejecuta la configuración, el SDK registra esas funciones como herramientas del servidor. El cliente puede descubrir sus nombres y los argumentos que acepta cada una.

1

Nombre

listar_notas, buscar_notas o leer_nota.

2

Argumentos

Se describen mediante la firma de las funciones y sus anotaciones de tipos.

3

Descripción

Los docstrings explican brevemente la finalidad de cada herramienta.

¿De dónde sale la variable store?

Las funciones registradas se definen dentro de create_server() y utilizan la variable store creada anteriormente.

Python permite que estas funciones internas conserven acceso a variables del contexto donde se definieron. Este mecanismo se conoce como closure o cierre léxico.

Ventaja: las tres herramientas comparten una instancia configurada de NotesStore, sin necesidad de exponer la carpeta raíz como argumento de cada llamada MCP.

Primera herramienta MCP: listar_notas()

Nuestra primera herramienta permite descubrir qué notas Markdown están disponibles para consultar. No recibe argumentos y devuelve una lista de cadenas.

listar_notas() → store.listar_notas() → Lista de nombres

Explicación del código

  1. Registro: @server.tool(annotations=READ_ONLY) convierte la función en una herramienta.
  2. Firma: listar_notas() -> list[str] indica que no hay argumentos y que el resultado es una lista de textos.
  3. Delegación: store.listar_notas() realiza la operación utilizando el núcleo que ya estudiamos.
  4. Error: si NotesStore genera NotesError, el servidor lo convierte en ToolError.

Resultado esperado: con los documentos del repositorio, el resultado lógico contiene python.md y seguridad.md. La capa MCP lo entrega al cliente como resultado de la herramienta.

Python
@server.tool(annotations=READ_ONLY)
    def listar_notas() -> list[str]:
        """Lista los nombres de archivos Markdown autorizados."""
        try:
            return store.listar_notas()
        except NotesError as exc:
            raise ToolError(str(exc)) from exc

Segunda herramienta MCP: buscar_notas()

A diferencia del listado, esta herramienta necesita un argumento. El cliente proporciona una consulta y el servidor devuelve los nombres de las notas que contienen ese término.

Contrato de la herramienta

  • Nombre: buscar_notas
  • Argumento: consulta, de tipo texto.
  • Retorno: list[str].
  • Ejemplo: consulta="permisos".

Qué hace cada capa

El SDK MCP recibe los argumentos estructurados y los entrega a la función Python registrada.

Luego el servidor llama a store.buscar_notas(consulta). Es NotesStore quien rechaza términos demasiado cortos, caracteres de control y valores que no sean texto.

Reutilización: no copiamos la validación ni la búsqueda en el servidor. La lógica sigue teniendo una única implementación en notas_core.py.

Consulta inválida: si enviamos consulta="x", el núcleo genera NotesError, porque exige entre dos y ochenta caracteres después de normalizar la consulta.

Python
@server.tool(annotations=READ_ONLY)
    def buscar_notas(consulta: str) -> list[str]:
        """Busca texto en las notas Markdown (hasta diez resultados)."""
        try:
            return store.buscar_notas(consulta)
        except NotesError as exc:
            raise ToolError(str(exc)) from exc

Tercera herramienta MCP: leer_nota()

Ya podemos descubrir archivos y buscar términos. Ahora necesitamos recuperar el contenido de un documento concreto.

La herramienta leer_nota recibe el nombre de una nota Markdown y devuelve su contenido como una cadena de texto.

IN

Argumento

nombre="python.md"

El cliente indica exactamente qué nota desea consultar.

OUT

Resultado

Una cadena con el contenido Markdown de la nota.

El servidor no modifica el documento original.

¿Quién comprueba la seguridad del archivo?

La función MCP no abre archivos directamente. Delega la operación en store.leer_nota(nombre).

Ya estudiamos las verificaciones de NotesStore: nombres permitidos, archivos regulares, límites de tamaño, apertura de lectura, codificación UTF-8 y cierre de recursos.

Ejemplo válido: leer_nota("python.md") devuelve el documento que comienza con el encabezado «Python para proyectos reales».

Ejemplo rechazado: leer_nota("../secreto.md") no permite acceder a una ruta externa. El núcleo rechaza el nombre antes de intentar leer ese documento.

Esto demuestra una ventaja de nuestra arquitectura: podemos cambiar la interfaz que consulta los documentos sin duplicar las reglas de validación.

Python
@server.tool(annotations=READ_ONLY)
    def leer_nota(nombre: str) -> str:
        """Lee por nombre exacto una nota Markdown, por ejemplo python.md."""
        try:
            return store.leer_nota(nombre)
        except NotesError as exc:
            raise ToolError(str(exc)) from exc

Errores de herramienta y ejecución del servidor

Las tres herramientas que registramos utilizan el mismo patrón para tratar los errores de NotesStore.

Cada función ejecuta su operación dentro de un bloque try. Cuando el núcleo genera NotesError, el servidor lo transforma en ToolError.

¿Por qué transformar la excepción?

01

NotesError

Pertenece al núcleo Python e identifica un problema al consultar las notas.

02

ToolError

Pertenece a la capa MCP y permite comunicar un fallo durante la ejecución de una herramienta.

03

Cliente

Recibe el resultado de la llamada y puede comprobar si la herramienta terminó con error.

El significado de raise ... from exc

La instrucción raise ToolError(str(exc)) from exc crea una nueva excepción conservando internamente la relación con la causa original.

Esto ayuda a comprender qué operación falló sin obligar a que la lógica de archivos conozca las clases del SDK MCP.

Una distinción importante: capturamos los errores previstos de NotesStore. Esto no significa que cualquier excepción del programa quede automáticamente convertida en un NotesError.

Cómo arranca realmente el servidor

Al terminar de registrar las herramientas, create_server() devuelve el objeto server.

La instrucción mcp = create_server() construye una instancia del servidor. Después se utiliza la condición if __name__ == "__main__" para distinguir entre ejecutar el archivo directamente e importarlo desde otro módulo.

Cuando ejecutamos server.py como programa, mcp.run() inicia el servidor utilizando el transporte STDIO predeterminado.

Cuando cliente_demo.py importa mcp, esa condición no se cumple. El cliente puede usar directamente el servidor en memoria, sin llamar a mcp.run().

Python
return server


mcp = create_server()

if __name__ == "__main__":
    mcp.run()

Comprobar que el servidor expone exactamente tres herramientas

Ya construimos toda la capa MCP del proyecto. Tenemos un servidor configurado, tres funciones registradas y una forma de comunicar los errores del núcleo.

¿Qué podemos comprobar mediante el cliente?

  1. Descubrir las herramientas con client.list_tools().
  2. Confirmar que existen exactamente listar_notas, buscar_notas y leer_nota.
  3. Invocar listar_notas sin argumentos.
  4. Buscar la palabra permisos.
  5. Leer python.md y comprobar su contenido.
  6. Intentar leer ../privado.md y comprobar que la llamada termina con un error de herramienta.

Estas operaciones ya cuentan con pruebas en tests/test_mcp.py. El archivo utiliza un cliente MCP real conectado a un servidor creado con una carpeta temporal de pruebas.

Resultado de esta fase: nuestros métodos Python ya están expuestos como herramientas MCP. El servidor tiene una responsabilidad clara: comunicar solicitudes y resultados, delegando la validación y lectura a NotesStore.

Lo que este servidor no hace

No ofrece herramientas para crear, modificar o borrar notas. Tampoco implementa autenticación, aislamiento multiusuario ni una API HTTP preparada para Internet público.

Las anotaciones de solo lectura ayudan a describir las herramientas, pero la seguridad depende también de las operaciones implementadas y del entorno donde ejecutamos el proceso.

Siguiente paso: utilizar el servidor desde Python

En la Fase E estudiaremos cliente_demo.py. Aprenderemos a conectarnos con Client, utilizar asyncio, invocar herramientas mediante call_tool e interpretar los resultados.

Fase E · Cliente MCP

Crear un cliente MCP con Python: del servidor a las llamadas reales

Ya construimos un servidor capaz de listar, buscar y leer notas Markdown. Ahora vamos a comprobarlo desde el otro extremo: una aplicación cliente que utiliza sus herramientas.

Para ello estudiaremos cliente_demo.py, un programa pequeño que utiliza el SDK MCP, programación asíncrona y llamadas estructuradas.

Nuestro cliente no necesita ChatGPT, un modelo de lenguaje ni una clave de API. Las operaciones se realizan en el entorno local de Python.

cliente_demo.py → Client → MCPServer → NotesStore

Qué aprenderemos

01

Conectarnos

Abrir una conexión con un servidor MCP mediante Client.

02

Invocar

Enviar nombres de herramientas y argumentos con call_tool.

03

Interpretar

Revisar errores y mostrar los resultados estructurados.

Idea principal: construir un servidor MCP es solo una parte del sistema. Un cliente compatible necesita establecer una conexión y utilizar las herramientas que el servidor ofrece.

Las importaciones de cliente_demo.py

El cliente necesita solamente tres importaciones. Cada una cumple una responsabilidad diferente.

01

asyncio

Biblioteca estándar de Python que permite ejecutar la función asíncrona principal.

02

Client

Clase del SDK MCP que permite establecer la conexión e invocar herramientas del servidor.

03

mcp

Instancia del servidor que construimos anteriormente en server.py.

¿Por qué importamos directamente el servidor?

La instrucción from server import mcp permite entregar al cliente la instancia Python del servidor.

Al importar server.py, se ejecuta mcp = create_server(). Sin embargo, no se ejecuta mcp.run(), porque esa instrucción está protegida por if __name__ == "__main__".

Distinción importante: este cliente utiliza una conexión en memoria con el servidor. No inicia un proceso STDIO externo como hace el flujo de MCP Inspector.

Python
import asyncio
from mcp import Client
from server import mcp

Programación asíncrona: comprender async y await

Las operaciones de comunicación pueden necesitar esperar respuestas. Python permite expresar estos flujos mediante programación asíncrona.

En nuestro cliente, la función principal se declara con async def. Esto significa que puede utilizar expresiones como await para esperar operaciones asíncronas.

ASYNC

async def

Define una función asíncrona, también llamada corrutina. Su cuerpo puede esperar operaciones sin bloquear de la misma manera que una llamada síncrona convencional.

AWAIT

await

Espera el resultado de una operación asíncrona. Mientras espera, el bucle de eventos puede ejecutar otras tareas disponibles.

El papel de asyncio.run()

Al ejecutar el programa, utilizamos asyncio.run(main()). Esta llamada crea y administra el bucle de eventos necesario para ejecutar nuestra corrutina principal.

No confundas los conceptos: asyncio pertenece a Python y no es parte exclusiva del protocolo MCP. El SDK aprovecha este mecanismo para implementar su cliente asíncrono.

Python
async def main() -> None:
    async with Client(mcp, raise_exceptions=True) as client:

Establecer una conexión con Client

Para comunicarse con nuestro servidor, el programa construye un cliente utilizando la clase Client del SDK MCP.

La instrucción async with Client(mcp, raise_exceptions=True) as client establece un contexto asíncrono. Dentro de ese contexto podemos realizar las llamadas a las herramientas disponibles.

Descomponer la instrucción

  1. Client: es la clase del SDK que implementa las operaciones del cliente MCP.
  2. mcp: es la instancia del servidor importada desde server.py.
  3. raise_exceptions=True: configura la propagación de excepciones según el comportamiento del SDK. El programa también comprueba explícitamente los resultados marcados como error.
  4. async with: administra la entrada y salida del contexto de conexión.
  5. as client: proporciona el objeto que usaremos para invocar las herramientas.

Ventaja de utilizar un contexto: al terminar el bloque async with, el SDK puede liberar los recursos asociados con la conexión.

Invocar herramientas MCP mediante call_tool()

Una vez conectado, el cliente necesita indicar qué herramienta quiere ejecutar y proporcionar los argumentos necesarios.

En lugar de escribir tres llamadas independientes, nuestro programa utiliza un bucle que recorre los nombres y argumentos de las tres herramientas.

Las tres solicitudes del ejemplo

01

listar_notas

Recibe un diccionario vacío {}, porque la herramienta no necesita argumentos.

02

buscar_notas

Recibe {"consulta": "permisos"} para buscar esa palabra en los documentos.

03

leer_nota

Recibe {"nombre": "python.md"} para recuperar el contenido de esa nota.

¿Qué devuelve call_tool?

El método client.call_tool ejecuta una herramienta y devuelve un objeto de resultado MCP.

El argumento nombre selecciona la herramienta, mientras que argumentos contiene el diccionario que se enviará.

Detalle de Python: el bucle for nombre, argumentos desempaqueta cada pareja de valores. Así podemos reutilizar la misma operación para diferentes herramientas.

Python
for nombre, argumentos in (
            ("listar_notas", {}),
            ("buscar_notas", {"consulta": "permisos"}),
            ("leer_nota", {"nombre": "python.md"}),
        ):
            result = await client.call_tool(nombre, argumentos)

Comprobar errores e interpretar los resultados del cliente

Después de cada llamada, el programa revisa el objeto que recibió. No debemos asumir que toda solicitud termina correctamente.

Primero: ¿la herramienta indicó un error?

La propiedad result.is_error permite comprobar si la respuesta corresponde a un fallo de herramienta.

Si su valor es verdadero, el programa genera un RuntimeError que incluye el nombre de la operación y el contenido asociado al error.

Después: acceder a los datos estructurados

Para las llamadas correctas, nuestro cliente imprime result.structured_content.

En este proyecto, los resultados correctos contienen una clave result con el dato devuelto por la herramienta: una lista de nombres o una cadena con el texto Markdown.

OK

Respuesta correcta

is_error es falso y podemos consultar structured_content.

ERR

Error de herramienta

Si is_error es verdadero, el cliente evita tratar la respuesta como si fuera un resultado correcto.

Una distinción importante: la opción raise_exceptions=True no sustituye el examen de result.is_error. Nuestro ejemplo contempla tanto las excepciones de la operación como los errores comunicados mediante resultados de herramienta.

Python
if result.is_error:
                raise RuntimeError(f"Error en {nombre}: {result.content}")
            print(f"{nombre}: {result.structured_content}")
Python
"""Cliente MCP en memoria; no requiere IA ni claves de API."""
import asyncio
from mcp import Client
from server import mcp


async def main() -> None:
    async with Client(mcp, raise_exceptions=True) as client:
        for nombre, argumentos in (
            ("listar_notas", {}),
            ("buscar_notas", {"consulta": "permisos"}),
            ("leer_nota", {"nombre": "python.md"}),
        ):
            result = await client.call_tool(nombre, argumentos)
            if result.is_error:
                raise RuntimeError(f"Error en {nombre}: {result.content}")
            print(f"{nombre}: {result.structured_content}")


if __name__ == "__main__":
    asyncio.run(main())

Ejecutar nuestro cliente MCP desde la terminal

Ahora que entendemos el código, podemos ejecutarlo. Necesitamos tener el proyecto en nuestro equipo, instalar sus dependencias y utilizar una versión de Python compatible.

Sitúate en la carpeta mcp-python-secure-notes, donde se encuentran cliente_demo.py, server.py y requirements.txt.

¿Qué instalaremos?

El archivo requirements.txt declara la dependencia mcp[cli]==2.3.0. La instalación mediante pip install -r requirements.txt utiliza esa versión concreta del SDK.

Por qué utilizar un entorno virtual: permite mantener las dependencias del proyecto separadas de otras instalaciones de Python.

Windows: PowerShell

En Windows crearemos un entorno virtual con Python 3.12, instalaremos las dependencias y ejecutaremos el cliente.

PowerShell
py -3.12 -m venv .venv

.\.venv\Scripts\python.exe -m pip install -r requirements.txt

.\.venv\Scripts\python.exe cliente_demo.py
Bash
python3 -m venv .venv

./.venv/bin/python -m pip install -r requirements.txt

./.venv/bin/python cliente_demo.py

Interpretar la salida de nuestro cliente MCP

Si el proyecto está correctamente instalado y las notas de ejemplo conservan su contenido original, las tres llamadas deben finalizar correctamente.

El cliente imprime una línea por herramienta utilizando result.structured_content. La salida esperada contiene un diccionario con la clave result.

Qué significa cada resultado

01

Listado correcto

Aparecen python.md y seguridad.md, ordenados por su nombre.

02

Búsqueda correcta

La palabra permisos aparece en el contenido de seguridad.md.

03

Lectura correcta

El cliente recibe el texto de python.md con su encabezado y párrafos Markdown.

¿Por qué aparecen caracteres como \n?

El programa imprime la representación de un diccionario Python. Por eso los saltos de línea contenidos en una cadena pueden aparecer representados como \n, en lugar de mostrarse como párrafos.

Qué acabamos de demostrar: un cliente Python puede comunicarse con nuestro servidor MCP, ejecutar herramientas y recuperar información local sin utilizar un modelo de lenguaje.

Texto plano
listar_notas: {'result': ['python.md', 'seguridad.md']}
buscar_notas: {'result': ['seguridad.md']}
leer_nota: {'result': '# Python para proyectos reales\n\nPython permite crear APIs, automatizaciones y agentes de IA.\nEste ejemplo usa MCP v2, funciones Python y pytest.\n'}

Resumen: nuestro primer cliente MCP está funcionando

Hemos estudiado el cliente completo, desde sus importaciones hasta el resultado de las tres herramientas.

Ahora podemos distinguir con claridad las responsabilidades de cada componente.

01

asyncio

Ejecuta nuestra función principal y permite esperar las llamadas asíncronas.

02

Client

Establece la conexión MCP con nuestro servidor Python.

03

call_tool

Invoca herramientas por nombre, envía argumentos y recibe resultados.

Lo que conviene recordar

  1. MCP permite que distintas aplicaciones utilicen herramientas mediante una interfaz de comunicación compartida.
  2. Nuestro cliente se conecta al objeto servidor directamente en memoria.
  3. Los argumentos de las herramientas se envían como diccionarios Python.
  4. Debemos comprobar si una llamada produjo un error antes de utilizar su resultado.
  5. No hace falta contratar una API de IA para construir y probar un cliente MCP.

Resultado de esta fase: hemos conectado un cliente con nuestro servidor y aprendido a invocar tres herramientas que consultan archivos Markdown.

Siguiente paso: comprobar el programa con pruebas automatizadas

En la Fase F estudiaremos las pruebas unitarias y de integración del proyecto. Aprenderemos a utilizar pytest para verificar que las herramientas funcionan correctamente y rechazan entradas no permitidas.

Fase F · Pruebas y seguridad

Probar nuestro servidor MCP con pytest: comprobar antes de confiar

Nuestro servidor ya puede listar, buscar y leer documentos Markdown. Pero que un programa funcione en una demostración no significa que responda correctamente ante entradas inválidas o situaciones inesperadas.

En esta fase utilizaremos pytest para comprobar automáticamente el comportamiento de nuestro núcleo Python y de las herramientas MCP.

Trabajaremos con las pruebas reales del repositorio, organizadas en tests/test_notas_core.py y tests/test_mcp.py.

¿Qué queremos comprobar?

01

Funcionalidad

Que las notas se puedan listar, buscar y leer correctamente.

02

Validaciones

Que los nombres peligrosos, tamaños excesivos y datos inválidos produzcan errores controlados.

03

Integración MCP

Que un cliente pueda invocar las tres herramientas y recibir las respuestas esperadas.

Principio fundamental: una prueba no demuestra que un sistema sea completamente seguro. Comprueba una propiedad concreta bajo determinadas condiciones. Por eso necesitamos casos normales, errores y situaciones límite.

Pruebas unitarias e integración: dos niveles de comprobación

Nuestro proyecto separa la lógica de archivos de la comunicación MCP. Esa arquitectura también nos permite probar ambas responsabilidades de forma independiente.

UNIT

Pruebas del núcleo

En test_notas_core.py comprobamos directamente los métodos de NotesStore. Estas pruebas no necesitan crear un cliente MCP.

MCP

Pruebas de integración

En test_mcp.py construimos un servidor, conectamos un cliente real del SDK e invocamos sus herramientas.

¿Por qué necesitamos ambas?

Si una prueba directa de NotesStore falla, podemos investigar la lógica del núcleo. Si esa prueba funciona, pero la integración falla, debemos revisar el registro de herramientas, los argumentos o el intercambio de resultados.

Ventaja de la separación: cada grupo de pruebas aporta información sobre una capa distinta del programa, lo que facilita localizar y corregir defectos.

Instalar pytest y preparar el entorno de pruebas

En la fase anterior instalamos las dependencias necesarias para ejecutar el cliente. Ahora utilizaremos requirements-dev.txt, que añade las herramientas de desarrollo.

Este archivo incluye el contenido de requirements.txt mediante la opción -r y fija la versión de pytest utilizada por el proyecto.

¿Qué significa cada dependencia?

  • -r requirements.txt: instala las dependencias principales, incluido el SDK MCP.
  • pytest==9.0.2: instala la herramienta con la que descubriremos y ejecutaremos las pruebas automatizadas.

Los archivos de pruebas utilizan nombres que empiezan por test_. pytest los descubre y ejecuta automáticamente.

Texto plano
-r requirements.txt
pytest==9.0.2
PowerShell
.\.venv\Scripts\python.exe -m pip install -r requirements-dev.txt

.\.venv\Scripts\python.exe -m pytest -q -rs
Bash
./.venv/bin/python -m pip install -r requirements-dev.txt

./.venv/bin/python -m pytest -q -rs

Estás viendo solo el 60% del contenido. Hazte Premium para acceder al tutorial completo.

Comunidad

Comentarios y valoraciones

No hay comentarios aún. ¡Sé el primero en opinar!