Skip to content

Commit 46f3bce

Browse files
committed
GH-1593: integrate basic mcp server into spring language server
Fixes GH-1593
1 parent decb736 commit 46f3bce

File tree

8 files changed

+309
-3
lines changed

8 files changed

+309
-3
lines changed

headless-services/spring-boot-language-server/pom.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<dependencies.version>${project.version}</dependencies.version>
1818
<jdt.core.version>3.41.0</jdt.core.version>
1919
<lsp4xml.version>0.24.0</lsp4xml.version>
20+
<spring-ai.version>1.0.0</spring-ai.version>
2021
</properties>
2122

2223
<repositories>
@@ -52,6 +53,13 @@
5253
</repositories>
5354

5455
<dependencies>
56+
57+
<!-- MCP -->
58+
<dependency>
59+
<groupId>org.springframework.ai</groupId>
60+
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
61+
</dependency>
62+
5563
<dependency>
5664
<groupId>org.springframework</groupId>
5765
<artifactId>spring-context-indexer</artifactId>
@@ -394,4 +402,16 @@
394402

395403
</plugins>
396404
</build>
405+
406+
<dependencyManagement>
407+
<dependencies>
408+
<dependency>
409+
<groupId>org.springframework.ai</groupId>
410+
<artifactId>spring-ai-bom</artifactId>
411+
<version>${spring-ai.version}</version>
412+
<type>pom</type>
413+
<scope>import</scope>
414+
</dependency>
415+
</dependencies>
416+
</dependencyManagement>
397417
</project>

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.boot.SpringApplication;
3434
import org.springframework.boot.SpringBootConfiguration;
3535
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
36+
import org.springframework.boot.autoconfigure.SpringBootApplication;
3637
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
3738
import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration;
3839
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
@@ -137,9 +138,9 @@
137138
ConfigurationPropertiesAutoConfiguration.class,
138139
PropertyPlaceholderAutoConfiguration.class
139140
})
140-
@ComponentScan
141+
@ComponentScan(basePackages = {"org.springframework.ide.vscode.boot.app", "org.springframework.ide.vscode.boot.mcp"})
141142
@EnableConfigurationProperties(BootLsConfigProperties.class)
142-
//@SpringBootApplication
143+
@SpringBootApplication
143144
public class BootLanguageServerBootApp {
144145

145146
private static final String SERVER_NAME = "boot-language-server";
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package org.springframework.ide.vscode.boot.mcp;
2+
3+
public record Dependency(String name, String version) {
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.mcp;
12+
13+
import java.io.File;
14+
import java.util.ArrayList;
15+
import java.util.Collection;
16+
import java.util.List;
17+
import java.util.Optional;
18+
19+
import org.springframework.ide.vscode.commons.java.IJavaProject;
20+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
21+
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
22+
import org.springframework.ide.vscode.commons.protocol.java.Classpath;
23+
import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE;
24+
import org.springframework.ide.vscode.commons.util.text.TextDocument;
25+
import org.springframework.stereotype.Component;
26+
27+
@Component
28+
public class IdeProjectEnvironment {
29+
30+
private SimpleLanguageServer server;
31+
private JavaProjectFinder projectFinder;
32+
33+
public IdeProjectEnvironment(SimpleLanguageServer server, JavaProjectFinder projectFinder) {
34+
this.server = server;
35+
this.projectFinder = projectFinder;
36+
}
37+
38+
public Dependency[] getDependencies(String projectName) {
39+
IJavaProject foundProject = null;
40+
41+
// identify the exact project by name
42+
Collection<? extends IJavaProject> allProjects = projectFinder.all();
43+
for (IJavaProject project : allProjects) {
44+
if (project.getElementName().equals(projectName)) {
45+
foundProject = project;
46+
}
47+
}
48+
49+
// identify the project via the open documents
50+
if (foundProject == null) {
51+
Collection<TextDocument> allOpenDocuments = server.getTextDocumentService().getAll();
52+
53+
if (allOpenDocuments.size() > 0) {
54+
TextDocument firstOpenDoc = allOpenDocuments.iterator().next();
55+
Optional<IJavaProject> optional = projectFinder.find(firstOpenDoc.getId());
56+
if (optional.isPresent()) {
57+
foundProject = optional.get();
58+
}
59+
}
60+
}
61+
62+
// fallback to the first project if nothing else helps
63+
if (foundProject == null) {
64+
if (allProjects.size() > 0) {
65+
foundProject = allProjects.iterator().next();
66+
}
67+
}
68+
69+
// if there is a project, use that
70+
if (foundProject != null) {
71+
List<Dependency> result = new ArrayList<>();
72+
73+
try {
74+
Collection<CPE> entries = foundProject.getClasspath().getClasspathEntries();
75+
for (CPE cpe : entries) {
76+
if (cpe.getKind().equals(Classpath.ENTRY_KIND_BINARY) && !cpe.isSystem()) {
77+
result.add(createDependencyFrom(cpe));
78+
}
79+
}
80+
} catch (Exception e) {
81+
}
82+
83+
return (Dependency[]) result.toArray(new Dependency[result.size()]);
84+
}
85+
86+
return new Dependency[0];
87+
}
88+
89+
private Dependency createDependencyFrom(CPE cpe) {
90+
String path = cpe.getPath();
91+
92+
// strip full path
93+
String name = path.substring(path.lastIndexOf(File.separator) + 1);
94+
95+
// strip version
96+
name = name.substring(0, name.lastIndexOf('-'));
97+
98+
String version = cpe.getVersion() != null ? cpe.getVersion().toString() : "";
99+
100+
return new Dependency(name, version);
101+
}
102+
103+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.mcp;
12+
13+
import java.util.List;
14+
15+
import org.springframework.ai.support.ToolCallbacks;
16+
import org.springframework.ai.tool.ToolCallback;
17+
import org.springframework.context.annotation.Bean;
18+
import org.springframework.context.annotation.Configuration;
19+
20+
/**
21+
* @author Martin Lippert
22+
*/
23+
@Configuration
24+
public class McpConfig {
25+
26+
@Bean
27+
public List<ToolCallback> registerTools(SpringIoApi springIoApi, SpringIndexAccess springIndexAccess) {
28+
return List.of(ToolCallbacks.from(springIoApi, springIndexAccess));
29+
}
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.mcp;
12+
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
import org.springframework.ai.tool.annotation.Tool;
16+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
17+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
18+
import org.springframework.stereotype.Component;
19+
20+
/**
21+
* @author Martin Lippert
22+
*/
23+
@Component
24+
public class SpringIndexAccess {
25+
26+
private static final Logger logger = LoggerFactory.getLogger(SpringIndexAccess.class);
27+
private SpringMetamodelIndex springIndex;
28+
29+
public SpringIndexAccess(SpringMetamodelIndex springIndex) {
30+
this.springIndex = springIndex;
31+
}
32+
33+
@Tool(description = "Get detailed information about the spring beans and their dependencies via injection points of the current projects in the workspace")
34+
public Bean[] getBeanDetails() {
35+
logger.info("get Spring project bean details");
36+
37+
return springIndex.getBeans();
38+
}
39+
40+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.ide.vscode.boot.mcp;
17+
18+
import java.time.LocalDate;
19+
import java.util.List;
20+
import java.util.Objects;
21+
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
import org.springframework.ai.tool.annotation.Tool;
25+
import org.springframework.beans.factory.annotation.Value;
26+
import org.springframework.core.ParameterizedTypeReference;
27+
import org.springframework.http.MediaType;
28+
import org.springframework.stereotype.Component;
29+
import org.springframework.web.client.RestClient;
30+
31+
/**
32+
* @author Martin Lippert
33+
*/
34+
@Component
35+
public class SpringIoApi {
36+
37+
private final RestClient apiClient;
38+
private final RestClient calClient;
39+
private final Long daysFromToday;
40+
41+
private static final Logger logger = LoggerFactory.getLogger(SpringIoApi.class);
42+
43+
public SpringIoApi(@Value("${calendar.window.days:180}") Long daysFromToday) {
44+
this.apiClient = RestClient.builder().baseUrl("https://api.spring.io").build();
45+
this.calClient = RestClient.builder().baseUrl("https://calendar.spring.io").build();
46+
this.daysFromToday = daysFromToday;
47+
}
48+
49+
public record ReleasesRoot(ReleasesEmbedded _embedded) {}
50+
public record ReleasesEmbedded(Release[] releases) {}
51+
public record Release(String version, String status, boolean current) {}
52+
53+
public record GenerationsRoot(GenerationsEmbedded _embedded) {}
54+
public record GenerationsEmbedded(Generation[] generations) {}
55+
public record Generation(String name, String initialReleaseDate, String ossSupportEndDate, String commercialSupportEndDate) {}
56+
57+
public record UpcomingRelease(boolean allDay, String backgroundColor, LocalDate start, String title, String url) {}
58+
59+
@Tool(description = "Get information about Spring project releases")
60+
public Release[] getReleases(String project) {
61+
logger.info("get Spring project releases for: " + project);
62+
ReleasesRoot release = apiClient.get()
63+
.uri(uriBuilder -> uriBuilder.path("/projects/" + project + "/releases").build())
64+
.accept(MediaType.valueOf("application/hal+json"))
65+
.retrieve()
66+
.body(ReleasesRoot.class);
67+
68+
return Objects.requireNonNull(release)._embedded.releases;
69+
}
70+
71+
@Tool(description = "Get information about support ranges and dates for Spring projects")
72+
public Generation[] getGenerations(String project) {
73+
logger.info("get Spring project support dates for: " + project);
74+
GenerationsRoot release = apiClient.get()
75+
.uri(uriBuilder -> uriBuilder.path("/projects/" + project + "/generations").build())
76+
.accept(MediaType.valueOf("application/hal+json"))
77+
.retrieve()
78+
.body(GenerationsRoot.class);
79+
80+
return Objects.requireNonNull(release)._embedded.generations;
81+
}
82+
83+
@Tool(description = "Get information about upcoming releases for Spring projects in the near future")
84+
public List<UpcomingRelease> getUpcomingReleases() {
85+
LocalDate start = LocalDate.now();
86+
LocalDate end = start.plusDays(this.daysFromToday);
87+
logger.info("Get information about upcoming releases for Spring projects in the next " + this.daysFromToday + " days");
88+
89+
return calClient.get()
90+
.uri(uriBuilder -> uriBuilder
91+
.path("/releases")
92+
.queryParam("start", start)
93+
.queryParam("end", end)
94+
.build())
95+
.accept(MediaType.APPLICATION_JSON)
96+
.retrieve()
97+
.body(new ParameterizedTypeReference<>() {
98+
});
99+
}
100+
}

headless-services/spring-boot-language-server/src/main/resources/application.properties

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,11 @@ logging.level.org.springframework.ide.vscode.boot.java.livehover.v2.SpringProces
1212
#logging.level.org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer=debug
1313
#logging.level.org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava=debug
1414
#logging.level.org.springframework.ide.vscode.boot.app.SpringSymbolIndex=debug
15-
#logging.level.org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp=off
15+
#logging.level.org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp=off
16+
17+
# embedded MCP server config
18+
spring.ai.mcp.server.enabled=true
19+
spring.ai.mcp.server.stdio=false
20+
spring.ai.mcp.server.name=spring-language-server-mcp
21+
spring.ai.mcp.server.version=2.0.0
22+
server.port=61375

0 commit comments

Comments
 (0)