Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/>
</parent>
<groupId>com.baeldung.mcp</groupId>
<artifactId>mcp-client-oauth2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mcp-client-oauth2</name>
<description>mcp-client-oauth2</description>

<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
<spring-boot.starter.test>3.5.4</spring-boot.starter.test>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
</dependency>
<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-test</artifactId>
<version>${spring-boot.starter.test}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.baeldung.mcp.mcpclientoauth2;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CalculatorController {

private final ChatClient chatClient;

public CalculatorController(ChatClient chatClient) {
this.chatClient = chatClient;
}

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

@GetMapping("/")
public String home() {
return """
<html>
<body>
<h1>MCP Calculator with OAuth2</h1>
<p>Try these examples:</p>
<ul>
<li><a href="/calculate?expression=5 + 3">/calculate?expression=5 + 3</a></li>
<li><a href="/calculate?expression=10 - 4">/calculate?expression=10 - 4</a></li>
<li><a href="/calculate?expression=6 * 7">/calculate?expression=6 * 7</a></li>
<li><a href="/calculate?expression=15 / 3">/calculate?expression=15 / 3</a></li>
</ul>
<p>Note: You'll be redirected to login if not authenticated.</p>
</body>
</html>
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.baeldung.mcp.mcpclientoauth2;

import java.util.List;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.reactive.function.client.WebClient;

import io.modelcontextprotocol.client.McpSyncClient;

@SpringBootApplication
public class McpClientOauth2Application {

public static void main(String[] args) {
SpringApplication.run(McpClientOauth2Application.class, args);
}

@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpClients) {
return chatClientBuilder.defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpClients))
.build();
}

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.baeldung.mcp.mcpclientoauth2;

import java.util.function.Consumer;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.WebClient;

import reactor.core.publisher.Mono;

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
spring.application.name=mcp-client-oauth2

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

# OAuth2 Client for User-Initiated Requests (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}

# OAuth2 Client for Machine-to-Machine Requests (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}

# Logging Configuration
logging.level.com.baeldung.mcp=DEBUG
logging.level.org.springframework.security.oauth2=INFO
logging.level.org.springframework.ai.mcp=DEBUG
logging.level.org.springframework.web.reactive.function.client=INFO
logging.level.io.modelcontextprotocol=INFO

# Spring Boot Configuration
spring.main.lazy-initialization=false
spring.task.execution.pool.core-size=4
spring.task.execution.pool.max-size=8

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.baeldung.mcp.mcpclientoauth2;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

public class CalculatorControllerUnitTest {

private MockMvc mockMvc;
private ChatClient chatClient;

@BeforeEach
void setUp() {
chatClient = mock(ChatClient.class, Mockito.RETURNS_DEEP_STUBS);
CalculatorController controller = new CalculatorController(chatClient);
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.build();
}

@Test
void givenValidExpression_whenCalculateEndpointCalled_thenReturnsExpectedResult() throws Exception {
when(chatClient.prompt()
.user(anyString())
.call()
.content()).thenReturn("42");
mockMvc.perform(MockMvcRequestBuilders.get("/calculate")
.param("expression", "40 + 2"))
.andExpect(MockMvcResultMatchers.status()
.isOk())
.andExpect(MockMvcResultMatchers.content()
.string("42"));
}

@Test
void givenHomeRequest_whenHomeEndpointCalled_thenReturnsHtmlWithTitle() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status()
.isOk())
.andExpect(MockMvcResultMatchers.content()
.string(org.hamcrest.Matchers.containsString("MCP Calculator with OAuth2")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/>
</parent>

<groupId>com.baeldung.mcp</groupId>
<artifactId>mcp-server-oauth2</artifactId>
<version>1.0.0</version>
<name>mcp-server-oauth2</name>

<properties>
<classmate.version>1.7.0</classmate.version>
<spring-ai.version>1.0.0-M7</spring-ai.version>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
<junit-version>5.10.2</junit-version>
</properties>

<dependencies>
<dependency>
<groupId>com.fasterxml</groupId>
<artifactId>classmate</artifactId>
<version>${classmate.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<version>${spring-ai.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit-version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Loading