Securing MCP Servers with Spring AI

Engineering | Daniel Garnier-Moiroux | September 30, 2025 | ...

Model Context Protocol, or MCP for short, has taken over the AI world. If you've been following our blog, you've probably read the introduction to the topic, Connect Your AI to Everything: Spring AI's MCP Boot Starters. The security aspects of MCP have been evolving fast, and the latest version of the spec is getting more and more support from the ecosystem. To meet the needs of Spring users, we have incubated a dedicated project on Github: spring-ai-community/mcp-security. This week, we pushed our first releases, and you can now add them to your Spring AI 1.1.x-based applications. In this post, we'll explore:

Securing MCP Servers with OAuth 2

According to the Authorization section of the MCP specification, MCP Servers exposed over HTTP must be secured with OAuth 2 access tokens. Any call to an MCP Server must have a header Authorization: Bearer <access_token>, where the access token is obtained from an authorization server (think: Okta, Github, ...) on behalf of the user. The MCP Server must also explicitly advertise the authorization servers it trusts, so MCP clients can discover them dynamically, register themselves with the auth servers, and obtain tokens. We'll discuss authorization servers later on, but for now we'll assume you have an auth server configured and running at <AUTH_SERVER_URL>, and we'll hook our MCP Server to it. If you need to setup an authorization server, see next section.

First, add the required dependencies to your project:

Maven:


<dependencies>

    <!-- Spring AI MCP starter -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
    <!-- MCP Security -->
    <dependency>
        <groupId>org.springaicommunity</groupId>
        <artifactId>mcp-server-security</artifactId>
        <version>0.0.2</version>
    </dependency>
    <!-- MCP Security dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

</dependencies>

Gradle:

implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc")
implementation("org.springaicommunity:mcp-server-security:0.0.2")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")

Ensure that MCP server is enabled in your application.properties, and inject your authorization server URL:

spring.ai.mcp.server.name=my-cool-mcp-server
# Supported protocols: STREAMABLE, STATELESS
spring.ai.mcp.server.protocol=STREAMABLE
# Choose any property name you'd like
# You MAY use the usual Spring well-known "spring.security.oauth2.resourceserver.jwt.issuer-uri".
authorization.server.url=<AUTH_SERVER_URL>

We will add a simple MCP tool that greets the user, based on an input language ("english", "french", ...) and on the user's name.


@Service
public class MyToolsService {

    @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 = "";
        }
        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);
        };
    }

}

In this example, the tool will look up the name of the user from the SecurityContext, and create a personalised greeting. The name of the user will be the sub claim from the JWT access token used to authenticate the request:

And, last but not least, we add a configuration class for security, for example McpServerSecurityConfiguration:


@Configuration
@EnableWebSecurity
class McpServerSecurityConfiguration {

    @Value("${authorization.server.url}")
    private String authServerUrl;

    @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 authserver's issuer URI
                            mcpAuthorization.authorizationServer(this.authServerUrl);
                            // OPTIONAL: enforce the `aud` claim in the JWT token.
                            mcpAuthorization.validateAudienceClaim(true);
                        }
                )
                .build();
    }

}

Run the application with ./mvnw spring-boot:run or ./gradlew bootRun. It should start on port 8080. If you try to access the MCP server at http://localhost:8080/mcp, you will get an WWW-authenticate indicating the OAuth2 resource metadata URL:

curl -XPOST  -w '%{http_code}\n%header{www-authenticate}' http://localhost:8080/mcp
#
# Will print out:
#
# 401
# Bearer resource_metadata=http://localhost:8080/.well-known/oauth-protected-resource/mcp

The metadata URL itself will indicate to potential clients where the authorization server is located:

curl http://localhost:8080/.well-known/oauth-protected-resource/mcp
#
# Will print out:
#
# {
#   "resource": "http://localhost:8080/mcp",
#   "authorization_servers": [
#     "<AUTH_SERVER_URL>"
#   ],
#   "resource_name": "Spring MCP Resource Server",
#   "bearer_methods_supported": [
#     "header"
#   ]
# }

This is not very useful to a human, but it helps other programs find the authentication entry points for your MCP Server. Every AI-based app has their own unique way of adding an MCP server, but a good tool to debug your server is the MCP inspector. You can run it easily with:

npx @modelcontextprotocol/[email protected]

In the UI, you must set the URL of your server, and then click "Open Auth Settings":

MCP inspector screenshot

In the auth settings, select the "Quick OAuth Flow".

MCP inspector screenshot: Authentication Settings page

This will redirect you to the authorization server. Once you log in, you will be redirected back to the MCP inspector, which will display a success message and the first few characters of an access token. From there, you should be able to connect and ultimately call our "greeter" tool:

MCP inspector screenshot: Call tool page

In the screenshot above, in order, you can do:

  1. Select the Tools tab
  2. Click List tools
  3. Select the greeter tool
  4. Fill the arguments and call the tool

And with this, you have your very first spec-compliant, OAuth2-secured MCP Server. There are variants around this implementation, for example a use-case where everything on the MCP server is publicly accessible (e.g. "list tools"), except calling the tools themselves. It is not spec compliant but matches certain specific needs. You can learn more in the dedicated section in the mcp-security docs.

Of course, for users to log in, you must connect your MCP Server to an authorization server, that complies with the required specifications for MCP, such as dynamic client registration. While there are many SaaS options available, you can also write you own with Spring Authorization Server.

MCP-compatible Spring Authorization Server

To create an MCP-compatible authorization server with Spring, create a new Spring project, with Spring Authorization Server, and add the MCP-specific:

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")

You can configure the Authorization Server in the usual way ( see reference documentation). Here is an example application.yml for registering a default client and a default user:

spring:
  application:
    name: sample-authorization-server
  security:
    oauth2:
      authorizationserver:
        client:
          default-client:
            token:
              access-token-time-to-live: 1h
            registration:
              client-id: "default-client-id"
              client-secret: "{noop}default-client-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"
    user:
      # A single user, named "user"
      name: user
      password: password

server:
  port: 9000
  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 API, 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(McpAuthorizationServerConfigurer.mcpAuthorizationServer(), withDefaults())
            // enable form-based login, for user "user"/"password"
            .formLogin(withDefaults())
            .build();
}

With this, your Spring Authorization Server will support OAuth 2 Dynamic Client Registration as well as Resource Indicators for OAuth 2. Connecting your MCP Server to this authorization server is compatible with the majority of AI tools, such as Claude Desktop, Cursor, or the MCP inspector.

Beyond OAuth 2: API keys

While the MCP specification mandates using OAuth2 for security, many environments do not have the infrastructure to support this use-case. To be usable in environments lacking OAuth 2, many clients, including the MCP inspector itself, allow you to pass custom headers when making requests. This opens the door to alternative authentication flows, including API key-based security. The MCP Security project supports API keys, which we'll showcase below.

First, add the dependencies to your project:


<dependencies>

    <!-- Spring AI MCP starter -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
    <!-- MCP Security -->
    <dependency>
        <groupId>org.springaicommunity</groupId>
        <artifactId>mcp-server-security</artifactId>
        <version>0.0.2</version>
    </dependency>
    <!-- MCP Security dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

</dependencies>

Gradle:

implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc")
implementation("org.springaicommunity:mcp-server-security:0.0.2")
implementation("org.springframework.boot:spring-boot-starter-security")

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

"Entites" authenticated by an API key, such as users or service accounts, are represented by ApiKeyEntity. The MCP server checks a specific header for an API key, loads the entity, and validates the secret. You can bring your own entity implementation, and your own entity repository, for specific security validations.

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(
                        McpApiKeyConfigurer.mcpServerApiKey(),
                        (apiKey) -> apiKey.apiKeyRepository(apiKeyRepository())
                )
                .build();
    }

    private ApiKeyEntityRepository<ApiKeyEntityImpl> apiKeyRepository() {
        var apiKey = ApiKeyEntityImpl.builder()
                .name("test api key")
                .id("api01")
                .secret("mycustomapikey")
                .build();

        return new InMemoryApiKeyEntityRepository<>(List.of(apiKey));
    }

}

Here we use an API Key repository that stores simple keys. Then you should be able to call your MCP server with a header X-API-key: api01.mycustomapikey. X-API-key is the default header name for passing API keys, followed by the header value {id}.{secret}. The secret is stored in bcrypt-hashed form on the server side. The mcpServerApiKey() configurer offers options for changing the header name, and even dedicated APIs to extract the API key from incoming HTTP requests.

Improving MCP security

If you would like to learn more, head over to the spring-ai-community/mcp-security project, for documentation and samples. You will also find support for client-side MCP security with Spring AI and Spring Security. Try it out with your own projects and applications, test it with the rest of the ecosystem, and help us improve it! We are open to contributions, including feedback and issues.

In another blog post, we'll cover how to implement OAuth 2 on the client side, with the org.springaiframework:mcp-client-security module.

Get the Spring newsletter

Stay connected with the Spring newsletter

Subscribe

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

Tanzu Spring offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all