Adios
← Hosting di server MCP

Tutorial per linguaggio

Server MCP in JavaScript

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

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

In javascript/, installa le dipendenze con npm ci. Registra get_order con uno schema Zod di input, verifica il JWT client, chiama l’API con timeout di cinque secondi e restituisci testo e dati strutturati. Ogni POST usa un nuovo trasporto senza stato di sessione MCP condiviso.

Dipendenze e comando di avvio
javascript/package.jsonScarica il file
{
  "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.mjsScarica il file
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");

Il file importato oauth.mjs è incluso nel download. La modalità predefinita convalida un JWT firmato; la sezione OAuth di questa guida spiega come cambiare modalità. Mantieni i controlli dell’host e dell’origine e imposta PUBLIC_ORIGIN sull’origine pubblica effettiva quando lo ospiti.

Riferimento ufficiale: SDK server MCP per JavaScript.

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

Aggiungere OAuth al server JavaScript

Controllo OAuth facoltativo in JavaScript

Il server JavaScript scaricabile include questo controllo di risorsa protetta. Verifica token di accesso JWT RS256 con il JWKS pubblico del provider e pubblica metadati MCP. Richiede un’audience uguale all’URL MCP completo e un claim scope separato da spazi contenente orders:read.

javascript/oauth.mjsScarica il file
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" });
    }
  });
}

Configura prima il provider: registra la risorsa, consenti il suo scope, supporta codice di autorizzazione PKCE e organizza la registrazione dei client. Usa gli URL esatti di emittente e JWKS dai metadati. Il middleware non implementa accesso, consenso, registrazione o emissione di token. I token opachi richiedono introspezione; altri algoritmi o claim di scope richiedono un verificatore adattato.

Sostituisci le impostazioni JWT di demo in 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.

Il controllo registra il subject verificato in res.locals.principal. L’handler lo trasmette in un nuovo JWT con audience API e l’API cerca il tenant in principals. Inserisci i subject reali del provider nella tabella; i dati di esempio includono solo demo-client e other-client. Controlla il provisioning e non permettere al chiamante di assegnarsi un tenant.

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
javascript/adios.yamlScarica il file
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

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 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.

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