Security and Authorization support for Model Context Protocol in Spring AI.
⚠️ This project only works Spring AI's 1.1.x branch.
- Overview
- MCP Server Security
- MCP Client Security
- Authorization Server
- Samples
- Integrations (Cursor, Claude Desktop, ...)
- License
This repository provides Authorization support for Spring AI integrations with the Model Context Protocol (MCP). It covers both MCP Clients, MCP Servers, and Spring Authorization Server.
The project enables developers to:
- Secure MCP servers with OAuth 2.0 authentication
- Configure MCP clients with OAuth 2.0 authorization flows
- Set up authorization servers specifically designed for MCP workflows
- Implement fine-grained access control for MCP tools and resources
Provides OAuth 2.0 resource server capabilities for Spring AI's MCP servers. It also provides basic support for API-key based servers. This module is compatible with Spring WebMVC-based servers only.
Maven
<dependencies>
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>mcp-server-security</artifactId>
<version>0.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OPTIONAL -->
<!-- If you would like to use OAuth2, ensure you import the Resource Server dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
</dependencies>
Gradle
implementation("org.springaicommunity:mcp-server-security:0.0.2")
implementation("org.springframework.boot:spring-boot-starter-security")
// OPTIONAL
// If you would like to use OAuth2, ensure you import the Resource Server dependencies
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
Ensure that MCP server is enabled in your application.properties
:
spring.ai.mcp.server.name=my-cool-mcp-server
# Supported protocols: STREAMABLE, STATELESS
spring.ai.mcp.server.protocol=STREAMABLE
Then, configure the security for your project in the usual Spring-Security way, adding the provided configurer.
Create a configuration class, and reference the authorization server's URI.
In this example, we have set the authz server's issuer URI in the well known Spring property
spring.security.oauth2.resourceserver.jwt.issuer-uri
.
Using this exact name is not a requirement, and you may use a custom property.
@Configuration
@EnableWebSecurity
class McpServerConfiguration {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUrl;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// Enforce authentication with token on EVERY request
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
// Configure OAuth2 on the MCP server
.with(
McpResourceServerConfigurer.mcpServerOAuth2(),
(mcpAuthorization) -> {
// REQUIRED: the issuerURI
mcpAuthorization.authorizationServer(issuerUrl);
// OPTIONAL: enforce the `aud` claim in the JWT token.
// Not all authorization servers support resource indicators,
// so it may be absent. Defaults to `false`.
// See RFC 8707 Resource Indicators for OAuth 2.0
// https://www.rfc-editor.org/rfc/rfc8707.html
mcpAuthorization.validateAUdienceClaim(true);
}
)
.build();
}
}
It is also possible to secure the tools only, and not the rest of the MCP Server. For example, both initialize
and
tools/list
are made public, but tools/call
is authenticated.
To enable this, update the security configuration, turn on method security and requests to /mcp
are allowed:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // ⬅️ enable annotation-driven security
class McpServerConfiguration {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUrl;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// ⬇️ Open every request on the server
.authorizeHttpRequests(auth -> {
auth.requestMatcher("/mcp").permitAll();
auth.anyRequest().authenticated();
})
// Configure OAuth2 on the MCP server
.with(
McpResourceServerConfigurer.mcpServerOAuth2(),
(mcpAuthorization) -> {
// REQUIRED: the issuerURI
mcpAuthorization.authorizationServer(issuerUrl);
}
)
.build();
}
}
Then, secure your tool calls using the @PreAuthorize
annotation,
using method security.
Inside the annotation, you can apply
a security-based SpEL expression.
At the most basic level, you can use isAuthenticated()
, ensuring that the MCP client sent a request with a valid
bearer token:
@Service
public class MyToolsService {
// Note: you can also use Spring AI's @Tool
@PreAuthorize("isAuthenticated()")
@McpTool(name = "greeter", description = "A tool that greets you, in the selected language")
public String greet(
@ToolParam(description = "The language for the greeting (example: english, french, ...)") String language
) {
if (!StringUtils.hasText(language)) {
language = "";
}
return switch (language.toLowerCase()) {
case "english" -> "Hello you!";
case "french" -> "Salut toi!";
default -> "I don't understand language \"%s\". So I'm just going to say Hello!".formatted(language);
};
}
}
Note that you can also access the current authentication directly from the tool method itself, using the thread-local
SecurityContextHolder
:
@McpTool(name = "greeter", description = "A tool that greets the user by name, in the selected language")
@PreAuthorize("isAuthenticated()")
public String greet(
@ToolParam(description = "The language for the greeting (example: english, french, ...)") String language
) {
if (!StringUtils.hasText(language)) {
language = "";
}
var authentication = SecurityContextHolder.getContext().getAuthentication();
var name = authentication.getName();
return switch (language.toLowerCase()) {
case "english" -> "Hello, %s!".formatted(name);
case "french" -> "Salut %s!".formatted(name);
default -> ("I don't understand language \"%s\". " +
"So I'm just going to say Hello %s!").formatted(language, name);
};
}
Ensure that MCP server is enabled in your application.properties
:
spring.ai.mcp.server.name=my-cool-mcp-server
# Supported protocols: STREAMABLE, STATELESS
spring.ai.mcp.server.protocol=STREAMABLE
For this, you'll need to provide your own implementation of ApiKeyEntityRepository
, for storing ApiKeyEntity
objects.
These represent the "entities" which have API keys.
Each entry has an ID, a secret for storing API keys in a secure way (e.g. bcrypt, argon2, ...), as well as a name used
for display purposes.
A sample implementation is available with an InMemoryApiKeyEntityRepository
along with a default ApiKeyEntityImpl
.
You can bring your own entity implementation with the in-memory repository.
⚠️ TheInMemoryApiKeyEntityRepository
uses on bcrypt for storing the API keys, and, as such, will be computationally expensive. It is not suited for high-traffic production use. In that case, you must ship your ownApiKeyEntityRepository
implementation.
With that, you can configure the security for your project in the usual Spring-Security way:
@Configuration
@EnableWebSecurity
class McpServerConfiguration {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
.with(
mcpServerApiKey(),
(apiKey) -> {
// REQUIRED: the repo for API keys
apiKey.apiKeyRepository(buildApiKeyRepository());
// OPTIONAL: name of the header containing the API key.
// Here for example, api keys will be sent with "CUSTOM-API-KEY: <value>"
// Replaces .authenticationConverter(...) (see below)
//
// apiKey.headerName("CUSTOM-API-KEY");
// OPTIONAL: custom converter for transforming an http request
// into an authentication object. Useful when the header is
// "Authorization: Bearer <value>".
// Replaces .headerName(...) (see above)
//
// apiKey.authenticationConverter(request -> {
// var key = extractKey(request);
// return ApiKeyAuthenticationToken.unauthenticated(key);
// });
}
)
.build();
}
/**
* Provide a repository of {@link ApiKeyEntity}.
*/
private ApiKeyEntityRepository<ApiKeyEntityImpl> apiKeyRepository() {
//@formatter:off
var apiKey = ApiKeyEntityImpl.builder()
.name("test api key")
.id("api01")
// "mycustomapikey
.secret("mycustomapikey")
.build();
//@formatter:on
return new InMemoryApiKeyEntityRepository<>(List.of(apiKey));
}
}
Then you should be able to call your MCP server with a header X-API-key: api01.mycustomapikey
.
- The deprecated SSE transport is not supported. Use Streamable HTTP or stateless transport. (the link for stateless does not work out of the box, reload the page if required)
- WebFlux-based servers are not supported.
- Opaque tokens are not supported. Use JWT.
Provides OAuth 2 support
for Spring AI's MCP clients,
with both HttpClient-based clients (from spring-ai-starter-mcp-client
) and
WebClient-based clients (from spring-ai-starter-mcp-client-webflux
).
This module supports McpSyncClient
s only.
Maven
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>mcp-client-security</artifactId>
<version>0.0.2</version>
</dependency>
Gradle
implementation("org.springaicommunity:mcp-client-security:0.0.2")
For our MCP clients, there are three flows available for obtaining tokens:
authorization_code
-based flows. This is the flow that the MCP spec illustrates. A user is present, and the MCP client makes HTTP requests using a bearer token on behalf of that user.client_credentials
-based flows. This is not detailed in the spec, but compatible. Client credentials is for machine-to-machine use-cases, where there is no human is in the loop. The MCP clients makes HTTP request with a token for itself.- Hybrid flows. In some use-cases, the user might not be present for some MCP client calls, such
as
initialize
ortools/list
. In that case, the MCP client makes calls withclient_credentials
tokens representing the client itself. But the user may be present fortools/call
, and in that case, the client will use anauthorization_code
token representing the user.
🤔 Which flow should I use?
- If there are user-level permission, AND you know every MCP request will be made within the context of a user request
(ensure there are not
tools/list
call no app startup), then use theauthorization_code
flow, with eitherOAuth2AuthorizationCodeSyncHttpRequestCustomizer
orMcpOAuth2AuthorizationCodeExchangeFilterFunction
. - If there are no user-level permissions, and you want to secure "client-to-server" communication with an access token,
use the
client_credentials
flow, with eitherOAuth2ClientCredentialsSyncHttpRequestCustomizer
orMcpOAuth2ClientCredentialsExchangeFilterFunction
. - If there are user-level permission, AND you configure your MCP clients using Spring Boot properties (such as
spring.ai.mcp.client.streamable-http.connections.<server-name>.url=<server-url>
), then, on application startup, Spring AI will try to list the tools. And startup happens without a user present. In that specific case, use a hybrid flow, with eitherOAuth2HybridSyncHttpRequestCustomizer
orMcpOAuth2HybridExchangeFilterFunction
.
In very case, you need to activate Spring Security's OAuth2 client support.
Add the following properties to your application.properties
file.
Depending on the flow you chose (see above), you may need one or both client registrations:
# Ensure MCP clients are sync
spring.ai.mcp.client.type=SYNC
#
#
# For obtaining tokens for calling the tool
# When using the hybrid flow or authorization_code flow, this registers a client
# called "authserver". If using client_credentials, do not include this:
spring.security.oauth2.client.registration.authserver.client-id=<THE CLIENT ID>
spring.security.oauth2.client.registration.authserver.client-secret=<THE CLIENT SECRET>
spring.security.oauth2.client.registration.authserver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.authserver.provider=authserver
#
# When using the hybrid flow or client_credentials flow, this registers a client
# called "authserver-client-credentials". If using authorization_code, do not include this:
spring.security.oauth2.client.registration.authserver-client-credentials.client-id=<THE CLIENT ID>
spring.security.oauth2.client.registration.authserver-client-credentials.client-secret=<THE CLIENT SECRET>
spring.security.oauth2.client.registration.authserver-client-credentials.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.authserver-client-credentials.provider=authserver
#
# Both clients above rely on the authorization server, specified by its issuer URI:
spring.security.oauth2.client.provider.authserver.issuer-uri=<THE ISSUER URI OF YOUR AUTH SERVER>
Then, create a configuration class, activating the OAuth2 client capabilities with a SecurityFilterChain
.
@Configuration
@EnableWebSecurity
class SecurityConfiguration {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// in this example, the client app has no security on its endpoints
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
// turn on OAuth2 support
.oauth2Client(Customizer.withDefaults())
.build();
}
}
If you already have a filter chain configured, ensure that .oauth2Client(...)
is on.
When using spring-ai-starter-mcp-client
, the underlying MCP client transport will be based on the JDK's
HttpClient
.
In that case, you can expose a bean of type McpSyncHttpClientRequestCustomizer
.
Depending on your authorization flow of choice, you may use one of the following
implementations:
OAuth2AuthorizationCodeSyncHttpRequestCustomizer
(preferred)OAuth2ClientCredentialsSyncHttpRequestCustomizer
(machine-to-machine)OAuth2HybridSyncHttpRequestCustomizer
(last resort)
All these request customizers rely on request and authentication data.
That data is passed through
McpTransportContext
(MCP docs).
To make that information available, you also need to add an AuthenticationMcpTransportContextProvider
to your MCP Sync
Client.
Tying it all together, taking OAuth2AuthorizationCodeSyncHttpRequestCustomizer
as an example:
@Configuration
class McpConfiguration {
@Bean
McpSyncClientCustomizer syncClientCustomizer() {
return (name, syncSpec) ->
syncSpec.transportContextProvider(
new AuthenticationMcpTransportContextProvider()
);
}
@Bean
McpSyncHttpClientRequestCustomizer requestCustomizer(
OAuth2AuthorizedClientManager clientManager
) {
// The clientRegistration name, "authserver",
// must match the name in application.properties
return new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(
clientManager,
"authserver"
);
}
}
When using spring-ai-starter-mcp-client-webflux
, the underlying MCP client transport will be based on a Spring
reactive WebClient
.
In that case, you can expose a bean of type WebClient.Builder
, configured with an MCP implementation of
ExchangeFilterFunction
.
Depending on your authorization flow of choice, you may use one of the following
implementations:
McpOAuth2AuthorizationCodeExchangeFilterFunction
(preferred)McpOAuth2ClientCredentialsExchangeFilterFunction
(machine-to-machine)McpOAuth2HybridExchangeFilterFunction
(last resort)
All these request customizers rely on request and authentication data.
That data is passed through
McpTransportContext
(MCP docs).
To make that information available, you also need to add an AuthenticationMcpTransportContextProvider
to your MCP Sync
Client.
Tying it all together, taking McpOAuth2AuthorizationCodeExchangeFilterFunction
as an example:
@Configuration
class McpConfiguration {
@Bean
McpSyncClientCustomizer syncClientCustomizer() {
return (name, syncSpec) ->
syncSpec.transportContextProvider(
new AuthenticationMcpTransportContextProvider()
);
}
@Bean
WebClient.Builder mcpWebClientBuilder(OAuth2AuthorizedClientManager clientManager) {
// The clientRegistration name, "authserver", must match the name in application.properties
return WebClient.builder().filter(
new McpOAuth2AuthorizationCodeExchangeFilterFunction(
clientManager,
"authserver"
)
);
}
}
Spring AI integrates MCP tools as if they were regular "tools" (e.g. @Tool
methods).
As such, they are discovered when application starts up.
This means that any MCP client that is configured through configuration properties, such
as spring.ai.mcp.client.streamable-http.connections.<SERVER-NAME>.url=...
will be initialized.
In practice, there will be multiple calls issued to the MCP Server (initialize
followed by tools/list
).
The server will require a token for these calls, and, without a user present, this is an issue in the general case.
To avoid this, you first need to ensure that the clients are not initialized on startup.
You can do so by setting the property spring.ai.mcp.client.initialized=false
.
Then, you need to ensure tools are not listed. There are a few ways to avoid this:
Disable the @Tool auto-configuration
You can turn off Spring AI's @Tool
autoconfiguration altogether.
This will disable all method and function-based tool calling, and only MCP tools will be available.
The easiest way to do so is to publish an empty ToolCallbackResolver
bean:
@Configuration
public class McpConfiguration {
@Bean
ToolCallbackResolver resolver() {
return new StaticToolCallbackResolver(List.of());
}
}
Programmatically configure MCP clients
You may also forego Spring AI's autoconfiguration altogether, and create the MCP clients programmatically. The easiest way is to draw some inspiration on the transport auto-configurations (HttpClient, WebClient) as well as the client auto-configuration.
All in all, it could look like so:
// For HttpClient-based clients
@Bean
McpSyncClient client(
ObjectMapper objectMapper,
McpSyncHttpClientRequestCustomizer requestCustomizer,
McpClientCommonProperties commonProps
) {
var transport = HttpClientStreamableHttpTransport.builder(mcpServerUrl)
.clientBuilder(HttpClient.newBuilder())
.jsonMapper(new JacksonMcpJsonMapper(objectMapper))
.httpRequestCustomizer(requestCustomizer)
.build();
var clientInfo = new McpSchema.Implementation("client-name", commonProps.getVersion());
return McpClient.sync(transport)
.clientInfo(clientInfo)
.requestTimeout(commonProps.getRequestTimeout())
.transportContextProvider(new AuthenticationMcpTransportContextProvider())
.build();
}
//
// -------------------------
//
// For WebClient based clients
@Bean
McpSyncClient client(
WebClient.Builder mcpWebClientBuilder,
ObjectMapper objectMapper,
McpClientCommonProperties commonProperties
) {
var builder = mcpWebClientBuilder.baseUrl(mcpServerUrl);
var transport = WebClientStreamableHttpTransport.builder(builder)
.jsonMapper(new JacksonMcpJsonMapper(objectMapper))
.build();
var clientInfo = new McpSchema.Implementation("clientName", commonProperties.getVersion());
return McpClient.sync(transport)
.clientInfo(clientInfo)
.requestTimeout(commonProperties.getRequestTimeout())
.transportContextProvider(new AuthenticationMcpTransportContextProvider())
.build();
}
You can then add it to the tools available to a chat client:
var chatResponse = chatClient.prompt("Prompt the LLM to _do the thing_")
.toolCallbacks(new SyncMcpToolCallbackProvider(mcpClient1, mcpClient2, mcpClient3))
.call()
.content();
- Spring WebFlux servers are not supported.
- Spring AI autoconfiguration initializes the MCP client app start. Most MCP servers want calls to be authenticated with a token, so you need to work around the Spring AI auto-config (see the workaround above)
Note:
- Unlike the
mcp-server-security
module, the client implementation supports the SSE transport, both withHttpClient
andWebClient
.
Enhances Spring Security's OAuth 2.0 Authorization Server support with the RFCs and features relevant to the MCP authorization spec, such as Dynamic Client Registration and Resource Indicators. It provides a simple configurer for an MCP server.
Maven
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>mcp-authorization-server</artifactId>
<version>0.0.2</version>
</dependency>
Gradle
implementation("org.springaicommunity:mcp-authorization-server:0.0.2")
Then configure the authorization server (
see reference documentatio).
Here is an example application.yml
for registering a default client:
spring:
application:
name: sample-authorization-server
security:
oauth2:
authorizationserver:
client:
default-client:
token:
access-token-time-to-live: 1h
registration:
client-id: "default-client"
client-secret: "{noop}default-secret"
client-authentication-methods:
- "client_secret_basic"
- "none"
authorization-grant-types:
- "authorization_code"
- "client_credentials"
redirect-uris:
- "http://127.0.0.1:8080/authorize/oauth2/code/authserver"
- "http://localhost:8080/authorize/oauth2/code/authserver"
# mcp-inspector
- "http://localhost:6274/oauth/callback"
# claude code
- "https://claude.ai/api/mcp/auth_callback"
user:
# A single user, named "user"
name: user
password: password
server:
servlet:
session:
cookie:
# Override the default cookie name (JSESSIONID).
# This allows running multiple Spring apps on localhost, and they'll each have their own cookie.
# Otherwise, since the cookies do not take the port into account, they are confused.
name: MCP_AUTHORIZATION_SERVER_SESSIONID
This is only an example, and you'll likely want to write your own configuration.
With this configuration, there will be a single user registered (username: user
, password: password
).
There will also be a single OAuth2 Client (default-client-id
/ default-client-secret
).
You can then activate all the authorization server capabilities with the usual Spring Security APIs,
the security filter chain:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// all requests must be authenticated
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
// enable authorization server customizations
.with(mcpAuthorizationServer(), withDefaults())
// enable form-based login, for user "user"/"password"
.formLogin(withDefaults())
.build();
}
- Spring WebFlux servers are not supported.
- Every client supports ALL
resource
identifiers.
The samples
directory contains samples for these libraries.
A README.md contains instructions for running
those samples.
A special directory is samples/integration-tests
, which contains integration tests for all the submodules in this
project.
This is a work-in-progress, but with mcp-server-security
, and a supporting mcp-authorization-server
, you should be
able to integrate with Cursor, Claude Code, and the MCP inspector.
Note: if you use the MCP Inspector you may need to turn off CSRF and CORS protection.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Note: This is a community-driven project and is not officially endorsed by Spring AI or the MCP project.