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 JavaScript 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 JavaScript
En javascript/, instala las dependencias con npm ci. Registra get_order con un esquema Zod de entrada, verifica el JWT del cliente, llama a la API con un límite de cinco segundos y devuelve texto y datos estructurados. Cada POST usa un transporte nuevo sin estado de sesión MCP compartido.
Dependencias y comando de inicio
{
"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"
}
}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");El archivo importado oauth.mjs se incluye en la descarga. El modo predeterminado valida un JWT firmado; la sección OAuth de esta guía explica cómo cambiar de modo. Conserva las comprobaciones del host y del origen y establece PUBLIC_ORIGIN con el origen público real al alojarlo.
Referencia oficial: SDK de servidor MCP para JavaScript.
Probar en un workspace de Adios
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.
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 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.
(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'}Inspeccionar la comprobación del cliente
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.
adios ws run stop "$MCP_WORKSPACE_ID"Añadir OAuth al servidor JavaScript
Control OAuth opcional en JavaScript
El servidor JavaScript descargable incluye este control de recurso protegido. Verifica tokens JWT de acceso RS256 con el JWKS público del proveedor y publica metadatos del recurso MCP. Exige una audiencia igual a la URL MCP completa y una declaración scope separada por espacios que incluya orders:read.
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 primero el proveedor: registra el recurso, permite su scope, admite código de autorización con PKCE y organiza el registro de clientes. Usa las URL exactas del emisor y JWKS de sus metadatos. Este middleware no implementa inicio de sesión, consentimiento, registro ni emisión de tokens. Los tokens opacos requieren introspección; otros algoritmos o declaraciones de permisos necesitan un verificador adaptado.
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.El control guarda el sujeto verificado en res.locals.principal. El manejador lo transmite en un JWT nuevo con audiencia API y esta consulta su tenant en principals. Aprovisiona los sujetos reales del proveedor en esa tabla; los datos de ejemplo solo incluyen demo-client y other-client. Controla el aprovisionamiento de identidades y nunca permitas que el solicitante se asigne un tenant.
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 compartidasname: 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_SECRETEn 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.
(cd javascript && adios up)
adios apps get orders-mcpadios up promotes a healthy release. It is a deployment command, not a local preview; verify the selected team and target before running it.
export MCP_ACCESS_TOKEN="$(node issue-token.mjs)"
export MCP_URL=https://YOUR-MCP-HOST/mcp
.client-venv/bin/python check.pyRegenera 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
Python
Usa FastMCP, herramientas tipadas, HTTPX y un control JWT ASGI. Mantén la configuración y el despliegue de Python en una sola guía.
Abrir la guía de PythonGo 1.25Go
Usa el SDK de Go, structs tipados de entrada y salida y middleware HTTP. Compila y despliega un único binario de servicio.
Abrir la guía de Go