# Directory Structure
```
├── .gitignore
├── .mvn
│ └── wrapper
│ └── maven-wrapper.properties
├── images
│ └── sample.png
├── mvnw
├── mvnw.cmd
├── pom.xml
├── README.md
└── src
└── main
├── java
│ └── org
│ └── tanzu
│ └── mcp
│ ├── advisor
│ │ └── AppAdvisorFunctions.java
│ ├── AppAdvisorMcpApplication.java
│ └── McpServerConfig.java
└── resources
└── application.properties
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Spring Application Advisor MCP Server
This MCP Server provides an LLM interface for interacting with [Spring Application Advisor](https://techdocs.broadcom.com/us/en/vmware-tanzu/spring/tanzu-spring/commercial/spring-tanzu/app-advisor-what-is-app-advisor.html). It was built with the [Spring AI MCP](https://spring.io/blog/2024/12/11/spring-ai-mcp-announcement) project, based on an implementation by [@Albertoimpl](https://github.com/Albertoimpl)

## Prerequisites
You need to install the [Advisor CLI](https://techdocs.broadcom.com/us/en/vmware-tanzu/spring/tanzu-spring/commercial/spring-tanzu/app-advisor-run-app-advisor-cli.html) in the path on your machine.
## Building the Server
```bash
./mvnw clean package
```
## Configuration
You will need to supply a configuration for the server for your MCP Client. Here's what the configuration looks like for [claude_desktop_config.json](https://modelcontextprotocol.io/quickstart/user):
```
{
"mcpServers": {
"app-advisor": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.transport=stdio", "-Dlogging.file.name=app-advisor-mcp.log", "-jar" ,
"/Users/pcorby/Projects/OpenAI/app-advisor-mcp/target/app-advisor-mcp-0.0.1-SNAPSHOT.jar",
"--server.port=8041"
],
"env": {
"ADVISOR_SERVER": "http://localhost:8080"
}
}
}
```
```
--------------------------------------------------------------------------------
/src/main/java/org/tanzu/mcp/AppAdvisorMcpApplication.java:
--------------------------------------------------------------------------------
```java
package org.tanzu.mcp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AppAdvisorMcpApplication {
public static void main(String[] args) {
SpringApplication.run(AppAdvisorMcpApplication.class, args);
}
}
```
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
```
spring.application.name=app-advisor-mcp
spring.main.web-application-type=NONE
spring.http.client.factory=simple
spring.ai.mcp.server.stdio=true
spring.ai.mcp.server.name=app-advisor-mcp
spring.ai.mcp.server.version=1.0.0
# NOTE: You must disable the banner and the console logging
# to allow the STDIO transport to work !!!
spring.main.banner-mode=off
logging.pattern.console=
logging.file.name=mcp.webmvc.log
```
--------------------------------------------------------------------------------
/src/main/java/org/tanzu/mcp/McpServerConfig.java:
--------------------------------------------------------------------------------
```java
package org.tanzu.mcp;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbacks;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.tanzu.mcp.advisor.AppAdvisorFunctions;
import java.util.List;
@Configuration
public class McpServerConfig {
@Bean
public List<ToolCallback> roleplayTools(AppAdvisorFunctions appAdvisorFunctions) {
return List.of(ToolCallbacks.from(appAdvisorFunctions));
}
}
```
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
```
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
```
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.2</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.tanzu</groupId>
<artifactId>app-advisor-mcp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>MCP Server for App Advisor</name>
<description>Spring AI MCP server that enables interaction with Spring Application Advisor</description>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<!-- <repositories>-->
<!-- <repository>-->
<!-- <name>Central Portal Snapshots</name>-->
<!-- <id>central-portal-snapshots</id>-->
<!-- <url>https://central.sonatype.com/repository/maven-snapshots/</url>-->
<!-- <releases>-->
<!-- <enabled>false</enabled>-->
<!-- </releases>-->
<!-- <snapshots>-->
<!-- <enabled>true</enabled>-->
<!-- </snapshots>-->
<!-- </repository>-->
<!-- <repository>-->
<!-- <id>spring-snapshots</id>-->
<!-- <name>Spring Snapshots</name>-->
<!-- <url>https://repo.spring.io/snapshot</url>-->
<!-- <releases>-->
<!-- <enabled>false</enabled>-->
<!-- </releases>-->
<!-- </repository>-->
<!-- </repositories>-->
</project>
```
--------------------------------------------------------------------------------
/src/main/java/org/tanzu/mcp/advisor/AppAdvisorFunctions.java:
--------------------------------------------------------------------------------
```java
package org.tanzu.mcp.advisor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@Component
public class AppAdvisorFunctions {
@Value("${ADVISOR_SERVER:http://localhost:8080}")
private String appAdvisorUrl;
private static final String ADVISOR_BUILD_CONFIG_GET = "Generate the build configuration of the source " +
"code repository. This configuration can be used to perform version upgrades of Spring applications." +
"Returns the output of the process.";
private static final String ADVISOR_BUILD_CONFIG_GET_PARAM = "The full directory path to the source code. Use the " +
"path of the current IDE project if possible. If you can not determine the path yourself, ask the user to " +
"provide it.";
@Tool(description = ADVISOR_BUILD_CONFIG_GET)
public String advisorBuildConfigGetFunction(@ToolParam(description = ADVISOR_BUILD_CONFIG_GET_PARAM) String pathToSourceCode) {
return executeCommand("advisor build-config get -p " + pathToSourceCode);
}
private static final String ADVISOR_UPGRADE_PLAN_GET = "Get the upgrade plan of the source code repository. " +
"This function depends on advisorBuildConfigGet to generate the build configuration file. " +
"That command must be executed first if the file in the relative project path: target/.advisor/build-config.json does not exist. " +
"Returns the output of the process.";
private static final String ADVISOR_UPGRADE_PLAN_GET_PARAM = "The full directory path to the source code. Use the " +
"path of the current IDE project if possible. If you can not determine the path yourself, ask the user to " +
"provide it.";
@Tool(description = ADVISOR_UPGRADE_PLAN_GET)
public String advisorUpgradePlanGetFunction(@ToolParam(description = ADVISOR_UPGRADE_PLAN_GET_PARAM) String pathToSourceCode) {
return executeCommand("advisor upgrade-plan get -p " + pathToSourceCode);
}
private static final String ADVISOR_UPGRADE_PLAN_APPLY = "Apply the first step of the upgrade plan of the source code repository. " +
"This function depends on advisorBuildConfigGet to generate the build-configuration file. " +
"That command must be executed first if the file in the relative project path: target/.advisor/build-config.json does not exist. " +
"Verify with the user before performing the apply. " +
"Use this method to perform all version upgrades of Spring applications. Returns the output of the process.";
private static final String ADVISOR_UPGRADE_PLAN_APPLY_PARAM = "The full directory path to the source code. Use the " +
"path of the current IDE project if possible. If you can not determine the path yourself, ask the user to " +
"provide it.";
@Tool(description = ADVISOR_UPGRADE_PLAN_APPLY)
public String advisorUpgradePlanApplyFunction(@ToolParam(description = ADVISOR_UPGRADE_PLAN_APPLY_PARAM) String pathToSourceCode) {
return executeCommand("advisor upgrade-plan apply -p " + pathToSourceCode);
}
private String executeCommand(String command) {
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("sh", "-c", command);
processBuilder.redirectErrorStream(true);
processBuilder.environment().put("ADVISOR_SERVER", appAdvisorUrl);
// Capture the output
Process process;
StringBuilder output = new StringBuilder();
try {
process = processBuilder.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
} catch (IOException e) {
throw new RuntimeException("Could not create process executor: " + e.getMessage());
}
// Wait for the process to complete and get exit code
int exitCode;
try {
exitCode = process.waitFor();
} catch (InterruptedException e) {
throw new RuntimeException("Process was interrupted: " + e.getMessage());
}
if (exitCode != 0) {
throw new RuntimeException("Process did not complete successfully. Exit code: " + exitCode);
}
return output.toString();
}
}
```
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
```
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
```