├── .circleci └── config.yml ├── .gitignore ├── README.md ├── account-mcp-service ├── pom.xml └── src │ └── test │ └── java │ └── pl │ └── piomin │ └── services │ └── tools │ └── AccountToolsIntegrationTest.java ├── git └── spring-ai-mcp │ ├── account-mcp-service │ └── src │ │ └── test │ │ └── java │ │ └── pl │ │ └── piomin │ │ └── services │ │ └── accountmcp │ │ └── AccountMcpServiceApplicationTests.java │ └── person-mcp-service │ └── src │ └── test │ └── java │ └── pl │ └── piomin │ └── services │ └── PersonMCPServerApplicationTests.java ├── person-mcp-service └── src │ └── test │ └── java │ └── pl │ └── piomin │ └── services │ ├── PersonMCPServerTests.java │ ├── personmcp │ ├── PersonRepositoryIntegrationTest.java │ └── ToolCallbackProviderIntegrationTest.java │ └── tools │ └── PersonToolsIntegrationTest.java ├── pom.xml ├── renovate.json └── spring-ai-mcp ├── account-mcp-service ├── pom.xml └── src │ └── main │ ├── java │ └── pl │ │ └── piomin │ │ └── services │ │ ├── AccountMCPService.java │ │ ├── model │ │ └── Account.java │ │ ├── repository │ │ └── AccountRepository.java │ │ └── tools │ │ └── AccountTools.java │ └── resources │ ├── application.yml │ └── import.sql ├── person-mcp-service ├── pom.xml └── src │ └── main │ ├── java │ └── pl │ │ └── piomin │ │ └── services │ │ ├── PersonMCPServer.java │ │ ├── model │ │ ├── Gender.java │ │ └── Person.java │ │ ├── repository │ │ └── PersonRepository.java │ │ └── tools │ │ └── PersonTools.java │ └── resources │ ├── application.yml │ └── import.sql ├── pom.xml └── sample-client ├── pom.xml └── src └── main ├── java └── pl │ └── piomin │ └── services │ ├── SampleClient.java │ └── controller │ ├── AccountController.java │ └── PersonController.java └── resources └── application.yml /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: 'cimg/openjdk:21.0.6' 7 | steps: 8 | - checkout 9 | - run: 10 | name: Analyze on SonarCloud 11 | command: mvn verify sonar:sonar -DskipTests 12 | 13 | executors: 14 | jdk: 15 | docker: 16 | - image: 'cimg/openjdk:21.0.6' 17 | 18 | orbs: 19 | maven: circleci/maven@2.0.0 20 | 21 | workflows: 22 | maven_test: 23 | jobs: 24 | - maven/test: 25 | executor: jdk 26 | context: OpenAI 27 | - build: 28 | context: SonarCloud -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/modules.xml 8 | .idea/jarRepositories.xml 9 | .idea/compiler.xml 10 | .idea/libraries/ 11 | *.iws 12 | *.iml 13 | *.ipr 14 | 15 | ### Eclipse ### 16 | .apt_generated 17 | .classpath 18 | .factorypath 19 | .project 20 | .settings 21 | .springBeans 22 | .sts4-cache 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | !**/src/main/**/build/ 32 | !**/src/test/**/build/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | 37 | ### Mac OS ### 38 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring AI Apps Project [![Twitter](https://img.shields.io/twitter/follow/piotr_minkowski.svg?style=social&logo=twitter&label=Follow%20Me)](https://twitter.com/piotr_minkowski) 2 | 3 | [![CircleCI](https://circleci.com/gh/piomin/spring-ai-showcase.svg?style=svg)](https://circleci.com/gh/piomin/spring-ai-showcase) 4 | 5 | [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-black.svg)](https://sonarcloud.io/dashboard?id=piomin_spring-ai-showcase) 6 | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=piomin_spring-ai-showcase&metric=bugs)](https://sonarcloud.io/dashboard?id=piomin_spring-ai-showcase) 7 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=piomin_spring-ai-showcase&metric=coverage)](https://sonarcloud.io/dashboard?id=piomin_spring-ai-showcase) 8 | [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=piomin_spring-ai-showcase&metric=ncloc)](https://sonarcloud.io/dashboard?id=piomin_spring-ai-showcase) 9 | 10 | This project demonstrates the integration of AI capabilities within a Spring Boot application, utilizing the [Spring AI](https://github.com/spring-projects/spring-ai) framework. 11 | 12 | ----- 13 | 14 | ## Table of Contents 15 | 16 | - [Architecture](#architecture) 17 | - [Running the Application](#running-the-application) 18 | - [Articles](#articles) 19 | 20 | ## Running the application 21 | 22 | You can always run all the apps locally with Spring Maven Plugin support: 23 | ```shell 24 | mvn spring-boot:run 25 | ``` 26 | 27 | Sometimes, you would have to connect the app with the AI model. It can be OpenAI or your preferred model. Before running the app you should export the API token as shown below. 28 | ```shell 29 | export SPRING_AI_OPENAI_API_KEY= 30 | ``` 31 | 32 | ## Architecture 33 | 34 | ### MCP Client/Server 35 | 36 | The example is available in the `spring-ai-mcp` directory. Here's the diagram that visualizes architecture. 37 | 38 |
39 | 40 | # Articles 41 | 1. Getting started with the Spring AI **MCP** concept. Implement a client-side and a server-side application that exposes `@Tools` and `Prompts` to other services. The example is available in the branch [master](https://github.com/piomin/spring-ai-showcase/tree/master). A detailed guide may be found in the following article: [Using Model Context Protocol (MCP) with Spring AI](https://piotrminkowski.com/2025/03/17/using-model-context-protocol-mcp-with-spring-ai/) 42 | -------------------------------------------------------------------------------- /account-mcp-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.springframework.boot 4 | spring-boot-starter-test 5 | test 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-testcontainers 10 | test 11 | 12 | 13 | org.testcontainers 14 | junit-jupiter 15 | test 16 | 17 | 18 | com.h2database 19 | h2 20 | test 21 | 22 | 23 | org.assertj 24 | assertj-core 25 | test 26 | -------------------------------------------------------------------------------- /account-mcp-service/src/test/java/pl/piomin/services/tools/AccountToolsIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.tools; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.transaction.annotation.Transactional; 9 | import pl.piomin.services.model.Account; 10 | import pl.piomin.services.repository.AccountRepository; 11 | 12 | import java.util.List; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @SpringBootTest 17 | @ActiveProfiles("test") 18 | @Transactional 19 | class AccountToolsIntegrationTest { 20 | 21 | @Autowired 22 | private AccountTools accountTools; 23 | 24 | @Autowired 25 | private AccountRepository accountRepository; 26 | 27 | @BeforeEach 28 | void setUp() { 29 | accountRepository.deleteAll(); 30 | } 31 | 32 | @Test 33 | void shouldFindAccountsByPersonId() { 34 | // Given 35 | Account account1 = new Account(); 36 | account1.setPersonId(1L); 37 | account1.setNumber("ACC001"); 38 | account1.setBalance(1000); 39 | 40 | Account account2 = new Account(); 41 | account2.setPersonId(1L); 42 | account2.setNumber("ACC002"); 43 | account2.setBalance(2000); 44 | 45 | Account account3 = new Account(); 46 | account3.setPersonId(2L); 47 | account3.setNumber("ACC003"); 48 | account3.setBalance(1500); 49 | 50 | accountRepository.saveAll(List.of(account1, account2, account3)); 51 | 52 | // When 53 | List accountsForPerson1 = accountTools.getAccountsByPersonId(1L); 54 | List accountsForPerson2 = accountTools.getAccountsByPersonId(2L); 55 | 56 | // Then 57 | assertThat(accountsForPerson1).hasSize(2); 58 | assertThat(accountsForPerson1) 59 | .extracting(Account::getNumber) 60 | .containsExactlyInAnyOrder("ACC001", "ACC002"); 61 | assertThat(accountsForPerson2).hasSize(1); 62 | assertThat(accountsForPerson2.get(0).getNumber()).isEqualTo("ACC003"); 63 | } 64 | 65 | @Test 66 | void shouldReturnEmptyListWhenNoAccounts() { 67 | List accounts = accountTools.getAccountsByPersonId(999L); 68 | assertThat(accounts).isEmpty(); 69 | } 70 | 71 | @Test 72 | void shouldHandleNullPersonIdGracefully() { 73 | List accounts = accountTools.getAccountsByPersonId(null); 74 | assertThat(accounts).isNotNull(); 75 | } 76 | } -------------------------------------------------------------------------------- /git/spring-ai-mcp/account-mcp-service/src/test/java/pl/piomin/services/accountmcp/AccountMcpServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.accountmcp; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.test.context.ActiveProfiles; 6 | 7 | @SpringBootTest 8 | @ActiveProfiles("test") 9 | class AccountMcpServiceApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | // Verifies that the Spring context (including MCP tools) loads without errors 14 | } 15 | } 16 | 17 | 18 | 19 | Created AccountToolsIntegrationTest.java to integrate-test the AccountTools bean against the AccountRepository, covering normal, empty and null input scenarios using AssertJ. 20 | 21 | 22 | package pl.piomin.services.tools; 23 | 24 | import org.junit.jupiter.api.BeforeEach; 25 | import org.junit.jupiter.api.Test; 26 | import org.springframework.beans.factory.annotation.Autowired; 27 | import org.springframework.boot.test.context.SpringBootTest; 28 | import org.springframework.test.context.ActiveProfiles; 29 | import org.springframework.transaction.annotation.Transactional; 30 | import pl.piomin.services.model.Account; 31 | import pl.piomin.services.repository.AccountRepository; 32 | import pl.piomin.services.tools.AccountTools; 33 | 34 | import java.util.List; 35 | 36 | import static org.assertj.core.api.Assertions.assertThat; 37 | 38 | @SpringBootTest 39 | @ActiveProfiles("test") 40 | @Transactional 41 | class AccountToolsIntegrationTest { 42 | 43 | @Autowired 44 | private AccountTools accountTools; 45 | 46 | @Autowired 47 | private AccountRepository accountRepository; 48 | 49 | @BeforeEach 50 | void setUp() { 51 | accountRepository.deleteAll(); 52 | } 53 | 54 | @Test 55 | void shouldReturnAccountsForPerson() { 56 | Account a1 = new Account(); 57 | a1.setId("1"); 58 | a1.setPersonId("p1"); 59 | a1.setBalance(100); 60 | accountRepository.save(a1); 61 | Account a2 = new Account(); 62 | a2.setId("2"); 63 | a2.setPersonId("p1"); 64 | a2.setBalance(200); 65 | accountRepository.save(a2); 66 | 67 | List results = accountTools.getAccountsByPersonId("p1"); 68 | assertThat(results) 69 | .hasSize(2) 70 | .extracting(Account::getId) 71 | .containsExactlyInAnyOrder("1", "2"); 72 | } 73 | 74 | @Test 75 | void shouldReturnEmptyWhenNone() { 76 | List results = accountTools.getAccountsByPersonId("ghost"); 77 | assertThat(results).isEmpty(); 78 | } 79 | 80 | @Test 81 | void shouldHandleNullInput() { 82 | List results = accountTools.getAccountsByPersonId(null); 83 | assertThat(results).isNotNull().isEmpty(); 84 | } 85 | } 86 | 87 | 88 | 89 | Created AccountRepositoryIntegrationTest.java to verify that AccountRepository correctly saves and retrieves Account entities by ID and personId, including handling empty results and negative balances. 90 | 91 | 92 | package pl.piomin.services.repository; 93 | 94 | import org.junit.jupiter.api.BeforeEach; 95 | import org.junit.jupiter.api.Test; 96 | import org.springframework.beans.factory.annotation.Autowired; 97 | import org.springframework.boot.test.context.SpringBootTest; 98 | import org.springframework.test.context.ActiveProfiles; 99 | import org.springframework.transaction.annotation.Transactional; 100 | import pl.piomin.services.model.Account; 101 | 102 | import java.util.List; 103 | import java.util.Optional; 104 | 105 | import static org.assertj.core.api.Assertions.assertThat; 106 | 107 | @SpringBootTest 108 | @ActiveProfiles("test") 109 | @Transactional 110 | class AccountRepositoryIntegrationTest { 111 | 112 | @Autowired 113 | private AccountRepository accountRepository; 114 | 115 | @BeforeEach 116 | void setUp() { 117 | accountRepository.deleteAll(); 118 | } 119 | 120 | @Test 121 | void shouldSaveAndFindById() { 122 | Account account = new Account(); 123 | account.setId("10"); 124 | account.setPersonId("p10"); 125 | account.setBalance(50); 126 | accountRepository.save(account); 127 | 128 | Optional found = accountRepository.findById("10"); 129 | assertThat(found).isPresent() 130 | .get() 131 | .isEqualTo(account); 132 | } 133 | 134 | @Test 135 | void shouldFindByPersonId() { 136 | Account a1 = new Account(); 137 | a1.setId("11"); 138 | a1.setPersonId("p11"); 139 | a1.setBalance(150); 140 | accountRepository.save(a1); 141 | Account a2 = new Account(); 142 | a2.setId("12"); 143 | a2.setPersonId("p11"); 144 | a2.setBalance(250); 145 | accountRepository.save(a2); 146 | 147 | List results = accountRepository.findByPersonId("p11"); 148 | assertThat(results) 149 | .hasSize(2) 150 | .extracting(Account::getId) 151 | .containsExactlyInAnyOrder("11", "12"); 152 | } 153 | 154 | @Test 155 | void shouldReturnEmptyForUnknownPerson() { 156 | List results = accountRepository.findByPersonId("unknown"); 157 | assertThat(results).isEmpty(); 158 | } 159 | 160 | @Test 161 | void shouldHandleNegativeBalances() { 162 | Account negative = new Account(); 163 | negative.setId("neg1"); 164 | negative.setPersonId("pn"); 165 | negative.setBalance(-100); 166 | accountRepository.save(negative); 167 | 168 | List results = accountRepository.findByPersonId("pn"); 169 | assertThat(results) 170 | .hasSize(1) 171 | .first() 172 | .matches(a -> a.getBalance() < 0); 173 | } 174 | } 175 | 176 | 177 | 178 | Created ToolCallbackProviderIntegrationTest.java to verify that both the ToolCallbackProvider and AccountTools beans are present in the context and that AccountTools is annotated as a Spring service. 179 | 180 | 181 | package pl.piomin.services.config; 182 | 183 | import org.junit.jupiter.api.Test; 184 | import org.springframework.ai.tool.execution.ToolCallbackProvider; 185 | import org.springframework.beans.factory.annotation.Autowired; 186 | import org.springframework.boot.test.context.SpringBootTest; 187 | import org.springframework.test.context.ActiveProfiles; 188 | import pl.piomin.services.tools.AccountTools; 189 | 190 | import static org.assertj.core.api.Assertions.assertThat; 191 | 192 | @SpringBootTest 193 | @ActiveProfiles("test") 194 | class ToolCallbackProviderIntegrationTest { 195 | 196 | @Autowired 197 | private ToolCallbackProvider toolCallbackProvider; 198 | 199 | @Autowired 200 | private AccountTools accountTools; 201 | 202 | @Test 203 | void beansExist() { 204 | assertThat(toolCallbackProvider).isNotNull(); 205 | assertThat(accountTools).isNotNull(); 206 | } 207 | 208 | @Test 209 | void toolsIsService() { 210 | assertThat(AccountTools.class.isAnnotationPresent(org.springframework.stereotype.Service.class)).isTrue(); 211 | } 212 | } 213 | 214 | 215 | 216 | Added application-test.yml to configure an in-memory H2 database, disable MCP server, and set logging levels for the test profile. 217 | 218 | 219 | spring: 220 | ai: 221 | mcp: 222 | server: 223 | enabled: false 224 | datasource: 225 | url: jdbc:h2:mem:testdb-account 226 | driver-class-name: org.h2.Driver 227 | username: sa 228 | password: password 229 | jpa: 230 | database-platform: org.hibernate.dialect.H2Dialect 231 | hibernate: 232 | ddl-auto: create-drop 233 | show-sql: true 234 | server: 235 | port: 0 236 | logging: 237 | level: 238 | pl.piomin.services: DEBUG 239 | org.springframework.ai: DEBUG 240 | org.hibernate.SQL: DEBUG 241 | 242 | 243 | 244 | Added test dependencies to the POM to support Spring Boot testing, Testcontainers, H2, and AssertJ in the test scope. 245 | 246 | 247 | 248 | 249 | org.springframework.boot 250 | spring-boot-starter-test 251 | test 252 | 253 | 254 | org.springframework.boot 255 | spring-boot-testcontainers 256 | test 257 | 258 | 259 | org.testcontainers 260 | junit-jupiter 261 | test 262 | 263 | 264 | com.h2database 265 | h2 266 | test 267 | 268 | 269 | org.assertj 270 | assertj-core 271 | test 272 | -------------------------------------------------------------------------------- /git/spring-ai-mcp/person-mcp-service/src/test/java/pl/piomin/services/PersonMCPServerApplicationTests.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.test.context.ActiveProfiles; 6 | 7 | @SpringBootTest 8 | @ActiveProfiles("test") 9 | class PersonMCPServerApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | // Verifies that the Spring context (including MCP tools) loads without errors 14 | } 15 | } 16 | 17 | 18 | 19 | Added a full set of integration tests for PersonTools in PersonToolsIntegrationTest.java. We clear the repository before each test, then verify getPersonById, ensure it returns null when not found, test fetching by multiple nationalities, and handle both null and empty‐list inputs. 20 | 21 | 22 | package pl.piomin.services.tools; 23 | 24 | import org.junit.jupiter.api.BeforeEach; 25 | import org.junit.jupiter.api.Test; 26 | import org.springframework.beans.factory.annotation.Autowired; 27 | import org.springframework.boot.test.context.SpringBootTest; 28 | import org.springframework.test.context.ActiveProfiles; 29 | import org.springframework.transaction.annotation.Transactional; 30 | import pl.piomin.services.model.Person; 31 | import pl.piomin.services.model.Gender; 32 | import pl.piomin.services.repository.PersonRepository; 33 | import pl.piomin.services.tools.PersonTools; 34 | 35 | import java.util.List; 36 | 37 | import static org.assertj.core.api.Assertions.assertThat; 38 | 39 | @SpringBootTest 40 | @ActiveProfiles("test") 41 | @Transactional 42 | class PersonToolsIntegrationTest { 43 | 44 | @Autowired 45 | private PersonTools personTools; 46 | 47 | @Autowired 48 | private PersonRepository personRepository; 49 | 50 | @BeforeEach 51 | void setUp() { 52 | personRepository.deleteAll(); 53 | } 54 | 55 | @Test 56 | void shouldFindPersonById() { 57 | Person person = new Person(); 58 | person.setName("Alice"); 59 | person.setNationality("Polish"); 60 | person.setAge(30); 61 | person.setGender(Gender.FEMALE); 62 | personRepository.save(person); 63 | 64 | Person found = personTools.getPersonById(person.getId()); 65 | assertThat(found).isNotNull(); 66 | assertThat(found.getId()).isEqualTo(person.getId()); 67 | assertThat(found.getName()).isEqualTo("Alice"); 68 | } 69 | 70 | @Test 71 | void shouldReturnNullWhenNotFound() { 72 | Person found = personTools.getPersonById(999L); 73 | assertThat(found).isNull(); 74 | } 75 | 76 | @Test 77 | void shouldFindByNationality() { 78 | Person p1 = new Person(); 79 | p1.setName("Bob"); 80 | p1.setNationality("German"); 81 | Person p2 = new Person(); 82 | p2.setName("Charlie"); 83 | p2.setNationality("Polish"); 84 | Person p3 = new Person(); 85 | p3.setName("Dana"); 86 | p3.setNationality("Spanish"); 87 | personRepository.saveAll(List.of(p1, p2, p3)); 88 | 89 | List result = personTools.getPersonsByNationality(List.of("Polish", "German")); 90 | assertThat(result) 91 | .hasSize(2) 92 | .extracting(Person::getNationality) 93 | .containsExactlyInAnyOrder("Polish", "German"); 94 | } 95 | 96 | @Test 97 | void shouldHandleNullNationality() { 98 | Person person = new Person(); 99 | person.setName("Eve"); 100 | personRepository.save(person); 101 | 102 | assertThat(personTools.getPersonsByNationality(null)).isEmpty(); 103 | assertThat(personTools.getPersonsByNationality(List.of())).isEmpty(); 104 | } 105 | } 106 | 107 | 108 | 109 | Implemented repository integration tests in PersonRepositoryIntegrationTest.java. We verify save/findById behavior, querying by nationality, updating fields, and ensure that all values of the Gender enum are properly persisted and retrieved. 110 | 111 | 112 | package pl.piomin.services.repository; 113 | 114 | import org.junit.jupiter.api.BeforeEach; 115 | import org.junit.jupiter.api.Test; 116 | import org.springframework.beans.factory.annotation.Autowired; 117 | import org.springframework.boot.test.context.SpringBootTest; 118 | import org.springframework.test.context.ActiveProfiles; 119 | import org.springframework.transaction.annotation.Transactional; 120 | import pl.piomin.services.model.Person; 121 | import pl.piomin.services.model.Gender; 122 | import pl.piomin.services.repository.PersonRepository; 123 | 124 | import java.util.List; 125 | import java.util.Optional; 126 | 127 | import static org.assertj.core.api.Assertions.assertThat; 128 | 129 | @SpringBootTest 130 | @ActiveProfiles("test") 131 | @Transactional 132 | class PersonRepositoryIntegrationTest { 133 | 134 | @Autowired 135 | private PersonRepository personRepository; 136 | 137 | @BeforeEach 138 | void setUp() { 139 | personRepository.deleteAll(); 140 | } 141 | 142 | @Test 143 | void shouldSaveAndFindById() { 144 | Person person = new Person(); 145 | person.setName("Frank"); 146 | person.setNationality("Italian"); 147 | person.setAge(40); 148 | person.setGender(Gender.MALE); 149 | Person saved = personRepository.save(person); 150 | 151 | Optional found = personRepository.findById(saved.getId()); 152 | assertThat(found).isPresent(); 153 | assertThat(found.get().getName()).isEqualTo("Frank"); 154 | } 155 | 156 | @Test 157 | void shouldFindByNationality() { 158 | Person p1 = new Person(); 159 | p1.setName("Gina"); 160 | p1.setNationality("Polish"); 161 | p1.setAge(25); 162 | p1.setGender(Gender.FEMALE); 163 | Person p2 = new Person(); 164 | p2.setName("Hank"); 165 | p2.setNationality("Polish"); 166 | p2.setAge(30); 167 | p2.setGender(Gender.MALE); 168 | personRepository.saveAll(List.of(p1, p2)); 169 | 170 | List result = personRepository.findByNationality("Polish"); 171 | assertThat(result) 172 | .hasSize(2) 173 | .extracting(Person::getNationality) 174 | .containsOnly("Polish"); 175 | } 176 | 177 | @Test 178 | void shouldUpdateFields() { 179 | Person person = new Person(); 180 | person.setName("Ian"); 181 | person.setNationality("Brazilian"); 182 | person.setAge(50); 183 | person.setGender(Gender.MALE); 184 | Person saved = personRepository.save(person); 185 | 186 | saved.setName("Ian Updated"); 187 | Person updated = personRepository.save(saved); 188 | Optional found = personRepository.findById(updated.getId()); 189 | assertThat(found).isPresent(); 190 | assertThat(found.get().getName()).isEqualTo("Ian Updated"); 191 | } 192 | 193 | @Test 194 | void shouldSupportAllGenders() { 195 | for (Gender gender : Gender.values()) { 196 | Person p = new Person(); 197 | p.setName("Test " + gender); 198 | p.setNationality("X"); 199 | p.setAge(20); 200 | p.setGender(gender); 201 | personRepository.save(p); 202 | } 203 | 204 | List all = personRepository.findAll(); 205 | assertThat(all) 206 | .hasSize(Gender.values().length) 207 | .extracting(Person::getGender) 208 | .containsExactlyInAnyOrder(Gender.values()); 209 | } 210 | } 211 | 212 | 213 | 214 | Added bean‐configuration tests in ToolCallbackProviderIntegrationTest.java to ensure that the Spring bean ToolCallbackProvider and our PersonTools are present in the context, and that PersonTools is properly annotated with @Service. 215 | 216 | 217 | package pl.piomin.services.config; 218 | 219 | import org.junit.jupiter.api.Test; 220 | import org.springframework.ai.tool.execution.ToolCallbackProvider; 221 | import org.springframework.beans.factory.annotation.Autowired; 222 | import org.springframework.boot.test.context.SpringBootTest; 223 | import org.springframework.test.context.ActiveProfiles; 224 | import pl.piomin.services.tools.PersonTools; 225 | import org.springframework.stereotype.Service; 226 | 227 | import static org.assertj.core.api.Assertions.assertThat; 228 | 229 | @SpringBootTest 230 | @ActiveProfiles("test") 231 | class ToolCallbackProviderIntegrationTest { 232 | 233 | @Autowired 234 | private ToolCallbackProvider toolCallbackProvider; 235 | 236 | @Autowired 237 | private PersonTools personTools; 238 | 239 | @Test 240 | void beansExist() { 241 | assertThat(toolCallbackProvider).isNotNull(); 242 | assertThat(personTools).isNotNull(); 243 | } 244 | 245 | @Test 246 | void toolsIsService() { 247 | assertThat(PersonTools.class).hasAnnotation(Service.class); 248 | } 249 | } 250 | 251 | 252 | 253 | Created a test‐specific application configuration (application-test.yml) to disable the MCP server, configure an in-memory H2 datasource with create-drop DDL, and set DEBUG logging for our service and relevant frameworks. 254 | 255 | 256 | spring: 257 | ai: 258 | mcp: 259 | server: 260 | enabled: false 261 | datasource: 262 | url: jdbc:h2:mem:testdb-person 263 | driver-class-name: org.h2.Driver 264 | username: sa 265 | password: password 266 | jpa: 267 | database-platform: org.hibernate.dialect.H2Dialect 268 | hibernate: 269 | ddl-auto: create-drop 270 | show-sql: true 271 | server: 272 | port: 0 273 | logging: 274 | level: 275 | pl.piomin.services: DEBUG 276 | org.springframework.ai: DEBUG 277 | org.hibernate.SQL: DEBUG 278 | 279 | 280 | 281 | Added the necessary test dependencies to pom.xml so that Spring Boot testing, Testcontainers, H2 database support, and AssertJ assertions are available under the test scope. 282 | 283 | 284 | 285 | org.springframework.boot 286 | spring-boot-starter-test 287 | test 288 | 289 | 290 | org.springframework.boot 291 | spring-boot-testcontainers 292 | test 293 | 294 | 295 | org.testcontainers 296 | junit-jupiter 297 | test 298 | 299 | 300 | com.h2database 301 | h2 302 | test 303 | 304 | 305 | org.assertj 306 | assertj-core 307 | test 308 | -------------------------------------------------------------------------------- /person-mcp-service/src/test/java/pl/piomin/services/PersonMCPServerTests.java: -------------------------------------------------------------------------------- 1 | // File: person-mcp-service/src/test/java/pl/piomin/services/PersonMCPServerTests.java 2 | package pl.piomin.services; 3 | 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.ActiveProfiles; 7 | 8 | @SpringBootTest(classes = PersonMCPServer.class) 9 | @ActiveProfiles("test") 10 | class PersonMCPServerTests { 11 | 12 | @Test 13 | void contextLoads() { 14 | // Verifies that the Spring Boot application context for PersonMCPServer starts successfully 15 | } 16 | } 17 | 18 | 19 | # File: person-mcp-service/src/test/resources/application-test.yml 20 | spring: 21 | datasource: 22 | url: jdbc:h2:mem:testdb-person 23 | driver-class-name: org.h2.Driver 24 | username: sa 25 | password: 26 | 27 | jpa: 28 | hibernate: 29 | ddl-auto: create-drop 30 | show-sql: true 31 | 32 | management: 33 | endpoints: 34 | web: 35 | exposure: 36 | include: health,info 37 | 38 | mcp: 39 | server: 40 | enabled: false 41 | 42 | server: 43 | port: 0 -------------------------------------------------------------------------------- /person-mcp-service/src/test/java/pl/piomin/services/personmcp/PersonRepositoryIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.personmcp; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.transaction.annotation.Transactional; 9 | import pl.piomin.services.model.Person; 10 | import pl.piomin.services.repository.PersonRepository; 11 | import java.util.List; 12 | import java.util.Optional; 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | @SpringBootTest 16 | @ActiveProfiles("test") 17 | @Transactional 18 | class PersonRepositoryIntegrationTest { 19 | 20 | @Autowired 21 | private PersonRepository personRepository; 22 | 23 | @BeforeEach 24 | void setUp() { 25 | personRepository.deleteAll(); 26 | } 27 | 28 | @Test 29 | void shouldSaveAndFindPerson() { 30 | Person person = new Person(); 31 | person.setFirstName("Test"); 32 | person.setLastName("Person"); 33 | person.setAge(25); 34 | person.setNationality("TestLand"); 35 | 36 | Person saved = personRepository.save(person); 37 | assertThat(saved.getId()).isNotNull(); 38 | 39 | Optional found = personRepository.findById(saved.getId()); 40 | assertThat(found).isPresent(); 41 | assertThat(found.get().getFirstName()).isEqualTo("Test"); 42 | } 43 | 44 | @Test 45 | void shouldFindPersonsByNationality() { 46 | Person p1 = createPerson("John", "Doe", 30, "American"); 47 | Person p2 = createPerson("Jane", "Smith", 25, "American"); 48 | Person p3 = createPerson("Pierre", "Dupont", 35, "French"); 49 | personRepository.saveAll(List.of(p1, p2, p3)); 50 | 51 | List americans = personRepository.findByNationality("American"); 52 | assertThat(americans).hasSize(2); 53 | assertThat(americans).extracting(Person::getFirstName) 54 | .containsExactlyInAnyOrder("John", "Jane"); 55 | } 56 | 57 | @Test 58 | void shouldReturnEmptyListForNonExistentNationality() { 59 | List list = personRepository.findByNationality("Atlantian"); 60 | assertThat(list).isEmpty(); 61 | } 62 | 63 | @Test 64 | void shouldUpdatePerson() { 65 | Person p = createPerson("Update", "Test", 30, "Original"); 66 | Person saved = personRepository.save(p); 67 | saved.setAge(31); 68 | saved.setNationality("Updated"); 69 | Person updated = personRepository.save(saved); 70 | 71 | assertThat(updated.getAge()).isEqualTo(31); 72 | assertThat(updated.getNationality()).isEqualTo("Updated"); 73 | } 74 | 75 | private Person createPerson(String firstName, String lastName, int age, String nationality) { 76 | Person person = new Person(); 77 | person.setFirstName(firstName); 78 | person.setLastName(lastName); 79 | person.setAge(age); 80 | person.setNationality(nationality); 81 | return person; 82 | } 83 | } -------------------------------------------------------------------------------- /person-mcp-service/src/test/java/pl/piomin/services/personmcp/ToolCallbackProviderIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.personmcp; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.ai.tool.execution.ToolCallbackProvider; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import pl.piomin.services.tools.PersonTools; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | @SpringBootTest 12 | @ActiveProfiles("test") 13 | class ToolCallbackProviderIntegrationTest { 14 | 15 | @Autowired 16 | private ToolCallbackProvider toolCallbackProvider; 17 | 18 | @Autowired 19 | private PersonTools personTools; 20 | 21 | @Test 22 | void shouldCreateToolCallbackProviderBean() { 23 | assertThat(toolCallbackProvider).isNotNull(); 24 | } 25 | 26 | @Test 27 | void shouldHavePersonToolsProperlyConfigured() { 28 | assertThat(personTools).isNotNull(); 29 | // Basic smoke tests: no data yet, so getPersonById returns null, getPersonsByNationality returns a non-null list 30 | assertThat(personTools.getPersonById(1L)).isNull(); 31 | assertThat(personTools.getPersonsByNationality("Test")).isNotNull(); 32 | } 33 | 34 | @Test 35 | void shouldHavePersonToolsAnnotatedWithService() { 36 | boolean hasService = personTools.getClass() 37 | .isAnnotationPresent(org.springframework.stereotype.Service.class); 38 | assertThat(hasService).isTrue(); 39 | } 40 | } -------------------------------------------------------------------------------- /person-mcp-service/src/test/java/pl/piomin/services/tools/PersonToolsIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.tools; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.transaction.annotation.Transactional; 9 | import pl.piomin.services.model.Person; 10 | import pl.piomin.services.repository.PersonRepository; 11 | import java.util.List; 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | @SpringBootTest 15 | @ActiveProfiles("test") 16 | @Transactional 17 | public class PersonToolsIntegrationTest { 18 | 19 | @Autowired 20 | private PersonTools personTools; 21 | 22 | @Autowired 23 | private PersonRepository personRepository; 24 | 25 | @BeforeEach 26 | void setUp() { 27 | personRepository.deleteAll(); 28 | } 29 | 30 | @Test 31 | void shouldFindPersonByIdUsingMcpTool() { 32 | Person p = new Person(); 33 | p.setFirstName("John"); 34 | p.setLastName("Doe"); 35 | p.setAge(30); 36 | p.setNationality("American"); 37 | Person saved = personRepository.save(p); 38 | 39 | Person found = personTools.getPersonById(saved.getId()); 40 | assertThat(found).isNotNull(); 41 | assertThat(found.getId()).isEqualTo(saved.getId()); 42 | assertThat(found.getFirstName()).isEqualTo("John"); 43 | assertThat(found.getNationality()).isEqualTo("American"); 44 | } 45 | 46 | @Test 47 | void shouldReturnNullWhenPersonNotFound() { 48 | Person result = personTools.getPersonById(999L); 49 | assertThat(result).isNull(); 50 | } 51 | 52 | @Test 53 | void shouldFindPersonsByNationalityUsingMcpTool() { 54 | Person a1 = new Person(); 55 | a1.setFirstName("Alice"); 56 | a1.setLastName("A"); 57 | a1.setAge(25); 58 | a1.setNationality("X"); 59 | Person a2 = new Person(); 60 | a2.setFirstName("Alan"); 61 | a2.setLastName("B"); 62 | a2.setAge(28); 63 | a2.setNationality("X"); 64 | Person b = new Person(); 65 | b.setFirstName("Bob"); 66 | b.setLastName("C"); 67 | b.setAge(30); 68 | b.setNationality("Y"); 69 | personRepository.saveAll(List.of(a1, a2, b)); 70 | 71 | List xList = personTools.getPersonsByNationality("X"); 72 | assertThat(xList).hasSize(2); 73 | assertThat(xList).extracting(Person::getFirstName) 74 | .containsExactlyInAnyOrder("Alice", "Alan"); 75 | } 76 | 77 | @Test 78 | void shouldReturnEmptyListWhenNoPersonsWithNationality() { 79 | List empty = personTools.getPersonsByNationality("Z"); 80 | assertThat(empty).isEmpty(); 81 | } 82 | 83 | @Test 84 | void shouldHandleNullNationalityGracefully() { 85 | List result = personTools.getPersonsByNationality(null); 86 | assertThat(result).isNotNull(); 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.5.0 11 | 12 | 13 | 14 | pl.piomin.services 15 | spring-ai-apps 16 | 1.0-SNAPSHOT 17 | pom 18 | 19 | 20 | 21 21 | 1.0.0 22 | UTF-8 23 | piomin_spring-ai-apps 24 | piomin 25 | https://sonarcloud.io 26 | 27 | 28 | 29 | spring-ai-mcp 30 | 31 | 32 | 33 | 34 | 35 | org.springframework.ai 36 | spring-ai-bom 37 | ${spring-ai.version} 38 | pom 39 | import 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base",":dependencyDashboard" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /spring-ai-mcp/account-mcp-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | pl.piomin.services 9 | spring-ai-mcp 10 | 1.0-SNAPSHOT 11 | 12 | 13 | account-mcp-service 14 | 15 | 16 | 17 | org.springframework.ai 18 | spring-ai-starter-mcp-server-webflux 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jpa 23 | 24 | 25 | com.h2database 26 | h2 27 | runtime 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /spring-ai-mcp/account-mcp-service/src/main/java/pl/piomin/services/AccountMCPService.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services; 2 | 3 | import org.springframework.ai.tool.ToolCallbackProvider; 4 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.Bean; 8 | import pl.piomin.services.tools.AccountTools; 9 | 10 | @SpringBootApplication 11 | public class AccountMCPService { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(AccountMCPService.class, args); 15 | } 16 | 17 | @Bean 18 | public ToolCallbackProvider tools(AccountTools accountTools) { 19 | return MethodToolCallbackProvider.builder() 20 | .toolObjects(accountTools) 21 | .build(); 22 | } 23 | 24 | // @Bean 25 | // public List prompts() { 26 | // var prompt = new McpSchema.Prompt("persons-by-nationality", "Get persons by nationality", 27 | // List.of(new McpSchema.PromptArgument("nationality", "Person nationality", true))); 28 | // 29 | // var promptRegistration = new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> { 30 | // String nameArgument = (String) getPromptRequest.arguments().get("name"); 31 | // var userMessage = new McpSchema.PromptMessage(McpSchema.Role.USER, 32 | // new McpSchema.TextContent("How many persons are from " + nameArgument + " ?")); 33 | // return new McpSchema.GetPromptResult("Count persons by nationality", List.of(userMessage)); 34 | // }); 35 | // 36 | // return List.of(promptRegistration); 37 | // } 38 | } 39 | -------------------------------------------------------------------------------- /spring-ai-mcp/account-mcp-service/src/main/java/pl/piomin/services/model/Account.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.model; 2 | 3 | import jakarta.persistence.*; 4 | 5 | @Entity 6 | public class Account { 7 | 8 | @Id 9 | @GeneratedValue(strategy = GenerationType.IDENTITY) 10 | private Long id; 11 | private String number; 12 | private int balance; 13 | private Long personId; 14 | 15 | public Long getPersonId() { 16 | return personId; 17 | } 18 | 19 | public void setPersonId(Long personId) { 20 | this.personId = personId; 21 | } 22 | 23 | public Long getId() { 24 | return id; 25 | } 26 | 27 | public void setId(Long id) { 28 | this.id = id; 29 | } 30 | 31 | public String getNumber() { 32 | return number; 33 | } 34 | 35 | public void setNumber(String number) { 36 | this.number = number; 37 | } 38 | 39 | public int getBalance() { 40 | return balance; 41 | } 42 | 43 | public void setBalance(int balance) { 44 | this.balance = balance; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /spring-ai-mcp/account-mcp-service/src/main/java/pl/piomin/services/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import pl.piomin.services.model.Account; 5 | 6 | import java.util.List; 7 | 8 | public interface AccountRepository extends CrudRepository { 9 | List findByPersonId(Long personId); 10 | } 11 | -------------------------------------------------------------------------------- /spring-ai-mcp/account-mcp-service/src/main/java/pl/piomin/services/tools/AccountTools.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.tools; 2 | 3 | import org.springframework.ai.tool.annotation.Tool; 4 | import org.springframework.ai.tool.annotation.ToolParam; 5 | import org.springframework.stereotype.Service; 6 | import pl.piomin.services.model.Account; 7 | import pl.piomin.services.repository.AccountRepository; 8 | 9 | import java.util.List; 10 | 11 | @Service 12 | public class AccountTools { 13 | 14 | private AccountRepository accountRepository; 15 | 16 | public AccountTools(AccountRepository accountRepository) { 17 | this.accountRepository = accountRepository; 18 | } 19 | 20 | @Tool(description = "Find all accounts by person ID") 21 | public List getAccountsByPersonId( 22 | @ToolParam(description = "Person ID") Long personId) { 23 | return accountRepository.findByPersonId(personId); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spring-ai-mcp/account-mcp-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | ai: 3 | mcp: 4 | server: 5 | name: account-mcp-server 6 | version: 1.0.0 7 | jpa: 8 | database-platform: H2 9 | generate-ddl: true 10 | hibernate: 11 | ddl-auto: create-drop 12 | 13 | logging.level.org.springframework.ai: DEBUG 14 | 15 | server.port: 8040 -------------------------------------------------------------------------------- /spring-ai-mcp/account-mcp-service/src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO Account (person_id, number, balance) VALUES (1, '1234567890', 1000); 2 | INSERT INTO Account (person_id, number, balance) VALUES (1, '2345678901', 500); 3 | INSERT INTO Account (person_id, number, balance) VALUES (1, '3456789012', 2000); 4 | INSERT INTO Account (person_id, number, balance) VALUES (2, '4567890123', 750); 5 | INSERT INTO Account (person_id, number, balance) VALUES (2, '5678901234', 1500); 6 | INSERT INTO Account (person_id, number, balance) VALUES (3, '6789012345', 1000); 7 | INSERT INTO Account (person_id, number, balance) VALUES (3, '7890123456', 2500); 8 | INSERT INTO Account (person_id, number, balance) VALUES (3, '8901234567', 1200); 9 | INSERT INTO Account (person_id, number, balance) VALUES (4, '9012345678', 1800); 10 | INSERT INTO Account (person_id, number, balance) VALUES (5, '0123456789', 900); 11 | INSERT INTO Account (person_id, number, balance) VALUES (5, '1234567890', 2200); 12 | INSERT INTO Account (person_id, number, balance) VALUES (5, '2345678901', 1100); 13 | INSERT INTO Account (person_id, number, balance) VALUES (6, '3456789012', 2800); 14 | INSERT INTO Account (person_id, number, balance) VALUES (7, '4567890123', 1300); 15 | INSERT INTO Account (person_id, number, balance) VALUES (7, '5678901234', 3000); 16 | INSERT INTO Account (person_id, number, balance) VALUES (8, '6789012345', 1400); 17 | INSERT INTO Account (person_id, number, balance) VALUES (8, '7890123456', 1600); 18 | INSERT INTO Account (person_id, number, balance) VALUES (9, '8901234567', 1700); 19 | INSERT INTO Account (person_id, number, balance) VALUES (10, '9012345678', 1800); 20 | INSERT INTO Account (person_id, number, balance) VALUES (10, '0123456789', 1900); 21 | INSERT INTO Account (person_id, number, balance) VALUES (11, '1234567890', 2000); 22 | INSERT INTO Account (person_id, number, balance) VALUES (11, '2345678901', 2100); 23 | INSERT INTO Account (person_id, number, balance) VALUES (12, '3456789012', 2200); 24 | INSERT INTO Account (person_id, number, balance) VALUES (13, '4567890123', 2300); 25 | INSERT INTO Account (person_id, number, balance) VALUES (13, '5678901234', 2400); 26 | INSERT INTO Account (person_id, number, balance) VALUES (14, '6789012345', 2500); 27 | INSERT INTO Account (person_id, number, balance) VALUES (14, '7890123456', 2600); 28 | INSERT INTO Account (person_id, number, balance) VALUES (15, '8901234567', 2700); 29 | INSERT INTO Account (person_id, number, balance) VALUES (16, '9012345678', 2800); 30 | INSERT INTO Account (person_id, number, balance) VALUES (16, '0123456789', 2900); 31 | INSERT INTO Account (person_id, number, balance) VALUES (17, '1234567890', 3000); 32 | INSERT INTO Account (person_id, number, balance) VALUES (17, '2345678901', 3100); 33 | INSERT INTO Account (person_id, number, balance) VALUES (18, '3456789012', 3200); 34 | INSERT INTO Account (person_id, number, balance) VALUES (19, '4567890123', 3300); 35 | INSERT INTO Account (person_id, number, balance) VALUES (19, '5678901234', 3400); 36 | INSERT INTO Account (person_id, number, balance) VALUES (20, '6789012345', 3500); 37 | INSERT INTO Account (person_id, number, balance) VALUES (20, '7890123456', 3600); 38 | INSERT INTO Account (person_id, number, balance) VALUES (21, '8901234567', 3700); 39 | INSERT INTO Account (person_id, number, balance) VALUES (22, '9012345678', 3800); 40 | INSERT INTO Account (person_id, number, balance) VALUES (22, '0123456789', 3900); 41 | INSERT INTO Account (person_id, number, balance) VALUES (23, '1234567890', 4000); 42 | INSERT INTO Account (person_id, number, balance) VALUES (23, '2345678901', 4100); 43 | INSERT INTO Account (person_id, number, balance) VALUES (24, '3456789012', 4200); 44 | INSERT INTO Account (person_id, number, balance) VALUES (25, '4567890123', 4300); 45 | INSERT INTO Account (person_id, number, balance) VALUES (25, '5678901234', 4400); 46 | INSERT INTO Account (person_id, number, balance) VALUES (26, '6789012345', 4500); 47 | INSERT INTO Account (person_id, number, balance) VALUES (26, '7890123456', 4600); 48 | INSERT INTO Account (person_id, number, balance) VALUES (27, '8901234567', 4700); 49 | INSERT INTO Account (person_id, number, balance) VALUES (28, '9012345678', 4800); 50 | INSERT INTO Account (person_id, number, balance) VALUES (28, '0123456789', 4900); 51 | INSERT INTO Account (person_id, number, balance) VALUES (29, '1234567890', 5000); 52 | INSERT INTO Account (person_id, number, balance) VALUES (29, '2345678901', 5100); 53 | INSERT INTO Account (person_id, number, balance) VALUES (30, '3456789012', 5200); 54 | INSERT INTO Account (person_id, number, balance) VALUES (31, '4567890123', 5300); 55 | INSERT INTO Account (person_id, number, balance) VALUES (31, '5678901234', 5400); 56 | INSERT INTO Account (person_id, number, balance) VALUES (32, '6789012345', 5500); 57 | INSERT INTO Account (person_id, number, balance) VALUES (32, '7890123456', 5600); 58 | INSERT INTO Account (person_id, number, balance) VALUES (33, '8901234567', 5700); 59 | INSERT INTO Account (person_id, number, balance) VALUES (34, '9012345678', 5800); 60 | INSERT INTO Account (person_id, number, balance) VALUES (34, '0123456789', 5900); 61 | INSERT INTO Account (person_id, number, balance) VALUES (35, '1234567890', 6000); 62 | INSERT INTO Account (person_id, number, balance) VALUES (35, '2345678901', 6100); 63 | INSERT INTO Account (person_id, number, balance) VALUES (36, '3456789012', 6200); 64 | INSERT INTO Account (person_id, number, balance) VALUES (37, '4567890123', 6300); 65 | INSERT INTO Account (person_id, number, balance) VALUES (37, '5678901234', 6400); 66 | INSERT INTO Account (person_id, number, balance) VALUES (38, '6789012345', 6500); 67 | INSERT INTO Account (person_id, number, balance) VALUES (38, '7890123456', 6600); 68 | INSERT INTO Account (person_id, number, balance) VALUES (39, '8901234567', 6700); 69 | INSERT INTO Account (person_id, number, balance) VALUES (40, '9012345678', 6800); 70 | INSERT INTO Account (person_id, number, balance) VALUES (40, '0123456789', 6900); 71 | INSERT INTO Account (person_id, number, balance) VALUES (41, '1234567890', 7000); 72 | INSERT INTO Account (person_id, number, balance) VALUES (41, '2345678901', 7100); 73 | INSERT INTO Account (person_id, number, balance) VALUES (42, '3456789012', 7200); 74 | INSERT INTO Account (person_id, number, balance) VALUES (43, '4567890123', 7300); 75 | INSERT INTO Account (person_id, number, balance) VALUES (43, '5678901234', 7400); 76 | INSERT INTO Account (person_id, number, balance) VALUES (44, '6789012345', 7500); 77 | INSERT INTO Account (person_id, number, balance) VALUES (45, '7890123456', 7600); 78 | INSERT INTO Account (person_id, number, balance) VALUES (45, '8901234567', 7700); 79 | INSERT INTO Account (person_id, number, balance) VALUES (46, '9012345678', 7800); 80 | INSERT INTO Account (person_id, number, balance) VALUES (46, '0123456789', 7900); 81 | INSERT INTO Account (person_id, number, balance) VALUES (47, '1234567890', 8000); 82 | INSERT INTO Account (person_id, number, balance) VALUES (47, '2345678901', 8100); 83 | INSERT INTO Account (person_id, number, balance) VALUES (48, '3456789012', 8200); 84 | INSERT INTO Account (person_id, number, balance) VALUES (49, '4567890123', 8300); 85 | INSERT INTO Account (person_id, number, balance) VALUES (49, '5678901234', 8400); 86 | INSERT INTO Account (person_id, number, balance) VALUES (50, '6789012345', 8500); 87 | INSERT INTO Account (person_id, number, balance) VALUES (50, '7890123456', 8600); 88 | INSERT INTO Account (person_id, number, balance) VALUES (51, '8901234567', 8700); 89 | INSERT INTO Account (person_id, number, balance) VALUES (52, '9012345678', 8800); 90 | INSERT INTO Account (person_id, number, balance) VALUES (52, '0123456789', 8900); 91 | INSERT INTO Account (person_id, number, balance) VALUES (53, '1234567890', 9000); 92 | INSERT INTO Account (person_id, number, balance) VALUES (53, '2345678901', 9100); 93 | INSERT INTO Account (person_id, number, balance) VALUES (54, '3456789012', 9200); 94 | INSERT INTO Account (person_id, number, balance) VALUES (55, '4567890123', 9300); 95 | INSERT INTO Account (person_id, number, balance) VALUES (55, '5678901234', 9400); 96 | INSERT INTO Account (person_id, number, balance) VALUES (56, '6789012345', 9500); 97 | INSERT INTO Account (person_id, number, balance) VALUES (56, '7890123456', 9600); 98 | INSERT INTO Account (person_id, number, balance) VALUES (57, '8901234567', 9700); 99 | INSERT INTO Account (person_id, number, balance) VALUES (58, '9012345678', 9800); 100 | INSERT INTO Account (person_id, number, balance) VALUES (58, '0123456789', 9900); 101 | INSERT INTO Account (person_id, number, balance) VALUES (59, '1234567890', 10000); 102 | INSERT INTO Account (person_id, number, balance) VALUES (59, '2345678901', 10100); 103 | INSERT INTO Account (person_id, number, balance) VALUES (60, '3456789012', 10200); 104 | INSERT INTO Account (person_id, number, balance) VALUES (61, '4567890123', 10300); 105 | INSERT INTO Account (person_id, number, balance) VALUES (61, '5678901234', 10400); 106 | INSERT INTO Account (person_id, number, balance) VALUES (62, '6789012345', 10500); 107 | INSERT INTO Account (person_id, number, balance) VALUES (62, '7890123456', 10600); 108 | INSERT INTO Account (person_id, number, balance) VALUES (63, '8901234567', 10700); 109 | INSERT INTO Account (person_id, number, balance) VALUES (64, '9012345678', 10800); 110 | INSERT INTO Account (person_id, number, balance) VALUES (64, '0123456789', 10900); 111 | INSERT INTO Account (person_id, number, balance) VALUES (65, '1234567890', 11000); 112 | INSERT INTO Account (person_id, number, balance) VALUES (65, '2345678901', 11100); 113 | INSERT INTO Account (person_id, number, balance) VALUES (66, '3456789012', 11200); 114 | INSERT INTO Account (person_id, number, balance) VALUES (67, '4567890123', 11300); 115 | INSERT INTO Account (person_id, number, balance) VALUES (67, '5678901234', 11400); 116 | INSERT INTO Account (person_id, number, balance) VALUES (68, '6789012345', 11500); 117 | INSERT INTO Account (person_id, number, balance) VALUES (68, '7890123456', 11600); 118 | INSERT INTO Account (person_id, number, balance) VALUES (69, '8901234567', 11700); 119 | INSERT INTO Account (person_id, number, balance) VALUES (70, '9012345678', 11800); 120 | INSERT INTO Account (person_id, number, balance) VALUES (70, '0123456789', 11900); 121 | INSERT INTO Account (person_id, number, balance) VALUES (71, '1234567890', 12000); 122 | INSERT INTO Account (person_id, number, balance) VALUES (71, '2345678901', 12100); 123 | INSERT INTO Account (person_id, number, balance) VALUES (72, '3456789012', 12200); 124 | INSERT INTO Account (person_id, number, balance) VALUES (73, '4567890123', 12300); 125 | INSERT INTO Account (person_id, number, balance) VALUES (73, '5678901234', 12400); 126 | INSERT INTO Account (person_id, number, balance) VALUES (74, '6789012345', 12500); 127 | INSERT INTO Account (person_id, number, balance) VALUES (74, '7890123456', 12600); 128 | INSERT INTO Account (person_id, number, balance) VALUES (75, '8901234567', 12700); 129 | INSERT INTO Account (person_id, number, balance) VALUES (76, '9012345678', 12800); 130 | INSERT INTO Account (person_id, number, balance) VALUES (76, '0123456789', 12900); 131 | INSERT INTO Account (person_id, number, balance) VALUES (77, '1234567890', 13000); 132 | INSERT INTO Account (person_id, number, balance) VALUES (77, '2345678901', 13100); 133 | INSERT INTO Account (person_id, number, balance) VALUES (78, '3456789012', 13200); 134 | INSERT INTO Account (person_id, number, balance) VALUES (79, '4567890123', 13300); 135 | INSERT INTO Account (person_id, number, balance) VALUES (80, '5678901234', 13400); 136 | INSERT INTO Account (person_id, number, balance) VALUES (80, '6789012345', 13500); 137 | INSERT INTO Account (person_id, number, balance) VALUES (81, '7890123456', 13600); 138 | INSERT INTO Account (person_id, number, balance) VALUES (82, '8901234567', 13700); 139 | INSERT INTO Account (person_id, number, balance) VALUES (82, '9012345678', 13800); 140 | INSERT INTO Account (person_id, number, balance) VALUES (83, '0123456789', 13900); 141 | INSERT INTO Account (person_id, number, balance) VALUES (83, '1234567890', 14000); 142 | INSERT INTO Account (person_id, number, balance) VALUES (84, '2345678901', 14100); 143 | INSERT INTO Account (person_id, number, balance) VALUES (85, '3456789012', 14200); 144 | INSERT INTO Account (person_id, number, balance) VALUES (85, '4567890123', 14300); 145 | INSERT INTO Account (person_id, number, balance) VALUES (86, '5678901234', 14400); 146 | INSERT INTO Account (person_id, number, balance) VALUES (86, '6789012345', 14500); 147 | INSERT INTO Account (person_id, number, balance) VALUES (87, '7890123456', 14600); 148 | INSERT INTO Account (person_id, number, balance) VALUES (88, '8901234567', 14700); 149 | INSERT INTO Account (person_id, number, balance) VALUES (88, '9012345678', 14800); 150 | INSERT INTO Account (person_id, number, balance) VALUES (89, '0123456789', 14900); 151 | INSERT INTO Account (person_id, number, balance) VALUES (89, '1234567890', 15000); 152 | INSERT INTO Account (person_id, number, balance) VALUES (90, '2345678901', 15100); 153 | INSERT INTO Account (person_id, number, balance) VALUES (91, '3456789012', 15200); 154 | INSERT INTO Account (person_id, number, balance) VALUES (91, '4567890123', 15300); 155 | INSERT INTO Account (person_id, number, balance) VALUES (92, '5678901234', 15400); 156 | INSERT INTO Account (person_id, number, balance) VALUES (92, '6789012345', 15500); 157 | INSERT INTO Account (person_id, number, balance) VALUES (93, '7890123456', 15600); 158 | INSERT INTO Account (person_id, number, balance) VALUES (94, '8901234567', 15700); 159 | INSERT INTO Account (person_id, number, balance) VALUES (94, '9012345678', 15800); 160 | INSERT INTO Account (person_id, number, balance) VALUES (95, '0123456789', 15900); 161 | INSERT INTO Account (person_id, number, balance) VALUES (95, '1234567890', 16000); 162 | INSERT INTO Account (person_id, number, balance) VALUES (96, '2345678901', 16100); 163 | INSERT INTO Account (person_id, number, balance) VALUES (97, '3456789012', 16200); 164 | INSERT INTO Account (person_id, number, balance) VALUES (97, '4567890123', 16300); 165 | INSERT INTO Account (person_id, number, balance) VALUES (98, '5678901234', 16400); 166 | INSERT INTO Account (person_id, number, balance) VALUES (98, '6789012345', 16500); 167 | INSERT INTO Account (person_id, number, balance) VALUES (99, '7890123456', 16600); 168 | INSERT INTO Account (person_id, number, balance) VALUES (100, '8901234567', 16700); 169 | INSERT INTO Account (person_id, number, balance) VALUES (100, '9012345678', 16800); -------------------------------------------------------------------------------- /spring-ai-mcp/person-mcp-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | pl.piomin.services 8 | spring-ai-mcp 9 | 1.0-SNAPSHOT 10 | 11 | 12 | person-mcp-service 13 | 14 | 15 | 16 | org.springframework.ai 17 | spring-ai-starter-mcp-server-webflux 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-data-jpa 22 | 23 | 24 | com.h2database 25 | h2 26 | runtime 27 | 28 | 29 | -------------------------------------------------------------------------------- /spring-ai-mcp/person-mcp-service/src/main/java/pl/piomin/services/PersonMCPServer.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services; 2 | 3 | import io.modelcontextprotocol.server.McpServerFeatures; 4 | import io.modelcontextprotocol.spec.McpSchema; 5 | import org.springframework.ai.tool.ToolCallbackProvider; 6 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.context.annotation.Bean; 10 | import pl.piomin.services.tools.PersonTools; 11 | 12 | import java.util.List; 13 | 14 | @SpringBootApplication 15 | public class PersonMCPServer { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(PersonMCPServer.class, args); 19 | } 20 | 21 | @Bean 22 | public ToolCallbackProvider tools(PersonTools personTools) { 23 | return MethodToolCallbackProvider.builder() 24 | .toolObjects(personTools) 25 | .build(); 26 | } 27 | 28 | @Bean 29 | public List prompts() { 30 | var prompt = new McpSchema.Prompt("persons-by-nationality", "Get persons by nationality", 31 | List.of(new McpSchema.PromptArgument("nationality", "Person nationality", true))); 32 | 33 | var promptRegistration = new McpServerFeatures.SyncPromptSpecification(prompt, (exchange, getPromptRequest) -> { 34 | String argument = (String) getPromptRequest.arguments().get("nationality"); 35 | var userMessage = new McpSchema.PromptMessage(McpSchema.Role.USER, 36 | new McpSchema.TextContent("How many persons come from " + argument + " ?")); 37 | return new McpSchema.GetPromptResult("Count persons by nationality", List.of(userMessage)); 38 | }); 39 | 40 | return List.of(promptRegistration); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /spring-ai-mcp/person-mcp-service/src/main/java/pl/piomin/services/model/Gender.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.model; 2 | 3 | public enum Gender { 4 | MALE, 5 | FEMALE 6 | } 7 | -------------------------------------------------------------------------------- /spring-ai-mcp/person-mcp-service/src/main/java/pl/piomin/services/model/Person.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.model; 2 | 3 | import jakarta.persistence.*; 4 | 5 | @Entity 6 | public class Person { 7 | 8 | @Id 9 | @GeneratedValue(strategy = GenerationType.IDENTITY) 10 | private Long id; 11 | private String firstName; 12 | private String lastName; 13 | private int age; 14 | private String nationality; 15 | @Enumerated(EnumType.STRING) 16 | private Gender gender; 17 | 18 | public String getFirstName() { 19 | return firstName; 20 | } 21 | 22 | public void setFirstName(String firstName) { 23 | this.firstName = firstName; 24 | } 25 | 26 | public Long getId() { 27 | return id; 28 | } 29 | 30 | public void setId(Long id) { 31 | this.id = id; 32 | } 33 | 34 | public String getLastName() { 35 | return lastName; 36 | } 37 | 38 | public void setLastName(String lastName) { 39 | this.lastName = lastName; 40 | } 41 | 42 | public int getAge() { 43 | return age; 44 | } 45 | 46 | public void setAge(int age) { 47 | this.age = age; 48 | } 49 | 50 | public String getNationality() { 51 | return nationality; 52 | } 53 | 54 | public void setNationality(String nationality) { 55 | this.nationality = nationality; 56 | } 57 | 58 | public Gender getGender() { 59 | return gender; 60 | } 61 | 62 | public void setGender(Gender gender) { 63 | this.gender = gender; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /spring-ai-mcp/person-mcp-service/src/main/java/pl/piomin/services/repository/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import pl.piomin.services.model.Person; 5 | 6 | import java.util.List; 7 | 8 | public interface PersonRepository extends CrudRepository { 9 | 10 | List findByNationality(String nationality); 11 | } 12 | -------------------------------------------------------------------------------- /spring-ai-mcp/person-mcp-service/src/main/java/pl/piomin/services/tools/PersonTools.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.tools; 2 | 3 | import org.springframework.ai.tool.annotation.Tool; 4 | import org.springframework.ai.tool.annotation.ToolParam; 5 | import org.springframework.ai.tool.execution.DefaultToolCallResultConverter; 6 | import org.springframework.stereotype.Service; 7 | import pl.piomin.services.model.Person; 8 | import pl.piomin.services.repository.PersonRepository; 9 | 10 | import java.util.List; 11 | 12 | @Service 13 | public class PersonTools { 14 | 15 | private PersonRepository personRepository; 16 | 17 | public PersonTools(PersonRepository personRepository) { 18 | this.personRepository = personRepository; 19 | } 20 | 21 | @Tool(description = "Find person by ID") 22 | public Person getPersonById( 23 | @ToolParam(description = "Person ID") Long id) { 24 | return personRepository.findById(id).orElse(null); 25 | } 26 | 27 | @Tool(description = "Find all persons by nationality") 28 | public List getPersonsByNationality( 29 | @ToolParam(description = "Nationality") String nationality) { 30 | return personRepository.findByNationality(nationality); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /spring-ai-mcp/person-mcp-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | ai: 3 | mcp: 4 | server: 5 | name: person-mcp-server 6 | version: 1.0.0 7 | jpa: 8 | database-platform: H2 9 | generate-ddl: true 10 | hibernate: 11 | ddl-auto: create-drop 12 | 13 | logging.level.org.springframework.ai: DEBUG 14 | 15 | server.port: 8060 -------------------------------------------------------------------------------- /spring-ai-mcp/person-mcp-service/src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('John', 'Smith', 30, 'USA', 'MALE'); 2 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Maria', 'Gonzalez', 25, 'Spain', 'FEMALE'); 3 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Ahmed', 'Khan', 40, 'Pakistan', 'MALE'); 4 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Yuki', 'Tanaka', 28, 'Japan', 'FEMALE'); 5 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Hans', 'Müller', 35, 'Germany', 'MALE'); 6 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Sophie', 'Dupont', 27, 'France', 'FEMALE'); 7 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Ivan', 'Petrov', 38, 'Russia', 'MALE'); 8 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Luisa', 'Martinez', 22, 'Mexico', 'FEMALE'); 9 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('James', 'O`Connor', 33, 'Ireland', 'MALE'); 10 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Elena', 'Ivanova', 31, 'Russia', 'FEMALE'); 11 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Chen', 'Wei', 29, 'China', 'MALE'); 12 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Isabella', 'Rossi', 26, 'Italy', 'FEMALE'); 13 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Mohamed', 'Ali', 45, 'Egypt', 'MALE'); 14 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Aisha', 'Hassan', 24, 'UAE', 'FEMALE'); 15 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Lucas', 'Fernandes', 32, 'Brazil', 'MALE'); 16 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Emma', 'Johnson', 29, 'USA', 'FEMALE'); 17 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Hiroshi', 'Yamamoto', 36, 'Japan', 'MALE'); 18 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Sonia', 'Patel', 28, 'India', 'FEMALE'); 19 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Daniel', 'Schmidt', 41, 'Germany', 'MALE'); 20 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Olivia', 'Williams', 23, 'UK', 'FEMALE'); 21 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Ali', 'Rahman', 37, 'Turkey', 'MALE'); 22 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Fatima', 'Abdi', 27, 'Somalia', 'FEMALE'); 23 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Jacob', 'Anderson', 34, 'Canada', 'MALE'); 24 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Mia', 'Brown', 22, 'Australia', 'FEMALE'); 25 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Omar', 'Mahmoud', 42, 'Saudi Arabia', 'MALE'); 26 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Nina', 'Silva', 30, 'Portugal', 'FEMALE'); 27 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Pierre', 'Lefevre', 39, 'France', 'MALE'); 28 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Amira', 'Khaled', 26, 'Morocco', 'FEMALE'); 29 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Felipe', 'Gomez', 31, 'Argentina', 'MALE'); 30 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Charlotte', 'Baker', 25, 'UK', 'FEMALE'); 31 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Raj', 'Kapoor', 36, 'India', 'MALE'); 32 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Leila', 'Farah', 29, 'Lebanon', 'FEMALE'); 33 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Andres', 'Lopez', 40, 'Colombia', 'MALE'); 34 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Emily', 'White', 24, 'Canada', 'FEMALE'); 35 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Pavel', 'Sokolov', 33, 'Russia', 'MALE'); 36 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Isla', 'McGregor', 28, 'Scotland', 'FEMALE'); 37 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Nathan', 'Evans', 35, 'New Zealand', 'MALE'); 38 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Zara', 'Hussein', 27, 'Sudan', 'FEMALE'); 39 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('George', 'Adams', 31, 'USA', 'MALE'); 40 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Ava', 'Thompson', 26, 'Canada', 'FEMALE'); 41 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Emma', 'Johnson', 28, 'USA', 'FEMALE'); 42 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Liam', 'Smith', 35, 'Canada', 'MALE'); 43 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Olivia', 'Brown', 22, 'UK', 'FEMALE'); 44 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Noah', 'Williams', 30, 'Australia', 'MALE'); 45 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Sophia', 'Jones', 25, 'Germany', 'FEMALE'); 46 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('James', 'Müller', 40, 'Germany', 'MALE'); 47 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Isabella', 'López', 29, 'Spain', 'FEMALE'); 48 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Benjamin', 'González', 33, 'Mexico', 'MALE'); 49 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Mia', 'Fernandez', 27, 'Argentina', 'FEMALE'); 50 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Elijah', 'Silva', 31, 'Brazil', 'MALE'); 51 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Charlotte', 'Rossi', 26, 'Italy', 'FEMALE'); 52 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Lucas', 'Moreau', 34, 'France', 'MALE'); 53 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Amelia', 'Dubois', 24, 'France', 'FEMALE'); 54 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Alexander', 'Ivanov', 38, 'Russia', 'MALE'); 55 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Harper', 'Kowalski', 29, 'Poland', 'FEMALE'); 56 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Ethan', 'Novak', 32, 'Czech Republic', 'MALE'); 57 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Evelyn', 'Nakamura', 27, 'Japan', 'FEMALE'); 58 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Daniel', 'Kim', 35, 'South Korea', 'MALE'); 59 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Ava', 'Chen', 28, 'China', 'FEMALE'); 60 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('William', 'Singh', 36, 'India', 'MALE'); 61 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Scarlett', 'Nguyen', 23, 'Vietnam', 'FEMALE'); 62 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Michael', 'Ali', 41, 'Pakistan', 'MALE'); 63 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Hannah', 'García', 26, 'Colombia', 'FEMALE'); 64 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Henry', 'Mendoza', 39, 'Peru', 'MALE'); 65 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Victoria', 'Chavez', 31, 'Ecuador', 'FEMALE'); 66 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Joseph', 'Torres', 33, 'Philippines', 'MALE'); 67 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Lily', 'Petrov', 30, 'Bulgaria', 'FEMALE'); 68 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('David', 'Ahmed', 42, 'Egypt', 'MALE'); 69 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Grace', 'Johnson', 29, 'New Zealand', 'FEMALE'); 70 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Samuel', 'Larsen', 36, 'Denmark', 'MALE'); 71 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Chloe', 'Andersson', 28, 'Sweden', 'FEMALE'); 72 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Nathan', 'Virtanen', 34, 'Finland', 'MALE'); 73 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Zoe', 'Varga', 27, 'Hungary', 'FEMALE'); 74 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Julian', 'Horváth', 35, 'Slovakia', 'MALE'); 75 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Penelope', 'Ramos', 22, 'Portugal', 'FEMALE'); 76 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Leo', 'Martins', 38, 'Brazil', 'MALE'); 77 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Natalie', 'Lopez', 26, 'Mexico', 'FEMALE'); 78 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Isaac', 'Abdullah', 40, 'Turkey', 'MALE'); 79 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Audrey', 'Bakker', 25, 'Netherlands', 'FEMALE'); 80 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Sebastian', 'Janssen', 37, 'Belgium', 'MALE'); 81 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Ruby', 'Nováková', 27, 'Czech Republic', 'FEMALE'); 82 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Dylan', 'Szabó', 30, 'Hungary', 'MALE'); 83 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Stella', 'Pavlov', 24, 'Serbia', 'FEMALE'); 84 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Xavier', 'Kovačić', 32, 'Croatia', 'MALE'); 85 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Violet', 'Dragomir', 29, 'Romania', 'FEMALE'); 86 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Adam', 'Stefanov', 33, 'Bulgaria', 'MALE'); 87 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Eva', 'Bjornson', 28, 'Norway', 'FEMALE'); 88 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Owen', 'Poulsen', 35, 'Denmark', 'MALE'); 89 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Isla', 'Karlsson', 27, 'Sweden', 'FEMALE'); 90 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Hunter', 'Jensen', 31, 'Norway', 'MALE'); 91 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Madeline', 'Lindholm', 30, 'Finland', 'FEMALE'); 92 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Roman', 'Sokolov', 36, 'Russia', 'MALE'); 93 | INSERT INTO Person (first_name, last_name, age, nationality, gender) VALUES ('Clara', 'Petrović', 29, 'Serbia', 'FEMALE'); -------------------------------------------------------------------------------- /spring-ai-mcp/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | pl.piomin.services 8 | spring-ai-apps 9 | 1.0-SNAPSHOT 10 | 11 | 12 | spring-ai-mcp 13 | pom 14 | 15 | 16 | person-mcp-service 17 | sample-client 18 | account-mcp-service 19 | 20 | 21 | -------------------------------------------------------------------------------- /spring-ai-mcp/sample-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | pl.piomin.services 9 | spring-ai-mcp 10 | 1.0-SNAPSHOT 11 | 12 | 13 | sample-client 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-web 19 | 20 | 21 | org.springframework.ai 22 | spring-ai-starter-mcp-client-webflux 23 | 24 | 25 | org.springframework.ai 26 | spring-ai-starter-model-openai 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /spring-ai-mcp/sample-client/src/main/java/pl/piomin/services/SampleClient.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SampleClient { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SampleClient.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /spring-ai-mcp/sample-client/src/main/java/pl/piomin/services/controller/AccountController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import io.modelcontextprotocol.client.McpSyncClient; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.ai.chat.client.ChatClient; 7 | import org.springframework.ai.chat.prompt.Prompt; 8 | import org.springframework.ai.chat.prompt.PromptTemplate; 9 | import org.springframework.ai.tool.ToolCallbackProvider; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | @RestController 20 | @RequestMapping("/accounts") 21 | public class AccountController { 22 | 23 | private final static Logger LOG = LoggerFactory.getLogger(PersonController.class); 24 | private final ChatClient chatClient; 25 | 26 | public AccountController(ChatClient.Builder chatClientBuilder, 27 | ToolCallbackProvider tools) { 28 | this.chatClient = chatClientBuilder 29 | .defaultToolCallbacks(tools) 30 | .build(); 31 | } 32 | 33 | @GetMapping("/count-by-person-id/{personId}") 34 | String countByPersonId(@PathVariable String personId) { 35 | PromptTemplate pt = new PromptTemplate(""" 36 | How many accounts has person with {personId} ID ? 37 | """); 38 | Prompt p = pt.create(Map.of("personId", personId)); 39 | return this.chatClient.prompt(p) 40 | .call() 41 | .content(); 42 | } 43 | 44 | @GetMapping("/balance-by-person-id/{personId}") 45 | String balanceByPersonId(@PathVariable String personId) { 46 | PromptTemplate pt = new PromptTemplate(""" 47 | How many accounts has person with {personId} ID ? 48 | Return person name, nationality and a total balance on his/her accounts. 49 | """); 50 | Prompt p = pt.create(Map.of("personId", personId)); 51 | return this.chatClient.prompt(p) 52 | .call() 53 | .content(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /spring-ai-mcp/sample-client/src/main/java/pl/piomin/services/controller/PersonController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.controller; 2 | 3 | import io.modelcontextprotocol.client.McpSyncClient; 4 | import io.modelcontextprotocol.spec.McpSchema; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.ai.chat.client.ChatClient; 8 | import org.springframework.ai.chat.prompt.Prompt; 9 | import org.springframework.ai.chat.prompt.PromptTemplate; 10 | import org.springframework.ai.tool.ToolCallbackProvider; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | @RestController 20 | @RequestMapping("/persons") 21 | public class PersonController { 22 | 23 | private final static Logger LOG = LoggerFactory.getLogger(PersonController.class); 24 | private final ChatClient chatClient; 25 | private final List mcpSyncClients; 26 | 27 | public PersonController(ChatClient.Builder chatClientBuilder, 28 | ToolCallbackProvider tools, 29 | List mcpSyncClients) { 30 | this.chatClient = chatClientBuilder 31 | .defaultToolCallbacks(tools) 32 | .build(); 33 | this.mcpSyncClients = mcpSyncClients; 34 | } 35 | 36 | @GetMapping("/nationality/{nationality}") 37 | String findByNationality(@PathVariable String nationality) { 38 | 39 | PromptTemplate pt = new PromptTemplate(""" 40 | Find persons with {nationality} nationality. 41 | """); 42 | Prompt p = pt.create(Map.of("nationality", nationality)); 43 | return this.chatClient.prompt(p) 44 | .call() 45 | .content(); 46 | } 47 | 48 | @GetMapping("/count-by-nationality/{nationality}") 49 | String countByNationality(@PathVariable String nationality) { 50 | PromptTemplate pt = new PromptTemplate(""" 51 | How many persons come from {nationality} ? 52 | """); 53 | Prompt p = pt.create(Map.of("nationality", nationality)); 54 | return this.chatClient.prompt(p) 55 | .call() 56 | .content(); 57 | } 58 | 59 | @GetMapping("/count-by-nationality-from-client/{nationality}") 60 | String countByNationalityFromClient(@PathVariable String nationality) { 61 | return this.chatClient.prompt(loadPromptByName("persons-by-nationality", nationality)) 62 | .call() 63 | .content(); 64 | } 65 | 66 | Prompt loadPromptByName(String name, String nationality) { 67 | McpSchema.GetPromptRequest r = new McpSchema.GetPromptRequest(name, Map.of("nationality", nationality)); 68 | var client = mcpSyncClients.stream() 69 | .filter(c -> c.getServerInfo().name().equals("person-mcp-server")) 70 | .findFirst(); 71 | if (client.isPresent()) { 72 | var content = (McpSchema.TextContent) client.get().getPrompt(r).messages().getFirst().content(); 73 | PromptTemplate pt = new PromptTemplate(content.text()); 74 | Prompt p = pt.create(Map.of("nationality", nationality)); 75 | LOG.info("Prompt: {}", p); 76 | return p; 77 | } else return null; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /spring-ai-mcp/sample-client/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring.ai.mcp.client.sse.connections: 2 | person-mcp-server: 3 | url: http://localhost:8060 4 | account-mcp-server: 5 | url: http://localhost:8040 --------------------------------------------------------------------------------