python
MCP sin sesiones: migra tu servidor Python al SDK v2
La especificación MCP 2026-07-28 eliminó el handshake y las sesiones, y el SDK de Python pasó a la versión 2. Qué cambia, cómo migrar de FastMCP a MCPServer y un ejemplo probado con confirmación del usuario.
Focuz Academy · 24 de agosto de 2026 · 5 min
El 28 de julio se publicó la especificación 2026-07-28 de MCP y, ese mismo día, la versión 2.0.0 del SDK de Python (mcp en PyPI). Agosto fue el mes en que el ecosistema se puso al día: el 18 de agosto el Claude Agent SDK para Python (versión 0.2.140) empezó a aceptar mcp 2.x, y el 22 de agosto los mantenedores publicaron la nueva hoja de ruta de MCP. Si mantienes un servidor MCP en Python, este es el momento de migrar.
Qué cambió en el protocolo
El cambio principal, en palabras de los mantenedores: MCP pasa de ser un protocolo bidireccional con estado a uno de petición/respuesta sin estado.
- Sin handshake ni sesiones. Desaparecen
initialize/initializedy el encabezadoMcp-Session-Id. Cada petición lleva su versión de protocolo, la identidad del cliente y sus capacidades en_meta. Existe un RPC opcional,server/discover, para conocer las capacidades del servidor. - Enrutamiento por encabezados. Las peticiones HTTP deben incluir
Mcp-MethodyMcp-Name, así que un gateway o un WAF puede enrutar sin leer el cuerpo JSON. - Multi Round-Trip Requests (MRTR). Las peticiones que el servidor le hacía al cliente, como elicitation y sampling, se rediseñan con MRTR. Ahora el servidor responde
resultType: "input_required"con lo que necesita, y el cliente reintenta la llamada original con las respuestas eninputResponses. - Listas cacheables.
tools/listy otras respuestas incluyenttlMsycacheScope. - Deprecaciones. Roots, Sampling y Logging quedan deprecados, con una ventana mínima de doce meses. El transporte HTTP+SSE antiguo también queda deprecado.
- Autorización. Validación del parámetro
isssegún RFC 9207, y el registro dinámico de clientes (DCR) se depreca a favor de los Client ID Metadata Documents (CIMD).
¿Y si tu servidor necesita estado? La recomendación oficial es generar un identificador explícito desde una herramienta y que el modelo lo pase como argumento en las siguientes llamadas.
Qué cambió en el SDK de Python
La guía de migración de v1 a v2 es extensa. Estos son los cambios que casi todo proyecto va a encontrar:
| v1 | v2 |
|---|---|
from mcp.server.fastmcp import FastMCP |
from mcp.server.mcpserver import MCPServer |
result.isError, tool.inputSchema |
result.is_error, tool.input_schema (snake_case) |
McpError |
MCPError |
FastMCP("x", port=9000, stateless_http=True) |
Los parámetros de transporte pasan a run() |
mcp.get_context() |
Se recibe ctx: Context como parámetro |
await ctx.elicit(...) |
En conexiones 2026-07-28 lanza NoBackChannelError |
Dos detalles que pueden cambiar el comportamiento sin dar error: las funciones síncronas (def) de tus herramientas ahora corren en un hilo de trabajo, y el segundo argumento posicional del constructor ya no es instructions sino title. Pasa todo por nombre, salvo el name.
Ejemplo: confirmar un pedido con MRTR
Supongamos un servidor que confirma pedidos de venta de Odoo, pero solo después de que el usuario lo apruebe. En v1 lo harías con ctx.elicit(). En v2, la forma que funciona en ambas versiones del protocolo es devolver la pregunta con un resolver. (Aquí los pedidos son un diccionario de ejemplo; en tu caso vendrían de Odoo).
# servidor_pedidos.py (mcp 2.0.0)
from typing import Annotated
from pydantic import BaseModel
from mcp.server.mcpserver import MCPServer, Elicit, Resolve
mcp = MCPServer("pedidos-odoo", version="0.1.0")
PEDIDOS = {"S00042": {"cliente": "Ferretería Lima", "total": 1250.0, "estado": "draft"}}
class Confirmacion(BaseModel):
confirmar: bool
async def preguntar(nombre: str) -> Elicit[Confirmacion]:
pedido = PEDIDOS[nombre]
return Elicit(
f"¿Confirmar {nombre} de {pedido['cliente']} por S/ {pedido['total']:.2f}?",
Confirmacion,
)
@mcp.tool()
async def confirmar_pedido(
nombre: str,
respuesta: Annotated[Confirmacion, Resolve(preguntar)],
) -> str:
"""Confirma un pedido de venta, previa aprobación del usuario."""
if not respuesta.confirmar:
return f"{nombre} sigue en borrador."
PEDIDOS[nombre]["estado"] = "sale"
return f"{nombre} confirmado."
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)
Y un cliente que responde a la pregunta. Client acepta la instancia del servidor (en memoria, ideal para tests) o una URL:
# probar.py
import sys
import anyio
from mcp import types
from mcp.client import Client
async def aprobar(context, params: types.ElicitRequestParams) -> types.ElicitResult:
print("El servidor pregunta:", params.message)
return types.ElicitResult(action="accept", content={"confirmar": True})
async def main(destino):
async with Client(destino, elicitation_callback=aprobar) as client:
r = await client.call_tool("confirmar_pedido", {"nombre": "S00042"})
print(r.content[0].text)
if len(sys.argv) > 1:
destino = sys.argv[1] # http://127.0.0.1:8000/mcp
else:
from servidor_pedidos import mcp as destino
anyio.run(main, destino)
Lo probamos con mcp 2.0.0 en Python 3.12, en memoria y por HTTP. En ambos casos la salida fue:
El servidor pregunta: ¿Confirmar S00042 de Ferretería Lima por S/ 1250.00?
S00042 confirmado.
En el log del servidor HTTP solo aparecieron peticiones POST /mcp: no hubo un stream abierto esperando. Esa es la idea de MRTR. La herramienta no "empuja" la pregunta, sino que la devuelve, y el cliente reintenta con la respuesta. El mismo elicitation_callback responde en ambos modos.
Si todavía necesitas el comportamiento antiguo (por ejemplo, un cliente que solo habla la versión anterior del protocolo), la guía indica que Client(server, mode="legacy", ...) reproduce el handshake initialize de v1.
Si usas el Claude Agent SDK
Según su CHANGELOG, la versión 0.2.140 del Claude Agent SDK para Python amplió la dependencia a mcp>=1.23.0,<3.0.0. Los servidores MCP en proceso ahora usan el transporte en memoria del propio mcp, así que una instancia de mcp.server.Server construida a mano funciona con fidelidad completa (recursos, prompts y todos los tipos de contenido). Además, con mcp 2.x se pueden cancelar herramientas cuando interrumpes al agente.
Qué hacer ahora
- Crea una rama y fija
mcp>=2,<3. Corre tus tests y anota cada error. - Aplica los renombres mecánicos:
FastMCP→MCPServer, camelCase → snake_case,McpError→MCPError. - Mueve los parámetros de transporte del constructor a
run()ostreamable_http_app(). - Reemplaza
ctx.elicit()por resolvers conElicityResolve, y prueba el flujo conClient(server). - Elimina el estado ligado a la sesión: devuelve identificadores explícitos desde tus herramientas.
- Revisa los avisos de deprecación de Roots, Sampling y Logging. Tienes doce meses, pero no los adoptes en código nuevo.
Fuentes: The 2026-07-28 Specification (blog de MCP), Guía de migración v1 a v2 del SDK de Python, mcp 2.0.0 en PyPI, The New MCP Roadmap, CHANGELOG de claude-agent-sdk-python.
Guía gratuita: 5 agentes Python para Odoo
Casos de Junior a Senior, con código verificado contra Odoo 20.
Descargar →¿Empiezas desde cero? Lee qué es el agent loop y sigue las lecciones gratuitas.