Adios
← Hosting di server MCP

Tutorial per linguaggio

Server MCP in Go

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

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

In go/, l’SDK ufficiale ricava gli schemi dalle strutture di input e output. Registra lo strumento con mcp.AddTool, poi monta il suo handler Streamable HTTP dietro l’autenticazione. La richiesta API usa il contesto dello strumento, un timeout e un corpo di risposta di dimensione limitata.

Modulo Go e versione SDK fissata
go/go.modScarica il file
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.goScarica il file
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())
}

Ogni richiesta crea un server stateless per il chiamante verificato e rifiuta ID non validi prima di chiamare l’API. readOnlyHint descrive lo strumento ai client; è un’indicazione, quindi l’API deve comunque applicare i permessi.

Riferimento ufficiale: SDK server MCP per Go.

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

Mantenere la verifica JWT e pianificare OAuth separatamente

Questa implementazione verifica i JWT emessi dall’operatore. Non implementa accesso degli utenti, consenso o rinnovo. Mantieni i controlli di emittente, audience, firma, scadenza, scope e tenant quando aggiungi un provider OAuth.

Per un’integrazione rivolta agli utenti, segui i requisiti della guida principale per i metadati della risorsa, la verifica dei token e l’integrazione dei client. Il controllo di accesso del server di risorse JavaScript incluso è un’implementazione separata; non attiva automaticamente OAuth su questo server.

Verifica i token del provider nel middleware prima del gestore MCP e trasmetti l’identità nel contesto della richiesta. Associa emittente e soggetto all’identità canonica dell’applicazione.

Rivedere l’architettura OAuth condivisa

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

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

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