MCP in Practice

Cómo crear un servidor MCP con inicio de sesión de Auth0 en Go

Aprende a crear un servidor MCP mínimo en Go que autentica a los usuarios mediante Auth0, con OAuth 2.0, verificación de JWT y el MCP Go SDK.

11 min de lectura

El Model Context Protocol (MCP) permite que asistentes de IA como Cursor y Claude llamen herramientas en servidores remotos. Cuando esas herramientas acceden a datos privados, necesitas autenticación. En este artículo crearemos un servidor MCP mínimo en Go que autentica a los usuarios mediante Auth0.

Al terminar, tendrás un servidor funcional con una sola herramienta autenticada y entenderás cada componente lo suficientemente bien como para ampliarlo.

Cómo funciona el flujo de autenticación

La autenticación de MCP sigue el estándar OAuth 2.0. Intervienen tres partes: el cliente MCP (Cursor, Claude), tu servidor MCP y Auth0.

User          MCP Client (Cursor)        MCP Server                   Auth0
 |                  |                        |                          |
 |  "run a tool"    |                        |                          |
 |----------------->|                        |                          |
 |                  |  GET /.well-known/      |                          |
 |                  |  oauth-protected-       |                          |
 |                  |  resource               |                          |
 |                  |----------------------->|                          |
 |                  |  {auth_servers:[...]}   |                          |
 |                  |<-----------------------|                          |
 |                  |                        |                          |
 |  Login (browser) |  Redirect to Auth0     |                          |
 |<-----------------|------------------------------------------->       |
 |  Credentials     |                        |                          |
 |---------------------------------------------------------->          |
 |  JWT             |                        |                          |
 |<--------------------|<----------------------------------------|     |
 |                  |                        |                          |
 |                  |  POST /mcp             |                          |
 |                  |  Authorization: Bearer  |                          |
 |                  |  <JWT>                  |                          |
 |                  |----------------------->|                          |
 |                  |                        |  Verify JWT (JWKS)       |
 |                  |                        |------------------------->|
 |                  |                        |  Public keys             |
 |                  |                        |<-------------------------|
 |                  |                        |                          |
 |                  |  Tool result           |                          |
 |                  |<-----------------------|                          |
 |  Response        |                        |                          |
 |<-----------------|                        |                          |

El flujo tiene cuatro etapas:

  1. Descubrimiento — El cliente MCP consulta /.well-known/oauth-protected-resource en tu servidor. Este endpoint devuelve un documento JSON que le indica al cliente dónde autenticarse (la URL de tu tenant de Auth0) y qué scopes están disponibles.

  2. Inicio de sesión — El cliente abre una ventana del navegador que dirige a Auth0. El usuario inicia sesión (con correo electrónico y contraseña, SSO, inicio de sesión social o cualquier opción que hayas configurado). Auth0 devuelve un JWT firmado.

  3. Solicitudes autenticadas — El cliente adjunta el JWT como un token Bearer en el encabezado Authorization de cada solicitud MCP.

  4. Verificación — Tu servidor valida el JWT: obtiene las claves públicas de Auth0 (JWKS) y comprueba la firma, la audiencia y la expiración. Si es válido, la solicitud pasa al handler de la herramienta.

Requisitos previos

  • Go 1.22+ instalado
  • Una cuenta de Auth0 (regístrate gratis)
  • Conocimientos básicos de Go y servidores HTTP

Paso 1: Configura Auth0

Debes crear dos recursos en el Dashboard de Auth0.

Crea una API (Resource Server)

Ve a Applications > APIs > Create API:

CampoValor
NameMy MCP API
Identifier (Audience)http://127.0.0.1:8080/mcp
Signing AlgorithmRS256

El identificador es el claim audience que tu servidor comprobará en cada JWT. Para el desarrollo local, http://127.0.0.1:8080/mcp funciona bien. En producción, usa tu URL pública.

Opcionalmente, agrega un permiso o scope llamado read:all en la pestaña Permissions.

Crea una Single Page Application (SPA)

Ve a Applications > Applications > Create Application:

CampoValor
NameCursor MCP Client
Application TypeSingle Page Application
Allowed Callback URLscursor://anysphere.cursor-mcp/oauth/callback
Grant TypesAuthorization Code, Refresh Token

Los clientes MCP como Cursor y Claude son clientes públicos: no almacenan un secreto de cliente. Por eso usamos el tipo de aplicación SPA.

Para Claude, agrega estas URL de callback en su lugar:

https://claude.ai/api/mcp/auth_callback,
https://claude.com/api/mcp/auth_callback

Autoriza al cliente

Ve a Applications > APIs > My MCP API > Machine to Machine Applications, busca tu cliente SPA, actívalo y concédele los scopes que definiste.

Toma nota de:

  • Auth0 Domain: https://your-tenant.auth0.com
  • API Identifier: http://127.0.0.1:8080/mcp

Paso 2: Crea el proyecto

mkdir my-mcp-auth && cd my-mcp-auth
go mod init my-mcp-auth

Necesitarás dos dependencias:

  • MCP Go SDK — maneja el protocolo MCP, el transporte HTTP y el middleware de autenticación
  • jwx — análisis de JWT y gestión de claves JWKS
go get github.com/modelcontextprotocol/go-sdk@latest
go get github.com/lestrrat-go/jwx/v2@latest

Paso 3: Escribe el servidor

Crea main.go. El servidor completo cabe en unas 120 líneas.

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/lestrrat-go/jwx/v2/jwk"
	"github.com/lestrrat-go/jwx/v2/jwt"
	"github.com/modelcontextprotocol/go-sdk/auth"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/modelcontextprotocol/go-sdk/oauthex"
)

const (
	authServer = "https://YOUR-TENANT.auth0.com" // Replace with your Auth0 domain
	audience   = "http://127.0.0.1:8080/mcp"     // Must match your Auth0 API Identifier
	listenAddr = "127.0.0.1:8080"
)

func main() {
	jwksCache := buildJWKSCache()
	verifyToken := buildTokenVerifier(jwksCache)

	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", healthHandler)
	mux.Handle("/mcp", authenticatedMCPHandler(verifyToken))
	mux.Handle("/.well-known/oauth-protected-resource", prmHandler())

	log.Printf("MCP server with Auth0 auth listening on http://%s/mcp", listenAddr)
	srv := &http.Server{
		Addr:              listenAddr,
		Handler:           mux,
		ReadHeaderTimeout: 10 * time.Second,
	}
	if err := srv.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}

Caché de JWKS

Auth0 firma los JWT con claves privadas y publica las claves públicas correspondientes en una URL conocida. La caché de JWKS obtiene estas claves y las actualiza periódicamente, de modo que tu servidor puede verificar las firmas de los JWT sin llamar a Auth0 en cada solicitud.

func buildJWKSCache() *jwk.Cache {
	cache := jwk.NewCache(context.Background())
	jwksURL := strings.TrimSuffix(authServer, "/") + "/.well-known/jwks.json"
	if err := cache.Register(jwksURL, jwk.WithMinRefreshInterval(15*time.Minute)); err != nil {
		log.Fatalf("Failed to register JWKS endpoint: %v", err)
	}
	return cache
}

Verificador de tokens

El middleware de autenticación del MCP SDK llama a esta función en cada solicitud. Recibe la cadena JWT sin procesar y debe devolver un struct TokenInfo o un error.

La verificación consta de dos pasos: primero se analiza el token sin verificarlo para extraer el emisor; después se verifican por completo la firma y los claims con las claves JWKS almacenadas en caché.

func buildTokenVerifier(jwksCache *jwk.Cache) auth.VerifyFunc {
	jwksURL := strings.TrimSuffix(authServer, "/") + "/.well-known/jwks.json"

	return func(ctx context.Context, tokenString string, _ *http.Request) (*auth.TokenInfo, error) {
		// Parse without verification to check the issuer.
		unverified, err := jwt.ParseString(tokenString,
			jwt.WithVerify(false), jwt.WithValidate(false))
		if err != nil {
			return nil, fmt.Errorf("malformed token: %w", err)
		}

		issuer := strings.TrimSuffix(unverified.Issuer(), "/")
		expected := strings.TrimSuffix(authServer, "/")
		if issuer != expected {
			return nil, fmt.Errorf("unexpected issuer: %s", issuer)
		}

		// Verify signature, audience, and expiration using JWKS.
		keySet, err := jwksCache.Get(ctx, jwksURL)
		if err != nil {
			return nil, fmt.Errorf("JWKS fetch failed: %w", err)
		}

		token, err := jwt.ParseString(tokenString,
			jwt.WithKeySet(keySet),
			jwt.WithValidate(true),
			jwt.WithAudience(audience),
		)
		if err != nil {
			return nil, fmt.Errorf("verification failed: %w", err)
		}

		userID := token.Subject()
		if userID == "" {
			userID = "_anonymous"
		}

		return &auth.TokenInfo{
			UserID:     userID,
			Expiration: token.Expiration(),
			Extra:      map[string]any{"raw_token": tokenString},
		}, nil
	}
}

Metadatos de recursos protegidos (PRM)

Este es el endpoint de descubrimiento. Cuando un cliente MCP se conecta, lo primero que hace es consultar este endpoint para saber adónde enviar al usuario para autenticarse.

func prmHandler() http.Handler {
	return auth.ProtectedResourceMetadataHandler(&oauthex.ProtectedResourceMetadata{
		Resource:             fmt.Sprintf("http://%s/mcp", listenAddr),
		AuthorizationServers: []string{authServer + "/"},
		ScopesSupported:      []string{"openid", "profile", "read:all"},
	})
}

La respuesta se ve así:

{
  "resource": "http://127.0.0.1:8080/mcp",
  "authorization_servers": ["https://your-tenant.auth0.com/"],
  "scopes_supported": ["openid", "profile", "read:all"]
}

Handler MCP con middleware de autenticación

El MCP SDK proporciona el middleware auth.RequireBearerToken, que intercepta las solicitudes, extrae el encabezado Authorization: Bearer <token> y llama a tu función de verificación. Si la verificación falla, devuelve un 401 con un encabezado WWW-Authenticate que apunta al endpoint PRM.

func authenticatedMCPHandler(verifyToken auth.VerifyFunc) http.Handler {
	createServer := func(_ *http.Request) *mcp.Server {
		server := mcp.NewServer(&mcp.Implementation{
			Name:    "my-mcp-auth-server",
			Version: "0.1.0",
		}, nil)
		registerTools(server)
		return server
	}

	handler := mcp.NewStreamableHTTPHandler(createServer, &mcp.StreamableHTTPOptions{
		Stateless: true,
	})

	prmURL := fmt.Sprintf("http://%s/.well-known/oauth-protected-resource", listenAddr)
	authMiddleware := auth.RequireBearerToken(verifyToken, &auth.RequireBearerTokenOptions{
		ResourceMetadataURL: prmURL,
	})

	return authMiddleware(handler)
}

Herramientas

Las herramientas son donde reside la funcionalidad. La herramienta whoami lee el ID del usuario autenticado de la información del token que el middleware adjuntó a la solicitud.

func registerTools(server *mcp.Server) {
	type WhoAmIArgs struct{}
	mcp.AddTool(server, &mcp.Tool{
		Name:        "whoami",
		Description: "Returns the authenticated user's ID from the JWT token",
	}, func(_ context.Context, req *mcp.CallToolRequest, _ WhoAmIArgs) (*mcp.CallToolResult, any, error) {
		userID := "_unknown"
		if req.Extra.TokenInfo != nil {
			userID = req.Extra.TokenInfo.UserID
		}
		return &mcp.CallToolResult{
			Content: []mcp.Content{
				&mcp.TextContent{Text: fmt.Sprintf("Authenticated as: %s", userID)},
			},
		}, nil, nil
	})
}

func healthHandler(w http.ResponseWriter, _ *http.Request) {
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, "ok")
}

Paso 4: Ejecútalo

go mod tidy
go run main.go

Salida:

MCP server with Auth0 auth listening on http://127.0.0.1:8080/mcp

Verifica el endpoint PRM

curl -s http://127.0.0.1:8080/.well-known/oauth-protected-resource | jq
{
  "resource": "http://127.0.0.1:8080/mcp",
  "authorization_servers": [
    "https://your-tenant.auth0.com/"
  ],
  "scopes_supported": [
    "openid",
    "profile",
    "read:all"
  ]
}

Verifica que se exija autenticación

curl -s -w "\nHTTP %{http_code}\n" -X POST http://127.0.0.1:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'

Deberías recibir una respuesta 401 Unauthorized con un encabezado WWW-Authenticate: eso significa que la autenticación funciona.

Paso 5: Conéctate desde Cursor

Agrega esto a tu archivo de configuración de MCP (~/.cursor/mcp.json):

{
  "mcpServers": {
    "my-auth-mcp": {
      "url": "http://127.0.0.1:8080/mcp"
    }
  }
}

Cuando Cursor se conecta:

  1. Consulta /.well-known/oauth-protected-resource
  2. Abre un navegador para iniciar sesión en Auth0
  3. Obtiene un JWT y lo envía en cada solicitud
  4. Ahora puedes pedirle a Cursor que use la herramienta whoami

Qué sucede internamente

Este es el desglose de los componentes:

┌─────────────────────────────────────────────────────────┐
│                     HTTP Request                         │
│              Authorization: Bearer <JWT>                 │
└──────────────────────┬──────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────┐
│           auth.RequireBearerToken middleware              │
│                                                          │
│  • Extracts Bearer token from Authorization header       │
│  • Calls verifyToken()                                   │
│  • On failure: returns 401 + WWW-Authenticate            │
│  • On success: attaches TokenInfo to request context     │
└──────────────────────┬───────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────┐
│               verifyToken function                       │
│                                                          │
│  1. Parse JWT (unverified) → extract issuer              │
│  2. Check issuer is our Auth0 tenant                     │
│  3. Fetch JWKS public keys (cached, refreshed ~15 min)   │
│  4. Verify: signature + audience + expiration            │
│  5. Return TokenInfo {UserID, Expiration, Extra}         │
└──────────────────────┬───────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────┐
│            MCP StreamableHTTP Handler                    │
│                                                          │
│  • Routes JSON-RPC methods (initialize, tools/call, ...) │
│  • Dispatches to registered tool handlers                │
│  • Tool accesses user via req.Extra.TokenInfo            │
└──────────────────────────────────────────────────────────┘

Bibliotecas principales

BibliotecaFunción
github.com/modelcontextprotocol/go-sdk/mcpServidor MCP, herramientas y transporte HTTP
github.com/modelcontextprotocol/go-sdk/authMiddleware RequireBearerToken, ProtectedResourceMetadataHandler
github.com/modelcontextprotocol/go-sdk/oauthexStruct ProtectedResourceMetadata
github.com/lestrrat-go/jwx/v2/jwkObtención y almacenamiento en caché de JWKS
github.com/lestrrat-go/jwx/v2/jwtAnálisis de JWT, verificación de firmas y validación de claims

Para ir más lejos

Este servidor mínimo cubre el patrón principal. Así puedes ampliarlo:

Agrega más herramientas — Sigue el mismo patrón: define un struct de argumentos, regístralo con mcp.AddTool y accede a req.Extra.TokenInfo para obtener la información del usuario autenticado.

Llama a API externas con el token del usuario — El JWT sin procesar se almacena en req.Extra.TokenInfo.Extra["raw_token"]. Úsalo para llamar a API que acepten el mismo JWT, o implementa intercambio de tokens RFC 8693 para cambiarlo por el token de otra API.

Admite múltiples tenants de Auth0 — Registra varias URL de JWKS en la caché y comprueba el emisor contra una lista de servidores permitidos.

Usa variables de entorno — Reemplaza las constantes definidas directamente en el código por variables de entorno o una biblioteca de configuración como koanf para despliegues en producción.

Agrega verificación de scopes — El campo TokenInfo.Scopes se completa durante la verificación. Compruébalo en los handlers de las herramientas para aplicar permisos granulares.

Código fuente completo

El archivo main.go completo tiene aproximadamente 120 líneas. El código fuente completo de una implementación apta para producción con intercambio de tokens, múltiples herramientas y charts de Helm está disponible en el repositorio LFX MCP Server.

Fuentes