#
tokens: 7692/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
 1 | # Created by https://www.toptal.com/developers/gitignore/api/intellij+all
 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all
 3 | 
 4 | ### Intellij+all ###
 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
 7 | 
 8 | # User-specific stuff
 9 | .idea/**/workspace.xml
10 | .idea/**/tasks.xml
11 | .idea/**/usage.statistics.xml
12 | .idea/**/dictionaries
13 | .idea/**/shelf
14 | 
15 | # AWS User-specific
16 | .idea/**/aws.xml
17 | 
18 | # Generated files
19 | .idea/**/contentModel.xml
20 | 
21 | # Sensitive or high-churn files
22 | .idea/**/dataSources/
23 | .idea/**/dataSources.ids
24 | .idea/**/dataSources.local.xml
25 | .idea/**/sqlDataSources.xml
26 | .idea/**/dynamic.xml
27 | .idea/**/uiDesigner.xml
28 | .idea/**/dbnavigator.xml
29 | 
30 | # Gradle
31 | .idea/**/gradle.xml
32 | .idea/**/libraries
33 | 
34 | # Gradle and Maven with auto-import
35 | # When using Gradle or Maven with auto-import, you should exclude module files,
36 | # since they will be recreated, and may cause churn.  Uncomment if using
37 | # auto-import.
38 | # .idea/artifacts
39 | # .idea/compiler.xml
40 | # .idea/jarRepositories.xml
41 | # .idea/modules.xml
42 | # .idea/*.iml
43 | # .idea/modules
44 | # *.iml
45 | # *.ipr
46 | 
47 | # CMake
48 | cmake-build-*/
49 | 
50 | # Mongo Explorer plugin
51 | .idea/**/mongoSettings.xml
52 | 
53 | # File-based project format
54 | *.iws
55 | 
56 | # IntelliJ
57 | out/
58 | 
59 | target
60 | 
61 | /src/main/resources/application-dev.yml
62 | # mpeltonen/sbt-idea plugin
63 | .idea_modules/
64 | 
65 | # JIRA plugin
66 | atlassian-ide-plugin.xml
67 | 
68 | # Cursive Clojure plugin
69 | .idea/replstate.xml
70 | 
71 | # SonarLint plugin
72 | .idea/sonarlint/
73 | 
74 | # Crashlytics plugin (for Android Studio and IntelliJ)
75 | com_crashlytics_export_strings.xml
76 | crashlytics.properties
77 | crashlytics-build.properties
78 | fabric.properties
79 | 
80 | # Editor-based Rest Client
81 | .idea/httpRequests
82 | 
83 | # Android studio 3.1+ serialized cache file
84 | .idea/caches/build_file_checksums.ser
85 | 
86 | ### Intellij+all Patch ###
87 | # Ignore everything but code style settings and run configurations
88 | # that are supposed to be shared within teams.
89 | 
90 | .idea/*
91 | 
92 | !.idea/codeStyles
93 | !.idea/runConfigurations
94 | 
95 | # End of https://www.toptal.com/developers/gitignore/api/intellij+all
```

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

```markdown
  1 | # OpenStack MCP Server
  2 | 
  3 | ## Overview
  4 | A lightweight and extensible service that enables AI assistants to securely execute OpenStack CLI commands via the Model Context Protocol (MCP).
  5 | 
  6 | 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.
  7 | 
  8 | 
  9 | ## Project Requirements
 10 | 
 11 | - Java 21
 12 | - Maven 3.8+
 13 | - Spring Boot 3.4.4
 14 | - Spring AI 1.0.0-M6
 15 | - OpenStack CLI installed on your machine
 16 | 
 17 | ## Getting Started
 18 | 
 19 | ### Prerequisites
 20 | 
 21 | Before you begin, ensure you have the following installed:
 22 | - Java 21
 23 | - Maven 3.8+
 24 | - OpenStack CLI
 25 | 
 26 | ### Configuration
 27 | 
 28 | 1. **OpenStack Configuration**: Update the `application.yml` file with your OpenStack credentials.
 29 |    - `authUrl`: Your OpenStack authentication URL.
 30 |    - `regionName`: Your OpenStack region name.
 31 |    - `applicationCredentialId`: Your OpenStack application credential ID.
 32 |    - `applicationCredentialSecret`: Your OpenStack application credential secret.
 33 | 
 34 | ```yaml
 35 | spring:
 36 |   main:
 37 |     web-application-type: none
 38 |     banner-mode: off
 39 |   ai:
 40 |     mcp:
 41 |       server:
 42 |         name: openstack-mcp-server
 43 |         version: 0.0.1
 44 | 
 45 | logging:
 46 |   pattern:
 47 |     console:
 48 | 
 49 | openstack:
 50 |   authType: v3applicationcredential
 51 |   authUrl: <your_auth_url>
 52 |   identityApiVersion: 3
 53 |   regionName: <your_region_name>
 54 |   interface: public
 55 |   applicationCredentialId: "<your_application_credential_id>"
 56 |   applicationCredentialSecret: "<your_application_credential_secret>"
 57 | 
 58 | server:
 59 |   port: 8080
 60 | ```
 61 | 
 62 | ### Packaging
 63 | 
 64 | To package the application as a JAR file, run the following command:
 65 | 
 66 | ```bash
 67 | mvn clean package
 68 | ```
 69 | 
 70 | ### Integration with Claude Desktop
 71 | 
 72 | 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.
 73 | 
 74 | Please update `<path_to_your_jar>` with the actual path to your JAR file.
 75 | 
 76 | ```json
 77 | {
 78 |   "mcpServers": {
 79 |     "openstack-mcp-server": {
 80 |       "command": "java",
 81 |       "args": [
 82 |         "-jar",
 83 |         "<path_to_your_jar>/openstack-mcp-server-0.0.1.jar",
 84 |         "--port",
 85 |         "8080",
 86 |         "--host",
 87 |         "localhost"
 88 |       ]
 89 |     }
 90 |   }
 91 | }
 92 | ```
 93 | 
 94 | ### Testing
 95 | 
 96 | To test the integration, prompt to Claude Desktop to list your servers, or projects:
 97 | 
 98 | - `List my servers`
 99 | - `List my projects`
100 | - `List my images`
101 | 
102 | ![get-server](/assets/get-server.png)
103 | ![get-flavors](/assets/list-flavors.png)
104 | ![get-servers](/assets/list-servers.png)
```

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

```java
 1 | package ro.dragomiralin.openstack_mcp_server;
 2 | 
 3 | import org.junit.jupiter.api.Test;
 4 | import org.springframework.boot.test.context.SpringBootTest;
 5 | 
 6 | @SpringBootTest
 7 | class OpenstackMcpServerApplicationTests {
 8 | 
 9 | 	@Test
10 | 	void contextLoads() {
11 | 	}
12 | 
13 | }
14 | 
```

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

```yaml
 1 | spring:
 2 |   main:
 3 |     web-application-type: none
 4 |     banner-mode: off
 5 |   ai:
 6 |     mcp:
 7 |       server:
 8 |         name: openstack-mcp-server
 9 |         version: 0.0.1
10 | 
11 | logging:
12 |   pattern:
13 |     console:
14 | 
15 | openstack:
16 |   authType: ""
17 |   authUrl: ""
18 |   identityApiVersion: "3"
19 |   regionName: ""
20 |   interface: public
21 |   applicationCredentialId: ""
22 |   applicationCredentialSecret: ""
23 | 
24 | server:
25 |   port: 8080
```

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

```yaml
 1 | spring:
 2 |   main:
 3 |     web-application-type: none
 4 |     banner-mode: off
 5 |   ai:
 6 |     mcp:
 7 |       server:
 8 |         name: openstack-mcp-server
 9 |         version: 0.0.1
10 | 
11 | logging:
12 |   pattern:
13 |     console:
14 | 
15 | openstack:
16 |   authType: v3applicationcredential
17 |   authUrl: ""
18 |   identityApiVersion: "3"
19 |   regionName: ""
20 |   interface: public
21 |   applicationCredentialId: ""
22 |   applicationCredentialSecret: ""
23 | 
24 | server:
25 |   port: 8080
```

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

```java
 1 | package ro.dragomiralin.openstack_mcp_server;
 2 | 
 3 | import java.util.List;
 4 | 
 5 | import org.springframework.ai.tool.ToolCallback;
 6 | import org.springframework.ai.tool.ToolCallbacks;
 7 | import org.springframework.boot.SpringApplication;
 8 | import org.springframework.boot.autoconfigure.SpringBootApplication;
 9 | import org.springframework.context.annotation.Bean;
10 | 
11 | import ro.dragomiralin.openstack_mcp_server.service.OpenStackCommander;
12 | 
13 | @SpringBootApplication
14 | public class OpenstackMcpServerApplication {
15 | 
16 | 	public static void main(String[] args) {
17 | 		SpringApplication.run(OpenstackMcpServerApplication.class, args);
18 | 	}
19 | 
20 | 	@Bean
21 | 	public List<ToolCallback> openstackTools(OpenStackCommander openStackCommander) {
22 | 		return List.of(ToolCallbacks.from(openStackCommander));
23 | 	}
24 | 
25 | }
26 | 
```

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

```markdown
 1 | # Read Me First
 2 | The following was discovered as part of building this project:
 3 | 
 4 | * The original package name 'ro.dragomiralin.openstack-mcp-server' is invalid and this project uses 'ro.dragomiralin.openstack_mcp_server' instead.
 5 | 
 6 | # Getting Started
 7 | 
 8 | ### Reference Documentation
 9 | For further reference, please consider the following sections:
10 | 
11 | * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
12 | * [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.4/maven-plugin)
13 | * [Create an OCI image](https://docs.spring.io/spring-boot/3.4.4/maven-plugin/build-image.html)
14 | * [Model Context Protocol Client](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html)
15 | * [OpenAI](https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html)
16 | 
17 | ### Maven Parent overrides
18 | 
19 | Due to Maven's design, elements are inherited from the parent POM to the project POM.
20 | While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent.
21 | To prevent this, the project POM contains empty overrides for these elements.
22 | If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.
23 | 
24 | 
```

--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------

```
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3 | 	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 4 | 	<modelVersion>4.0.0</modelVersion>
 5 | 	<parent>
 6 | 		<groupId>org.springframework.boot</groupId>
 7 | 		<artifactId>spring-boot-starter-parent</artifactId>
 8 | 		<version>3.4.4</version>
 9 | 		<relativePath/> <!-- lookup parent from repository -->
10 | 	</parent>
11 | 	<groupId>ro.dragomiralin</groupId>
12 | 	<artifactId>openstack-mcp-server</artifactId>
13 | 	<version>0.0.1-SNAPSHOT</version>
14 | 	<name>openstack-mcp-server</name>
15 | 	<description>OpenStack MCP Server</description>
16 | 	<url/>
17 | 	<licenses>
18 | 		<license/>
19 | 	</licenses>
20 | 	<developers>
21 | 		<developer/>
22 | 	</developers>
23 | 	<scm>
24 | 		<connection/>
25 | 		<developerConnection/>
26 | 		<tag/>
27 | 		<url/>
28 | 	</scm>
29 | 	<properties>
30 | 		<java.version>21</java.version>
31 | 		<spring-ai.version>1.0.0-M6</spring-ai.version>
32 | 	</properties>
33 | 	<dependencies>
34 | 		<dependency>
35 | 			<groupId>org.springframework.ai</groupId>
36 | 			<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
37 | 		</dependency>
38 | 
39 | 		<dependency>
40 | 			<groupId>org.springframework.boot</groupId>
41 | 			<artifactId>spring-boot-starter-test</artifactId>
42 | 			<scope>test</scope>
43 | 		</dependency>
44 | 	</dependencies>
45 | 	<dependencyManagement>
46 | 		<dependencies>
47 | 			<dependency>
48 | 				<groupId>org.springframework.ai</groupId>
49 | 				<artifactId>spring-ai-bom</artifactId>
50 | 				<version>${spring-ai.version}</version>
51 | 				<type>pom</type>
52 | 				<scope>import</scope>
53 | 			</dependency>
54 | 		</dependencies>
55 | 	</dependencyManagement>
56 | 
57 | 	<build>
58 | 		<plugins>
59 | 			<plugin>
60 | 				<groupId>org.springframework.boot</groupId>
61 | 				<artifactId>spring-boot-maven-plugin</artifactId>
62 | 			</plugin>
63 | 		</plugins>
64 | 	</build>
65 | 
66 | </project>
67 | 
```

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

```java
  1 | package ro.dragomiralin.openstack_mcp_server.service;
  2 | 
  3 | import java.io.BufferedReader;
  4 | import java.io.IOException;
  5 | import java.io.InputStreamReader;
  6 | import java.util.List;
  7 | import java.util.Map;
  8 | import java.util.StringJoiner;
  9 | import java.util.concurrent.TimeUnit;
 10 | 
 11 | import org.springframework.ai.tool.annotation.Tool;
 12 | import org.springframework.beans.factory.annotation.Value;
 13 | import org.springframework.stereotype.Service;
 14 | 
 15 | @Service
 16 | public class OpenStackCommander {
 17 | 
 18 |     @Value("${openstack.authType}")
 19 |     private String authType;
 20 |     @Value("${openstack.authUrl}")
 21 |     private String authUrl;
 22 |     @Value("${openstack.identityApiVersion}")
 23 |     private String apiVersion;
 24 |     @Value("${openstack.regionName}")
 25 |     private String region;
 26 |     @Value("${openstack.interface}")
 27 |     private String iface;
 28 |     @Value("${openstack.applicationCredentialId}")
 29 |     private String appCredId;
 30 |     @Value("${openstack.applicationCredentialSecret}")
 31 |     private String appCredSecret;
 32 | 
 33 |     private static final int PROCESS_TIMEOUT_SECONDS = 60;
 34 | 
 35 |     /**
 36 |      * Runs an OpenStack command using the configured connection credentials.
 37 |      *
 38 |      * @param command The command to execute
 39 |      * @return The output of the command execution
 40 |      */
 41 |     @Tool(name = "run_openstack_command", description = "Run OpenStack command")
 42 |     public String runOpenStackCommand(String command) {
 43 |         if (isOpenStackInstalled()) {
 44 |             return execute(command);
 45 |         } else {
 46 |             return getOpenStackDiagnostics();
 47 |         }
 48 |     }
 49 | 
 50 |     /**
 51 |      * Executes an OpenStack command with the configured connection credentials.
 52 |      *
 53 |      * @param command The command to execute
 54 |      * @return The output of the command execution
 55 |      */
 56 |     public String execute(String command) {
 57 |         try {
 58 |             List<String> cmd = List.of("/bin/sh", "-c", command);
 59 |             ProcessBuilder builder = new ProcessBuilder(cmd);
 60 |             builder.redirectErrorStream(true);
 61 | 
 62 |             Map<String, String> env = builder.environment();
 63 |             env.put("OS_AUTH_TYPE", authType);
 64 |             env.put("OS_AUTH_URL", authUrl);
 65 |             env.put("OS_IDENTITY_API_VERSION", apiVersion);
 66 |             env.put("OS_REGION_NAME", region);
 67 |             env.put("OS_INTERFACE", iface);
 68 |             env.put("OS_APPLICATION_CREDENTIAL_ID", appCredId);
 69 |             env.put("OS_APPLICATION_CREDENTIAL_SECRET", appCredSecret);
 70 | 
 71 |             Process process = builder.start();
 72 |             StringBuilder output = new StringBuilder();
 73 | 
 74 |             try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
 75 |                 String line;
 76 |                 while ((line = reader.readLine()) != null) {
 77 |                     output.append(line)
 78 |                         .append(System.lineSeparator());
 79 |                 }
 80 |             }
 81 | 
 82 |             if (!process.waitFor(PROCESS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
 83 |                 process.destroyForcibly();
 84 |                 return "Command execution timed out after " + PROCESS_TIMEOUT_SECONDS + " seconds";
 85 |             }
 86 | 
 87 |             int exitCode = process.exitValue();
 88 |             if (exitCode != 0) {
 89 |                 return "Command execution failed with exit code: " + exitCode;
 90 |             }
 91 | 
 92 |             return output.toString();
 93 |         } catch (IOException | InterruptedException e) {
 94 |             Thread.currentThread()
 95 |                 .interrupt();
 96 |             return "An error occurred while executing the command: " + e.getMessage();
 97 |         }
 98 |     }
 99 | 
100 |     /**
101 |      * Checks if OpenStack is installed on the system.
102 |      *
103 |      * @return true if OpenStack is installed, false otherwise
104 |      */
105 |     public boolean isOpenStackInstalled() {
106 |         String result = execute("which openstack");
107 |         return result != null && !result.trim()
108 |             .isEmpty() && !result.contains("Command execution failed");
109 |     }
110 | 
111 |     /**
112 |      * Provides diagnostics information about the OpenStack installation and configuration.
113 |      *
114 |      * @return A string containing diagnostics information
115 |      */
116 |     public String getOpenStackDiagnostics() {
117 |         StringJoiner diagnostics = new StringJoiner("\n");
118 | 
119 |         String whichResult = execute("which openstack");
120 |         diagnostics.add("OpenStack binary location: " + (whichResult.contains("Command execution failed") ? "Not found" : whichResult.trim()));
121 | 
122 |         String versionResult = execute("openstack --version");
123 |         diagnostics.add("OpenStack version: " + (versionResult.contains("Command execution failed") ? "Unable to determine" : versionResult.trim()));
124 | 
125 |         diagnostics.add("Environment configuration:");
126 |         diagnostics.add("  OS_AUTH_TYPE: " + (authType != null ? "Set" : "Not set"));
127 |         diagnostics.add("  OS_AUTH_URL: " + (authUrl != null ? "Set" : "Not set"));
128 |         diagnostics.add("  OS_IDENTITY_API_VERSION: " + (apiVersion != null ? "Set" : "Not set"));
129 |         diagnostics.add("  OS_REGION_NAME: " + (region != null ? "Set" : "Not set"));
130 |         diagnostics.add("  OS_INTERFACE: " + (iface != null ? "Set" : "Not set"));
131 |         diagnostics.add("  OS_APPLICATION_CREDENTIAL_ID: " + (appCredId != null ? "Set" : "Not set"));
132 |         diagnostics.add("  OS_APPLICATION_CREDENTIAL_SECRET: " + (appCredSecret != null ? "Set (hidden)" : "Not set"));
133 | 
134 |         return diagnostics.toString();
135 |     }
136 | 
137 | }
138 | 
```

--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------

```
  1 | <# : batch portion
  2 | @REM ----------------------------------------------------------------------------
  3 | @REM Licensed to the Apache Software Foundation (ASF) under one
  4 | @REM or more contributor license agreements.  See the NOTICE file
  5 | @REM distributed with this work for additional information
  6 | @REM regarding copyright ownership.  The ASF licenses this file
  7 | @REM to you under the Apache License, Version 2.0 (the
  8 | @REM "License"); you may not use this file except in compliance
  9 | @REM with the License.  You may obtain a copy of the License at
 10 | @REM
 11 | @REM    http://www.apache.org/licenses/LICENSE-2.0
 12 | @REM
 13 | @REM Unless required by applicable law or agreed to in writing,
 14 | @REM software distributed under the License is distributed on an
 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 16 | @REM KIND, either express or implied.  See the License for the
 17 | @REM specific language governing permissions and limitations
 18 | @REM under the License.
 19 | @REM ----------------------------------------------------------------------------
 20 | 
 21 | @REM ----------------------------------------------------------------------------
 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2
 23 | @REM
 24 | @REM Optional ENV vars
 25 | @REM   MVNW_REPOURL - repo url base for downloading maven distribution
 26 | @REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
 27 | @REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output
 28 | @REM ----------------------------------------------------------------------------
 29 | 
 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
 31 | @SET __MVNW_CMD__=
 32 | @SET __MVNW_ERROR__=
 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
 34 | @SET PSModulePath=
 35 | @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 @(
 36 |   IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
 37 | )
 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
 39 | @SET __MVNW_PSMODULEP_SAVE=
 40 | @SET __MVNW_ARG0_NAME__=
 41 | @SET MVNW_USERNAME=
 42 | @SET MVNW_PASSWORD=
 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1
 45 | @GOTO :EOF
 46 | : end batch / begin powershell #>
 47 | 
 48 | $ErrorActionPreference = "Stop"
 49 | if ($env:MVNW_VERBOSE -eq "true") {
 50 |   $VerbosePreference = "Continue"
 51 | }
 52 | 
 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
 55 | if (!$distributionUrl) {
 56 |   Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
 57 | }
 58 | 
 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
 60 |   "maven-mvnd-*" {
 61 |     $USE_MVND = $true
 62 |     $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
 63 |     $MVN_CMD = "mvnd.cmd"
 64 |     break
 65 |   }
 66 |   default {
 67 |     $USE_MVND = $false
 68 |     $MVN_CMD = $script -replace '^mvnw','mvn'
 69 |     break
 70 |   }
 71 | }
 72 | 
 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME
 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
 75 | if ($env:MVNW_REPOURL) {
 76 |   $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
 77 |   $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
 78 | }
 79 | $distributionUrlName = $distributionUrl -replace '^.*/',''
 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
 82 | if ($env:MAVEN_USER_HOME) {
 83 |   $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
 84 | }
 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
 87 | 
 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
 89 |   Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
 90 |   Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
 91 |   exit $?
 92 | }
 93 | 
 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
 95 |   Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
 96 | }
 97 | 
 98 | # prepare tmp dir
 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
102 | trap {
103 |   if ($TMP_DOWNLOAD_DIR.Exists) {
104 |     try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
105 |     catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
106 |   }
107 | }
108 | 
109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
110 | 
111 | # Download and Install Apache Maven
112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
113 | Write-Verbose "Downloading from: $distributionUrl"
114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
115 | 
116 | $webclient = New-Object System.Net.WebClient
117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
118 |   $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
119 | }
120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
122 | 
123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file
124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
125 | if ($distributionSha256Sum) {
126 |   if ($USE_MVND) {
127 |     Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
128 |   }
129 |   Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
130 |   if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
131 |     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."
132 |   }
133 | }
134 | 
135 | # unzip and move
136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
138 | try {
139 |   Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
140 | } catch {
141 |   if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
142 |     Write-Error "fail to move MAVEN_HOME"
143 |   }
144 | } finally {
145 |   try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
146 |   catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
147 | }
148 | 
149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
150 | 
```