diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/pom.xml b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/pom.xml
new file mode 100644
index 000000000000..54e599d905fa
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/pom.xml
@@ -0,0 +1,67 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.4
+
+
+ com.baeldung.mcp
+ mcp-client-oauth2
+ 0.0.1-SNAPSHOT
+ mcp-client-oauth2
+ mcp-client-oauth2
+
+
+ 17
+ 1.0.0
+ 3.5.4
+
+
+
+
+ org.springframework.ai
+ spring-ai-starter-model-anthropic
+
+
+ org.springframework.ai
+ spring-ai-starter-mcp-client-webflux
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ ${spring-boot.starter.test}
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-bom
+ ${spring-ai.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/java/com/baeldung/mcp/mcpclientoauth2/CalculatorController.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/java/com/baeldung/mcp/mcpclientoauth2/CalculatorController.java
new file mode 100644
index 000000000000..7bddf8ea6f42
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/java/com/baeldung/mcp/mcpclientoauth2/CalculatorController.java
@@ -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 """
+
+
+ MCP Calculator with OAuth2
+ Try these examples:
+
+ Note: You'll be redirected to login if not authenticated.
+
+
+ """;
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/java/com/baeldung/mcp/mcpclientoauth2/McpClientOauth2Application.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/java/com/baeldung/mcp/mcpclientoauth2/McpClientOauth2Application.java
new file mode 100644
index 000000000000..43125eaf645e
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/java/com/baeldung/mcp/mcpclientoauth2/McpClientOauth2Application.java
@@ -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 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();
+ }
+
+}
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/java/com/baeldung/mcp/mcpclientoauth2/McpSyncClientExchangeFilterFunction.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/java/com/baeldung/mcp/mcpclientoauth2/McpSyncClientExchangeFilterFunction.java
new file mode 100644
index 000000000000..111752e5ec39
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/java/com/baeldung/mcp/mcpclientoauth2/McpSyncClientExchangeFilterFunction.java
@@ -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 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 configuration() {
+ return builder -> builder.defaultRequest(this.delegate.defaultRequest())
+ .filter(this);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/resources/application.properties b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/resources/application.properties
new file mode 100644
index 000000000000..f7de042acd55
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/main/resources/application.properties
@@ -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
+
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/test/java/com/baeldung/mcp/mcpclientoauth2/CalculatorControllerUnitTest.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/test/java/com/baeldung/mcp/mcpclientoauth2/CalculatorControllerUnitTest.java
new file mode 100644
index 000000000000..03316dccf55c
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-client-oauth2/src/test/java/com/baeldung/mcp/mcpclientoauth2/CalculatorControllerUnitTest.java
@@ -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")));
+ }
+}
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/pom.xml b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/pom.xml
new file mode 100644
index 000000000000..c79adea28aff
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.4
+
+
+
+ com.baeldung.mcp
+ mcp-server-oauth2
+ 1.0.0
+ mcp-server-oauth2
+
+
+ 1.7.0
+ 1.0.0-M7
+ 17
+ 1.0.0
+ 5.10.2
+
+
+
+
+ com.fasterxml
+ classmate
+ ${classmate.version}
+
+
+ org.springframework.ai
+ spring-ai-starter-mcp-server-webmvc
+ ${spring-ai.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit-version}
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/java/com/baeldung/mcp/mcpserveroauth2/CalculatorService.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/java/com/baeldung/mcp/mcpserveroauth2/CalculatorService.java
new file mode 100644
index 000000000000..2a14b1a1869a
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/java/com/baeldung/mcp/mcpserveroauth2/CalculatorService.java
@@ -0,0 +1,43 @@
+package com.baeldung.mcp.mcpserveroauth2;
+
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.stereotype.Service;
+
+import com.baeldung.mcp.mcpserveroauth2.model.CalculationResult;
+
+@Service
+public class CalculatorService {
+
+ @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 = "Subtract two numbers")
+ public CalculationResult subtract(@ToolParam(description = "First number") double a, @ToolParam(description = "Second number") double b) {
+
+ double result = a - b;
+ return new CalculationResult("subtraction", 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);
+ }
+
+ @Tool(description = "Divide two numbers")
+ public CalculationResult divide(@ToolParam(description = "First number") double a, @ToolParam(description = "Second number") double b) {
+
+ if (b == 0) {
+ throw new IllegalArgumentException("Cannot divide by zero");
+ }
+
+ double result = a / b;
+ return new CalculationResult("division", a, b, result);
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/java/com/baeldung/mcp/mcpserveroauth2/McpServerOauth2Application.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/java/com/baeldung/mcp/mcpserveroauth2/McpServerOauth2Application.java
new file mode 100644
index 000000000000..a1bf401127f3
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/java/com/baeldung/mcp/mcpserveroauth2/McpServerOauth2Application.java
@@ -0,0 +1,22 @@
+package com.baeldung.mcp.mcpserveroauth2;
+
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.ai.tool.method.MethodToolCallbackProvider;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+public class McpServerOauth2Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(McpServerOauth2Application.class, args);
+ }
+
+ @Bean
+ public ToolCallbackProvider calculatorTools(CalculatorService calculatorService) {
+ return MethodToolCallbackProvider.builder()
+ .toolObjects(calculatorService)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/java/com/baeldung/mcp/mcpserveroauth2/model/CalculationResult.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/java/com/baeldung/mcp/mcpserveroauth2/model/CalculationResult.java
new file mode 100644
index 000000000000..2781d27e5b9e
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/java/com/baeldung/mcp/mcpserveroauth2/model/CalculationResult.java
@@ -0,0 +1,5 @@
+package com.baeldung.mcp.mcpserveroauth2.model;
+
+public record CalculationResult(String operation, double operand1, double operand2, double result) {
+
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/resources/application.properties b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/resources/application.properties
new file mode 100644
index 000000000000..a57b9576e8c9
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/main/resources/application.properties
@@ -0,0 +1,10 @@
+spring.application.name=mcp-server-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
+logging.level.org.springframework.security=DEBUG
+logging.level.org.springframework.ai.mcp=INFO
+logging.level.root=INFO
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/test/java/com/baeldung/mcp/mcpserveroauth2/CalculatorServiceUnitTest.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/test/java/com/baeldung/mcp/mcpserveroauth2/CalculatorServiceUnitTest.java
new file mode 100644
index 000000000000..0e73a06633d1
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/mcp-server-oauth2/src/test/java/com/baeldung/mcp/mcpserveroauth2/CalculatorServiceUnitTest.java
@@ -0,0 +1,66 @@
+package com.baeldung.mcp.mcpserveroauth2;
+
+import com.baeldung.mcp.mcpserveroauth2.model.CalculationResult;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class CalculatorServiceUnitTest {
+ private final CalculatorService calculatorService = new CalculatorService();
+
+ @Test
+ void givenTwoNumbers_whenAdd_thenReturnsSum() {
+ CalculationResult result = calculatorService.add(5.0, 3.0);
+ assertEquals("addition", result.operation());
+ assertEquals(5.0, result.operand1());
+ assertEquals(3.0, result.operand2());
+ assertEquals(8.0, result.result());
+ }
+
+ @Test
+ void givenTwoNumbers_whenSubtract_thenReturnsDifference() {
+ CalculationResult result = calculatorService.subtract(10.0, 4.0);
+ assertEquals("subtraction", result.operation());
+ assertEquals(10.0, result.operand1());
+ assertEquals(4.0, result.operand2());
+ assertEquals(6.0, result.result());
+ }
+
+ @Test
+ void givenTwoNumbers_whenMultiply_thenReturnsProduct() {
+ CalculationResult result = calculatorService.multiply(6.0, 7.0);
+ assertEquals("multiplication", result.operation());
+ assertEquals(6.0, result.operand1());
+ assertEquals(7.0, result.operand2());
+ assertEquals(42.0, result.result());
+ }
+
+ @Test
+ void givenTwoNumbers_whenDivide_thenReturnsQuotient() {
+ CalculationResult result = calculatorService.divide(15.0, 3.0);
+ assertEquals("division", result.operation());
+ assertEquals(15.0, result.operand1());
+ assertEquals(3.0, result.operand2());
+ assertEquals(5.0, result.result());
+ }
+
+ @Test
+ void givenZeroDivisor_whenDivide_thenThrowsException() {
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () ->
+ calculatorService.divide(10.0, 0.0)
+ );
+ assertEquals("Cannot divide by zero", ex.getMessage());
+ }
+
+ @Test
+ void givenNegativeNumbers_whenAdd_thenReturnsSum() {
+ CalculationResult result = calculatorService.add(-2.0, -3.0);
+ assertEquals(-5.0, result.result());
+ }
+
+ @Test
+ void givenLargeNumbers_whenMultiply_thenReturnsProduct() {
+ CalculationResult result = calculatorService.multiply(1e6, 1e6);
+ assertEquals(1e12, result.result());
+ }
+}
+
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/pom.xml b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/pom.xml
new file mode 100644
index 000000000000..a0a2e85e19c2
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/pom.xml
@@ -0,0 +1,70 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.4
+
+
+ com.baeldung.mcp
+ oauth2-authorization-server
+ 0.0.1-SNAPSHOT
+ oauth2-authorization-server
+ oauth2-authorization-server
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 17
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-authorization-server
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/main/java/com/baeldung/mcp/oauth2authorizationserver/Oauth2AuthorizationServerApplication.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/main/java/com/baeldung/mcp/oauth2authorizationserver/Oauth2AuthorizationServerApplication.java
new file mode 100644
index 000000000000..58393e97b6af
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/main/java/com/baeldung/mcp/oauth2authorizationserver/Oauth2AuthorizationServerApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.mcp.oauth2authorizationserver;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Oauth2AuthorizationServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Oauth2AuthorizationServerApplication.class, args);
+ }
+
+}
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/main/java/com/baeldung/mcp/oauth2authorizationserver/config/AuthorizationServerConfig.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/main/java/com/baeldung/mcp/oauth2authorizationserver/config/AuthorizationServerConfig.java
new file mode 100644
index 000000000000..b8b5419698d9
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/main/java/com/baeldung/mcp/oauth2authorizationserver/config/AuthorizationServerConfig.java
@@ -0,0 +1,97 @@
+package com.baeldung.mcp.oauth2authorizationserver.config;
+
+import java.time.Duration;
+import java.util.UUID;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.MediaType;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
+import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
+
+@Configuration
+@EnableWebSecurity
+public class AuthorizationServerConfig {
+
+ @Bean
+ @Order(1)
+ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+ OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
+
+ http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
+ .oidc(Customizer.withDefaults());
+
+ http.exceptionHandling(exceptions -> exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"),
+ new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
+ .oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults()));
+
+ return http.build();
+ }
+
+ @Bean
+ @Order(2)
+ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
+ http.authorizeHttpRequests(authorize -> authorize.anyRequest()
+ .authenticated())
+ .formLogin(Customizer.withDefaults());
+
+ return http.build();
+ }
+
+ @Bean
+ public RegisteredClientRepository registeredClientRepository() {
+ RegisteredClient mcpClient = RegisteredClient.withId(UUID.randomUUID()
+ .toString())
+ .clientId("mcp-client")
+ .clientSecret("{noop}mcp-secret")
+ .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
+ .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
+ .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+ .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
+ .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+ .redirectUri("http://localhost:8080/authorize/oauth2/code/authserver")
+ .redirectUri("http://127.0.0.1:8080/authorize/oauth2/code/authserver")
+ .postLogoutRedirectUri("http://localhost:8080/")
+ // Standard OAuth2/OIDC scopes
+ .scope(OidcScopes.OPENID)
+ .scope(OidcScopes.PROFILE)
+ // Custom MCP scopes
+ .scope("mcp.read")
+ .scope("mcp.write")
+ .clientSettings(ClientSettings.builder()
+ .requireAuthorizationConsent(false)
+ .requireProofKey(false)
+ .build())
+ .tokenSettings(TokenSettings.builder()
+ .accessTokenTimeToLive(Duration.ofHours(1))
+ .refreshTokenTimeToLive(Duration.ofDays(1))
+ .reuseRefreshTokens(false)
+ .build())
+ .build();
+
+ return new InMemoryRegisteredClientRepository(mcpClient);
+ }
+
+ @Bean
+ public AuthorizationServerSettings authorizationServerSettings() {
+ return AuthorizationServerSettings.builder()
+ .issuer("http://localhost:9000")
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/main/resources/application.yml b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/main/resources/application.yml
new file mode 100644
index 000000000000..0ce17329286f
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/main/resources/application.yml
@@ -0,0 +1,28 @@
+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"
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/test/java/com/baeldung/mcp/oauth2authorizationserver/config/AuthorizationServerConfigUnitTest.java b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/test/java/com/baeldung/mcp/oauth2authorizationserver/config/AuthorizationServerConfigUnitTest.java
new file mode 100644
index 000000000000..cd2d7fe54141
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/oauth2-authorization-server/src/test/java/com/baeldung/mcp/oauth2authorizationserver/config/AuthorizationServerConfigUnitTest.java
@@ -0,0 +1,63 @@
+package com.baeldung.mcp.oauth2authorizationserver.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.web.SecurityFilterChain;
+
+@SpringBootTest(classes = AuthorizationServerConfig.class)
+class AuthorizationServerConfigUnitTest {
+
+ private final ApplicationContext context;
+
+ private final RegisteredClientRepository registeredClientRepository;
+
+ private final AuthorizationServerSettings authorizationServerSettings;
+
+ public AuthorizationServerConfigUnitTest(ApplicationContext context, RegisteredClientRepository registeredClientRepository,
+ AuthorizationServerSettings authorizationServerSettings) {
+ this.authorizationServerSettings = authorizationServerSettings;
+ this.registeredClientRepository = registeredClientRepository;
+ this.context = context;
+ }
+
+ @Test
+ void givenContext_whenLoaded_thenSecurityFilterChainsPresent() {
+ SecurityFilterChain chain1 = (SecurityFilterChain) context.getBean("authorizationServerSecurityFilterChain");
+ SecurityFilterChain chain2 = (SecurityFilterChain) context.getBean("defaultSecurityFilterChain");
+ assertNotNull(chain1);
+ assertNotNull(chain2);
+ }
+
+ @Test
+ void givenRegisteredClientRepository_whenQueried_thenContainsExpectedClient() {
+ RegisteredClient client = registeredClientRepository.findByClientId("mcp-client");
+ assertNotNull(client);
+ assertEquals("mcp-client", client.getClientId());
+ assertTrue(client.getClientAuthenticationMethods()
+ .stream()
+ .anyMatch(m -> m.getValue()
+ .equals("client_secret_basic")));
+ assertTrue(client.getAuthorizationGrantTypes()
+ .stream()
+ .anyMatch(g -> g.getValue()
+ .equals("authorization_code")));
+ assertTrue(client.getScopes()
+ .contains("mcp.read"));
+ assertTrue(client.getScopes()
+ .contains("mcp.write"));
+ }
+
+ @Test
+ void givenAuthorizationServerSettings_whenLoaded_thenIssuerIsCorrect() {
+ assertEquals("http://localhost:9000", authorizationServerSettings.getIssuer());
+ }
+}
+
diff --git a/spring-ai-modules/spring-ai-mcp/mcp-spring/pom.xml b/spring-ai-modules/spring-ai-mcp/mcp-spring/pom.xml
new file mode 100644
index 000000000000..f0197fe1d1a8
--- /dev/null
+++ b/spring-ai-modules/spring-ai-mcp/mcp-spring/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+ mcp-spring
+ pom
+ mcp-spring
+
+
+ com.baeldung
+ spring-ai-mcp
+ 0.0.1
+ ../pom.xml
+
+
+
+ mcp-client-oauth2
+ mcp-server-oauth2
+ oauth2-authorization-server
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/spring-ai-modules/spring-ai-mcp/pom.xml b/spring-ai-modules/spring-ai-mcp/pom.xml
index 1d124c0256d1..ef18c1fbb5ef 100644
--- a/spring-ai-modules/spring-ai-mcp/pom.xml
+++ b/spring-ai-modules/spring-ai-mcp/pom.xml
@@ -13,6 +13,7 @@
com.baeldung
spring-ai-mcp
0.0.1
+ pom
spring-ai-mcp