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:
-
Descubrimiento — El cliente MCP consulta
/.well-known/oauth-protected-resourceen 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. -
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.
-
Solicitudes autenticadas — El cliente adjunta el JWT como un token
Beareren el encabezadoAuthorizationde cada solicitud MCP. -
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:
| Campo | Valor |
|---|---|
| Name | My MCP API |
| Identifier (Audience) | http://127.0.0.1:8080/mcp |
| Signing Algorithm | RS256 |
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:
| Campo | Valor |
|---|---|
| Name | Cursor MCP Client |
| Application Type | Single Page Application |
| Allowed Callback URLs | cursor://anysphere.cursor-mcp/oauth/callback |
| Grant Types | Authorization 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:
- Consulta
/.well-known/oauth-protected-resource - Abre un navegador para iniciar sesión en Auth0
- Obtiene un JWT y lo envía en cada solicitud
- 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
| Biblioteca | Función |
|---|---|
github.com/modelcontextprotocol/go-sdk/mcp | Servidor MCP, herramientas y transporte HTTP |
github.com/modelcontextprotocol/go-sdk/auth | Middleware RequireBearerToken, ProtectedResourceMetadataHandler |
github.com/modelcontextprotocol/go-sdk/oauthex | Struct ProtectedResourceMetadata |
github.com/lestrrat-go/jwx/v2/jwk | Obtención y almacenamiento en caché de JWKS |
github.com/lestrrat-go/jwx/v2/jwt | Aná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
- Especificación de MCP — Autorización (2025-11-25) — Especificación oficial de MCP que define la autorización de servidores de recursos con OAuth 2.1 y el descubrimiento de metadatos de recursos protegidos de RFC 9728.
- RFC 9728 — Metadatos de recursos protegidos de OAuth 2.0 — El estándar de la IETF detrás del endpoint de descubrimiento
/.well-known/oauth-protected-resourceque usan los servidores MCP. - MCP Go SDK (
modelcontextprotocol/go-sdk) — SDK de Go que proporcionamcp.Server, el middlewareauth.RequireBearerTokenyProtectedResourceMetadataHandler. - lestrrat-go/jwx v2 — Biblioteca de Go para JWx (JWA/JWE/JWK/JWS/JWT), que incluye
jwk.Cachepara la actualización automática de JWKS yjwt.Parsepara la verificación de firmas. - Auth0 — Guía de RS256 y JWKS — Guía de Auth0 sobre firmas RS256 y verificación de JWT basada en JWKS.
- Auth0 — Configuración de API — Documentación para configurar identificadores de API (audiencia), algoritmos de firma y scopes en Auth0.
- Cursor — Documentación de Model Context Protocol (MCP) — Documentación oficial de Cursor sobre la conexión a servidores MCP compatibles con OAuth.
- Claude.ai — Creación de conectores personalizados — Documentación de Anthropic sobre la integración de MCP con OAuth y las URL de callback para Claude.