1. Visión general
Protocolo de Contexto del Modelo (MCP) permite que los modelos de IA accedan a datos empresariales a través de APIs seguras. Cuando construimos servidores MCP que manejan información sensible, necesitamos una autorización adecuada para controlar quién puede acceder a qué datos.
OAuth2 proporciona seguridad basada en tokens que funciona bien con sistemas MCP. En lugar de crear autenticaciones personalizadas, podemos usar los estándares OAuth2 para proteger nuestros servidores MCP y gestionar el acceso de los clientes.
En este artículo veremos cómo asegurar servidores y clientes MCP usando Spring AI y OAuth2. Construiremos un ejemplo completo con tres componentes: un servidor de autorización, un servidor MCP protegido con herramientas de cálculo y un cliente que maneja tanto las peticiones de usuarios como las del sistema.
2. Arquitectura de Seguridad de MCP
Para asegurar servidores MCP, es importante entender cómo integraremos un servidor de autorización antes del servidor MCP.
Nuestro sistema incluye un Servidor de Autorización para emitir tokens JWT con los permisos adecuados, un Servidor MCP para validar tokens y controlar el acceso a las herramientas de cálculo. Además, hay un Cliente MCP que obtiene tokens y gestiona la autenticación para diferentes tipos de peticiones:
Los servidores MCP actúan como Servidores de Recursos OAuth2. Verifican los tokens JWT en los encabezados de las peticiones antes de procesar cualquier operación. Esto separa las preocupaciones de seguridad de la lógica empresarial. Los clientes obtienen tokens de acceso de un servidor de autorización OAuth2. Luego, los clientes incluyen estos tokens en las peticiones MCP. Finalmente, los servidores MCP validan los tokens antes de permitir las operaciones. Así funcionarán nuestros componentes:
3. Construyendo el Servidor de Autorización
Comenzaremos con el servidor de autorización ya que los demás componentes dependen de él.
3.1. Añadiendo Dependencias
Necesitamos agregar la dependencia del servidor de autorización OAuth2 para usarla:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3.2. Configurando el Servidor de Autorización
Configuraremos el servidor en application.yml:
server:
port: 9000
spring:
security:
user:
name: user
password: password
oauth2:
authorizationserver:
client:
oidc-client:
registration:
client-id: "mcp-client"
client-secret: "{noop}mcp-secret"
client-authentication-methods:
- "client_secret_basic"
authorization-grant-types:
- "authorization_code"
- "client_credentials"
- "refresh_token"
redirect-uris:
- "http://localhost:8080/authorize/oauth2/code/authserver"
scopes:
- "openid"
- "profile"
- "calc.read"
- "calc.write"
Esto configura un servidor de autorización en el puerto 9000 con un cliente que admite tanto el flujo de código de autorización (para usuarios) como el flujo de credenciales de cliente (para sistemas).
4. Asegurando el Servidor MCP
Ahora crearemos un servidor MCP que requiera tokens OAuth2 y proporcione herramientas de cálculo.
4.1. Configurando Dependencias para el Servidor MCP
Agregamos el soporte del servidor de recursos OAuth2:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<version>1.0.0-M7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
4.2. Configuración del Servidor
Configuramos el servidor MCP como un servidor de recursos OAuth2:
server.port=8090
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000
spring.ai.mcp.server.enabled=true
spring.ai.mcp.server.name=mcp-calculator-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.stdio=false
Spring Boot maneja automáticamente la validación JWT cuando configuramos la URI del emisor. Cada solicitud a nuestro servidor MCP ahora requiere un token JWT válido en el encabezado Authorization.
4.3. Creando Herramientas MCP
Los LLMs suelen ser malos en matemáticas. Por eso, debemos darles acceso a herramientas que puedan calcular los resultados según la solicitud:
@Tool(description = "Add two numbers")
public CalculationResult add(
@ToolParam(description = "First number") double a,
@ToolParam(description = "Second number") double b) {
double result = a + b;
return new CalculationResult("addition", a, b, result);
}
@Tool(description = "Multiply two numbers")
public CalculationResult multiply(
@ToolParam(description = "First number") double a,
@ToolParam(description = "Second number") double b) {
double result = a * b;
return new CalculationResult("multiplication", a, b, result);
}
La configuración de seguridad protege automáticamente todas las herramientas MCP. Las peticiones sin tokens válidos son rechazadas. Estas herramientas se añaden al contexto y se entregan al LLM en cada consulta del usuario. Luego el LLM decide qué herramienta usar para responder la consulta del usuario.
5. Construyendo el Cliente MCP
Ahora necesitamos el cliente para manejar la parte más compleja ya que debe gestionar tanto las peticiones de usuarios como la inicialización del sistema.
5.1. Dependencias para el Cliente
Agregamos las dependencias del mcp-client y del oauth2-client:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
5.2. Configuración del Cliente
Ahora necesitamos configurar dos registros de cliente OAuth2 en application.properties:
server.port=8080
spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8090
spring.ai.mcp.client.type=SYNC
spring.security.oauth2.client.provider.authserver.issuer-uri=http://localhost:9000
# Cliente OAuth2 para peticiones iniciadas por el usuario (Authorization Code Grant)
spring.security.oauth2.client.registration.authserver.client-id=mcp-client
spring.security.oauth2.client.registration.authserver.client-secret=mcp-secret
spring.security.oauth2.client.registration.authserver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.authserver.provider=authserver
spring.security.oauth2.client.registration.authserver.scope=openid,profile,mcp.read,mcp.write
spring.security.oauth2.client.registration.authserver.redirect-uri={baseUrl}/authorize/oauth2/code/{registrationId}
# Cliente OAuth2 para peticiones máquina a máquina (Client Credentials Grant)
spring.security.oauth2.client.registration.authserver-client-credentials.client-id=mcp-client
spring.security.oauth2.client.registration.authserver-client-credentials.client-secret=mcp-secret
spring.security.oauth2.client.registration.authserver-client-credentials.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.authserver-client-credentials.provider=authserver
spring.security.oauth2.client.registration.authserver-client-credentials.scope=mcp.read,mcp.write
spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
Necesitamos dos registros para distintos flujos de autenticación. El registro authserver utiliza el flujo de código de autorización para peticiones iniciadas por usuarios. El registro authserver-client-credentials utiliza el flujo de credenciales de cliente para el arranque del sistema.
5.3. Configuración de Seguridad
Configuramos Spring Security para manejar OAuth2:
@Bean
WebClient.Builder webClientBuilder(McpSyncClientExchangeFilterFunction filterFunction) {
return WebClient.builder()
.apply(filterFunction.configuration());
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.oauth2Client(Customizer.withDefaults())
.csrf(CsrfConfigurer::disable)
.build();
}
5.4. Seleccionando el Token Adecuado
Todo el reto aquí es elegir el token correcto para cada petición. Necesitamos una implementación personalizada de ExchangeFilterFunction que detecte el contexto de la petición:
@Component
public class McpSyncClientExchangeFilterFunction implements ExchangeFilterFunction {
private final ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialTokenProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
private final ServletOAuth2AuthorizedClientExchangeFilterFunction delegate;
private final ClientRegistrationRepository clientRegistrationRepository;
private static final String AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID = "authserver";
private static final String CLIENT_CREDENTIALS_CLIENT_REGISTRATION_ID = "authserver-client-credentials";
public McpSyncClientExchangeFilterFunction(OAuth2AuthorizedClientManager clientManager,
ClientRegistrationRepository clientRegistrationRepository) {
this.delegate = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientManager);
this.delegate.setDefaultClientRegistrationId(AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID);
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes) {
return this.delegate.filter(request, next);
}
else {
var accessToken = getClientCredentialsAccessToken();
var requestWithToken = ClientRequest.from(request)
.headers(headers -> headers.setBearerAuth(accessToken))
.build();
return next.exchange(requestWithToken);
}
}
private String getClientCredentialsAccessToken() {
var clientRegistration = this.clientRegistrationRepository
.findByRegistrationId(CLIENT_CREDENTIALS_CLIENT_REGISTRATION_ID);
var authRequest = OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
.principal(new AnonymousAuthenticationToken("client-credentials-client", "client-credentials-client",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")))
.build();
return this.clientCredentialTokenProvider.authorize(authRequest).getAccessToken().getTokenValue();
}
public Consumer<WebClient.Builder> configuration() {
return builder -> builder.defaultRequest(this.delegate.defaultRequest()).filter(this);
}
}
El filtro comprueba si hay una solicitud web activa. Si la hay, usa el token de código de autorización del usuario. Si no (por ejemplo, durante el arranque de la aplicación), usa credenciales de cliente.
6. Utilizando el Sistema MCP Seguro
Ahora que tenemos todos los componentes cubiertos, veamos cómo podemos usar el sistema MCP seguro de manera efectiva.
6.1. Creando un ChatClient
Conectamos todo con un ChatClient:
@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpClients) {
return chatClientBuilder.defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpClients))
.build();
}
6.2. Haciendo Peticiones
Ahora podemos usar el ChatClient normalmente. La seguridad ocurre automáticamente:
@GetMapping("/calculate")
public String calculate(@RequestParam String expression, @RegisteredOAuth2AuthorizedClient("authserver") OAuth2AuthorizedClient authorizedClient) {
String prompt = String.format("Please calculate the following mathematical expression using the available calculator tools: %s", expression);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
Durante el arranque, la inicialización del cliente MCP utiliza tokens de credenciales de cliente. Cuando los usuarios hacen peticiones a través de la interfaz web, se usan sus tokens de código de autorización.
7. Verificando la Configuración
Para entender cómo funciona la aplicación, necesitamos revisar los resultados que produce. Antes de iniciar cualquier aplicación, debemos configurar las variables de entorno necesarias para el LLM. Tras la configuración, arrancamos el servidor de autorización en el puerto 9000 primero. Esto será importante ya que todos los demás módulos dependen del servidor de autorización. Luego arrancamos el servidor MCP en el puerto 8090, seguido por el cliente MCP en el puerto 8080.
Probar el flujo completo se vuelve sencillo. Visita el endpoint del cliente MCP y prueba lo siguiente:
http://{base_url}:8080/calculate?expression=15+25
El cliente obtendrá un token del servidor de autorización, llamará al servidor MCP con el token y devolverá el resultado del cálculo. Debemos asegurarnos de iniciar sesión en el servidor de autorización con las credenciales especificadas en los archivos de configuración.
8. Conclusión
En este tutorial exploramos cómo OAuth2 proporciona una seguridad robusta para sistemas MCP mediante la autorización basada en tokens estándar. El soporte OAuth2 de Spring Security permite una buena protección con una configuración mínima. Al separar el servidor de autorización, el servidor MCP y el cliente MCP, creamos una arquitectura donde cada componente se centra en su propia responsabilidad, otorgando flexibilidad.
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.
