Adios
← Hospedagem de servidores MCP

Tutorial por linguagem

Servidor MCP em Python

Crie um serviço MCP autenticado, conecte-o à API de pedidos, teste uma chamada real de ferramenta e faça o deploy no Adios.

Python 3.13 · Streamable HTTP · JWT · Adios

Comece com um exemplo em execução

Inicie o exemplo e adapte-o

Crie a API Orders, um banco PostgreSQL e o servidor MCP escolhido no Adios. Enviamos o código, configuramos credenciais JWT separadas e iniciamos as prévias de desenvolvimento.

Iniciar no Adios →

Ainda não tem conta? Crie uma e volte a este exemplo. Escolha uma equipe e confira os recursos antes de iniciar. O exemplo completo requer um plano pago com capacidade para dois espaços e um banco de dados.

Crie passo a passo

Seguir o guia manual

Projete a API e o esquema, adicione autenticação JWT, escreva as ferramentas MCP e execute-as nos seus espaços de desenvolvimento. Cada etapa inclui código e comandos.

Seguir as etapas manuais →

Explore primeiro o exemplo em execução. OAuth para login de usuários e a implantação em produção vêm depois.

Começar pela API e pelo banco de dados

Crie um servidor MCP em Python que consulte pedidos por uma API REST. Você adicionará autenticação JWT, testará o servidor em um workspace do Adios e fará o deploy.

Antes de começar, você precisa de uma API em execução e da sua chave de assinatura JWT. O guia principal inclui uma API de pedidos de exemplo com Node.js 24 e PostgreSQL, além das instruções para iniciar seu workspace de desenvolvimento.

O processo da API escuta na porta 8081 e o MCP na porta 8080 dentro dos workspaces. Os clientes usam as URLs HTTPS geradas das prévias. Mantenha API_JWT_SECRET e MCP_JWT_SECRET separados.

Crie um servidor MCP em Python

Em python/, crie um ambiente virtual e instale requirements.txt. O FastMCP gera o esquema pela assinatura da função. O handler valida o ID, chama a mesma API com HTTPX e retorna um dicionário tipado. O wrapper ASGI verifica o JWT do cliente preservando o ciclo de vida da aplicação SDK.

python/requirements.txtBaixar arquivo
mcp==1.30.0
httpx==0.28.1
uvicorn==0.35.0
PyJWT==2.15.1
python/server.pyBaixar arquivo
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")))

Use o import mcp.server.fastmcp do SDK oficial mostrado aqui. Este exemplo fixa a versão v1 do SDK Python MCP; pacotes com nomes semelhantes e versões principais mais recentes podem ter outras APIs de configuração e autorização.

Referência oficial: SDK Python MCP v1.

Testar em um workspace do Adios

O workspace MCP verifica o solicitante antes de chamar a API compartilhada.

Primeiro inicie o workspace da API do guia principal. No manifesto desta linguagem, defina API_BASE_URL com a origem da prévia da API e use os segredos de assinatura da mesma equipe de desenvolvimento.

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"

Copie a origem gerada da prévia MCP para PUBLIC_ORIGIN em adios.yaml. Sincronize e reinicie antes de testar: a verificação de host deve corresponder ao nome real da prévia. As sondas de integridade podem funcionar antes dessa atualização.

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'}
Examinar a verificação do cliente
check.pyBaixar arquivo
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())

Teste um JWT expirado, uma audiência incorreta e a ausência do escopo orders:read. Um token de demo-client não deve conseguir ler other-1002. A API retorna 404 para o pedido desse outro tenant; o MCP retorna um erro de ferramenta.

Revise os logs de build e execução no workspace. Após alterar o código, sincronize e reinicie a prévia; este guia não pressupõe recarga automática.

Terminal
adios ws run stop "$MCP_WORKSPACE_ID"

Manter a verificação JWT e planejar OAuth separadamente

Esta implementação verifica JWTs emitidos pelo operador. Ela não implementa login de usuários, consentimento nem renovação. Mantenha as verificações de emissor, audiência, assinatura, expiração, escopo e tenant ao adicionar um provedor OAuth.

Para uma integração voltada aos usuários, siga os requisitos do guia principal sobre metadados do recurso, verificação de tokens e integração dos clientes. O controle de acesso do servidor de recursos JavaScript incluído é uma implementação separada; ele não ativa OAuth automaticamente neste servidor.

Use o suporte de autorização do servidor de recursos do SDK com um verificador de tokens e metadados do provedor. Transmita a identidade verificada na requisição à API.

Revisar a arquitetura OAuth compartilhada

Fazer deploy deste servidor MCP no Adios

Após verificar a prévia do workspace, siga o guia principal para fazer o deploy do banco de dados e da API da versão publicada. Use os segredos de assinatura da equipe de publicação e a origem dessa API para este serviço MCP.

Fazer deploy do banco de dados e da API compartilhados
python/adios.yamlBaixar arquivo
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

Neste manifesto, substitua API_BASE_URL pela origem HTTPS da API implantada e PUBLIC_ORIGIN pela origem real do serviço MCP. Mantenha as referências aos segredos, a porta 8080 e o caminho público de verificação de integridade.

Se o nome de host padrão do MCP ainda não for conhecido, obtenha-o no primeiro deploy, atualize PUBLIC_ORIGIN e faça o deploy novamente antes de conectar um cliente. A verificação de host deve corresponder à rota real.

Fazer deploy do serviço 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.

Verificar a ferramenta MCP hospedada
export MCP_ACCESS_TOKEN="$(node issue-token.mjs)"
export MCP_URL=https://YOUR-MCP-HOST/mcp
.client-venv/bin/python check.py

Gere novamente o token de demonstração após 15 minutos. Confirme a chamada à ferramenta hospedada e as verificações de permissão antes de compartilhar o endpoint.

Continuar o tutorial compartilhado

Quando a chamada à ferramenta hospedada funcionar, siga o guia principal para conectar um cliente de IA, verificar permissões, examinar os logs e operar o serviço.

Conectar e operar o servidor hospedado →

Explorar outra implementação