Adios
← Hosting di server MCP

Tutorial per linguaggio

Server MCP in Python

Crea un servizio MCP autenticato, collegalo all’API degli ordini, prova una vera chiamata a uno strumento e distribuiscilo su Adios.

Python 3.13 · Streamable HTTP · JWT · Adios

Inizia con un esempio funzionante

Avvia l’esempio e personalizzalo

Crea l’API Orders, un database PostgreSQL e il server MCP che preferisci in Adios. Carichiamo il codice, configuriamo credenziali JWT separate e avviamo le anteprime di sviluppo.

Avvia su Adios →

Non hai ancora un account? Creane uno e torna a questo esempio. Scegli un team e controlla le risorse prima di avviare. L’esempio completo richiede un piano a pagamento con spazio per due workspace e un database.

Costruiscilo passo dopo passo

Segui la guida manuale

Progetta l’API e lo schema, aggiungi l’autenticazione JWT, scrivi gli strumenti MCP ed eseguili nei tuoi workspace di sviluppo. Ogni passaggio include codice e comandi.

Segui i passaggi manuali →

Esplora prima l’esempio in esecuzione. OAuth per l’accesso degli utenti e il deployment in produzione vengono dopo.

Iniziare dall’API e dal database

Crea un server MCP in Python che consulti gli ordini tramite un’API REST. Aggiungerai l’autenticazione JWT, proverai il server in un workspace Adios e lo distribuirai.

Prima di iniziare, ti servono un’API in esecuzione e la sua chiave di firma JWT. La guida principale include un’API degli ordini di esempio con Node.js 24 e PostgreSQL e le istruzioni per avviare il suo workspace di sviluppo.

Il processo API ascolta sulla porta 8081 e MCP sulla porta 8080 nei rispettivi workspace. I client usano gli URL HTTPS generati delle anteprime. Mantieni separati API_JWT_SECRET e MCP_JWT_SECRET.

Crea un server MCP in Python

In python/, crea un ambiente virtuale e installa requirements.txt. FastMCP genera lo schema dalla firma della funzione. L’handler valida l’ID, chiama la stessa API con HTTPX e restituisce un dizionario tipizzato. Il wrapper ASGI verifica il JWT client preservando il ciclo di vita dell’applicazione SDK.

python/requirements.txtScarica il file
mcp==1.30.0
httpx==0.28.1
uvicorn==0.35.0
PyJWT==2.15.1
python/server.pyScarica il file
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 l’import mcp.server.fastmcp dell’SDK ufficiale mostrato qui. Questo esempio fissa la versione v1 dell’SDK Python MCP; pacchetti con nomi simili e versioni principali più recenti possono avere API di configurazione e autorizzazione diverse.

Riferimento ufficiale: SDK Python MCP v1.

Provare in un workspace Adios

Il workspace MCP verifica il chiamante prima di chiamare l’API condivisa.

Avvia prima il workspace API della guida principale. Nel manifesto di questo linguaggio, imposta API_BASE_URL sull’origine dell’anteprima dell’API e usa i segreti di firma dello stesso team di sviluppo.

Terminale
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 l’origine generata dell’anteprima MCP in PUBLIC_ORIGIN in adios.yaml. Sincronizza e riavvia prima dei test: il controllo dell’host deve corrispondere al nome effettivo dell’anteprima. Le sonde di stato possono funzionare prima di questo aggiornamento.

Terminale
(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'}
Esaminare il test del client
check.pyScarica il file
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())

Prova un JWT scaduto, un’audience errata e l’assenza dello scope orders:read. Un token demo-client non deve poter leggere other-1002. L’API restituisce 404 per l’ordine di quell’altro tenant; MCP restituisce un errore dello strumento.

Esamina i log di build ed esecuzione nel workspace. Dopo le modifiche al codice, sincronizza e riavvia l’anteprima; questa guida non presuppone il ricaricamento automatico.

Terminale
adios ws run stop "$MCP_WORKSPACE_ID"

Mantenere la verifica JWT e pianificare OAuth separatamente

Questa implementazione verifica i JWT emessi dall’operatore. Non implementa accesso degli utenti, consenso o rinnovo. Mantieni i controlli di emittente, audience, firma, scadenza, scope e tenant quando aggiungi un provider OAuth.

Per un’integrazione rivolta agli utenti, segui i requisiti della guida principale per i metadati della risorsa, la verifica dei token e l’integrazione dei client. Il controllo di accesso del server di risorse JavaScript incluso è un’implementazione separata; non attiva automaticamente OAuth su questo server.

Usa il supporto di autorizzazione del server di risorse dell’SDK con un verificatore di token e i metadati del provider. Trasmetti l’identità verificata nella richiesta API.

Rivedere l’architettura OAuth condivisa

Distribuire questo server MCP su Adios

Dopo aver verificato l’anteprima del workspace, segui la guida principale per distribuire il database e l’API della versione pubblicata. Usa i segreti di firma del team di pubblicazione e l’origine di quell’API per questo servizio MCP.

Distribuire il database e l’API condivisi
python/adios.yamlScarica il file
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

In questo manifesto, sostituisci API_BASE_URL con l’origine HTTPS dell’API distribuita e PUBLIC_ORIGIN con l’origine effettiva del servizio MCP. Mantieni i riferimenti ai segreti, la porta 8080 e il percorso pubblico di controllo dello stato.

Se il nome host MCP predefinito non è ancora noto, ricavalo dal primo deploy, aggiorna PUBLIC_ORIGIN e distribuisci nuovamente prima di collegare un client. Il controllo dell’host deve corrispondere alla route effettiva.

Distribuire il servizio 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.

Verificare lo strumento MCP ospitato
export MCP_ACCESS_TOKEN="$(node issue-token.mjs)"
export MCP_URL=https://YOUR-MCP-HOST/mcp
.client-venv/bin/python check.py

Rigenera il token dimostrativo dopo 15 minuti. Conferma la chiamata allo strumento ospitato e i controlli dei permessi prima di condividere l’endpoint.

Continuare il tutorial condiviso

Quando la chiamata allo strumento ospitato riesce, segui la guida principale per collegare un client IA, verificare i permessi, esaminare i log e gestire il servizio.

Collegare e gestire il server ospitato →

Esplorare un’altra implementazione