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 | }
--------------------------------------------------------------------------------