#
tokens: 5798/50000 10/10 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── assets
│   ├── get-server.png
│   ├── list-flavors.png
│   └── list-servers.png
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── README.md
└── src
    ├── main
    │   ├── java
    │   │   └── ro
    │   │       └── dragomiralin
    │   │           └── openstack_mcp_server
    │   │               ├── OpenstackMcpServerApplication.java
    │   │               └── service
    │   │                   └── OpenStackCommander.java
    │   └── resources
    │       └── application.yml
    └── test
        ├── java
        │   └── ro
        │       └── dragomiralin
        │           └── openstack_mcp_server
        │               └── OpenstackMcpServerApplicationTests.java
        └── resources
            └── application.yml
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Created by https://www.toptal.com/developers/gitignore/api/intellij+all
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all

### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# AWS User-specific
.idea/**/aws.xml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn.  Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

target

/src/main/resources/application-dev.yml
# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# SonarLint plugin
.idea/sonarlint/

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### Intellij+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.

.idea/*

!.idea/codeStyles
!.idea/runConfigurations

# End of https://www.toptal.com/developers/gitignore/api/intellij+all
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# OpenStack MCP Server

## Overview
A lightweight and extensible service that enables AI assistants to securely execute OpenStack CLI commands via the Model Context Protocol (MCP).

This project is designed to work with the [Claude Desktop](https://www.anthropic.com/claude-desktop) application, allowing you to interact with OpenStack resources directly from the AI assistant.


## Project Requirements

- Java 21
- Maven 3.8+
- Spring Boot 3.4.4
- Spring AI 1.0.0-M6
- OpenStack CLI installed on your machine

## Getting Started

### Prerequisites

Before you begin, ensure you have the following installed:
- Java 21
- Maven 3.8+
- OpenStack CLI

### Configuration

1. **OpenStack Configuration**: Update the `application.yml` file with your OpenStack credentials.
   - `authUrl`: Your OpenStack authentication URL.
   - `regionName`: Your OpenStack region name.
   - `applicationCredentialId`: Your OpenStack application credential ID.
   - `applicationCredentialSecret`: Your OpenStack application credential secret.

```yaml
spring:
  main:
    web-application-type: none
    banner-mode: off
  ai:
    mcp:
      server:
        name: openstack-mcp-server
        version: 0.0.1

logging:
  pattern:
    console:

openstack:
  authType: v3applicationcredential
  authUrl: <your_auth_url>
  identityApiVersion: 3
  regionName: <your_region_name>
  interface: public
  applicationCredentialId: "<your_application_credential_id>"
  applicationCredentialSecret: "<your_application_credential_secret>"

server:
  port: 8080
```

### Packaging

To package the application as a JAR file, run the following command:

```bash
mvn clean package
```

### Integration with Claude Desktop

To integrate the OpenStack MCP server with Claude Desktop, you need to configure the `claude-desktop.json` file. This file contains the necessary configuration for the integration.

Please update `<path_to_your_jar>` with the actual path to your JAR file.

```json
{
  "mcpServers": {
    "openstack-mcp-server": {
      "command": "java",
      "args": [
        "-jar",
        "<path_to_your_jar>/openstack-mcp-server-0.0.1.jar",
        "--port",
        "8080",
        "--host",
        "localhost"
      ]
    }
  }
}
```

### Testing

To test the integration, prompt to Claude Desktop to list your servers, or projects:

- `List my servers`
- `List my projects`
- `List my images`

![get-server](/assets/get-server.png)
![get-flavors](/assets/list-flavors.png)
![get-servers](/assets/list-servers.png)
```

--------------------------------------------------------------------------------
/src/test/java/ro/dragomiralin/openstack_mcp_server/OpenstackMcpServerApplicationTests.java:
--------------------------------------------------------------------------------

```java
package ro.dragomiralin.openstack_mcp_server;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class OpenstackMcpServerApplicationTests {

	@Test
	void contextLoads() {
	}

}

```

--------------------------------------------------------------------------------
/src/test/resources/application.yml:
--------------------------------------------------------------------------------

```yaml
spring:
  main:
    web-application-type: none
    banner-mode: off
  ai:
    mcp:
      server:
        name: openstack-mcp-server
        version: 0.0.1

logging:
  pattern:
    console:

openstack:
  authType: ""
  authUrl: ""
  identityApiVersion: "3"
  regionName: ""
  interface: public
  applicationCredentialId: ""
  applicationCredentialSecret: ""

server:
  port: 8080
```

--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------

```yaml
spring:
  main:
    web-application-type: none
    banner-mode: off
  ai:
    mcp:
      server:
        name: openstack-mcp-server
        version: 0.0.1

logging:
  pattern:
    console:

openstack:
  authType: v3applicationcredential
  authUrl: ""
  identityApiVersion: "3"
  regionName: ""
  interface: public
  applicationCredentialId: ""
  applicationCredentialSecret: ""

server:
  port: 8080
```

--------------------------------------------------------------------------------
/src/main/java/ro/dragomiralin/openstack_mcp_server/OpenstackMcpServerApplication.java:
--------------------------------------------------------------------------------

```java
package ro.dragomiralin.openstack_mcp_server;

import java.util.List;

import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbacks;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import ro.dragomiralin.openstack_mcp_server.service.OpenStackCommander;

@SpringBootApplication
public class OpenstackMcpServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(OpenstackMcpServerApplication.class, args);
	}

	@Bean
	public List<ToolCallback> openstackTools(OpenStackCommander openStackCommander) {
		return List.of(ToolCallbacks.from(openStackCommander));
	}

}

```

--------------------------------------------------------------------------------
/HELP.md:
--------------------------------------------------------------------------------

```markdown
# Read Me First
The following was discovered as part of building this project:

* The original package name 'ro.dragomiralin.openstack-mcp-server' is invalid and this project uses 'ro.dragomiralin.openstack_mcp_server' instead.

# Getting Started

### Reference Documentation
For further reference, please consider the following sections:

* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.4/maven-plugin)
* [Create an OCI image](https://docs.spring.io/spring-boot/3.4.4/maven-plugin/build-image.html)
* [Model Context Protocol Client](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html)
* [OpenAI](https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html)

### Maven Parent overrides

Due to Maven's design, elements are inherited from the parent POM to the project POM.
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent.
To prevent this, the project POM contains empty overrides for these elements.
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.


```

--------------------------------------------------------------------------------
/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 https://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.4</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>ro.dragomiralin</groupId>
	<artifactId>openstack-mcp-server</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>openstack-mcp-server</name>
	<description>OpenStack MCP Server</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>21</java.version>
		<spring-ai.version>1.0.0-M6</spring-ai.version>
	</properties>
	<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>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.ai</groupId>
				<artifactId>spring-ai-bom</artifactId>
				<version>${spring-ai.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

```

--------------------------------------------------------------------------------
/src/main/java/ro/dragomiralin/openstack_mcp_server/service/OpenStackCommander.java:
--------------------------------------------------------------------------------

```java
package ro.dragomiralin.openstack_mcp_server.service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class OpenStackCommander {

    @Value("${openstack.authType}")
    private String authType;
    @Value("${openstack.authUrl}")
    private String authUrl;
    @Value("${openstack.identityApiVersion}")
    private String apiVersion;
    @Value("${openstack.regionName}")
    private String region;
    @Value("${openstack.interface}")
    private String iface;
    @Value("${openstack.applicationCredentialId}")
    private String appCredId;
    @Value("${openstack.applicationCredentialSecret}")
    private String appCredSecret;

    private static final int PROCESS_TIMEOUT_SECONDS = 60;

    /**
     * Runs an OpenStack command using the configured connection credentials.
     *
     * @param command The command to execute
     * @return The output of the command execution
     */
    @Tool(name = "run_openstack_command", description = "Run OpenStack command")
    public String runOpenStackCommand(String command) {
        if (isOpenStackInstalled()) {
            return execute(command);
        } else {
            return getOpenStackDiagnostics();
        }
    }

    /**
     * Executes an OpenStack command with the configured connection credentials.
     *
     * @param command The command to execute
     * @return The output of the command execution
     */
    public String execute(String command) {
        try {
            List<String> cmd = List.of("/bin/sh", "-c", command);
            ProcessBuilder builder = new ProcessBuilder(cmd);
            builder.redirectErrorStream(true);

            Map<String, String> env = builder.environment();
            env.put("OS_AUTH_TYPE", authType);
            env.put("OS_AUTH_URL", authUrl);
            env.put("OS_IDENTITY_API_VERSION", apiVersion);
            env.put("OS_REGION_NAME", region);
            env.put("OS_INTERFACE", iface);
            env.put("OS_APPLICATION_CREDENTIAL_ID", appCredId);
            env.put("OS_APPLICATION_CREDENTIAL_SECRET", appCredSecret);

            Process process = builder.start();
            StringBuilder output = new StringBuilder();

            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    output.append(line)
                        .append(System.lineSeparator());
                }
            }

            if (!process.waitFor(PROCESS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
                process.destroyForcibly();
                return "Command execution timed out after " + PROCESS_TIMEOUT_SECONDS + " seconds";
            }

            int exitCode = process.exitValue();
            if (exitCode != 0) {
                return "Command execution failed with exit code: " + exitCode;
            }

            return output.toString();
        } catch (IOException | InterruptedException e) {
            Thread.currentThread()
                .interrupt();
            return "An error occurred while executing the command: " + e.getMessage();
        }
    }

    /**
     * Checks if OpenStack is installed on the system.
     *
     * @return true if OpenStack is installed, false otherwise
     */
    public boolean isOpenStackInstalled() {
        String result = execute("which openstack");
        return result != null && !result.trim()
            .isEmpty() && !result.contains("Command execution failed");
    }

    /**
     * Provides diagnostics information about the OpenStack installation and configuration.
     *
     * @return A string containing diagnostics information
     */
    public String getOpenStackDiagnostics() {
        StringJoiner diagnostics = new StringJoiner("\n");

        String whichResult = execute("which openstack");
        diagnostics.add("OpenStack binary location: " + (whichResult.contains("Command execution failed") ? "Not found" : whichResult.trim()));

        String versionResult = execute("openstack --version");
        diagnostics.add("OpenStack version: " + (versionResult.contains("Command execution failed") ? "Unable to determine" : versionResult.trim()));

        diagnostics.add("Environment configuration:");
        diagnostics.add("  OS_AUTH_TYPE: " + (authType != null ? "Set" : "Not set"));
        diagnostics.add("  OS_AUTH_URL: " + (authUrl != null ? "Set" : "Not set"));
        diagnostics.add("  OS_IDENTITY_API_VERSION: " + (apiVersion != null ? "Set" : "Not set"));
        diagnostics.add("  OS_REGION_NAME: " + (region != null ? "Set" : "Not set"));
        diagnostics.add("  OS_INTERFACE: " + (iface != null ? "Set" : "Not set"));
        diagnostics.add("  OS_APPLICATION_CREDENTIAL_ID: " + (appCredId != null ? "Set" : "Not set"));
        diagnostics.add("  OS_APPLICATION_CREDENTIAL_SECRET: " + (appCredSecret != null ? "Set (hidden)" : "Not set"));

        return diagnostics.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"

```