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
{
"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");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
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.
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.
(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
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.
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.
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.
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 condivisiname: 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_SECRETIn 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.
(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.pyRigenera 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
Python
Usa FastMCP, strumenti tipizzati, HTTPX e un controllo JWT ASGI. Trova la configurazione e la distribuzione Python in un’unica guida.
Aprire la guida PythonGo 1.25Go
Usa l’SDK Go, struct tipizzate di input e output e middleware HTTP. Compila e distribuisci un unico binario del servizio.
Aprire la guida Go