Adios
← Alojamiento de servidores MCP

Tutorial por lenguaje

Servidor MCP en Go

Crea un servicio MCP autenticado, conéctalo a la API de pedidos, prueba una llamada real a una herramienta y despliégalo en Adios.

Go 1.25 · Streamable HTTP · JWT · Adios

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

En go/, el SDK oficial deriva los esquemas de las estructuras de entrada y salida. Registra la herramienta con mcp.AddTool y monta su manejador Streamable HTTP detrás de la autenticación. La solicitud a la API usa el contexto de la herramienta, un tiempo límite y un cuerpo de respuesta de tamaño limitado.

Módulo Go y SDK con versión fijada
go/go.modDescargar archivo
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.goDescargar archivo
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())
}

Cada solicitud crea un servidor sin estado para el solicitante verificado y rechaza IDs no válidos antes de llamar a la API. readOnlyHint describe la herramienta a los clientes; es una indicación, por lo que la API debe seguir aplicando los permisos.

Referencia oficial: SDK de servidor MCP para Go.

Probar en un workspace de Adios

El workspace MCP verifica al solicitante antes de llamar a la API compartida.

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.

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"

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.

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'}
Inspeccionar la comprobación del cliente
check.pyDescargar archivo
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.

Terminal
adios ws run stop "$MCP_WORKSPACE_ID"

Mantener la verificación JWT y planificar OAuth por separado

Esta implementación verifica los JWT emitidos por el operador. No implementa inicio de sesión de usuarios, consentimiento ni renovación. Conserva las comprobaciones de emisor, audiencia, firma, caducidad, ámbito y tenant al añadir un proveedor OAuth.

Para una integración con usuarios, sigue los requisitos de metadatos del recurso, verificación de tokens e incorporación de clientes de la guía principal. El control de acceso del servidor de recursos JavaScript incluido es una implementación independiente; no activa OAuth automáticamente en este servidor.

Verifica los tokens del proveedor en el middleware antes del controlador MCP y transmite la identidad en el contexto de la solicitud. Asocia el emisor y el sujeto con la identidad canónica de la aplicación.

Revisar la arquitectura OAuth compartida

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 compartidas
go/adios.yamlDescargar archivo
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

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

Desplegar el servicio 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.

Comprobar la herramienta MCP alojada
export MCP_ACCESS_TOKEN="$(node issue-token.mjs)"
export MCP_URL=https://YOUR-MCP-HOST/mcp
.client-venv/bin/python check.py

Regenera 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