#
tokens: 4955/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# 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)

![Sample](images/sample.png)

## 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"

```