1. Introducción
MCP (Model Context Protocol) es un estándar abierto, introducido por Anthropic, diseñado para permitir que los modelos de IA interactúen con herramientas externas, fuentes de datos y servicios de manera estructurada. Un servidor MCP es una aplicación ligera que expone capacidades específicas a través de la interfaz MCP, como acceder a archivos, consultar bases de datos o llamar a APIs.
Para hacer que los servidores MCP estén listos para producción, podemos considerar separarlos en aplicaciones independientes. Esto nos ayuda a escalar y mantenerlos individualmente. Sin embargo, dado que estos servidores pueden manejar tareas sensibles, necesitamos asegurar sus puntos finales y limitar el acceso a clientes confiables.
Ahí es donde entra en juego OAuth2. OAuth2 es un protocolo bien conocido para la delegación segura de acceso basado en tokens a APIs. En lugar de gestionar las credenciales de los usuarios directamente, nuestro servidor MCP confía en los tokens de acceso validados emitidos por un servidor de autorización central. Podemos usar OAuth2 para conceder o restringir el acceso de las aplicaciones cliente a capacidades específicas de MCP basadas en alcances y roles.
En este tutorial, aprenderemos cómo asegurar un servidor MCP usando OAuth2 en una aplicación Spring AI.
2. Dependencias
Primero, agreguemos la dependencia del Spring AI MCP server que usaremos para obtener el transporte HTTP y SSE y el soporte core de MCP:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
</dependency>
Ahora, agreguemos la dependencia del servidor de autorización OAuth. La usaremos para emitir los tokens de acceso OAuth2:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<version>3.3.3</version>
</dependency>
Finalmente, agreguemos la dependencia del resource‑server de Spring Security:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>3.4.2</version>
</dependency>
Con esta dependencia, aseguraremos que nuestros puntos finales MCP rechacen tokens Bearer inválidos o faltantes.
3. Creando un Servidor MCP de Información de Acciones
Ahora, implementemos un servidor MCP simple. Dentro de él, tendremos una herramienta que devuelve los precios de las acciones para un símbolo proporcionado.
Creemos una clase StockInformationHolder:
public class StockInformationHolder {
@Tool(description = "Get stock price for a company symbol")
public String getStockPrice(@ToolParam String symbol) {
if ("AAPL".equalsIgnoreCase(symbol)) {
return "AAPL: $150.00";
} else if ("GOOGL".equalsIgnoreCase(symbol)) {
return "GOOGL: $2800.00";
} else {
return symbol + ": Data not available";
}
}
}
Aquí, tenemos el método getStockPrice() que usamos para devolver los precios de las acciones de empresas conocidas y la respuesta por defecto para los símbolos que no conocemos. El método está marcado con la anotación @Tool, por lo que se usará para construir la definición de la herramienta. Además, hemos marcado el parámetro symbol con la anotación @ToolParam para asegurar que se considere durante el proceso de construcción de la definición de la herramienta.
A continuación, creemos la clase McpServerConfiguration:
@Configuration
public class McpServerConfiguration {
@Bean
public ToolCallbackProvider stockTools() {
return MethodToolCallbackProvider
.builder()
.toolObjects(new StockInformationHolder())
.build();
}
}
Aquí, proporcionamos el bean ToolCallbackProvider. Lo construimos adjuntando la clase StockInformationHolder. Ahora ya tenemos un servidor MCP listo para usar y podemos iniciar nuestra aplicación y abrir la conexión SSE llamando al punto final GET /sse. Para enviar mensajes a nuestro servidor MCP, usemos el punto final POST /mcp/message con el cuerpo JSON:
{
"jsonrpc": "2.0",
"id": "1",
"method": "tools/call",
"params": {
"name": "getStockPrice",
"arguments": {
"arg0": "AAPL"
}
}
}
Hemos especificado "tools/call" para el method, indicando que queremos invocar la funcionalidad de nuestra herramienta. En el objeto params, estamos enviando parámetros al método especificado, incluyendo el name de la herramienta, que por defecto es el nombre del método anotado, junto con el mapa de arguments.
4. Añadiendo la Configuración de Seguridad
Ahora, aseguremos nuestro servidor MCP. Primero, configuraremos nuestro servidor de autorización usando el archivo application.yml:
spring:
security:
oauth2:
authorizationserver:
client:
oidc-client:
registration:
client-id: mcp-client
client-secret: "{noop}secret"
client-authentication-methods: client_secret_basic
authorization-grant-types: client_credentials
Especificamos el identificador único para la aplicación cliente que solicita tokens. Para la clave secreta compartida, usamos {noop}secret, que solo es adecuado para propósitos de demostración. El prefijo {noop} indica a Spring que no hashee la clave, lo que lo hace útil para escenarios de prueba.
A continuación, creemos la clase McpServerSecurityConfiguration:
@Configuration
@EnableWebSecurity
public class McpServerSecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/mcp/**").authenticated()
.requestMatchers("/sse").authenticated()
.anyRequest().permitAll())
.with(OAuth2AuthorizationServerConfigurer.authorizationServer(), Customizer.withDefaults())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.csrf(CsrfConfigurer::disable)
.cors(Customizer.withDefaults())
.build();
}
}
Aquí, permitimos todas las solicitudes autorizadas a los puntos finales /mcp y /sse. Todos los demás puntos finales permanecerán abiertos. Este enfoque simplifica el acceso al punto final de autenticación. Sin embargo, en una aplicación en vivo, restringiríamos el acceso de manera más cuidadosa.
Usamos los métodos authorizationServer() y oauth2ResourceServer() para configurar la aplicación. Este conjunto indica que la aplicación proporciona puntos finales de token de acceso. También actúa como un servidor de recursos, validando las solicitudes entrantes utilizando tokens JWT.
5. Probar el Servidor MCP Seguro
Ahora, necesitamos probar nuestro servidor MCP seguro. Creemos la clase McpServerOAuth2LiveTest:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class McpServerOAuth2LiveTest {
private static final Logger log = LoggerFactory.getLogger(McpServerOAuth2LiveTest.class);
@LocalServerPort
private int port;
private WebClient webClient;
@BeforeEach
void setup() {
webClient = WebClient.create("http://localhost:" + port);
}
}
Iniciamos nuestra aplicación en un puerto aleatorio e inicializamos el WebClient. Luego, llamemos al punto final /sse para abrir una conexión de eventos enviados por el servidor:
Flux<String> eventStream = webClient.get()
.uri("/sse")
.header("Authorization", obtainAccessToken())
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(String.class);
eventStream.subscribe(
data -> {
log.info("Response received: {}", data);
if (!isRequestMessage(data)) {
assertThat(data).containsSequence("AAPL", "$150");
}
},
error -> log.error("Stream error: {}", error.getMessage()),
() -> log.info("Stream completed")
);
Hemos afirmado que los mensajes de respuesta contienen los datos esperados. A continuación, enviemos una solicitud al punto final /mcp/message:
Flux<String> sendMessage = webClient.post()
.uri("/mcp/message")
.header("Authorization", obtainAccessToken())
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.TEXT_EVENT_STREAM)
.bodyValue("""
{
"jsonrpc": "2.0",
"id": "1",
"method": "tools/call",
"params": {
"name": "getStockPrice",
"arguments": {
"arg0": "AAPL"
}
}
}
""")
.retrieve()
.bodyToFlux(String.class);
Enviamos una solicitud para obtener el precio de la acción de AAPL. Ambas solicitudes incluyen la cabecera Authorization. Ahora, implementemos el método para obtener el token de acceso:
public String obtainAccessToken() {
String clientId = "mcp-client";
String clientSecret = "secret";
String basicToken = Base64.getEncoder()
.encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8));
return "Bearer " + webClient.post()
.uri("/oauth2/token")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header(HttpHeaders.AUTHORIZATION, "Basic " + basicToken)
.body(BodyInserters.fromFormData("grant_type", "client_credentials"))
.retrieve()
.bodyToMono(JsonNode.class)
.map(node -> node.get("access_token").asText())
.block(Duration.ofSeconds(5));
}
Después de la ejecución, podemos ver que los datos de la respuesta fueron recibidos con éxito. Esto confirma que hemos pasado los filtros de seguridad.
6. Conclusión
En este tutorial, hemos asegurado nuestro servidor MCP usando OAuth2 en una aplicación Spring AI. Para proteger los puntos finales clave de MCP, OAuth2 se integró sin problemas a través de Spring Boot. Además, esta configuración es flexible y puede ampliarse. Por ejemplo, podríamos introducir control de acceso basado en roles y alcances para restringir herramientas o acciones específicas a ciertos clientes.
En un entorno de producción, podríamos integrar con un proveedor de identidad completo como Keycloak o Okta. Además, podríamos enriquecer nuestros tokens con reclamos o alcances personalizados para controlar el acceso a herramientas individuales dentro de la plataforma MCP.
Newsletter Semanal de Java
Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.
Sin spam. Cancela cuando quieras.