Adios
← Hébergement de serveurs MCP

Tutoriel par langage

Serveur MCP en Go

Créez un service MCP authentifié, connectez-le à l’API de commandes, testez un véritable appel d’outil et déployez-le sur Adios.

Go 1.25 · Streamable HTTP · JWT · Adios

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

Dans go/, le SDK officiel déduit les schémas des structures d’entrée et de sortie. Enregistrez l’outil avec mcp.AddTool, puis placez son gestionnaire Streamable HTTP derrière l’authentification. La requête API utilise le contexte de l’outil, un délai limite et un corps de réponse de taille limitée.

Module Go et version du SDK fixée
go/go.modTélécharger le fichier
module example.com/orders-mcp

go 1.25.0

require (
	github.com/golang-jwt/jwt/v5 v5.3.1
	github.com/modelcontextprotocol/go-sdk v1.8.0
)

require (
	github.com/google/jsonschema-go v0.4.3 // indirect
	github.com/segmentio/asm v1.1.3 // indirect
	github.com/segmentio/encoding v0.5.4 // indirect
	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
	golang.org/x/oauth2 v0.35.0 // indirect
	golang.org/x/sync v0.20.0 // indirect
	golang.org/x/sys v0.41.0 // indirect
	golang.org/x/time v0.15.0 // indirect
)
go/main.goTélécharger le fichier
package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

type OrderInput struct {
	OrderID string `json:"order_id" jsonschema:"Order ID, using letters, numbers, or hyphens"`
}
type Order struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

func envOr(key, fallback string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return fallback
}
func main() {
	apiSecret, mcpSecret := os.Getenv("API_JWT_SECRET"), os.Getenv("MCP_JWT_SECRET")
	if len(apiSecret) < 32 || len(mcpSecret) < 32 || apiSecret == mcpSecret {
		log.Fatal("Use separate API and MCP signing secrets of at least 32 characters")
	}
	apiBase, err := url.Parse(envOr("API_BASE_URL", "http://127.0.0.1:8081"))
	if err != nil || apiBase.Host == "" {
		log.Fatal("Set a valid API_BASE_URL")
	}
	origin, err := url.Parse(envOr("PUBLIC_ORIGIN", "http://127.0.0.1:8080"))
	if err != nil || origin.Host == "" {
		log.Fatal("Set a valid PUBLIC_ORIGIN")
	}
	client := &http.Client{Timeout: 5 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return errors.New("redirects are not allowed") }}
	validID := regexp.MustCompile(`^[a-zA-Z0-9-]{1,64}$`)
	type principalKey struct{}
	createServer := func(subject string) *mcp.Server {
		server := mcp.NewServer(&mcp.Implementation{Name: "orders-mcp", Version: "1.0.0"}, nil)
		mcp.AddTool(server, &mcp.Tool{Name: "get_order", Description: "Read an order's shipping status from the Orders API.", Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true}},
			func(ctx context.Context, _ *mcp.CallToolRequest, input OrderInput) (*mcp.CallToolResult, Order, error) {
				if !validID.MatchString(input.OrderID) {
					return nil, Order{}, errors.New("invalid order ID")
				}
				// Carry the verified caller in a new JWT intended for the API.
				now := time.Now()
				apiToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
					"iss": "orders-demo", "aud": "orders-api", "sub": subject,
					"scope": "orders:read", "iat": now.Unix(), "exp": now.Add(5 * time.Minute).Unix(),
				}).SignedString([]byte(apiSecret))
				if err != nil {
					return nil, Order{}, errors.New("could not authorize API request")
				}
				endpoint := apiBase.ResolveReference(&url.URL{Path: "/orders/" + input.OrderID})
				req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
				if err != nil {
					return nil, Order{}, errors.New("could not prepare API request")
				}
				req.Header.Set("Authorization", "Bearer "+apiToken)
				res, err := client.Do(req)
				if err != nil {
					return nil, Order{}, errors.New("could not read order")
				}
				defer res.Body.Close()
				var order Order
				if res.StatusCode != http.StatusOK || json.NewDecoder(io.LimitReader(res.Body, 65536)).Decode(&order) != nil || order.ID == "" || order.Status == "" {
					return nil, Order{}, errors.New("could not read order; check its ID and API access")
				}
				return nil, order, nil
			})
		return server
	}
	handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
		subject, _ := r.Context().Value(principalKey{}).(string)
		return createServer(subject)
	}, &mcp.StreamableHTTPOptions{Stateless: true, JSONResponse: true})
	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprint(w, `{"ok":true}`)
	})
	mux.HandleFunc("/mcp", func(w http.ResponseWriter, r *http.Request) {
		if (r.Host != origin.Host && r.Host != "127.0.0.1:8080" && r.Host != "localhost:8080") || (r.Header.Get("Origin") != "" && r.Header.Get("Origin") != origin.Scheme+"://"+origin.Host) {
			http.Error(w, "Invalid host or origin", http.StatusForbidden)
			return
		}
		header := r.Header.Get("Authorization")
		if !strings.HasPrefix(header, "Bearer ") {
			http.Error(w, "Missing access token", http.StatusUnauthorized)
			return
		}
		token, err := jwt.Parse(strings.TrimPrefix(header, "Bearer "), func(*jwt.Token) (any, error) { return []byte(mcpSecret), nil },
			jwt.WithValidMethods([]string{"HS256"}), jwt.WithIssuer("orders-demo"), jwt.WithAudience("orders-mcp"), jwt.WithExpirationRequired())
		if err != nil || !token.Valid {
			http.Error(w, "Invalid or expired access token", http.StatusUnauthorized)
			return
		}
		claims, ok := token.Claims.(jwt.MapClaims)
		subject, subjectErr := claims.GetSubject()
		if !ok || subjectErr != nil || subject == "" {
			http.Error(w, "Missing subject", http.StatusUnauthorized)
			return
		}
		scopes, _ := claims["scope"].(string)
		permitted := false
		for _, scope := range strings.Fields(scopes) {
			if scope == "orders:read" {
				permitted = true
			}
		}
		if !permitted {
			http.Error(w, "orders:read permission required", http.StatusForbidden)
			return
		}
		r.Body = http.MaxBytesReader(w, r.Body, 65536)
		handler.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), principalKey{}, subject)))
	})
	httpServer := &http.Server{Addr: envOr("HOST", "127.0.0.1") + ":" + envOr("PORT", "8080"), Handler: mux, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 60 * time.Second}
	log.Fatal(httpServer.ListenAndServe())
}

Chaque requête crée un serveur sans état pour l’appelant vérifié et rejette les identifiants invalides avant d’appeler l’API. readOnlyHint décrit l’outil aux clients ; ce n’est qu’une indication, donc l’API doit toujours faire respecter les permissions.

Référence officielle : SDK serveur MCP pour Go.

Tester dans un workspace Adios

Le workspace MCP vérifie l’appelant avant d’appeler l’API commune.

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.

Terminal
adios ws create --name orders-mcp-dev --json
export MCP_WORKSPACE_ID=YOUR_MCP_WORKSPACE_ID
(cd go && 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.

Terminal
(cd go && 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
check.pyTélécharger le fichier
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.

Terminal
adios ws run stop "$MCP_WORKSPACE_ID"

Conserver la vérification JWT et prévoir OAuth séparément

Cette implémentation vérifie les JWT émis par l’opérateur. Elle n’implémente ni connexion utilisateur, ni consentement, ni renouvellement. Conservez les contrôles d’émetteur, d’audience, de signature, d’expiration, de portée et de tenant lors de l’ajout d’un fournisseur OAuth.

Pour une intégration destinée aux utilisateurs, suivez les exigences du guide principal concernant les métadonnées de ressource, la vérification des jetons et l’intégration des clients. Le contrôle d’accès du serveur de ressources JavaScript fourni est une implémentation distincte ; il ne suffit pas à activer OAuth sur ce serveur.

Vérifiez les jetons du fournisseur dans le middleware avant le gestionnaire MCP et transmettez l’identité dans le contexte de la requête. Associez l’émetteur et le sujet à l’identité canonique de l’application.

Revoir l’architecture OAuth commune

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 communes
go/adios.yamlTélécharger le fichier
name: orders-mcp
region: de
replicas: 1
build_cmd: go build -o orders-mcp .
start_cmd: ./orders-mcp
runtime:
  name: go@1.25
  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

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

Déployer le service MCP
(cd go && 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.

Vérifier l’outil MCP hébergé
export MCP_ACCESS_TOKEN="$(node issue-token.mjs)"
export MCP_URL=https://YOUR-MCP-HOST/mcp
.client-venv/bin/python check.py

Ré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