Cuando expones tu API como herramientas MCP para clientes IA, enfrentas un problema de mantenimiento: cada endpoint tiene dos esquemas. El esquema REST (OpenAPI) y el esquema de herramienta MCP (JSON Schema en inputSchema). Describen la misma operación pero viven en archivos diferentes, escritos por personas (o sesiones) diferentes, en momentos diferentes. Van a divergir.
Este artículo describe cómo sh0 resolvió esto generando definiciones de herramientas MCP directamente desde anotaciones OpenAPI, usando el sistema de extensiones de utoipa.
El problema: 12 herramientas, 24 esquemas
El servidor MCP de sh0 (Fase 1) se entregó con 12 herramientas curadas manualmente. Cada herramienta tenía:
- Una definición
McpToolentools.rscon un JSON Schema escrito manualmente - Un manejador REST en
handlers/*.rscon anotaciones utoipa generando un esquema OpenAPI
Ambos describían los mismos parámetros para la misma operación. La herramienta list_apps tenía page y per_page como enteros opcionales. El endpoint GET /api/v1/apps tenía PaginationParams con los mismos campos. Dos fuentes de verdad para una sola realidad.
Agregar una nueva herramienta MCP requería tocar tres ubicaciones: la anotación utoipa, la función tool_definitions(), y el brazo execute_tool() del match. Olvidar uno y obtienes una herramienta que la IA puede llamar pero el servidor no puede ejecutar, o un esquema que promete parámetros que el manejador ignora.
La solución: extensiones OpenAPI como metadatos MCP
utoipa v5 soporta extensiones OpenAPI personalizadas en anotaciones #[utoipa::path]:
rust#[utoipa::path(
get,
path = "/api/v1/apps",
tag = "Apps",
params(PaginationParams),
responses(...),
security(("bearer" = [])),
extensions(
("x-mcp-enabled" = json!(true)),
("x-mcp-risk" = json!("read")),
("x-mcp-description" = json!("List all deployed applications."))
)
)]
pub async fn list_apps(...) -> ... { ... }La extensión x-mcp-enabled: true marca este endpoint como herramienta MCP. Al arrancar, sh0 parsea su propia especificación OpenAPI y genera definiciones de herramientas MCP desde las operaciones anotadas. Los parámetros del manejador se convierten en el inputSchema de la herramienta. La descripción se convierte en la descripción de la herramienta. El operationId se convierte en el nombre de la herramienta.
Una anotación. Un esquema. Cero deriva.

El protocolo de extensiones
Definimos cinco extensiones:
| Extensión | Propósito |
|---|---|
x-mcp-enabled | Marca un endpoint como herramienta MCP |
x-mcp-risk | Nivel de riesgo (read, write, admin) para futura aplicación de claves con alcance |
x-mcp-name | Sobreescribe el nombre de la herramienta cuando difiere del operationId |
x-mcp-description | Sobreescribe la descripción con redacción específica para MCP |
x-mcp-param-map | Remapea nombres de parámetros (ej. parámetro de ruta id se convierte en app_id) |
El x-mcp-param-map merece explicación. Los parámetros de ruta OpenAPI a menudo usan nombres genéricos como {id}. Pero las herramientas MCP se benefician de nombres descriptivos: app_id le dice a la IA qué tipo de identificador proporcionar. El mapeo es declarativo:
rust("x-mcp-param-map" = json!({"id": {"name": "app_id", "description": "App ID or app name"}}))El generador: 150 líneas de Rust
El módulo openapi.rs es intencionalmente simple. No intenta ser un conversor genérico de OpenAPI a MCP. Lee la especificación específica de sh0 y produce las herramientas específicas de sh0:
- Iterar todas las rutas y operaciones en la especificación OpenAPI
- Filtrar por
x-mcp-enabled: true - Para cada operación coincidente, construir un
McpTool: - - Nombre desde
x-mcp-nameuoperationId - - Descripción desde
x-mcp-description,summaryodescription - -
inputSchemadesde parámetros de ruta y consulta, con remapeo de nombres - Agregar herramientas definidas manualmente (una herramienta,
get_app_logs, llama a Docker directamente y no tiene endpoint REST)
El resultado: 12 herramientas, idénticas a las versiones escritas a mano, derivadas de las mismas anotaciones que generan la especificación OpenAPI.
El híbrido: definiciones automáticas, ejecución manual
Un sistema completamente automático también enrutaría las llamadas de herramientas a los manejadores automáticamente. Elegimos no hacer esto para la Fase 2. Las definiciones de herramientas (lo que la IA ve) se generan desde OpenAPI. La ejecución de herramientas (lo que sucede cuando la IA llama una herramienta) permanece en la función de despacho manual execute_tool().
Esto significa que agregar una nueva herramienta MCP aún requiere dos pasos:
1. Agregar las extensiones utoipa al manejador
2. Agregar el brazo del ejecutor en tools.rs
Pero el esquema nunca se escribe a mano. La forma de los argumentos, sus tipos, cuáles son requeridos -- todo derivado de las anotaciones utoipa existentes del manejador.
¿Por qué no auto-enrutamiento completo? Porque el ejecutor MCP hace más que simplemente llamar al manejador REST. Resuelve aplicaciones por nombre (no solo ID), obtiene datos relacionados (dominios, conteo de variables de entorno), y formatea la salida de manera diferente a la respuesta REST. La lógica de ejecución vale la pena escribirla explícitamente. La lógica de definición no.
Verificación: pruebas unitarias para paridad
La parte más arriesgada de esta migración son cambios sutiles en los esquemas. Si el esquema generado de list_apps tiene un nombre de propiedad o tipo diferente al de la versión escrita a mano, el cliente IA podría enviar argumentos que el ejecutor no espera.
Cuatro pruebas unitarias verifican la paridad:
- Los 12 nombres de herramientas esperados están presentes
- get_app tiene app_id como parámetro requerido (remapeado desde id)
- list_apps tiene propiedades page y per_page
- get_server_status tiene un objeto de propiedades vacío
Estas pruebas se ejecutan contra la especificación OpenAPI real generada por utoipa, detectando cualquier deriva entre anotaciones y expectativas.

La auditoría atrapa lo que el constructor pasa por alto
Aquí es donde la metodología multi-sesión demuestra su valor. La sesión principal construyó la funcionalidad y siguió adelante. Una sesión de auditor separada -- contexto fresco, sin apego a la implementación -- detectó inmediatamente un problema de rendimiento: la especificación OpenAPI se parseaba en cada solicitud tools/list, aunque la especificación es estática en tiempo de ejecución (se deriva de anotaciones utoipa en tiempo de compilación).
La corrección fue un caché LazyLock: parsear la especificación una vez en el primer acceso, servir el resultado cacheado en cada llamada subsiguiente. Tres líneas de código, cero asignaciones por solicitud después de la primera.

Este es el valor del flujo de trabajo construir-auditar-auditar: el constructor optimiza para la corrección. El auditor optimiza para todo lo demás. Ninguna sesión por sí sola habría producido código que fuera tanto correcto como eficiente.
Lo que aprendimos
Las extensiones están subutilizadas. Las extensiones OpenAPI (x-*) son un mecanismo estándar que la mayoría de los código base ignoran. Son el lugar correcto para metadatos específicos de tu sistema pero que no son parte del estándar OpenAPI. Metadatos de herramientas MCP, pistas de límite de velocidad, feature flags, cronogramas de deprecación -- todo encaja naturalmente como extensiones.
El generador debe ser específico, no general. Un conversor genérico de OpenAPI a MCP necesitaría manejar cuerpos de solicitud, esquemas de respuesta, flujos de autenticación, y docenas de casos extremos. Nuestro generador maneja parámetros de ruta, parámetros de consulta, y cinco extensiones personalizadas. Son 150 líneas y hace exactamente lo que necesitamos.
La nomenclatura de parámetros importa para la ergonomía IA. La diferencia entre id y app_id es la diferencia entre una IA que adivina y una IA que sabe qué proporcionar. La extensión x-mcp-param-map permite que la API REST mantenga sus convenciones RESTful mientras la herramienta MCP usa nombres de argumentos descriptivos.
Lo que viene después
La Fase 3 usará la extensión x-mcp-risk para claves API con alcance. Una clave con alcance read solo verá herramientas con x-mcp-risk: "read". Una clave con alcance write verá herramientas read y write. Los metadatos de riesgo ya están en la especificación OpenAPI, integrados en cada endpoint anotado. La capa de aplicación solo necesita filtrar la lista de herramientas.
La base está puesta. Cada futura herramienta MCP está a cinco líneas de anotación de distancia.