# Directory Structure ``` ├── .gitattributes ├── .github │ └── workflows │ └── build-and-release.yaml ├── .gitignore ├── .idea │ └── copyright │ ├── broadcom.xml │ └── profiles_settings.xml ├── .mvn │ └── wrapper │ └── maven-wrapper.properties ├── LICENSE ├── mvnw ├── mvnw.cmd ├── pom.xml ├── README.md └── src ├── main │ ├── java │ │ └── com │ │ └── broadcom │ │ └── tanzu │ │ └── demos │ │ └── mcp │ │ └── chess │ │ ├── Application.java │ │ ├── ChessEngine.java │ │ ├── ChessTools.java │ │ ├── McpConfig.java │ │ └── stockfishonline │ │ ├── StockfishOnline.java │ │ ├── StockfishOnlineEngine.java │ │ └── StockfishOnlineEngineConfig.java │ └── resources │ └── application.properties └── test └── java └── com └── broadcom └── tanzu └── demos └── mcp └── chess └── ApplicationTests.java ``` # Files -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- ``` /mvnw text eol=lf *.cmd text eol=crlf ``` -------------------------------------------------------------------------------- /.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 # MCP Chess A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that provides chess functionality for [Claude AI](https://claude.ai/) Assistant. https://github.com/user-attachments/assets/324ed381-35f3-45b7-b877-127ef27fd97d ## Features This server implements tools that extend Claude's capabilities to: - Generate chess board images from a Forsyth-Edwards Notation (FEN) string - Suggest the next move in a chess game - Check if a move is legal ## Installation for Claude Desktop 1. Download the latest binary from the [Releases](https://github.com/alexandreroman/mcp-chess/releases) page: - For Windows: `mcp-chess-windows.exe` - For macOS: `mcp-chess-darwin` - For Linux: `mcp-chess-linux` 2. Make the file executable (macOS/Linux only): ```bash chmod +x mcp-chess-darwin # for macOS chmod +x mcp-chess-linux # for Linux ``` 3. For macOS users - Bypassing Security Warnings: When you first try to run the application, macOS may display a security warning because the application is not signed by an identified developer. To bypass this: - Right-click (or Control-click) on the `mcp-chess-darwin` file - Select "Open" from the context menu - Click "Open" in the dialog box that appears Alternatively, you can use Terminal: ```bash xattr -d com.apple.quarantine /path/to/mcp-chess-darwin ``` This only needs to be done once. 4. Configure Claude Desktop: - Open Claude Desktop - Select "Settings", and click on the "Developer" tab - Click "Edit Config" - Add the MCP server configuration - Save the file - Restart Claude Desktop Here's an example for the MCP server configuration: ```json { "mcpServers": { "mcp-chess": { "command": "/path/to/mcp-chess-binary" } } } ``` ## Using with Claude Once properly configured, you can ask Claude to perform various chess-related tasks: ``` Show me the starting position of a chess game. ``` ``` Let's play a chess game. Check that each move is legal. Suggest the best move to play. ``` ``` Is Nf3 a legal move from the starting position? ``` ``` What's a good move for white in this position: "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2"? ``` ## Technical Details ### Development This project is built with: - Spring Boot - Spring AI (MCP server implementation) - Java 21 - GraalVM native compilation ### Building from Source ```bash # Clone the repository git clone https://github.com/alexandreroman/mcp-chess.git cd mcp-chess # Build with Maven ./mvnw clean package # Build a native executable ./mvnw -Pnative native:compile ``` ## License This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. ## Credits - [ChessGame](https://github.com/wolfraam/chess-game) - Java chess library - [ChessImage](https://github.com/alexandreroman/chessimage) - Chess board renderer - [Stockfish.online](https://stockfish.online/) - Chess engine API - [Spring AI](https://spring.io/projects/spring-ai) - AI application framework ``` -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- ``` <component name="CopyrightManager"> <settings default="broadcom" /> </component> ``` -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- ``` spring.application.name=mcp-chess spring.main.web-application-type=none spring.main.banner-mode=off logging.pattern.console= spring.ai.mcp.server.name=${spring.application.name} [email protected]@ spring.ai.mcp.server.stdio=true app.stockfish-online.url=https://stockfish.online ``` -------------------------------------------------------------------------------- /.idea/copyright/broadcom.xml: -------------------------------------------------------------------------------- ``` <component name="CopyrightManager"> <copyright> <option name="notice" value="Copyright (c) &#36;today.year Broadcom, Inc. or its affiliates Licensed 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." /> <option name="myName" value="broadcom" /> </copyright> </component> ``` -------------------------------------------------------------------------------- /src/test/java/com/broadcom/tanzu/demos/mcp/chess/ApplicationTests.java: -------------------------------------------------------------------------------- ```java /* * Copyright (c) 2025 Broadcom, Inc. or its affiliates * * Licensed 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. */ package com.broadcom.tanzu.demos.mcp.chess; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ApplicationTests { @Test void contextLoads() { } } ``` -------------------------------------------------------------------------------- /.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 ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/ChessEngine.java: -------------------------------------------------------------------------------- ```java /* * Copyright (c) 2025 Broadcom, Inc. or its affiliates * * Licensed 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. */ package com.broadcom.tanzu.demos.mcp.chess; import io.github.wolfraam.chessgame.ChessGame; import io.github.wolfraam.chessgame.move.Move; import java.util.Optional; public interface ChessEngine { /** * Get the next move to play. * * @param game board game instance * @return the move to play eventually */ Optional<Move> getNextMove(ChessGame game); } ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/Application.java: -------------------------------------------------------------------------------- ```java /* * Copyright (c) 2025 Broadcom, Inc. or its affiliates * * Licensed 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. */ package com.broadcom.tanzu.demos.mcp.chess; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { // Run this app in headless move (no AWT window required). System.setProperty("java.awt.headless", "true"); SpringApplication.run(Application.class, args); } } ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/stockfishonline/StockfishOnline.java: -------------------------------------------------------------------------------- ```java /* * Copyright (c) 2025 Broadcom, Inc. or its affiliates * * Licensed 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. */ package com.broadcom.tanzu.demos.mcp.chess.stockfishonline; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.service.annotation.GetExchange; interface StockfishOnline { @GetExchange("/api/s/v2.php") StockfishOnlineResponse getNextMove(@RequestParam String fen, @RequestParam(defaultValue = "10") int depth); } record StockfishOnlineResponse( boolean success, Integer mate, String bestmove ) { } ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/stockfishonline/StockfishOnlineEngineConfig.java: -------------------------------------------------------------------------------- ```java /* * Copyright (c) 2025 Broadcom, Inc. or its affiliates * * Licensed 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. */ package com.broadcom.tanzu.demos.mcp.chess.stockfishonline; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestClient; import org.springframework.web.client.support.RestClientAdapter; import org.springframework.web.service.invoker.HttpServiceProxyFactory; @Configuration(proxyBeanMethods = false) class StockfishOnlineEngineConfig { @Bean StockfishOnline stockfishOnline(RestClient.Builder clientBuilder, @Value("${app.stockfish-online.url}") String stockfishOnlineUrl) { final var client = clientBuilder .clone() .requestFactory(new HttpComponentsClientHttpRequestFactory()) .baseUrl(stockfishOnlineUrl) .build(); return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)) .build().createClient(StockfishOnline.class); } @Bean StockfishOnlineEngine chessEngine(StockfishOnline stockfishOnline) { return new StockfishOnlineEngine(stockfishOnline); } } ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/stockfishonline/StockfishOnlineEngine.java: -------------------------------------------------------------------------------- ```java /* * Copyright (c) 2025 Broadcom, Inc. or its affiliates * * Licensed 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. */ package com.broadcom.tanzu.demos.mcp.chess.stockfishonline; import com.broadcom.tanzu.demos.mcp.chess.ChessEngine; import io.github.wolfraam.chessgame.ChessGame; import io.github.wolfraam.chessgame.move.Move; import io.github.wolfraam.chessgame.notation.NotationType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Optional; import java.util.regex.Pattern; class StockfishOnlineEngine implements ChessEngine { private final Logger logger = LoggerFactory.getLogger(StockfishOnlineEngine.class); private final Pattern bestmovePattern = Pattern.compile("bestmove\\s(\\S+)"); private final StockfishOnline api; StockfishOnlineEngine(StockfishOnline api) { this.api = api; } public Optional<Move> getNextMove(ChessGame game) { final var fen = game.getFen(); logger.atDebug().log("Using Stockfish.online to guess next move using FEN: {}", fen); final var resp = api.getNextMove(fen, 3); if (resp == null || !resp.success()) { logger.atWarn().log("No next move found using Stockfish.online using FEN: {}", fen); return Optional.empty(); } final var matcher = bestmovePattern.matcher(resp.bestmove()); if (!matcher.find()) { logger.atWarn().log("Unable to read best move from Stockfish.online using FEN '{}': {}", fen, resp.bestmove()); return Optional.empty(); } final var bestMove = matcher.group(1); final var nextMove = game.getMove(NotationType.UCI, bestMove); logger.atInfo().log("Found next move with Stockfish.online using FEN '{}': {}", fen, nextMove); return Optional.of(nextMove); } @Override public String toString() { return "Stockfish.online"; } } ``` -------------------------------------------------------------------------------- /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.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.broadcom.tanzu.demos</groupId> <artifactId>mcp-chess</artifactId> <version>1.1.0-SNAPSHOT</version> <name>MCP Server :: Chess</name> <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>io.github.wolfraam</groupId> <artifactId>chessgame</artifactId> <version>2.3</version> </dependency> <dependency> <groupId>com.github.alexandreroman</groupId> <artifactId>chessimage</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Workaround for a strange issue when using GitHub Actions: --> <!-- this dependency brought by spring-ai-mcp is somehow missing --> <dependency> <groupId>com.fasterxml</groupId> <artifactId>classmate</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</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> <repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> </repositories> <build> <plugins> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/ChessTools.java: -------------------------------------------------------------------------------- ```java /* * Copyright (c) 2025 Broadcom, Inc. or its affiliates * * Licensed 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. */ package com.broadcom.tanzu.demos.mcp.chess; import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import io.github.wolfraam.chessgame.ChessGame; import io.github.wolfraam.chessgame.notation.NotationType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; @Component class ChessTools { private final Logger logger = LoggerFactory.getLogger(ChessTools.class); private final ChessEngine chessEngine; ChessTools(ChessEngine chessEngine) { this.chessEngine = chessEngine; } @Tool(name = "chess_guess_next_move", description = """ Guess the next move to play in a chess game. """) NextMove guessNextMove(@ToolParam(description = "Board state in Forsyth-Edwards Notation") String fen) { logger.atDebug().log("Guessing next move from FEN: {}", fen); final var game = new ChessGame(fen); final var resp = chessEngine.getNextMove(game) .map(move -> game.getNotation(NotationType.UCI, move)) .orElse(null); logger.atInfo().log("Guessed next move from FEN: {}=>{}", fen, resp); return new NextMove(fen, resp); } @Tool(name = "chess_is_legal_move", description = """ Check if a move is legal in a chess game. """) MoveLegality isLegalMove(@ToolParam(description = "Board state in Forsyth-Edwards Notation") String fen, @ToolParam(description = "Move in UCI format") String move) { logger.atDebug().log("Checking if the move {} is legal in FEN: {}", move, fen); final var game = new ChessGame(fen); final var resp = game.isLegalMove(game.getMove(NotationType.UCI, move)); logger.atInfo().log("Is move {} legal in FEN {}? {}", move, fen, resp ? "yes" : "no"); return new MoveLegality(fen, move, resp); } } @JsonClassDescription("A structure holding the legality of a move in a chess game") record MoveLegality( @JsonPropertyDescription("Board state in Forsyth-Edwards Notation (FEN) before the move") String fen, @JsonPropertyDescription("Move to check in UCI format (for instance: d2d3)") String move, @JsonPropertyDescription("Move legality: true if the move is legal in the current board state") boolean legal ) { } @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonClassDescription("A structure holding a board state and the best move to play in a chess game") record NextMove( @JsonPropertyDescription("Board state in Forsyth-Edwards Notation (FEN)") String fen, @JsonPropertyDescription("Next move to play in UCI format (for instance: d2d3), if any") String nextMove ) { } ``` -------------------------------------------------------------------------------- /.github/workflows/build-and-release.yaml: -------------------------------------------------------------------------------- ```yaml name: Build and release on: push: branches: - main tags: - "v*" pull_request: branches: - main permissions: contents: write concurrency: group: ${{ github.workflow }} cancel-in-progress: true env: JAVA_VERSION: 21 jobs: init: name: Initializing runs-on: ubuntu-latest outputs: artifact-id: ${{ steps.artifact-id.outputs.artifact-id }} steps: - uses: actions/checkout@v4 - name: Get artifact id id: artifact-id run: | ARTIFACT_ID=$(./mvnw -B org.apache.maven.plugins:maven-help-plugin:evaluate -Dexpression=project.artifactId -q -DforceStdout) echo artifact-id=$ARTIFACT_ID >> $GITHUB_OUTPUT build: name: Building on ${{ matrix.os }} needs: init runs-on: ${{ matrix.os }} strategy: matrix: os: [ macos-latest, windows-latest, ubuntu-latest ] steps: - uses: actions/checkout@v4 - uses: graalvm/setup-graalvm@v1 with: java-version: ${{ env.JAVA_VERSION }} distribution: liberica cache: maven github-token: ${{ secrets.GITHUB_TOKEN }} native-image-job-reports: 'true' - name: Build and test on Windows if: runner.os == 'Windows' run: | .\mvnw -B -Pnative native:compile - name: Build and test if: runner.os != 'Windows' run: | ./mvnw -B -Pnative native:compile - uses: actions/upload-artifact@v4 if: startsWith(github.ref, 'refs/tags/v') && runner.os == 'Windows' with: name: artifact-windows path: target/${{ needs.init.outputs.artifact-id }}.exe if-no-files-found: error compression-level: 0 - uses: actions/upload-artifact@v4 if: startsWith(github.ref, 'refs/tags/v') && runner.os == 'macOS' with: name: artifact-darwin path: target/${{ needs.init.outputs.artifact-id }} if-no-files-found: error compression-level: 0 - uses: actions/upload-artifact@v4 if: startsWith(github.ref, 'refs/tags/v') && runner.os == 'Linux' with: name: artifact-linux path: target/${{ needs.init.outputs.artifact-id }} if-no-files-found: error compression-level: 0 release: name: Creating release if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') needs: [ init, build ] runs-on: ubuntu-latest env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} steps: - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts - name: Display structure of downloaded files run: ls -R artifacts - uses: actions/create-release@v1 id: create-release with: release_name: ${{ needs.init.outputs.artifact-id }}-${{ github.ref_name }} tag_name: ${{ github.ref_name }} draft: 'true' - uses: actions/upload-release-asset@v1 with: upload_url: ${{ steps.create-release.outputs.upload_url }} asset_path: ./artifacts/artifact-windows/${{ needs.init.outputs.artifact-id }}.exe asset_name: ${{ needs.init.outputs.artifact-id }}-windows.exe asset_content_type: application/x-msdownload - uses: actions/upload-release-asset@v1 with: upload_url: ${{ steps.create-release.outputs.upload_url }} asset_path: ./artifacts/artifact-darwin/${{ needs.init.outputs.artifact-id }} asset_name: ${{ needs.init.outputs.artifact-id }}-darwin asset_content_type: application/x-mach-binary - uses: actions/upload-release-asset@v1 with: upload_url: ${{ steps.create-release.outputs.upload_url }} asset_path: ./artifacts/artifact-linux/${{ needs.init.outputs.artifact-id }} asset_name: ${{ needs.init.outputs.artifact-id }}-linux asset_content_type: application/x-executable ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/McpConfig.java: -------------------------------------------------------------------------------- ```java /* * Copyright (c) 2025 Broadcom, Inc. or its affiliates * * Licensed 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. */ package com.broadcom.tanzu.demos.mcp.chess; import com.github.alexandreroman.chessimage.ChessRenderer; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.spec.McpSchema; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.ai.util.json.schema.JsonSchemaGenerator; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.annotation.RegisterReflection; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.ReflectionUtils; import java.io.ByteArrayOutputStream; import java.util.Base64; import java.util.Collections; import java.util.List; @Configuration(proxyBeanMethods = false) @RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_METHODS) class McpConfig { private final Logger logger = LoggerFactory.getLogger(McpConfig.class); @Bean ToolCallbackProvider chessToolsProvider(ChessTools chessTools) { // Register Spring AI tools as MCP tools. return MethodToolCallbackProvider.builder().toolObjects(chessTools).build(); } @Bean public List<McpServerFeatures.SyncPromptRegistration> prompts() { return List.of( new McpServerFeatures.SyncPromptRegistration(new McpSchema.Prompt( "start-a-new-game", "A prompt to start playing a chess game", Collections.emptyList()), req -> { final var m = new McpSchema.PromptMessage(McpSchema.Role.USER, new McpSchema.TextContent("Let's play a chess game. Check that each move is legal. Suggest the best move to play.")); return new McpSchema.GetPromptResult("A message to start playing a chess game", List.of(m)); }) ); } @Bean List<McpServerFeatures.SyncToolRegistration> syncToolsReg() { // We're about to declare our own tool for generating board images. // We need to generate a JSON schema for the tool signature: // let's reuse utility classes from Spring AI with a dummy tool method. final var generateBoardImageMethod = ReflectionUtils.findMethod(McpConfig.class, "dummyGenerateBoardImage", String.class); assert generateBoardImageMethod != null; final var generateBoardImageInputSchema = JsonSchemaGenerator.generateForMethodInput(generateBoardImageMethod); // This is the MCP tool definition we need, including a JSON schema for the signature and a description. final var tool = new McpSchema.Tool("chess_generate_board_image", "Generate a board image in a chess game from a Forsyth-Edwards Notation (FEN).", generateBoardImageInputSchema); return List.of( new McpServerFeatures.SyncToolRegistration(tool, req -> { try { // Get FEN from the tool arguments. final var fen = (String) req.get("fen"); logger.atInfo().log("Rendering board to PNG image: {}", fen); final var out = new ByteArrayOutputStream(1024 * 4); new ChessRenderer().render(fen, out); logger.atInfo().log("Encoding PNG board image to base64: {}", fen); final var imgB64 = Base64.getEncoder().encodeToString(out.toByteArray()); // We take care of returning an image using the MCP schema: // Spring AI currently doesn't support this feature. final List<McpSchema.Content> contents = List.of( new McpSchema.ImageContent(Collections.singletonList(McpSchema.Role.USER), 1.0d, "image", imgB64, "image/png") ); return new McpSchema.CallToolResult(contents, false); } catch (Exception e) { final var msg = "Failed to generate board image: %s".formatted(e.getMessage()); return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(msg)), true); } }) ); } private void dummyGenerateBoardImage(@ToolParam(description = "Board state in Forsyth-Edwards Notation") String fen) { // This method does nothing on purpose. // We just want to generate a JSON schema from its arguments. } } ``` -------------------------------------------------------------------------------- /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" ```