Adios
← Alojamiento de servidores MCP

Tutorial por lenguaje

Servidor MCP en Python

Crea un servicio MCP autenticado, conéctalo a la API de pedidos, prueba una llamada real a una herramienta y despliégalo en Adios.

Python 3.13 · Streamable HTTP · JWT · Adios

Empezar con un ejemplo en ejecución

Lánzalo y adáptalo a tu proyecto

Crea la API Orders, una base PostgreSQL y el servidor MCP que elijas en Adios. Subimos el código, configuramos credenciales JWT independientes e iniciamos las vistas previas de desarrollo.

Lanzar en Adios →

¿Aún no tienes cuenta? Crea una y vuelve a este ejemplo. Selecciona un equipo y revisa los recursos antes de lanzarlo. El ejemplo completo requiere un plan de pago con capacidad para dos espacios y una base de datos.

Créalo paso a paso

Seguir la guía manual

Diseña la API y el esquema, añade autenticación JWT, escribe las herramientas MCP y ejecútalas en tus espacios de desarrollo. Cada paso incluye código y comandos.

Seguir los pasos manuales →

Explora primero el ejemplo en ejecución. OAuth para el acceso de usuarios y el despliegue en producción son pasos posteriores.

Empezar por la API y la base de datos

Crea un servidor MCP en Python que consulte pedidos a través de una API REST. Añadirás autenticación JWT, probarás el servidor en un workspace de Adios y lo desplegarás.

Antes de empezar, necesitas una API en ejecución y su clave de firma JWT. La guía principal incluye una API de pedidos de ejemplo con Node.js 24 y PostgreSQL, e instrucciones para iniciar su workspace de desarrollo.

El proceso API escucha en el puerto 8081 y MCP en el 8080 dentro de sus workspaces. Los clientes usan las URL HTTPS generadas de las vistas previas. Mantén API_JWT_SECRET y MCP_JWT_SECRET separados.

Crea un servidor MCP en Python

En python/, crea un entorno virtual e instala requirements.txt. FastMCP genera el esquema a partir de la firma de la función. El manejador valida el ID, llama a la misma API con HTTPX y devuelve un diccionario tipado. El envoltorio ASGI verifica el JWT del cliente y conserva la gestión del ciclo de vida de la aplicación del SDK.

python/requirements.txtDescargar archivo
mcp==1.30.0
httpx==0.28.1
uvicorn==0.35.0
PyJWT==2.15.1
python/server.pyDescargar archivo
import os
import re
import time
from urllib.parse import urlparse

import httpx
import jwt
import uvicorn
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.transport_security import TransportSecuritySettings
from mcp.types import ToolAnnotations
from starlette.responses import JSONResponse

api_base = os.getenv("API_BASE_URL", "http://127.0.0.1:8081").rstrip("/")
api_secret = os.environ["API_JWT_SECRET"]
mcp_secret = os.environ["MCP_JWT_SECRET"]
if min(len(api_secret), len(mcp_secret)) < 32 or api_secret == mcp_secret:
    raise ValueError("Use separate API and MCP signing secrets of at least 32 characters")
origin = os.getenv("PUBLIC_ORIGIN", "http://127.0.0.1:8080").rstrip("/")
mcp = FastMCP(
    "orders-mcp", stateless_http=True, json_response=True,
    transport_security=TransportSecuritySettings(
        enable_dns_rebinding_protection=True,
        allowed_hosts=[urlparse(origin).netloc, "127.0.0.1:8080", "localhost:8080"],
        allowed_origins=[origin],
    ),
)


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def get_order(order_id: str, ctx: Context) -> dict[str, str]:
    """Read an order's shipping status from the Orders API."""
    if not re.fullmatch(r"[a-zA-Z0-9-]{1,64}", order_id):
        raise ValueError("Use an order ID containing letters, numbers, or hyphens")
    # Identity comes from verified HTTP state, not a user-supplied tool argument.
    subject = ctx.request_context.request.state.principal
    now = int(time.time())
    api_token = jwt.encode(
        {"iss": "orders-demo", "aud": "orders-api", "sub": subject,
         "scope": "orders:read", "iat": now, "exp": now + 300},
        api_secret, algorithm="HS256",
    )
    try:
        async with httpx.AsyncClient(timeout=5.0, follow_redirects=False) as client:
            response = await client.get(
                f"{api_base}/orders/{order_id}",
                headers={"Authorization": f"Bearer {api_token}"},
            )
            response.raise_for_status()
            data = response.json()
            if not isinstance(data.get("id"), str) or not isinstance(data.get("status"), str):
                raise ValueError("Unexpected API response")
            return {"id": data["id"], "status": data["status"]}
    except (httpx.HTTPError, ValueError, AttributeError):
        raise ValueError("Could not read this order. Check its ID and your API access.") from None


@mcp.custom_route("/healthz", methods=["GET"])
async def health(_request):
    return JSONResponse({"ok": True})


class JWTGate:
    def __init__(self, application):
        self.application = application

    async def __call__(self, scope, receive, send):
        if scope["type"] == "http" and scope["path"] == "/mcp":
            headers = dict(scope["headers"])
            try:
                match = re.fullmatch(rb"Bearer (\S+)", headers.get(b"authorization", b""))
                if not match:
                    raise ValueError("Missing token")
                claims = jwt.decode(
                    match[1], mcp_secret, algorithms=["HS256"],
                    issuer="orders-demo", audience="orders-mcp",
                    options={"require": ["sub", "exp"]},
                )
                if not isinstance(claims["sub"], str) or not claims["sub"]:
                    raise ValueError("Missing subject")
                if not isinstance(claims.get("scope"), str) or "orders:read" not in claims["scope"].split():
                    await JSONResponse({"error": "orders:read permission required"}, status_code=403)(scope, receive, send)
                    return
                scope.setdefault("state", {})["principal"] = claims["sub"]
            except (jwt.PyJWTError, ValueError):
                await JSONResponse(
                    {"error": "Invalid or expired access token"}, status_code=401,
                    headers={"WWW-Authenticate": 'Bearer error="invalid_token"'},
                )(scope, receive, send)
                return
        await self.application(scope, receive, send)


app = JWTGate(mcp.streamable_http_app())
if __name__ == "__main__":
    uvicorn.run(app, host=os.getenv("HOST", "127.0.0.1"), port=int(os.getenv("PORT", "8080")))

Usa la importación mcp.server.fastmcp del SDK oficial que se muestra aquí. Este ejemplo fija la versión v1 del SDK de Python para MCP; paquetes con nombres similares y versiones mayores más recientes pueden tener otras API de configuración y autorización.

Referencia oficial: SDK de Python para MCP v1.

Probar en un workspace de Adios

El workspace MCP verifica al solicitante antes de llamar a la API compartida.

Primero inicia el workspace API de la guía principal. En el manifiesto de este lenguaje, establece API_BASE_URL con el origen de la vista previa de la API y usa los secretos de firma del mismo equipo de desarrollo.

Terminal
adios ws create --name orders-mcp-dev --json
export MCP_WORKSPACE_ID=YOUR_MCP_WORKSPACE_ID
(cd python && adios sync "$MCP_WORKSPACE_ID")
adios ws run start "$MCP_WORKSPACE_ID" --wait --json
adios ws get "$MCP_WORKSPACE_ID"

Copia el origen generado de la vista previa MCP en PUBLIC_ORIGIN en adios.yaml. Sincroniza y reinicia antes de probar: la comprobación del host debe coincidir con el nombre real de la vista previa. Las sondas de salud pueden ejecutarse antes de esta actualización.

Terminal
(cd python && adios sync "$MCP_WORKSPACE_ID")
adios ws run restart "$MCP_WORKSPACE_ID" --wait --json
export MCP_URL=https://YOUR-MCP-PREVIEW-HOST/mcp
curl --fail https://YOUR-MCP-PREVIEW-HOST/healthz
curl -i "$MCP_URL"
# Expected: 401 without a JWT.
export MCP_ACCESS_TOKEN="$(node issue-token.mjs)"
python3.13 -m venv .client-venv
.client-venv/bin/pip install -r python/requirements.txt
.client-venv/bin/python check.py
# Expected result: {'id': 'demo-1001', 'status': 'shipped'}
Inspeccionar la comprobación del cliente
check.pyDescargar archivo
import asyncio
import os

from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client


async def main():
    url = os.getenv("MCP_URL", "http://127.0.0.1:8080/mcp")
    headers = {"Authorization": "Bearer " + os.environ["MCP_ACCESS_TOKEN"]}
    async with streamablehttp_client(url, headers=headers) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            assert any(tool.name == "get_order" for tool in tools.tools)
            result = await session.call_tool("get_order", {"order_id": "demo-1001"})
            assert not result.isError, result
            assert result.structuredContent == {"id": "demo-1001", "status": "shipped"}, result
            print("MCP initialize, tools/list, and get_order passed:", result.structuredContent)


asyncio.run(main())

Prueba un JWT caducado, una audiencia incorrecta y la ausencia del ámbito orders:read. Un token de demo-client no debe poder leer other-1002. La API devuelve 404 para el pedido de ese otro tenant; MCP devuelve un error de herramienta.

Revisa los registros de compilación y ejecución en el workspace. Tras cambiar el código, sincroniza y reinicia la vista previa; esta guía no presupone recarga automática.

Terminal
adios ws run stop "$MCP_WORKSPACE_ID"

Mantener la verificación JWT y planificar OAuth por separado

Esta implementación verifica los JWT emitidos por el operador. No implementa inicio de sesión de usuarios, consentimiento ni renovación. Conserva las comprobaciones de emisor, audiencia, firma, caducidad, ámbito y tenant al añadir un proveedor OAuth.

Para una integración con usuarios, sigue los requisitos de metadatos del recurso, verificación de tokens e incorporación de clientes de la guía principal. El control de acceso del servidor de recursos JavaScript incluido es una implementación independiente; no activa OAuth automáticamente en este servidor.

Usa el soporte de autorización del servidor de recursos del SDK con un verificador de tokens y metadatos del proveedor. Transmite la identidad verificada a la solicitud de la API.

Revisar la arquitectura OAuth compartida

Desplegar este servidor MCP en Adios

Tras verificar la vista previa del workspace, sigue la guía principal para desplegar la base de datos y la API de la versión publicada. Usa los secretos de firma del equipo de publicación y el origen de esa API para este servicio MCP.

Desplegar la base de datos y la API compartidas
python/adios.yamlDescargar archivo
name: orders-mcp
region: de
replicas: 1
build_cmd: python -m venv .venv && .venv/bin/pip install -r requirements.txt
start_cmd: .venv/bin/python server.py
runtime:
  name: python@3.13
  port: 8080
  health_path: /healthz
env:
  HOST: 0.0.0.0
  PORT: "8080"
  PUBLIC_ORIGIN: https://mcp.example.com
  API_BASE_URL: https://api.example.com
  API_JWT_SECRET: secret://ORDERS_API_JWT_SECRET
  MCP_JWT_SECRET: secret://ORDERS_MCP_JWT_SECRET

En este manifiesto, sustituye API_BASE_URL por el origen HTTPS de la API desplegada y PUBLIC_ORIGIN por el origen real del servicio MCP. Conserva las referencias a secretos, el puerto 8080 y la ruta pública de salud.

Si aún no conoces el nombre de host MCP predeterminado, obténlo del primer despliegue, actualiza PUBLIC_ORIGIN y vuelve a desplegar antes de conectar un cliente. La comprobación del host debe coincidir con la ruta real.

Desplegar el servicio MCP
(cd python && adios up)
adios apps get orders-mcp

adios up promotes a healthy release. It is a deployment command, not a local preview; verify the selected team and target before running it.

Comprobar la herramienta MCP alojada
export MCP_ACCESS_TOKEN="$(node issue-token.mjs)"
export MCP_URL=https://YOUR-MCP-HOST/mcp
.client-venv/bin/python check.py

Regenera el token de demostración al cabo de 15 minutos. Confirma la llamada a la herramienta alojada y las comprobaciones de permisos antes de compartir el endpoint.

Continuar el tutorial compartido

Cuando funcione la llamada a la herramienta alojada, sigue la guía principal para conectar un cliente de IA, comprobar los permisos, inspeccionar los registros y administrar el servicio.

Conectar y administrar el servidor alojado →

Explorar otra implementación