├── .github └── workflows │ └── maven.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── openrewrite │ │ └── java │ │ └── spring │ │ └── ai │ │ └── mcp │ │ ├── recipe │ │ ├── config │ │ │ └── AddSpringAIMcpProperties.java │ │ └── mcp │ │ │ ├── AddToolAnnotationToMappingMethod.java │ │ │ └── AddToolCallbackProviderBean.java │ │ └── visitor │ │ ├── McpToolVisitor.java │ │ └── SpringAIMcpVisitor.java └── resources │ └── META-INF │ └── rewrite │ └── mcp.yml └── test └── java └── org └── openrewrite └── java └── spring └── ai └── mcp └── recipe ├── config └── AddSpringAIMcpPropertiesTest.java └── mcp ├── AddToolAnnotationToMappingMethodTest.java └── AddToolCallbackProviderBeanTest.java /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v4 26 | with: 27 | java-version: '17' 28 | distribution: 'temurin' 29 | cache: maven 30 | - name: Build with Maven 31 | run: mvn -B package --file pom.xml 32 | 33 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 34 | # - name: Update dependency graph 35 | # uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/ 8 | *.iws 9 | *.iml 10 | *.ipr 11 | 12 | ### Eclipse ### 13 | .apt_generated 14 | .classpath 15 | .factorypath 16 | .project 17 | .settings 18 | .springBeans 19 | .sts4-cache 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### Mac OS ### 35 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Web to MCP Converter 🚀 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | 5 | An [OpenRewrite](https://docs.openrewrite.org/) recipe collection that automatically converts Spring Web REST APIs to Spring AI Model Context Protocol (MCP) server tools. 6 | 7 | ## 📋 Introduction 8 | 9 | This project provides a set of OpenRewrite recipes that help you migrate traditional Spring Web REST APIs to Spring AI's Model Context Protocol (MCP) server tools. The transformation includes: 10 | 11 | 1. 🔄 Converting Spring Web annotations to Spring AI MCP `@Tool` annotations 12 | 2. 🔧 Adding necessary MCP configuration and components 13 | 3. 📦 Updating Maven dependencies to include Spring AI MCP server components 14 | 15 | The recipes automatically extract documentation from your existing REST controllers to create properly documented MCP tools, making your APIs accessible to AI agents through the [Model Context Protocol](https://modelcontextprotocol.io/). 16 | 17 | For more details about Spring AI's implementation of MCP, see the [Spring AI MCP documentation](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html). 18 | 19 | ## 🛠️ How to Build and Install 20 | 21 | ### Prerequisites 22 | 23 | - Java 17 or higher 24 | - Maven 3.6+ 25 | 26 | ### Prerequisites for Target REST API Projects 27 | 28 | To successfully migrate your Spring Web REST API to MCP, your project should: 29 | 30 | - Use Spring Boot 3.2+ (3.2.0 or newer) 31 | - Use Spring Web MVC for REST controllers 32 | - Use Maven build tool 33 | 34 | The recipe adds Spring AI MCP dependencies (version 1.0.0-SNAPSHOT or newer) to your project automatically. 35 | 36 | ### Build Steps 37 | 38 | 1. Clone this repository: 39 | ```bash 40 | git clone https://github.com/yourusername/web-to-mcp.git 41 | cd web-to-mcp 42 | ``` 43 | 44 | 2. Build the project: 45 | ```bash 46 | mvn clean install 47 | ``` 48 | 49 | This will compile the code and install the artifact to your local Maven repository. 50 | 51 | ## 🔥 How to Use 52 | 53 | To apply the recipes to your Spring Web project, run the following Maven command: 54 | 55 | ```bash 56 | mvn org.openrewrite.maven:rewrite-maven-plugin:6.4.0:run \ 57 | -Drewrite.activeRecipes=RewriteWebToMCP \ 58 | -Drewrite.recipeArtifactCoordinates=com.atbug.rewrite:web-to-mcp:1.0-SNAPSHOT \ 59 | -Drewrite.exportDatatables=true 60 | ``` 61 | 62 | **Important**: This command needs to be executed twice: 63 | 1. First execution will update your pom.xml to add necessary repositories and dependencies 64 | 2. Second execution will perform the actual code conversion of your Spring Web controllers to MCP tools 65 | 66 | ## ✨ Features 67 | 68 | The recipe performs several transformations that are organized into three main components: 69 | 70 | ### 1. POM Updates (`UpdatePom`) 71 | - Adds Spring Snapshots repository (`https://repo.spring.io/snapshot`) 72 | - Adds Central Portal Snapshots repository (`https://central.sonatype.com/repository/maven-snapshots/`) 73 | - Adds Spring AI MCP server WebMVC dependency (`spring-ai-starter-mcp-server-webmvc`) 74 | 75 | ### 2. Code Transformations 76 | - **`AddToolAnnotationToMappingMethod`**: Automatically converts Spring Web controller methods to MCP tools 77 | - Adds `@Tool` annotations to methods with Spring Web mapping annotations (`@GetMapping`, `@PostMapping`, etc.) 78 | - Extracts method descriptions from JavaDoc comments to populate the `description` attribute 79 | - Adds `@ToolParam` annotations to method parameters, preserving their descriptions from JavaDoc 80 | 81 | - **`AddToolCallbackProviderBean`**: Creates or updates a bean to register MCP tools 82 | - Identifies Spring Boot application entry point class 83 | - Creates a `ToolCallbackProvider` bean to register all controllers with `@Tool` annotations 84 | - Intelligently updates existing provider beans if they already exist 85 | 86 | - **`AddSpringAIMcpProperties`**: Configures MCP server properties 87 | - Adds required MCP server configuration to `application.properties` or `application.yml` 88 | - Sets server name, version, type, and message endpoints 89 | - Supports both YAML and Properties file formats 90 | 91 | ## 🧪 Example 92 | 93 | ### Before (Spring Web Controller) 94 | 95 | ```java 96 | @RestController 97 | @RequestMapping("/api/users") 98 | public class UserController { 99 | 100 | /** 101 | * Get a user by ID 102 | * @param id The user identifier 103 | * @return The user details 104 | */ 105 | @GetMapping("/{id}") 106 | public User getUserById(@PathVariable Long id) { 107 | // Implementation 108 | } 109 | } 110 | ``` 111 | 112 | ### After (MCP Tool) 113 | 114 | ```java 115 | @RestController 116 | @RequestMapping("/api/users") 117 | public class UserController { 118 | 119 | /** 120 | * Get a user by ID 121 | * @param id The user identifier 122 | * @return The user details 123 | */ 124 | @GetMapping("/{id}") 125 | @Tool(description = "Get a user by ID") 126 | public User getUserById(@ToolParam(description = "The user identifier") @PathVariable Long id) { 127 | // Implementation 128 | } 129 | } 130 | ``` 131 | 132 | ### Generated MCP Configuration 133 | 134 | The recipe will also automatically add MCP server configuration to your application properties: 135 | 136 | ```properties 137 | spring.ai.mcp.server.name=webmvc-mcp-server 138 | spring.ai.mcp.server.sse-message-endpoint=/mcp/messages 139 | spring.ai.mcp.server.type=SYNC 140 | spring.ai.mcp.server.version=1.0.0 141 | ``` 142 | 143 | And automatically register your tools by adding a `ToolCallbackProvider` bean to your Spring Boot application class: 144 | 145 | ```java 146 | @Bean 147 | ToolCallbackProvider toolCallbackProvider(UserController userController) { 148 | return MethodToolCallbackProvider.builder() 149 | .toolObjects(userController) 150 | .build(); 151 | } 152 | ``` 153 | 154 | ## 🌟 Demonstration 155 | 156 | You can try out this conversion tool with a sample Spring Boot 3 REST API project that's ready for conversion. 157 | 158 | ### Sample Project Setup 159 | 160 | 1. Clone the sample project: 161 | ```bash 162 | git clone https://github.com/addozhang/spring-boot-3-rest-api-sample.git 163 | cd spring-boot-3-rest-api-sample 164 | ``` 165 | 166 | 2. Review the sample project structure: 167 | - It's a standard Spring Boot 3 application with REST controllers 168 | - Includes typical REST endpoints with various HTTP methods (GET, POST, PUT, DELETE) 169 | - Contains proper JavaDoc comments that will be converted to MCP tool descriptions 170 | 171 | ### Conversion Process 172 | 173 | 1. First, run the Maven command to update the POM file with required dependencies: 174 | ```bash 175 | mvn org.openrewrite.maven:rewrite-maven-plugin:6.4.0:run \ 176 | -Drewrite.activeRecipes=RewriteWebToMCP \ 177 | -Drewrite.recipeArtifactCoordinates=com.atbug.rewrite:spring-rest-to-mcp:1.0-SNAPSHOT \ 178 | -Drewrite.exportDatatables=true 179 | ``` 180 | 181 | 2. Then, run the same command again to perform the actual code conversion: 182 | ```bash 183 | mvn org.openrewrite.maven:rewrite-maven-plugin:6.4.0:run \ 184 | -Drewrite.activeRecipes=RewriteWebToMCP \ 185 | -Drewrite.recipeArtifactCoordinates=com.atbug.rewrite:spring-rest-to-mcp:1.0-SNAPSHOT \ 186 | -Drewrite.exportDatatables=true 187 | ``` 188 | 189 | 3. Verify the changes: 190 | - Check your controller classes for added `@Tool` and `@ToolParam` annotations 191 | - Look for the new `ToolCallbackProvider` bean in your main application class 192 | - Check that `application.properties` or `application.yml` has MCP server configuration 193 | 194 | 4. Run the application: 195 | ```bash 196 | mvn spring-boot:run 197 | ``` 198 | 199 | 5. Test your MCP server using the official MCP Inspector: 200 | - Clone the MCP Inspector repository: 201 | ```bash 202 | git clone https://github.com/modelcontextprotocol/inspector.git 203 | cd inspector 204 | ``` 205 | - Install dependencies and start the inspector: 206 | ```bash 207 | npm install 208 | npm run dev 209 | ``` 210 | - Access the inspector in your browser at: http://localhost:5173/ 211 | - In the left side panel, configure your MCP server with: 212 | - Type: SSE 213 | - Address: http://localhost:8080/sse 214 | - Once connected, you can: 215 | - View all available tools in the main panel 216 | - Test each tool interactively 217 | - See the responses from your MCP server 218 | 219 | ### What to Expect 220 | 221 | After conversion, your Spring Boot application will function both as a traditional REST API and as an MCP server. This means: 222 | 223 | - All your existing endpoints continue to work as before 224 | - Applications that support the MCP protocol can discover and interact with your API 225 | - AI assistants can understand how to use your tools through the MCP protocol's standardized format 226 | 227 | Applications consuming your MCP server can be configured to connect to it with configuration like: 228 | 229 | ```json 230 | { 231 | "mcpServers": { 232 | "spring-ai-mcp-sample": { 233 | "autoApprove": [], 234 | "disabled": false, 235 | "timeout": 60, 236 | "url": "http://localhost:8080/sse", 237 | "transportType": "sse" 238 | } 239 | } 240 | } 241 | ``` 242 | 243 | This allows client applications to seamlessly discover and utilize the tools provided by your converted API. 244 | 245 | ## 📄 License 246 | 247 | This project is licensed under the Apache License 2.0 - see the LICENSE file for details. 248 | 249 | ## 👥 Contributing 250 | 251 | Contributions are welcome! Please feel free to submit a Pull Request. 252 | 253 | ## 📞 Support 254 | 255 | If you have any questions or need help, please open an issue on GitHub. 256 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.atbug.rewrite 8 | spring-rest-to-mcp 9 | 1.0-SNAPSHOT 10 | 11 | 12 | UTF-8 13 | 1.8 14 | 1.8 15 | 17 16 | 17 17 | 18 | 19 | 20 | 21 | 22 | org.junit 23 | junit-bom 24 | 5.11.0 25 | pom 26 | import 27 | 28 | 29 | org.openrewrite.recipe 30 | rewrite-recipe-bom 31 | 3.5.0 32 | pom 33 | import 34 | 35 | 36 | org.projectlombok 37 | lombok 38 | 1.18.36 39 | 40 | 41 | org.springframework.ai 42 | spring-ai-bom 43 | 1.0.0-SNAPSHOT 44 | pom 45 | import 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.openrewrite 54 | rewrite-java 55 | compile 56 | 57 | 58 | 61 | 62 | org.openrewrite 63 | rewrite-java-8 64 | runtime 65 | 66 | 67 | org.openrewrite 68 | rewrite-java-11 69 | runtime 70 | 71 | 72 | org.openrewrite 73 | rewrite-java-17 74 | runtime 75 | 76 | 77 | 78 | 79 | org.openrewrite 80 | rewrite-maven 81 | compile 82 | 83 | 84 | 85 | 86 | org.openrewrite 87 | rewrite-yaml 88 | compile 89 | 90 | 91 | 92 | 93 | org.openrewrite 94 | rewrite-properties 95 | compile 96 | 97 | 98 | 99 | 100 | org.openrewrite 101 | rewrite-xml 102 | compile 103 | 104 | 105 | 106 | 107 | org.projectlombok 108 | lombok 109 | true 110 | 111 | 112 | 113 | 114 | org.openrewrite 115 | rewrite-test 116 | test 117 | 118 | 119 | 120 | org.projectlombok 121 | lombok 122 | 1.18.36 123 | provided 124 | 125 | 126 | 127 | org.springframework.ai 128 | spring-ai-starter-mcp-server-webmvc 129 | provided 130 | 131 | 132 | 133 | 134 | 135 | 136 | maven-surefire-plugin 137 | 138 | 139 | org.apache.maven.plugins 140 | maven-compiler-plugin 141 | 3.13.0 142 | 143 | 144 | 145 | -parameters 146 | 147 | 148 | 149 | 150 | org.projectlombok 151 | lombok 152 | 153 | 154 | 16 155 | 16 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | spring-snapshots 164 | Spring Snapshots 165 | https://repo.spring.io/snapshot 166 | 167 | false 168 | 169 | 170 | 171 | Central Portal Snapshots 172 | central-portal-snapshots 173 | https://central.sonatype.com/repository/maven-snapshots/ 174 | 175 | false 176 | 177 | 178 | true 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/spring/ai/mcp/recipe/config/AddSpringAIMcpProperties.java: -------------------------------------------------------------------------------- 1 | package org.openrewrite.java.spring.ai.mcp.recipe.config; 2 | 3 | import org.openrewrite.java.spring.ai.mcp.visitor.SpringAIMcpVisitor; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Value; 6 | import org.intellij.lang.annotations.Language; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jspecify.annotations.Nullable; 9 | import org.openrewrite.*; 10 | import org.openrewrite.properties.AddProperty; 11 | import org.openrewrite.properties.tree.Properties; 12 | import org.openrewrite.yaml.MergeYaml; 13 | import org.openrewrite.yaml.tree.Yaml; 14 | 15 | import java.nio.file.Path; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.concurrent.atomic.AtomicBoolean; 19 | 20 | @Value 21 | @EqualsAndHashCode(callSuper = false) 22 | public class AddSpringAIMcpProperties extends ScanningRecipe { 23 | List SpringDefaultConfigurationPaths = Arrays.asList("**/application.yml", "**/application.properties", "**/application.yaml"); 24 | 25 | @Option(displayName = "MCP server name", 26 | description = "The name of the MCP server.", 27 | example = "webmvc-mcp-server") 28 | @Nullable 29 | String serverName = "webmvc-mcp-server"; 30 | 31 | @Option(displayName = "MCP server version", 32 | description = "The version of the MCP server.", 33 | example = "1.0.0") 34 | @Nullable 35 | String serverVersion = "1.0.0"; 36 | 37 | @Option(displayName = "MCP server type", 38 | description = "The type of the MCP server.", 39 | example = "SYNC") 40 | @Nullable 41 | String serverType = "SYNC"; 42 | 43 | @Option(displayName = "MCP server SSE message endpoint", 44 | description = "The SSE message endpoint of the MCP server.", 45 | example = "/mcp/messages") 46 | @Nullable 47 | String sseMessageEndpoint = "/mcp/messages"; 48 | 49 | @Option(displayName = "Optional list of file path matcher", 50 | description = "Each value in this list represents a glob expression that is used to match which files will " + 51 | "be modified. If this value is not present, this recipe will use the defaults. " + 52 | "(\"**/application.yml\", \"**/application.yml\", and \"**/application.properties\".", 53 | required = false, 54 | example = "[\"**/application.yml\"]") 55 | @Nullable 56 | List pathExpressions; 57 | 58 | // TODO: to handle the async type in future 59 | @Language("yml") 60 | String yaml = """ 61 | spring: 62 | ai: 63 | mcp: 64 | server: 65 | name: %s 66 | version: %s 67 | type: %s 68 | sse-message-endpoint: %s 69 | """; 70 | @Language("properties") 71 | String properties = """ 72 | spring.ai.mcp.server.name=%s 73 | spring.ai.mcp.server.version=%s 74 | spring.ai.mcp.server.type=%s 75 | spring.ai.mcp.server.sse-message-endpoint=%s 76 | """; 77 | 78 | @Override 79 | public @NotNull AtomicBoolean getInitialValue(@NotNull ExecutionContext ctx) { 80 | return new AtomicBoolean(false); 81 | } 82 | 83 | @Override 84 | public @NotNull TreeVisitor getScanner(@NotNull AtomicBoolean aiMcpEnabled) { 85 | return new SpringAIMcpVisitor<>(aiMcpEnabled); 86 | } 87 | 88 | @Override 89 | public @NotNull TreeVisitor getVisitor(AtomicBoolean aiMcpEnabled) { 90 | TreeVisitor visitor = new TreeVisitor<>() { 91 | @Override 92 | public @Nullable Tree visit(@Nullable Tree t, @NotNull ExecutionContext ctx, @NotNull Cursor parent) { 93 | if (t instanceof Yaml.Documents && sourcePathMatch(((SourceFile) t).getSourcePath())) { 94 | t = new MergeYaml("$", updateContent(yaml), true, null, null, null, null, null) 95 | .getVisitor().visit(t, ctx, parent); 96 | } else if (t instanceof Properties.File && sourcePathMatch(((SourceFile) t).getSourcePath())) { 97 | List props = Arrays.stream(updateContent(properties).split("\n")) 98 | .map(line -> line.split("=", 2)) 99 | .filter(parts -> parts.length == 2) 100 | .toList(); 101 | for (String[] prop : props) { 102 | String key = prop[0].trim(); 103 | String value = prop[1].trim(); 104 | if (key.isEmpty() || value.isEmpty()) { 105 | continue; 106 | } 107 | t = new AddProperty(key, value, null, null) 108 | .getVisitor().visit(t, ctx, parent); 109 | } 110 | } 111 | return t; 112 | } 113 | 114 | }; 115 | return Preconditions.check(aiMcpEnabled.get(), visitor); 116 | } 117 | 118 | private String updateContent(String content) { 119 | return String.format(content, serverName, serverVersion, serverType, sseMessageEndpoint); 120 | } 121 | 122 | private boolean sourcePathMatch(Path sourcePath) { 123 | List expressions = pathExpressions; 124 | if (expressions == null || pathExpressions.isEmpty()) { 125 | //If not defined, get defaults. 126 | expressions = SpringDefaultConfigurationPaths; 127 | } 128 | if (expressions.isEmpty()) { 129 | return true; 130 | } 131 | for (String filePattern : expressions) { 132 | if (PathUtils.matchesGlob(sourcePath, filePattern)) { 133 | return true; 134 | } 135 | } 136 | return false; 137 | } 138 | 139 | @Override 140 | public @NlsRewrite.DisplayName @NotNull String getDisplayName() { 141 | return "Add Spring AI MCP properties"; 142 | } 143 | 144 | @Override 145 | public @NlsRewrite.Description @NotNull String getDescription() { 146 | return "Add properties to the Spring AI MCP configuration file."; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/spring/ai/mcp/recipe/mcp/AddToolAnnotationToMappingMethod.java: -------------------------------------------------------------------------------- 1 | package org.openrewrite.java.spring.ai.mcp.recipe.mcp; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.openrewrite.*; 5 | import org.openrewrite.java.JavaIsoVisitor; 6 | import org.openrewrite.java.JavaParser; 7 | import org.openrewrite.java.JavaTemplate; 8 | import org.openrewrite.java.search.FindAnnotations; 9 | import org.openrewrite.java.search.UsesType; 10 | import org.openrewrite.java.spring.ai.mcp.visitor.SpringAIMcpVisitor; 11 | import org.openrewrite.java.tree.J; 12 | import org.openrewrite.java.tree.Javadoc; 13 | 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | import java.util.concurrent.atomic.AtomicBoolean; 19 | import java.util.concurrent.atomic.AtomicReference; 20 | import java.util.stream.Collectors; 21 | 22 | public class AddToolAnnotationToMappingMethod extends ScanningRecipe { 23 | private static final String MCP_TOOL_PACKAGE = "org.springframework.ai.tool.annotation"; 24 | private static final String MCP_TOOL_SIMPLE_NAME = "Tool"; 25 | private static final String MCP_TOOL_PARAM_SIMPLE_NAME = "ToolParam"; 26 | private static final String MCP_TOOL_FULLY_QUALIFIED_NAME = MCP_TOOL_PACKAGE + "." + MCP_TOOL_SIMPLE_NAME; 27 | private static final String MCP_TOOL_PARAM_FULLY_QUALIFIED_NAME = MCP_TOOL_PACKAGE + "." + MCP_TOOL_PARAM_SIMPLE_NAME; 28 | private boolean causesAnotherCycle; 29 | /** 30 | * JavaTemplate for @Tool(description="") annotation 31 | */ 32 | private static final JavaTemplate TOOL_ANNO_TEMPLATE = JavaTemplate.builder("@" + MCP_TOOL_SIMPLE_NAME + "(description = \"#{}\")") 33 | .imports(MCP_TOOL_FULLY_QUALIFIED_NAME) 34 | .javaParser(JavaParser.fromJavaVersion().dependsOn("package " + MCP_TOOL_PACKAGE + 35 | "; public @interface " + MCP_TOOL_SIMPLE_NAME + " {}")) 36 | .build(); 37 | /** 38 | * JavaTemplate for @ToolParam(description="") annotation 39 | */ 40 | private static final JavaTemplate TOOL_PARAM_ANNO_TEMPLATE = JavaTemplate.builder("@" + MCP_TOOL_PARAM_SIMPLE_NAME + "(description = \"#{}\")") 41 | .imports(MCP_TOOL_PARAM_FULLY_QUALIFIED_NAME) 42 | .javaParser(JavaParser.fromJavaVersion().dependsOn("package " + MCP_TOOL_PACKAGE + 43 | "; public @interface " + MCP_TOOL_PARAM_SIMPLE_NAME + " {}")) 44 | .build(); 45 | 46 | @Override 47 | public @NotNull AtomicBoolean getInitialValue(@NotNull ExecutionContext ctx) { 48 | return new AtomicBoolean(false); 49 | } 50 | 51 | @Override 52 | public @NotNull TreeVisitor getScanner(@NotNull AtomicBoolean aiMcpEnabled) { 53 | return Preconditions.check(!aiMcpEnabled.get(), new SpringAIMcpVisitor<>(aiMcpEnabled)); 54 | } 55 | 56 | @Override 57 | public @NotNull TreeVisitor getVisitor(@NotNull AtomicBoolean aiMcpEnabled) { 58 | JavaIsoVisitor visitor = new JavaIsoVisitor<>() { 59 | 60 | @Override 61 | public J.@NotNull MethodDeclaration visitMethodDeclaration(J.@NotNull MethodDeclaration method, @NotNull ExecutionContext ctx) { 62 | if (FindAnnotations.find(method, "@" + MCP_TOOL_FULLY_QUALIFIED_NAME).isEmpty() 63 | && (!FindAnnotations.find(method, "@org.springframework.web.bind.annotation.GetMapping").isEmpty() 64 | || !FindAnnotations.find(method, "@org.springframework.web.bind.annotation.PostMapping").isEmpty() 65 | || !FindAnnotations.find(method, "@org.springframework.web.bind.annotation.RequestMapping").isEmpty() 66 | || !FindAnnotations.find(method, "@org.springframework.web.bind.annotation.PatchMapping").isEmpty() 67 | || !FindAnnotations.find(method, "@org.springframework.web.bind.annotation.DeleteMapping").isEmpty() 68 | || !FindAnnotations.find(method, "@org.springframework.web.bind.annotation.PutMapping").isEmpty())) { // has any web mapping annotation, but no mcp tool annotation 69 | AtomicReference toolDesc = new AtomicReference<>(); 70 | Map toolParamMap = new HashMap<>(); 71 | Optional docComment = method.getComments().stream() 72 | .map(comment -> ((Javadoc.DocComment) comment)) 73 | .findFirst(); 74 | if (docComment.isPresent()) { 75 | Javadoc.DocComment comment = docComment.get(); 76 | String methodDesc = getDescription(comment.getBody()); 77 | toolDesc.set(methodDesc); 78 | 79 | comment.getBody().stream() 80 | .filter(l -> l instanceof Javadoc.Parameter) 81 | .map(l -> (Javadoc.Parameter) l) 82 | .filter(p -> p.getNameReference() != null && p.getNameReference().getTree() != null) 83 | .forEach(p -> { 84 | String pName = p.getNameReference().getTree().toString(); 85 | String pDesc = getDescription(p.getDescription()); 86 | toolParamMap.put(pName, pDesc); 87 | }); 88 | } else { 89 | toolDesc.set(method.getSimpleName()); 90 | } 91 | //Add Tool annotation to method 92 | method = TOOL_ANNO_TEMPLATE.apply(getCursor(), method.getCoordinates().addAnnotation((an1, an2) -> 0), toolDesc.get()); // insert to the last position 93 | maybeAddImport(MCP_TOOL_FULLY_QUALIFIED_NAME); 94 | updateCursor(method); 95 | //Add ToolParam annotation to method parameters 96 | List params = method.getParameters().stream() 97 | .filter(statement -> statement instanceof J.VariableDeclarations) 98 | //TODO: maybe filter with variable annotations like PathVariable, RequestParam, RequestBody or RequestHeader 99 | // The parameters with these annotations are resolved from request. And in mcp, they will be taken as tool parameters. 100 | // The required option should be extract from these annotations and set to the tool parameters. 101 | .map(statement -> (J.VariableDeclarations) statement) 102 | .toList(); 103 | for (J.VariableDeclarations varDecl : params) { 104 | String paramName = varDecl.getVariables().get(0).getSimpleName(); 105 | String paraDesc = toolParamMap.get(paramName); 106 | paraDesc = paraDesc != null ? paraDesc : paramName; 107 | method = TOOL_PARAM_ANNO_TEMPLATE 108 | .apply(getCursor(), varDecl.getCoordinates().addAnnotation((an1, an2) -> 0), paraDesc); 109 | } 110 | maybeAddImport(MCP_TOOL_PARAM_FULLY_QUALIFIED_NAME); 111 | causesAnotherCycle = true; 112 | } 113 | return method; 114 | } 115 | }; 116 | 117 | //make sure the target class is a Spring Bean 118 | TreeVisitor beanChecker = Preconditions.or( 119 | new UsesType<>("org.springframework.stereotype.Controller", false), 120 | new UsesType<>("org.springframework.stereotype.Component", false), 121 | new UsesType<>("org.springframework.stereotype.Service", false), 122 | new UsesType<>("org.springframework.stereotype.Repository", false), 123 | new UsesType<>("org.springframework.web.bind.annotation.RestController", false) 124 | ); 125 | return Preconditions.check( 126 | aiMcpEnabled.get(), Preconditions.check(beanChecker, visitor)); 127 | } 128 | 129 | @Override 130 | public boolean causesAnotherCycle() { 131 | return causesAnotherCycle || super.causesAnotherCycle(); 132 | } 133 | 134 | /** 135 | * Get the description of the javaDocs. 136 | * 137 | * @param javaDocs the javaDocs 138 | * @return the description 139 | */ 140 | private static @NotNull String getDescription(List javaDocs) { 141 | return javaDocs.stream() 142 | .filter(l -> l instanceof Javadoc.Text) 143 | .map(l -> ((Javadoc.Text) l).getText().trim()) 144 | .filter(s -> !s.isEmpty()) 145 | .collect(Collectors.joining(", ")); 146 | } 147 | 148 | @Override 149 | public @NlsRewrite.DisplayName @NotNull String getDisplayName() { 150 | return "Add MCP Tool annotation to mapping method"; 151 | } 152 | 153 | @Override 154 | public @NlsRewrite.Description @NotNull String getDescription() { 155 | return "Add MCP annotations (Tool/ToolParam) to the method with Spring Web mapping annotations."; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/spring/ai/mcp/recipe/mcp/AddToolCallbackProviderBean.java: -------------------------------------------------------------------------------- 1 | package org.openrewrite.java.spring.ai.mcp.recipe.mcp; 2 | 3 | import lombok.NonNull; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.intellij.lang.annotations.Language; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.openrewrite.*; 8 | import org.openrewrite.java.JavaIsoVisitor; 9 | import org.openrewrite.java.JavaParser; 10 | import org.openrewrite.java.JavaTemplate; 11 | import org.openrewrite.java.search.FindAnnotations; 12 | import org.openrewrite.java.spring.ai.mcp.visitor.McpToolVisitor; 13 | import org.openrewrite.java.tree.J; 14 | 15 | import java.util.*; 16 | import java.util.stream.Collectors; 17 | 18 | @Slf4j 19 | public class AddToolCallbackProviderBean extends ScanningRecipe> { 20 | private static final String SPRING_BOOT_APPLICATION_FQN = "org.springframework.boot.autoconfigure.SpringBootApplication"; 21 | private static final String SPRING_BEAN_FQN = "org.springframework.context.annotation.Bean"; 22 | private static final String METHOD_TOOL_CB_PROVIDER_FQN = "org.springframework.ai.tool.method.MethodToolCallbackProvider"; 23 | public static final String TOOL_CB_PROVIDER_PACKAGE = "org.springframework.ai.tool"; 24 | public static final String TOOL_CB_PROVIDER_SIMPLE_NAME = "ToolCallbackProvider"; 25 | public static final String TOOL_CB_PROVIDER_FQN = TOOL_CB_PROVIDER_PACKAGE + "." + TOOL_CB_PROVIDER_SIMPLE_NAME; 26 | public static final String PROVIDER_METHOD_TEMPLATE = """ 27 | @Bean 28 | ToolCallbackProvider %s(#{}) { 29 | return MethodToolCallbackProvider.builder() 30 | .toolObjects(#{}) 31 | .build(); 32 | } 33 | """; 34 | private static final String BEAN_METHOD_NAME = "toolCallbackProvider"; 35 | 36 | @Override 37 | public @NotNull Set getInitialValue(@NotNull ExecutionContext ctx) { 38 | return new HashSet<>(); 39 | } 40 | 41 | @Override 42 | public @NotNull TreeVisitor getScanner(@NotNull Set toolSet) { 43 | return new McpToolVisitor(toolSet); 44 | } 45 | 46 | @Override 47 | public @NotNull TreeVisitor getVisitor(@NotNull Set toolObjectSet) { 48 | JavaIsoVisitor visitor = new JavaIsoVisitor<>() { 49 | @Override 50 | public J.@NotNull ClassDeclaration visitClassDeclaration(J.@NotNull ClassDeclaration classDecl, @NotNull ExecutionContext ctx) { 51 | classDecl = super.visitClassDeclaration(classDecl, ctx); 52 | if (toolObjectSet.isEmpty()) { 53 | return classDecl; // No tool objects found, return early 54 | } 55 | Set annotations = FindAnnotations.find(classDecl, SPRING_BOOT_APPLICATION_FQN); 56 | if (annotations.isEmpty()) { 57 | return classDecl; // No @SpringBootApplication annotation found, return early 58 | } 59 | List providerMethodList = classDecl.getBody().getStatements().stream() 60 | .filter(s -> s instanceof J.MethodDeclaration) 61 | .map(s -> (J.MethodDeclaration) s) 62 | .filter(m -> m.getReturnTypeExpression() != null && TOOL_CB_PROVIDER_SIMPLE_NAME.equals(m.getReturnTypeExpression().toString())) 63 | .toList(); 64 | 65 | assert providerMethodList.size() <= 1 : String.format("There should be at most one method with return type %s", TOOL_CB_PROVIDER_SIMPLE_NAME); 66 | 67 | if (providerMethodList.size() == 1) { 68 | J.MethodDeclaration m = providerMethodList.get(0); 69 | List params = m.getParameters().stream() 70 | .filter(s -> s instanceof J.VariableDeclarations) 71 | .map(s -> (J.VariableDeclarations) s) 72 | .toList(); 73 | 74 | if (params.size() == toolObjectSet.size() 75 | && params.stream().filter(varDecl -> toolObjectSet.contains(varDecl.getTypeAsFullyQualified().toString())).count() == params.size()) { 76 | return classDecl; 77 | } 78 | // Update the method to use the new tool object list 79 | J.Block block = buildJavaTemplate(toolObjectSet, m.getName().toString()) 80 | .apply(new Cursor(getCursor(), classDecl.getBody()), m.getCoordinates().replace(), buildArguments(toolObjectSet), buildVariables(toolObjectSet)); 81 | classDecl = classDecl.withBody(block); 82 | } else { 83 | // Create a new method with the tool object list 84 | classDecl = buildJavaTemplate(toolObjectSet, BEAN_METHOD_NAME) 85 | .apply(getCursor(), classDecl.getBody().getCoordinates().lastStatement(), buildArguments(toolObjectSet), buildVariables(toolObjectSet)); 86 | } 87 | Arrays.stream(buildImports(toolObjectSet)).forEach(this::maybeAddImport); 88 | return classDecl; 89 | } 90 | }; 91 | return Preconditions.check(!toolObjectSet.isEmpty(), visitor); 92 | } 93 | 94 | private @NotNull String buildVariables(Set toolObjectList) { 95 | return toolObjectList.stream() 96 | .map(clazzName -> clazzName.substring(clazzName.lastIndexOf('.') + 1)) 97 | .map(clazzName -> Character.toLowerCase(clazzName.charAt(0)) + clazzName.substring(1)) 98 | .collect(Collectors.joining(", ")); 99 | } 100 | 101 | private @NotNull String buildArguments(Set toolObjectList) { 102 | return toolObjectList.stream() 103 | .map(clazzName -> { 104 | String clazzSimpleName = clazzName.substring(clazzName.lastIndexOf('.') + 1); 105 | String clazzVarName = Character.toLowerCase(clazzSimpleName.charAt(0)) + clazzSimpleName.substring(1); 106 | return String.format("%s %s", clazzSimpleName, clazzVarName); 107 | }) 108 | .collect(Collectors.joining(", ")); 109 | } 110 | 111 | private JavaTemplate buildJavaTemplate(Set toolObjectList, @NonNull String methodName) { 112 | @Language("java") String[] dependsOn = buildDependsOn(toolObjectList); 113 | String[] imports = buildImports(toolObjectList); 114 | return JavaTemplate.builder(String.format(PROVIDER_METHOD_TEMPLATE, methodName)) 115 | .imports(imports) 116 | .contextSensitive() 117 | .javaParser(JavaParser.fromJavaVersion().dependsOn(dependsOn).logCompilationWarningsAndErrors(true)) 118 | .build(); 119 | } 120 | 121 | private @NotNull String[] buildImports(Set toolObjectList) { 122 | HashSet importsToAdd = moreToImportAndDependOn(toolObjectList); 123 | return importsToAdd.toArray(new String[0]); 124 | } 125 | 126 | private @NotNull HashSet moreToImportAndDependOn(Set toolObjectList) { 127 | HashSet importsToAdd = new HashSet<>(toolObjectList); 128 | importsToAdd.add(SPRING_BEAN_FQN); 129 | importsToAdd.add(METHOD_TOOL_CB_PROVIDER_FQN); 130 | importsToAdd.add(TOOL_CB_PROVIDER_FQN); 131 | return importsToAdd; 132 | } 133 | 134 | private @NotNull String[] buildDependsOn(Set toolObjectList) { 135 | List dependsOnList = new ArrayList<>(moreToImportAndDependOn(toolObjectList).stream() 136 | .filter(dep -> !METHOD_TOOL_CB_PROVIDER_FQN.equals(dep)) // ignore the MethodToolCallbackProvider 137 | .map(clazzName -> { 138 | String clazzSimpleName = clazzName.substring(clazzName.lastIndexOf('.') + 1); 139 | String packageName = clazzName.substring(0, clazzName.lastIndexOf('.')); 140 | return String.format(""" 141 | package %s; 142 | public class %s {} 143 | """, packageName, clazzSimpleName); 144 | }) 145 | .toList()); 146 | 147 | // special case for MethodToolCallbackProvider 148 | @Language("java") 149 | String methodCallbackProvider = String.format(""" 150 | package %s; 151 | 152 | import java.util.List; 153 | public class %s { 154 | public static Builder builder() { 155 | return new Builder(); 156 | } 157 | public static class Builder { 158 | private List toolObjects; 159 | private Builder() {} 160 | public Builder toolObjects(Object... toolObjects) { 161 | return this; 162 | } 163 | public MethodToolCallbackProvider build() { 164 | return null; 165 | } 166 | } 167 | """, METHOD_TOOL_CB_PROVIDER_FQN.substring(0, METHOD_TOOL_CB_PROVIDER_FQN.lastIndexOf('.')), METHOD_TOOL_CB_PROVIDER_FQN.substring(METHOD_TOOL_CB_PROVIDER_FQN.lastIndexOf('.') + 1)); 168 | dependsOnList = new ArrayList<>(dependsOnList); 169 | dependsOnList.add(methodCallbackProvider); 170 | return dependsOnList.toArray(new String[0]); 171 | } 172 | 173 | @Override 174 | public @NlsRewrite.DisplayName @NotNull String getDisplayName() { 175 | return "Add tool callback provider bean"; 176 | } 177 | 178 | @Override 179 | public @NlsRewrite.Description @NotNull String getDescription() { 180 | return "Add tool callback provider bean to the class annotated with @SpringBootApplication."; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/spring/ai/mcp/visitor/McpToolVisitor.java: -------------------------------------------------------------------------------- 1 | package org.openrewrite.java.spring.ai.mcp.visitor; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Value; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.openrewrite.ExecutionContext; 7 | import org.openrewrite.java.JavaIsoVisitor; 8 | import org.openrewrite.java.search.FindAnnotations; 9 | import org.openrewrite.java.tree.J; 10 | 11 | import java.util.Set; 12 | 13 | /** 14 | * This visitor is used to find all classes that have the @Tool annotation. 15 | * It collects the fully qualified names of these classes into a set. 16 | */ 17 | @Value 18 | @EqualsAndHashCode(callSuper = false) 19 | public class McpToolVisitor extends JavaIsoVisitor { 20 | 21 | @NotNull Set toolSet; 22 | 23 | @Override 24 | public J.@NotNull ClassDeclaration visitClassDeclaration(J.@NotNull ClassDeclaration classDecl, @NotNull ExecutionContext ctx) { 25 | boolean toolFound = classDecl.getBody().getStatements().stream() 26 | .filter(s -> s instanceof J.MethodDeclaration) 27 | .map(s -> (J.MethodDeclaration) s) 28 | .anyMatch(m -> !FindAnnotations.find(m, "@org.springframework.ai.tool.annotation.Tool").isEmpty()); 29 | if (toolFound && classDecl.getType() != null) { 30 | toolSet.add(classDecl.getType().getFullyQualifiedName()); 31 | } 32 | return super.visitClassDeclaration(classDecl, ctx); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/spring/ai/mcp/visitor/SpringAIMcpVisitor.java: -------------------------------------------------------------------------------- 1 | package org.openrewrite.java.spring.ai.mcp.visitor; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Value; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jspecify.annotations.NonNull; 7 | import org.openrewrite.maven.MavenIsoVisitor; 8 | import org.openrewrite.maven.search.FindDependency; 9 | import org.openrewrite.xml.tree.Xml; 10 | 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | @Value 14 | @EqualsAndHashCode(callSuper = false) 15 | public class SpringAIMcpVisitor

extends MavenIsoVisitor

{ 16 | 17 | @NonNull AtomicBoolean aiMcpEnabled; 18 | 19 | @Override 20 | public Xml.@NotNull Document visitDocument(Xml.@NotNull Document document, @NotNull P p) { 21 | if (!FindDependency.find(document, "org.springframework.ai", "spring-ai-starter-mcp-server-webmvc").isEmpty()) {// supports spring-ai-starter-mcp-server-webmvc only 22 | aiMcpEnabled.set(true); 23 | } 24 | return super.visitDocument(document, p); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/rewrite/mcp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | type: specs.openrewrite.org/v1beta/recipe 3 | name: UpdatePom 4 | description: Update POM to use Spring AI Maven repository and Spring AI MCP dependencies 5 | causesAnotherCycle: true 6 | recipeList: 7 | - org.openrewrite.maven.AddRepository: 8 | id: spring-snapshots 9 | repoName: 'Spring Snapshots' 10 | url: https://repo.spring.io/snapshot 11 | releasesEnabled: false 12 | - org.openrewrite.maven.AddRepository: 13 | id: central-portal-snapshots 14 | repoName: 'Central Portal Snapshots' 15 | url: https://central.sonatype.com/repository/maven-snapshots/ 16 | releasesEnabled: false 17 | snapshotsEnabled: true 18 | - org.openrewrite.maven.AddDependency: 19 | groupId: org.springframework.ai 20 | artifactId: spring-ai-starter-mcp-server-webmvc 21 | version: 1.0.0-SNAPSHOT 22 | 23 | 24 | --- 25 | type: specs.openrewrite.org/v1beta/recipe 26 | name: SpringBoot3 27 | recipeList: 28 | - org.openrewrite.maven.search.FindDependency: 29 | groupId: org.springframework 30 | artifactId: spring-webmvc 31 | versionPattern: '6.0.0 - 6.2.2' 32 | 33 | --- 34 | type: specs.openrewrite.org/v1beta/recipe 35 | name: RewriteWebToMCP 36 | description: Rewrite Web REST API to MCP Server 37 | recipeList: 38 | - UpdatePom 39 | - org.openrewrite.java.spring.ai.mcp.recipe.mcp.AddToolAnnotationToMappingMethod 40 | - org.openrewrite.java.spring.ai.mcp.recipe.mcp.AddToolCallbackProviderBean 41 | - org.openrewrite.java.spring.ai.mcp.recipe.config.AddSpringAIMcpProperties -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/spring/ai/mcp/recipe/config/AddSpringAIMcpPropertiesTest.java: -------------------------------------------------------------------------------- 1 | package org.openrewrite.java.spring.ai.mcp.recipe.config; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.openrewrite.InMemoryExecutionContext; 5 | import org.openrewrite.maven.MavenExecutionContextView; 6 | import org.openrewrite.maven.MavenSettings; 7 | import org.openrewrite.test.RecipeSpec; 8 | import org.openrewrite.test.RewriteTest; 9 | 10 | import static org.openrewrite.java.spring.ai.mcp.recipe.mcp.AddToolAnnotationToMappingMethodTest.pom; 11 | import static org.openrewrite.maven.Assertions.pomXml; 12 | import static org.openrewrite.properties.Assertions.properties; 13 | import static org.openrewrite.yaml.Assertions.yaml; 14 | 15 | class AddSpringAIMcpPropertiesTest implements RewriteTest { 16 | @Override 17 | public void defaults(RecipeSpec spec) { 18 | InMemoryExecutionContext ctx = new InMemoryExecutionContext(); 19 | MavenExecutionContextView context = MavenExecutionContextView.view(ctx).setMavenSettings(MavenSettings.readMavenSettingsFromDisk(ctx)); 20 | spec.recipe(new AddSpringAIMcpProperties(null)) 21 | .executionContext(context); 22 | } 23 | 24 | @Test 25 | public void addToPropertiesSuccess() { 26 | rewriteRun( 27 | pomXml(pom), 28 | properties(""" 29 | server.port=8080 30 | """, """ 31 | server.port=8080 32 | spring.ai.mcp.server.name=webmvc-mcp-server 33 | spring.ai.mcp.server.sse-message-endpoint=/mcp/messages 34 | spring.ai.mcp.server.type=SYNC 35 | spring.ai.mcp.server.version=1.0.0 36 | """, spec -> spec.path("src/main/resources/application.properties")) 37 | ); 38 | } 39 | 40 | @Test 41 | public void addToYamlSuccess() { 42 | rewriteRun( 43 | pomXml(pom), 44 | yaml(""" 45 | server: 46 | port: 8080 47 | """, """ 48 | server: 49 | port: 8080 50 | spring: 51 | ai: 52 | mcp: 53 | server: 54 | name: webmvc-mcp-server 55 | version: 1.0.0 56 | type: SYNC 57 | sse-message-endpoint: /mcp/messages 58 | """, spec -> spec.path("src/main/resources/application.yml")) 59 | ); 60 | } 61 | } -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/spring/ai/mcp/recipe/mcp/AddToolAnnotationToMappingMethodTest.java: -------------------------------------------------------------------------------- 1 | package org.openrewrite.java.spring.ai.mcp.recipe.mcp; 2 | 3 | import org.intellij.lang.annotations.Language; 4 | import org.junit.jupiter.api.Test; 5 | import org.openrewrite.ExecutionContext; 6 | import org.openrewrite.InMemoryExecutionContext; 7 | import org.openrewrite.maven.MavenExecutionContextView; 8 | import org.openrewrite.maven.MavenSettings; 9 | import org.openrewrite.test.RecipeSpec; 10 | import org.openrewrite.test.RewriteTest; 11 | 12 | import static org.openrewrite.java.Assertions.java; 13 | import static org.openrewrite.maven.Assertions.pomXml; 14 | 15 | public class AddToolAnnotationToMappingMethodTest implements RewriteTest { 16 | @Override 17 | public void defaults(RecipeSpec spec) { 18 | spec.recipe(new AddToolAnnotationToMappingMethod()); 19 | } 20 | 21 | @Test 22 | public void addAnnotationsSuccess() { 23 | ExecutionContext ctx = new InMemoryExecutionContext(); 24 | ExecutionContext context = MavenExecutionContextView.view(ctx).setMavenSettings(MavenSettings.readMavenSettingsFromDisk(ctx)); 25 | rewriteRun( 26 | spec -> spec.executionContext(context), 27 | pomXml(pom), 28 | java(originHelloController, expectedHelloTool), 29 | java(originUserController, expectedUserTool) 30 | ); 31 | } 32 | 33 | @Test 34 | public void skipDueToNoDependency() { 35 | rewriteRun( 36 | java(originHelloController) 37 | ); 38 | } 39 | 40 | @Test 41 | public void skipDueToNotBean() { 42 | ExecutionContext ctx = new InMemoryExecutionContext(); 43 | ExecutionContext context = MavenExecutionContextView.view(ctx).setMavenSettings(MavenSettings.readMavenSettingsFromDisk(ctx)); 44 | 45 | rewriteRun( 46 | spec -> spec.executionContext(context), 47 | pomXml(pom), 48 | java(""" 49 | package com.atbug.rewrite.test.controller; 50 | 51 | import org.springframework.web.bind.annotation.GetMapping; 52 | 53 | public class HelloController { 54 | 55 | @GetMapping("/hi") 56 | public String hello() { 57 | return "Hello, OpenRewrite"; 58 | } 59 | } 60 | """) 61 | ); 62 | } 63 | 64 | @Language("java") 65 | public static final String originHelloController = """ 66 | package com.atbug.rewrite.test.controller; 67 | 68 | import org.springframework.web.bind.annotation.RequestMapping; 69 | import org.springframework.web.bind.annotation.PutMapping; 70 | import org.springframework.web.bind.annotation.GetMapping; 71 | import org.springframework.web.bind.annotation.PostMapping; 72 | import org.springframework.web.bind.annotation.PatchMapping; 73 | import org.springframework.web.bind.annotation.DeleteMapping; 74 | import org.springframework.web.bind.annotation.PathVariable; 75 | import org.springframework.web.bind.annotation.RestController; 76 | 77 | @RestController 78 | public class HelloController { 79 | 80 | @GetMapping("/hi") 81 | public String hello() { 82 | return "Hello, OpenRewrite"; 83 | } 84 | 85 | /** 86 | * say hello to someone 87 | * @param name name of the guy you want to say hello 88 | * @return hello message 89 | */ 90 | @RequestMapping("/hi/{name}") 91 | @GetMapping(path = "/hi/{name}") 92 | @PostMapping("/hi/{name}") 93 | @PutMapping("/hi/{name}") 94 | @PatchMapping("/hi/{name}") 95 | @DeleteMapping("/hi/{name}") 96 | public String helloTo(@PathVariable("name") String name) { 97 | return "Hello, " + name; 98 | } 99 | 100 | public String helloWithoutMapping() { 101 | return "Hello, OpenRewrite"; 102 | } 103 | } 104 | """; 105 | 106 | @Language("java") 107 | public static final String expectedHelloTool = """ 108 | package com.atbug.rewrite.test.controller; 109 | 110 | import org.springframework.web.bind.annotation.RequestMapping; 111 | import org.springframework.web.bind.annotation.PutMapping; 112 | import org.springframework.web.bind.annotation.GetMapping; 113 | import org.springframework.web.bind.annotation.PostMapping; 114 | import org.springframework.web.bind.annotation.PatchMapping; 115 | import org.springframework.ai.tool.annotation.Tool; 116 | import org.springframework.ai.tool.annotation.ToolParam; 117 | import org.springframework.web.bind.annotation.DeleteMapping; 118 | import org.springframework.web.bind.annotation.PathVariable; 119 | import org.springframework.web.bind.annotation.RestController; 120 | 121 | @RestController 122 | public class HelloController { 123 | 124 | @GetMapping("/hi") 125 | @Tool(description = "hello") 126 | public String hello() { 127 | return "Hello, OpenRewrite"; 128 | } 129 | 130 | /** 131 | * say hello to someone 132 | * @param name name of the guy you want to say hello 133 | * @return hello message 134 | */ 135 | @RequestMapping("/hi/{name}") 136 | @GetMapping(path = "/hi/{name}") 137 | @PostMapping("/hi/{name}") 138 | @PutMapping("/hi/{name}") 139 | @PatchMapping("/hi/{name}") 140 | @DeleteMapping("/hi/{name}") 141 | @Tool(description = "say hello to someone") 142 | public String helloTo(@PathVariable("name") @ToolParam(description = "name of the guy you want to say hello") String name) { 143 | return "Hello, " + name; 144 | } 145 | 146 | public String helloWithoutMapping() { 147 | return "Hello, OpenRewrite"; 148 | } 149 | } 150 | """; 151 | 152 | @Language("java") 153 | public static final String originUserController = """ 154 | package com.atbug.rewrite.test.controller; 155 | 156 | import org.springframework.web.bind.annotation.GetMapping; 157 | import org.springframework.web.bind.annotation.PostMapping; 158 | import org.springframework.web.bind.annotation.RestController; 159 | 160 | import java.util.ArrayList; 161 | import java.util.List; 162 | 163 | @RestController 164 | public class UserController { 165 | 166 | public record User(String name, String email) {} 167 | 168 | private final List users = new ArrayList<>(List.of(new User("John", "john@example.com"), new User("Jane", "jane@example.com"))); 169 | 170 | @GetMapping("/users") 171 | public List getUsers() { 172 | return users; 173 | } 174 | 175 | @PostMapping("/users") 176 | public String addUser(User user) { 177 | users.add(user); 178 | return "User added successfully!"; 179 | } 180 | } 181 | """; 182 | 183 | @Language("java") 184 | public static final String expectedUserTool = """ 185 | package com.atbug.rewrite.test.controller; 186 | 187 | import org.springframework.ai.tool.annotation.Tool; 188 | import org.springframework.ai.tool.annotation.ToolParam; 189 | import org.springframework.web.bind.annotation.GetMapping; 190 | import org.springframework.web.bind.annotation.PostMapping; 191 | import org.springframework.web.bind.annotation.RestController; 192 | 193 | import java.util.ArrayList; 194 | import java.util.List; 195 | 196 | @RestController 197 | public class UserController { 198 | 199 | public record User(String name, String email) {} 200 | 201 | private final List users = new ArrayList<>(List.of(new User("John", "john@example.com"), new User("Jane", "jane@example.com"))); 202 | 203 | @GetMapping("/users") 204 | @Tool(description = "getUsers") 205 | public List getUsers() { 206 | return users; 207 | } 208 | 209 | @PostMapping("/users") 210 | @Tool(description = "addUser") 211 | public String addUser(@ToolParam(description = "user") User user) { 212 | users.add(user); 213 | return "User added successfully!"; 214 | } 215 | } 216 | """; 217 | 218 | @Language("xml") 219 | public static final String pom = """ 220 | 221 | com.atbug.rewrite 222 | web-to-mcp 223 | 1.0-SNAPSHOT 224 | 225 | 226 | org.springframework.ai 227 | spring-ai-starter-mcp-server-webmvc 228 | 1.0.0-SNAPSHOT 229 | 230 | 231 | 232 | 233 | spring-snapshots 234 | Spring Snapshots 235 | https://repo.spring.io/snapshot 236 | 237 | false 238 | 239 | 240 | 241 | Central Portal Snapshots 242 | central-portal-snapshots 243 | https://central.sonatype.com/repository/maven-snapshots/ 244 | 245 | false 246 | 247 | 248 | true 249 | 250 | 251 | 252 | 253 | """; 254 | } -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/spring/ai/mcp/recipe/mcp/AddToolCallbackProviderBeanTest.java: -------------------------------------------------------------------------------- 1 | package org.openrewrite.java.spring.ai.mcp.recipe.mcp; 2 | 3 | import org.intellij.lang.annotations.Language; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import org.openrewrite.test.RecipeSpec; 7 | import org.openrewrite.test.RewriteTest; 8 | 9 | import static org.openrewrite.java.Assertions.java; 10 | 11 | class AddToolCallbackProviderBeanTest implements RewriteTest { 12 | @Override 13 | public void defaults(RecipeSpec spec) { 14 | spec.recipes(new AddToolCallbackProviderBean()); 15 | } 16 | 17 | @Test 18 | public void addToolCallbackProviderBeanSuccess() { 19 | rewriteRun( 20 | java(AddToolAnnotationToMappingMethodTest.expectedHelloTool), 21 | java(entryClassWithoutTargetBeanMethod, entryClassWithTargetBeanMethod) 22 | ); 23 | } 24 | 25 | @Test 26 | public void successUpdateBeanDefinition() { 27 | rewriteRun( 28 | java(AddToolAnnotationToMappingMethodTest.expectedHelloTool), 29 | java(AddToolAnnotationToMappingMethodTest.expectedUserTool), 30 | java(entryClassWithTargetBeanMethod, entryClassWithBeanMethodUpdated) 31 | ); 32 | } 33 | 34 | @Test 35 | public void failDueToBadSituation() { 36 | Assertions.assertThrows(AssertionError.class, () -> rewriteRun( 37 | java(AddToolAnnotationToMappingMethodTest.expectedHelloTool), 38 | java(entryClassWithDuplicatedTargetBeanMethod, entryClassWithDuplicatedTargetBeanMethodWithFailMessage) 39 | ), "There should be at most one method with return type ToolCallbackProvider"); 40 | } 41 | 42 | @Language("java") 43 | public static final String entryClassWithoutTargetBeanMethod = """ 44 | package com.atbug.rewrite.test; 45 | 46 | import org.springframework.boot.SpringApplication; 47 | import org.springframework.boot.autoconfigure.SpringBootApplication; 48 | 49 | @SpringBootApplication 50 | public class SpringMainApp { 51 | 52 | public static void main(String[] args) { 53 | SpringApplication.run(SpringMainApp.class, args); 54 | } 55 | } 56 | """; 57 | 58 | @Language("java") 59 | public static final String entryClassWithTargetBeanMethod = """ 60 | package com.atbug.rewrite.test; 61 | 62 | import com.atbug.rewrite.test.controller.HelloController; 63 | import org.springframework.ai.tool.ToolCallbackProvider; 64 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 65 | import org.springframework.boot.SpringApplication; 66 | import org.springframework.boot.autoconfigure.SpringBootApplication; 67 | import org.springframework.context.annotation.Bean; 68 | 69 | @SpringBootApplication 70 | public class SpringMainApp { 71 | 72 | public static void main(String[] args) { 73 | SpringApplication.run(SpringMainApp.class, args); 74 | } 75 | 76 | @Bean 77 | ToolCallbackProvider toolCallbackProvider(HelloController helloController) { 78 | return MethodToolCallbackProvider.builder() 79 | .toolObjects(helloController) 80 | .build(); 81 | } 82 | } 83 | """; 84 | 85 | @Language("java") 86 | public static final String entryClassWithBeanMethodUpdated = """ 87 | package com.atbug.rewrite.test; 88 | 89 | import com.atbug.rewrite.test.controller.HelloController; 90 | import com.atbug.rewrite.test.controller.UserController; 91 | import org.springframework.ai.tool.ToolCallbackProvider; 92 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 93 | import org.springframework.boot.SpringApplication; 94 | import org.springframework.boot.autoconfigure.SpringBootApplication; 95 | import org.springframework.context.annotation.Bean; 96 | 97 | @SpringBootApplication 98 | public class SpringMainApp { 99 | 100 | public static void main(String[] args) { 101 | SpringApplication.run(SpringMainApp.class, args); 102 | } 103 | 104 | @Bean 105 | ToolCallbackProvider toolCallbackProvider(HelloController helloController, UserController userController) { 106 | return MethodToolCallbackProvider.builder() 107 | .toolObjects(helloController, userController) 108 | .build(); 109 | } 110 | } 111 | """; 112 | 113 | @Language("java") 114 | public static final String entryClassWithDuplicatedTargetBeanMethod = """ 115 | package com.atbug.rewrite.test; 116 | 117 | import org.springframework.ai.tool.ToolCallbackProvider; 118 | import com.atbug.rewrite.test.controller.HelloController; 119 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 120 | import org.springframework.boot.SpringApplication; 121 | import org.springframework.boot.autoconfigure.SpringBootApplication; 122 | import org.springframework.context.annotation.Bean; 123 | 124 | @SpringBootApplication 125 | public class SpringMainApp { 126 | 127 | public static void main(String[] args) { 128 | SpringApplication.run(SpringMainApp.class, args); 129 | } 130 | 131 | @Bean 132 | ToolCallbackProvider toolCallbackProvider() { 133 | return MethodToolCallbackProvider.builder() 134 | .toolObjects(new Object()) 135 | .build(); 136 | } 137 | 138 | @Bean 139 | ToolCallbackProvider toolCallbackProvider2() { 140 | return MethodToolCallbackProvider.builder() 141 | .toolObjects(new Object()) 142 | .build(); 143 | } 144 | } 145 | """; 146 | 147 | @Language("java") 148 | public static final String entryClassWithDuplicatedTargetBeanMethodWithFailMessage = """ 149 | package com.atbug.rewrite.test; 150 | 151 | import org.springframework.ai.tool.ToolCallbackProvider; 152 | import com.atbug.rewrite.test.controller.HelloController; 153 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 154 | import org.springframework.boot.SpringApplication; 155 | import org.springframework.boot.autoconfigure.SpringBootApplication; 156 | import org.springframework.context.annotation.Bean; 157 | 158 | /*~~(There should be at most one method with return type ToolCallbackProvider)~~>*/@SpringBootApplication 159 | public class SpringMainApp { 160 | 161 | public static void main(String[] args) { 162 | SpringApplication.run(SpringMainApp.class, args); 163 | } 164 | 165 | @Bean 166 | ToolCallbackProvider toolCallbackProvider() { 167 | return MethodToolCallbackProvider.builder() 168 | .toolObjects(new Object()) 169 | .build(); 170 | } 171 | 172 | @Bean 173 | ToolCallbackProvider toolCallbackProvider2() { 174 | return MethodToolCallbackProvider.builder() 175 | .toolObjects(new Object()) 176 | .build(); 177 | } 178 | } 179 | """; 180 | } --------------------------------------------------------------------------------