Commencer avec un exemple fonctionnel
Lancez l’exemple, puis adaptez-le
Créez l’API Orders, une base PostgreSQL et le serveur MCP de votre choix dans Adios. Nous importons le code, configurons des identifiants JWT distincts et lançons les aperçus de développement.
Lancer sur Adios →Pas encore de compte ? Créez-en un et revenez à cet exemple. Choisissez une équipe et examinez les ressources avant de lancer. L’exemple complet nécessite une offre payante avec une capacité suffisante pour deux espaces et une base de données.
Construisez-le étape par étape
Suivre le guide manuel
Concevez l’API et le schéma, ajoutez l’authentification JWT, écrivez les outils MCP et exécutez-les dans vos espaces de développement. Chaque étape comprend le code et les commandes.
Suivre les étapes manuelles →Explorez d’abord l’exemple en cours d’exécution. OAuth pour la connexion des utilisateurs et le déploiement en production viennent ensuite.
Commencer par l’API et la base de données
Créez un serveur MCP en JavaScript qui consulte les commandes via une API REST. Vous ajouterez l’authentification JWT, testerez le serveur dans un workspace Adios et le déploierez.
Avant de commencer, vous aurez besoin d’une API en cours d’exécution et de sa clé de signature JWT. Le guide principal fournit une API de commandes d’exemple avec Node.js 24 et PostgreSQL, ainsi que les instructions pour démarrer son workspace de développement.
Le processus API écoute sur le port 8081 et MCP sur le port 8080 dans leurs workspaces. Les clients utilisent les URL HTTPS générées des aperçus. Gardez API_JWT_SECRET et MCP_JWT_SECRET séparés.
Créez un serveur MCP en JavaScript
Dans javascript/, installez les dépendances avec npm ci. Enregistrez get_order avec un schéma Zod d’entrée, vérifiez le JWT client, appelez l’API avec un délai limite de cinq secondes et renvoyez texte et données structurées. Chaque POST utilise un nouveau transport, sans état de session MCP partagé.
Dépendances et commande de démarrage
{
"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");Le fichier importé oauth.mjs est inclus dans le téléchargement. Le mode par défaut valide un JWT signé ; la section OAuth de ce guide explique comment changer de mode. Conservez les contrôles de l’hôte et de l’origine, et définissez PUBLIC_ORIGIN avec l’origine publique réelle lors de l’hébergement.
Référence officielle : SDK serveur MCP pour JavaScript.
Tester dans un workspace Adios
Démarrez d’abord le workspace API du guide principal. Dans le manifeste de ce langage, définissez API_BASE_URL avec l’origine de l’aperçu de l’API et utilisez les secrets de signature de la même équipe de développement.
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"Copiez l’origine générée de l’aperçu MCP dans PUBLIC_ORIGIN dans adios.yaml. Synchronisez et redémarrez avant les tests : le contrôle de l’hôte doit correspondre au nom réel de l’aperçu. Les sondes de santé peuvent fonctionner avant cette mise à jour.
(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'}Examiner le test client
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())Testez un JWT expiré, une audience incorrecte et l’absence de la portée orders:read. Un jeton demo-client ne doit pas pouvoir lire other-1002. L’API renvoie 404 pour la commande de cet autre tenant ; MCP renvoie une erreur d’outil.
Examinez les journaux de build et d’exécution dans le workspace. Après toute modification du code, synchronisez et redémarrez l’aperçu ; ce guide ne suppose pas de rechargement automatique.
adios ws run stop "$MCP_WORKSPACE_ID"Ajouter OAuth au serveur JavaScript
Contrôle OAuth facultatif en JavaScript
Le serveur JavaScript téléchargeable inclut ce contrôle de ressource protégée. Il vérifie les jetons d’accès JWT RS256 avec le JWKS public du fournisseur et publie les métadonnées MCP. Il exige une audience égale à l’URL MCP complète et une déclaration scope séparée par des espaces contenant 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" });
}
});
}Configurez d’abord le fournisseur : enregistrez la ressource, autorisez son scope, prenez en charge le code d’autorisation PKCE et organisez l’enregistrement des clients. Utilisez les URL exactes de l’émetteur et du JWKS issues de ses métadonnées. Ce middleware ne gère ni connexion, ni consentement, ni enregistrement, ni émission de jetons. Les jetons opaques nécessitent une introspection ; d’autres algorithmes ou déclarations de permissions nécessitent un vérificateur adapté.
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.Le contrôle enregistre le sujet vérifié dans res.locals.principal. Le gestionnaire le transmet dans un nouveau JWT d’audience API, puis l’API cherche son tenant dans principals. Provisionnez les sujets réels du fournisseur dans cette table ; les données d’exemple n’incluent que demo-client et other-client. Contrôlez le provisionnement et ne laissez jamais l’appelant choisir son tenant.
Déployer ce serveur MCP sur Adios
Après vérification de l’aperçu du workspace, suivez le guide principal pour déployer la base de données et l’API de la version publiée. Utilisez les secrets de signature de l’équipe de publication et l’origine de cette API pour ce service MCP.
Déployer la base de données et l’API communesname: 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_SECRETDans ce manifeste, remplacez API_BASE_URL par l’origine HTTPS de l’API déployée et PUBLIC_ORIGIN par l’origine réelle du service MCP. Conservez les références aux secrets, le port 8080 et le chemin public de vérification de santé.
Si le nom d’hôte MCP par défaut n’est pas encore connu, récupérez-le lors du premier déploiement, mettez à jour PUBLIC_ORIGIN et redéployez avant de connecter un client. Le contrôle de l’hôte doit correspondre à la route réelle.
(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.pyRégénérez le jeton de démonstration après 15 minutes. Confirmez l’appel à l’outil hébergé et les contrôles d’autorisation avant de partager l’endpoint.
Poursuivre le tutoriel commun
Une fois l’appel à l’outil hébergé réussi, suivez le guide principal pour connecter un client IA, vérifier les autorisations, examiner les journaux et exploiter le service.
Connecter et exploiter le serveur hébergé →Explorer une autre implémentation
Python
Utilisez FastMCP, des outils typés, HTTPX et un contrôle JWT ASGI. Retrouvez la configuration et le déploiement Python dans un seul guide.
Ouvrir le guide PythonGo 1.25Go
Utilisez le SDK Go, des structs typés d’entrée et de sortie et un middleware HTTP. Compilez et déployez un seul binaire de service.
Ouvrir le guide Go