Creación de un servidor MCP con el SDK de Python
El SDK oficial de Python para MCP te proporciona un pequeño conjunto de decoradores para registrar herramientas, recursos y prompts.
Todo lo demás, como la conexión, la negociación de capacidades y el enrutamiento de solicitudes, se maneja por ti.
Resumen
Un servidor MCP de Python se construye alrededor de FastMCP, la clase de servidor de alto nivel del SDK.
Registras capacidades con tres decoradores: @mcp.tool(), @mcp.resource() y @mcp.prompt().
Las anotaciones de tipo en cada función sirven como el esquema de entrada que el cliente ve durante la negociación de capacidades.
Los errores que se generan dentro de un manejador se convierten en respuestas de error estructuradas en lugar de bloquear el servidor.
Esta página se basa en el esqueleto mínimo de la página de conceptos básicos y profundiza en los argumentos tipados, los recursos parametrizados y el manejo de errores.
Receta
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("tickets-server")
@mcp.tool()
def create_ticket(title: str, priority: str = "normal") -> str:
"""Crea un ticket de soporte y devuelve su ID."""
return "T-1042"
@mcp.resource("tickets://{ticket_id}")
def get_ticket(ticket_id: str) -> str:
"""Recupera un ticket individual por ID."""
return f"Ticket {ticket_id}: abierto"
@mcp.prompt()
def triage_prompt(ticket_id: str) -> str:
return f"Revisa el ticket {ticket_id} y recomienda el siguiente paso."
if __name__ == "__main__":
mcp.run(transport="stdio")Cuándo recurrir a esto:
- Quieres exponer una API interna, base de datos o sistema de archivos a Claude como herramientas que se puedan llamar.
- Necesitas datos de solo lectura (configuración, documentación, registros) disponibles para un cliente sin que cuenten como una "acción".
- Quieres plantillas de prompt reutilizables compartidas entre todos los clientes que se conectan a este servidor.
- Estás construyendo primero para desarrollo local;
stdioes el transporte adecuado antes de añadir una capa de red.
Ejemplo de trabajo
from dataclasses import dataclass
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("tickets-server")
@dataclass
class Ticket:
id: str
title: str
priority: str
status: str
_tickets: dict[str, Ticket] = {
"T-1": Ticket(id="T-1", title="Impresora sin conexión", priority="low", status="open"),
"T-2": Ticket(id="T-2", title="Interrupción de VPN", priority="high", status="closed"),
}
_next_id = 3
@mcp.tool()
def create_ticket(title: str, priority: str = "normal") -> str:
"""Crea un nuevo ticket de soporte y devuelve su ID.
La prioridad debe ser una de las siguientes: low, normal, high.
"""
global _next_id
if priority not in ("low", "normal", "high"):
raise ValueError(f"prioridad no válida '{priority}', se esperaba low, normal o high")
ticket_id = f"T-{_next_id}"
_next_id += 1
_tickets[ticket_id] = Ticket(id=ticket_id, title=title, priority=priority, status="open")
return ticket_id
@mcp.tool()
def close_ticket(ticket_id: str) -> str:
"""Marca un ticket como cerrado."""
ticket = _tickets.get(ticket_id)
if ticket is None:
raise ValueError(f"no existe ticket con id '{ticket_id}'")
ticket.status = "closed"
return f"{ticket_id} cerrado"
@mcp.resource("tickets://{ticket_id}")
def get_ticket(ticket_id: str) -> str:
"""Recupera los detalles de un ticket individual por ID."""
ticket = _tickets.get(ticket_id)
if ticket is None:
raise ValueError(f"no existe ticket con id '{ticket_id}'")
return f"{ticket.id} [{ticket.priority}] {ticket.title} - {ticket.status}"
@mcp.resource("tickets://open")
def list_open_tickets() -> str:
"""Lista todos los tickets abiertos."""
open_ids = [t.id for t in _tickets.values() if t.status == "open"]
return "\n".join(open_ids) if open_ids else "no hay tickets abiertos"
@mcp.prompt()
def triage_prompt(ticket_id: str) -> str:
"""Crea un prompt pidiendo una recomendación de triaje para un ticket."""
return f"Revisa el ticket {ticket_id} y recomienda si escalarlo o cerrarlo."
if __name__ == "__main__":
mcp.run(transport="stdio")Lo que esto demuestra:
- Dos herramientas,
create_ticketyclose_ticket, que modifican el estado del lado del servidor y validan sus propias entradas antes de hacerlo. - Un recurso parametrizado,
tickets://{ticket_id}, junto a un recurso fijo,tickets://open, mostrando ambos estilos. - Errores de validación generados como
ValueErrorsimples, que el SDK convierte en una respuesta de error estructurada en lugar de bloquear el proceso. - Una plantilla de prompt que hace referencia a un ID de ticket, mantenida intencionalmente simple ya que los prompts deben permanecer declarativos.
Inmersión Profunda
Cómo funciona
FastMCP("tickets-server")crea una instancia de servidor y conecta inmediatamente las etapas de conexión y negociación del ciclo de vida de la solicitud.- Cada llamada a
@mcp.tool()registra la función decorada en la tabla de herramientas interna del servidor, indexada por el nombre de la función a menos que se proporcione un nombre explícito. - Las anotaciones de tipo y la cadena de documentación de la función se introspeccionan en el momento del registro para construir el esquema JSON que se envía a los clientes durante la negociación de capacidades.
- Cuando un cliente llama a una herramienta, el SDK valida los argumentos entrantes contra ese esquema antes de que se ejecute el cuerpo de tu función.
- Los recursos siguen el mismo patrón de registro pero se abordan por URI; un
{placeholder}en la plantilla URI se convierte en un parámetro que se pasa a tu función.
Manejo de errores
Generar una excepción estándar de Python dentro de cualquier manejador es la forma correcta de señalar un fallo.
@mcp.tool()
def get_priority_multiplier(priority: str) -> float:
multipliers = {"low": 0.5, "normal": 1.0, "high": 2.0}
if priority not in multipliers:
raise ValueError(f"prioridad desconocida '{priority}'")
return multipliers[priority]El SDK captura la excepción y la convierte en un resultado de error estructurado que el cliente puede mostrar o razonar, en lugar de dejar que bloquee el proceso del servidor.
Prefiere generar errores temprano con un mensaje claro en lugar de devolver una cadena como "error: entrada incorrecta", que el cliente trataría como un resultado exitoso.
Herramientas vs. Recursos vs. Prompts de un vistazo
| Capacidad | Propósito | ¿Tiene efectos secundarios? | Ejemplo |
|---|---|---|---|
| Herramienta | Realizar una acción | A menudo sí | create_ticket, send_email |
| Recurso | Leer datos por URI | No | tickets://open, config://settings |
| Prompt | Plantilla de solicitud reutilizable | No | triage_prompt |
Notas de Python
# Usa anotaciones de tipo precisas; se convierten en el esquema contra el cual el cliente valida.
@mcp.tool()
def set_priority(ticket_id: str, priority: str) -> str:
...
# Los argumentos opcionales con valores predeterminados se reflejan como opcionales en el esquema.
@mcp.tool()
def search_tickets(query: str, limit: int = 10) -> list[str]:
...Las anotaciones de tipo precisas son más importantes aquí que en el código interno típico, ya que se convierten en parte de un contrato del que depende un cliente, no solo en documentación para otro desarrollador.
Parámetros y valores de retorno
| Decorador | Registra | Convención de tipo de retorno |
|---|---|---|
@mcp.tool() | Una acción invocable | Cualquier valor serializable en JSON, o un resumen en cadena |
@mcp.resource(uri) | Contenido de solo lectura en una URI | Una cadena o un bloque de contenido estructurado |
@mcp.prompt() | Una plantilla de prompt reutilizable | Una cadena, o una lista de objetos de mensaje para plantillas de múltiples turnos |
Trampas comunes
- Anotaciones de tipo vagas producen un esquema inútil. Un parámetro tipado como
strcuando en realidad es un conjunto fijo de valores (comopriority) no da al cliente ninguna pista sobre las entradas válidas. Solución: valida contra un conjunto explícito dentro de la función y genera unValueErrorclaro, o usa una anotación de tipoLiteraldonde el SDK lo soporte. - Devolver una cadena de error en lugar de generar una excepción. Devolver
"error: no encontrado"de una herramienta parece un resultado exitoso para el cliente. Solución: genera una excepción; deja que el SDK la convierta en una respuesta de error adecuada. - Modificar estado compartido sin protección para llamadas concurrentes. Un diccionario a nivel de módulo como
_ticketspuede ser modificado por solicitudes superpuestas si el servidor maneja más de una sesión. Solución: respalda el estado real con una base de datos u otra tienda segura para concurrencia una vez que hayas superado la prototipación local. - Olvidar la cadena de documentación. Una herramienta sin cadena de documentación se registra bien pero no proporciona nada a Claude para razonar al decidir si llamarla. Solución: escribe siempre una cadena de documentación de una línea que describa lo que hace la herramienta y cualquier restricción en sus argumentos.
- Renombrar los argumentos de una herramienta en una versión posterior. Los clientes existentes que llaman a la herramienta con los nombres de argumento antiguos comenzarán a fallar la validación. Solución: trata los nombres de los argumentos como parte de tu contrato público y consulta las estrategias de versionado de esquemas antes de cambiarlos.
- Bloquear E/S dentro de un manejador. Una herramienta que realiza una llamada de red síncrona lenta bloquea el servidor para esa solicitud. Solución: usa
async defpara las funciones de herramienta que realizan E/S; el SDK soporta manejadores asíncronos directamente.
Alternativas
| Alternativa | Úsala cuando | No la uses cuando |
|---|---|---|
| SDK de TypeScript para MCP | Tu servicio o equipo existente se basa en TypeScript/Node | Quieres reutilizar una pila existente de datos o ML en Python |
Clases de protocolo MCP de bajo nivel (sin FastMCP) | Necesitas un control detallado del protocolo que la API de alto nivel no expone | Solo necesitas herramientas, recursos y prompts estándar; FastMCP cubre esto |
| Una API REST simple (sin MCP) | El consumidor no es un cliente LLM y no necesita negociación de capacidades | Quieres que Claude descubra y llame a tu funcionalidad dinámicamente |
Preguntas frecuentes
¿Tengo que usar FastMCP o puedo usar el protocolo directamente?
FastMCP es la API de alto nivel recomendada y cubre la gran mayoría de los casos de uso. El SDK también expone clases de protocolo de bajo nivel para escenarios avanzados que requieren un control más detallado, pero la mayoría de los servidores nunca las necesitan.
¿Cómo sabe el SDK qué argumentos espera una herramienta?
- Introspecciona las anotaciones de tipo de la función en el momento del registro.
- Esas anotaciones se convierten en un esquema JSON que se envía a los clientes durante la negociación de capacidades.
- El SDK valida los argumentos entrantes contra ese esquema antes de que se ejecute tu función.
¿Qué sucede si mi función de herramienta genera una excepción?
@mcp.tool()
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("no se puede dividir por cero")
return a / bEl SDK captura la excepción y devuelve un error estructurado al cliente en lugar de bloquear el proceso del servidor.
¿Puede un recurso tomar parámetros como una herramienta?
Sí. Una plantilla URI con un {placeholder}, como tickets://{ticket_id}, pasa ese segmento a tu función como un argumento, de la misma manera que funciona un argumento de herramienta.
¿Debo usar una herramienta o un recurso para datos de solo lectura?
Prefiere un recurso. Las herramientas implican una acción que el cliente elige invocar, mientras que los recursos están destinados a contenido que el cliente lee directamente. Usar una herramienta para la recuperación pura de datos funciona, pero es una falta de coincidencia semántica que puede confundir cómo un cliente presenta la capacidad.
¿Pueden los manejadores de herramientas ser asíncronos?
Sí, async def es soportado directamente por el decorador. Úsalo para cualquier manejador que realice E/S de red o de archivos, de modo que una llamada lenta no bloquee otras solicitudes en el mismo servidor.
¿Cómo pruebo un servidor sin un cliente real?
Escribe pruebas unitarias que llamen directamente a tus funciones manejadoras y, por separado, ejércítalas a través de un cliente simulado que hable el protocolo. Consulta la página dedicada a pruebas de servidores MCP para ver el patrón completo.
¿Cuál es la diferencia entre la cadena de documentación de una herramienta y su descripción mostrada a Claude?
La cadena de documentación es la descripción. El SDK la lee directamente de la función y la incluye en el manifiesto de capacidades, por lo que Claude ve exactamente lo que escribiste como comentario de documentación.
¿Puede un servidor registrar herramientas síncronas y asíncronas?
Sí, el SDK maneja ambos. Elige según si el manejador individual realiza trabajo bloqueante, no según una regla general del servidor.
¿Hay un límite en la cantidad de herramientas que un servidor puede registrar?
El SDK en sí mismo no impone ningún límite fijo, pero un servidor con docenas de herramientas poco relacionadas se vuelve difícil de razonar tanto para Claude como para los mantenedores humanos. Agrupa las capacidades relacionadas en servidores enfocados en lugar de un único servidor "cajón de sastre".
¿Cómo cambio los argumentos de una herramienta sin romper los clientes existentes?
Añade argumentos nuevos como opcionales con valores predeterminados sensatos en lugar de renombrar o eliminar los existentes. Para cambios más grandes, consulta la página dedicada al versionado de esquemas de herramientas.
¿FastMCP requiere una versión específica de Python?
El SDK se dirige a Python moderno (mínimo 3.10+) para soportar las características de anotación de tipo de las que depende para la generación de esquemas. Consulta los requisitos publicados del SDK para conocer la versión mínima exacta en el momento de la instalación.
Relacionado
- Conceptos básicos de servidores MCP - el esqueleto mínimo en el que se basa este artículo
- Cómo los servidores MCP manejan las solicitudes - el ciclo de vida en el que se integran estos manejadores
- Creación de un servidor MCP con el SDK de TypeScript - los mismos patrones en TypeScript
- Pruebas de servidores MCP antes del despliegue - ejercicio de estos manejadores con un cliente simulado
- Referencia de versionado y limitación de velocidad de esquemas de herramientas MCP - mantener los cambios de herramientas compatibles con versiones anteriores
Versiones de la pila: Escrito contra la línea de modelos de Claude actual a partir de ~junio de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (el predeterminado) y Claude Haiku 4.5 - y los SDK actuales del Protocolo de Contexto del Modelo de Python/TypeScript. Los nombres de los modelos, las versiones de los SDK y la especificación MCP evolucionan rápidamente; verifica los detalles actuales en platform.claude.com/docs y modelcontextprotocol.io antes de confiar en ellos.