Adios
← Hospedagem de servidores MCP

Tutorial por linguagem

Servidor MCP em JavaScript

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

Node.js 24 · 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 JavaScript 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 JavaScript

Em javascript/, instale as dependências com npm ci. Registre get_order com um esquema Zod de entrada, verifique o JWT do cliente, chame a API com limite de cinco segundos e retorne texto e dados estruturados. Cada POST usa um novo transporte, sem estado de sessão MCP compartilhado.

Dependências e comando de inicialização
javascript/package.jsonBaixar arquivo
{
  "name": "adios-orders-mcp-example",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.mjs"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.32.1",
    "express": "^5.1.0",
    "jose": "^6.1.0",
    "zod": "^4.1.0"
  }
}
javascript/server.mjsBaixar arquivo
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { jwtVerify, SignJWT } from "jose";
import { z } from "zod";
import { installOAuthGate } from "./oauth.mjs";

const apiBase = new URL(process.env.API_BASE_URL || "http://127.0.0.1:8081");
const apiSecret = process.env.API_JWT_SECRET;
const mcpSecret = process.env.MCP_JWT_SECRET;
const authMode = process.env.MCP_AUTH_MODE || "jwt";
if (!["jwt", "oauth"].includes(authMode))
  throw new Error("Unknown MCP_AUTH_MODE");
if (
  !apiSecret ||
  apiSecret.length < 32 ||
  (authMode === "jwt" && (!mcpSecret || mcpSecret.length < 32))
) {
  throw new Error(
    "Set distinct API_JWT_SECRET and MCP_JWT_SECRET values of at least 32 characters",
  );
}
if (apiSecret === mcpSecret)
  throw new Error("Use separate API and MCP credentials");
const apiKey = new TextEncoder().encode(apiSecret);
const origin = new URL(process.env.PUBLIC_ORIGIN || "http://127.0.0.1:8080");
const app = express();
app.get("/healthz", (_req, res) => res.json({ ok: true }));
app.use("/mcp", (req, res, next) => {
  if (
    ![origin.host, "127.0.0.1:8080", "localhost:8080"].includes(
      req.headers.host,
    )
  ) {
    return res.status(403).json({ error: "Invalid host" });
  }
  if (req.headers.origin && req.headers.origin !== origin.origin) {
    return res.status(403).json({ error: "Invalid origin" });
  }
  next();
});
if (authMode === "oauth") {
  installOAuthGate(app, origin.origin);
} else {
  app.use("/mcp", async (req, res, next) => {
    try {
      const match = /^Bearer (\S+)$/.exec(req.headers.authorization || "");
      if (!match) throw new Error("Missing token");
      const { payload } = await jwtVerify(
        match[1],
        new TextEncoder().encode(mcpSecret),
        {
          issuer: "orders-demo",
          audience: "orders-mcp",
          algorithms: ["HS256"],
          requiredClaims: ["sub", "exp"],
        },
      );
      if (typeof payload.sub !== "string" || !payload.sub)
        throw new Error("Missing subject");
      if (
        typeof payload.scope !== "string" ||
        !payload.scope.split(" ").includes("orders:read")
      ) {
        return res
          .status(403)
          .json({ error: "orders:read permission required" });
      }
      res.locals.principal = { subject: payload.sub };
      next();
    } catch {
      res.set("WWW-Authenticate", 'Bearer error="invalid_token"');
      res.status(401).json({ error: "Invalid or expired access token" });
    }
  });
}
app.use(express.json({ limit: "64kb" }));

function createServer(subject) {
  const server = new McpServer({ name: "orders-mcp", version: "1.0.0" });
  server.registerTool(
    "get_order",
    {
      description: "Read an order's shipping status from the Orders API.",
      inputSchema: { order_id: z.string().regex(/^[a-zA-Z0-9-]{1,64}$/) },
      annotations: { readOnlyHint: true },
    },
    async ({ order_id }) => {
      try {
        // A new API-audience JWT carries the verified caller, never tool input.
        const apiToken = await new SignJWT({ scope: "orders:read" })
          .setProtectedHeader({ alg: "HS256", typ: "JWT" })
          .setIssuer("orders-demo")
          .setAudience("orders-api")
          .setSubject(subject)
          .setIssuedAt()
          .setExpirationTime("5m")
          .sign(apiKey);
        const response = await fetch(new URL(`/orders/${order_id}`, apiBase), {
          headers: { Authorization: `Bearer ${apiToken}` },
          signal: AbortSignal.timeout(5000),
          redirect: "error",
        });
        if (!response.ok) throw new Error("API lookup failed");
        const order = z
          .object({ id: z.string(), status: z.string() })
          .parse(await response.json());
        return {
          content: [{ type: "text", text: JSON.stringify(order) }],
          structuredContent: order,
        };
      } catch {
        return {
          isError: true,
          content: [
            {
              type: "text",
              text: "Could not read this order. Check its ID and your API access.",
            },
          ],
        };
      }
    },
  );
  return server;
}

app.post("/mcp", async (req, res) => {
  const server = createServer(res.locals.principal.subject);
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true,
  });
  res.on("close", () => {
    void transport.close();
    void server.close();
  });
  try {
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch {
    if (!res.headersSent) res.status(500).json({ error: "MCP request failed" });
  }
});
// This stateless example does not keep an SSE stream or session to delete.
app.get("/mcp", (_req, res) => res.sendStatus(405));
app.delete("/mcp", (_req, res) => res.sendStatus(405));
app.listen(Number(process.env.PORT || 8080), process.env.HOST || "127.0.0.1");

O arquivo importado oauth.mjs está incluído no download. O modo padrão valida um JWT assinado; a seção OAuth deste guia explica como mudar de modo. Mantenha as verificações de host e origem e defina PUBLIC_ORIGIN com a origem pública real ao hospedá-lo.

Referência oficial: SDK de servidor MCP para JavaScript.

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 javascript && 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 javascript && 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"

Adicionar OAuth ao servidor JavaScript

Controle OAuth opcional em JavaScript

O servidor JavaScript para download inclui este controle de recurso protegido. Ele verifica tokens JWT RS256 com o JWKS público do provedor e publica metadados MCP. Exige uma audiência igual à URL MCP completa e uma declaração scope separada por espaços contendo orders:read.

javascript/oauth.mjsBaixar arquivo
import { createRemoteJWKSet, jwtVerify } from "jose";

// This is a resource server, not an OAuth authorization server.
// Configure a provider that issues RS256 JWT access tokens for this resource.
export function installOAuthGate(app, publicOrigin) {
  const issuer = process.env.OAUTH_ISSUER;
  const jwksURL = process.env.OAUTH_JWKS_URL;
  if (
    !issuer ||
    !jwksURL ||
    new URL(issuer).protocol !== "https:" ||
    new URL(jwksURL).protocol !== "https:"
  ) {
    throw new Error(
      "Set HTTPS OAUTH_ISSUER and OAUTH_JWKS_URL from your provider",
    );
  }
  const resource = `${publicOrigin}/mcp`;
  const metadataURL = `${publicOrigin}/.well-known/oauth-protected-resource/mcp`;
  const keys = createRemoteJWKSet(new URL(jwksURL));
  app.get("/.well-known/oauth-protected-resource/mcp", (_req, res) => {
    res.json({
      resource,
      authorization_servers: [issuer],
      scopes_supported: ["orders:read"],
      bearer_methods_supported: ["header"],
    });
  });
  app.use("/mcp", async (req, res, next) => {
    const match = /^Bearer (\S+)$/.exec(req.headers.authorization || "");
    if (!match) {
      res.set(
        "WWW-Authenticate",
        `Bearer resource_metadata="${metadataURL}", scope="orders:read"`,
      );
      return res.status(401).json({ error: "Sign in through your MCP client" });
    }
    try {
      const { payload } = await jwtVerify(match[1], keys, {
        issuer,
        audience: resource,
        algorithms: ["RS256"],
        requiredClaims: ["sub", "exp"],
      });
      if (typeof payload.sub !== "string" || !payload.sub)
        throw new Error("Missing subject");
      const scopes =
        typeof payload.scope === "string" ? payload.scope.split(" ") : [];
      if (!scopes.includes("orders:read")) {
        res.set(
          "WWW-Authenticate",
          `Bearer error="insufficient_scope", scope="orders:read", resource_metadata="${metadataURL}"`,
        );
        return res
          .status(403)
          .json({ error: "orders:read permission required" });
      }
      // The handler carries this identity in a distinct API-audience JWT.
      res.locals.principal = { subject: payload.sub, scopes };
      next();
    } catch {
      res.set(
        "WWW-Authenticate",
        `Bearer error="invalid_token", resource_metadata="${metadataURL}"`,
      );
      res.status(401).json({ error: "Invalid or expired access token" });
    }
  });
}

Configure o provedor primeiro: registre o recurso, permita seu escopo, aceite código de autorização com PKCE e organize o cadastro de clientes. Use as URLs exatas do emissor e do JWKS presentes nos metadados. Este middleware não implementa login, consentimento, cadastro nem emissão de tokens. Tokens opacos exigem introspecção; outros algoritmos ou declarações de escopo precisam de um verificador adaptado.

Substitua as configurações JWT de demo em javascript/adios.yaml
env:
  HOST: 0.0.0.0
  PORT: "8080"
  PUBLIC_ORIGIN: https://YOUR-MCP-HOST
  API_BASE_URL: https://YOUR-API-HOST
  API_JWT_SECRET: secret://ORDERS_API_JWT_SECRET
  MCP_AUTH_MODE: oauth
  OAUTH_ISSUER: https://YOUR-AUTHORIZATION-SERVER
  OAUTH_JWKS_URL: https://YOUR-AUTHORIZATION-SERVER/YOUR-JWKS-PATH
# Remove MCP_JWT_SECRET in OAuth mode.

O controle registra o sujeito verificado em res.locals.principal. O handler o leva em um novo JWT com audiência API, e a API busca seu tenant em principals. Provisione os sujeitos reais do provedor nessa tabela; os dados de exemplo só incluem demo-client e other-client. Controle o provisionamento e nunca deixe o solicitante atribuir seu próprio tenant.

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
javascript/adios.yamlBaixar arquivo
name: orders-mcp
region: de
replicas: 1
build_cmd: npm ci
start_cmd: npm start
runtime:
  name: node@24
  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 javascript && 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