# 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: -------------------------------------------------------------------------------- ``` 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Chess 2 | 3 | A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that provides 4 | chess functionality for [Claude AI](https://claude.ai/) Assistant. 5 | 6 | https://github.com/user-attachments/assets/324ed381-35f3-45b7-b877-127ef27fd97d 7 | 8 | ## Features 9 | 10 | This server implements tools that extend Claude's capabilities to: 11 | 12 | - Generate chess board images from a Forsyth-Edwards Notation (FEN) string 13 | - Suggest the next move in a chess game 14 | - Check if a move is legal 15 | 16 | ## Installation for Claude Desktop 17 | 18 | 1. Download the latest binary from the [Releases](https://github.com/alexandreroman/mcp-chess/releases) page: 19 | - For Windows: `mcp-chess-windows.exe` 20 | - For macOS: `mcp-chess-darwin` 21 | - For Linux: `mcp-chess-linux` 22 | 23 | 2. Make the file executable (macOS/Linux only): 24 | ```bash 25 | chmod +x mcp-chess-darwin # for macOS 26 | chmod +x mcp-chess-linux # for Linux 27 | ``` 28 | 29 | 3. For macOS users - Bypassing Security Warnings: 30 | 31 | 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: 32 | 33 | - Right-click (or Control-click) on the `mcp-chess-darwin` file 34 | - Select "Open" from the context menu 35 | - Click "Open" in the dialog box that appears 36 | 37 | Alternatively, you can use Terminal: 38 | ```bash 39 | xattr -d com.apple.quarantine /path/to/mcp-chess-darwin 40 | ``` 41 | 42 | This only needs to be done once. 43 | 44 | 4. Configure Claude Desktop: 45 | - Open Claude Desktop 46 | - Select "Settings", and click on the "Developer" tab 47 | - Click "Edit Config" 48 | - Add the MCP server configuration 49 | - Save the file 50 | - Restart Claude Desktop 51 | 52 | Here's an example for the MCP server configuration: 53 | 54 | ```json 55 | { 56 | "mcpServers": { 57 | "mcp-chess": { 58 | "command": "/path/to/mcp-chess-binary" 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | ## Using with Claude 65 | 66 | Once properly configured, you can ask Claude to perform various chess-related tasks: 67 | 68 | ``` 69 | Show me the starting position of a chess game. 70 | ``` 71 | 72 | ``` 73 | Let's play a chess game. Check that each move is legal. Suggest the best move to play. 74 | ``` 75 | 76 | ``` 77 | Is Nf3 a legal move from the starting position? 78 | ``` 79 | 80 | ``` 81 | What's a good move for white in this position: "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2"? 82 | ``` 83 | 84 | ## Technical Details 85 | 86 | ### Development 87 | 88 | This project is built with: 89 | - Spring Boot 90 | - Spring AI (MCP server implementation) 91 | - Java 21 92 | - GraalVM native compilation 93 | 94 | ### Building from Source 95 | 96 | ```bash 97 | # Clone the repository 98 | git clone https://github.com/alexandreroman/mcp-chess.git 99 | cd mcp-chess 100 | 101 | # Build with Maven 102 | ./mvnw clean package 103 | 104 | # Build a native executable 105 | ./mvnw -Pnative native:compile 106 | ``` 107 | 108 | ## License 109 | 110 | This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. 111 | 112 | ## Credits 113 | 114 | - [ChessGame](https://github.com/wolfraam/chess-game) - Java chess library 115 | - [ChessImage](https://github.com/alexandreroman/chessimage) - Chess board renderer 116 | - [Stockfish.online](https://stockfish.online/) - Chess engine API 117 | - [Spring AI](https://spring.io/projects/spring-ai) - AI application framework 118 | ``` -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- ``` 1 | <component name="CopyrightManager"> 2 | <settings default="broadcom" /> 3 | </component> ``` -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- ``` 1 | spring.application.name=mcp-chess 2 | 3 | spring.main.web-application-type=none 4 | spring.main.banner-mode=off 5 | logging.pattern.console= 6 | 7 | spring.ai.mcp.server.name=${spring.application.name} 8 | [email protected]@ 9 | spring.ai.mcp.server.stdio=true 10 | 11 | app.stockfish-online.url=https://stockfish.online 12 | ``` -------------------------------------------------------------------------------- /.idea/copyright/broadcom.xml: -------------------------------------------------------------------------------- ``` 1 | <component name="CopyrightManager"> 2 | <copyright> 3 | <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." /> 4 | <option name="myName" value="broadcom" /> 5 | </copyright> 6 | </component> ``` -------------------------------------------------------------------------------- /src/test/java/com/broadcom/tanzu/demos/mcp/chess/ApplicationTests.java: -------------------------------------------------------------------------------- ```java 1 | /* 2 | * Copyright (c) 2025 Broadcom, Inc. or its affiliates 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.broadcom.tanzu.demos.mcp.chess; 18 | 19 | import org.junit.jupiter.api.Test; 20 | import org.springframework.boot.test.context.SpringBootTest; 21 | 22 | @SpringBootTest 23 | class ApplicationTests { 24 | @Test 25 | void contextLoads() { 26 | } 27 | } 28 | ``` -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- ``` 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/ChessEngine.java: -------------------------------------------------------------------------------- ```java 1 | /* 2 | * Copyright (c) 2025 Broadcom, Inc. or its affiliates 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.broadcom.tanzu.demos.mcp.chess; 18 | 19 | import io.github.wolfraam.chessgame.ChessGame; 20 | import io.github.wolfraam.chessgame.move.Move; 21 | 22 | import java.util.Optional; 23 | 24 | public interface ChessEngine { 25 | /** 26 | * Get the next move to play. 27 | * 28 | * @param game board game instance 29 | * @return the move to play eventually 30 | */ 31 | Optional<Move> getNextMove(ChessGame game); 32 | } 33 | ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/Application.java: -------------------------------------------------------------------------------- ```java 1 | /* 2 | * Copyright (c) 2025 Broadcom, Inc. or its affiliates 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.broadcom.tanzu.demos.mcp.chess; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | 22 | @SpringBootApplication 23 | public class Application { 24 | public static void main(String[] args) { 25 | // Run this app in headless move (no AWT window required). 26 | System.setProperty("java.awt.headless", "true"); 27 | SpringApplication.run(Application.class, args); 28 | } 29 | } 30 | ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/stockfishonline/StockfishOnline.java: -------------------------------------------------------------------------------- ```java 1 | /* 2 | * Copyright (c) 2025 Broadcom, Inc. or its affiliates 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.broadcom.tanzu.demos.mcp.chess.stockfishonline; 18 | 19 | import org.springframework.web.bind.annotation.RequestParam; 20 | import org.springframework.web.service.annotation.GetExchange; 21 | 22 | interface StockfishOnline { 23 | @GetExchange("/api/s/v2.php") 24 | StockfishOnlineResponse getNextMove(@RequestParam String fen, @RequestParam(defaultValue = "10") int depth); 25 | } 26 | 27 | record StockfishOnlineResponse( 28 | boolean success, 29 | Integer mate, 30 | String bestmove 31 | ) { 32 | } 33 | ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/stockfishonline/StockfishOnlineEngineConfig.java: -------------------------------------------------------------------------------- ```java 1 | /* 2 | * Copyright (c) 2025 Broadcom, Inc. or its affiliates 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.broadcom.tanzu.demos.mcp.chess.stockfishonline; 18 | 19 | import org.springframework.beans.factory.annotation.Value; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.annotation.Configuration; 22 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 23 | import org.springframework.web.client.RestClient; 24 | import org.springframework.web.client.support.RestClientAdapter; 25 | import org.springframework.web.service.invoker.HttpServiceProxyFactory; 26 | 27 | @Configuration(proxyBeanMethods = false) 28 | class StockfishOnlineEngineConfig { 29 | @Bean 30 | StockfishOnline stockfishOnline(RestClient.Builder clientBuilder, 31 | @Value("${app.stockfish-online.url}") String stockfishOnlineUrl) { 32 | final var client = clientBuilder 33 | .clone() 34 | .requestFactory(new HttpComponentsClientHttpRequestFactory()) 35 | .baseUrl(stockfishOnlineUrl) 36 | .build(); 37 | return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)) 38 | .build().createClient(StockfishOnline.class); 39 | } 40 | 41 | @Bean 42 | StockfishOnlineEngine chessEngine(StockfishOnline stockfishOnline) { 43 | return new StockfishOnlineEngine(stockfishOnline); 44 | } 45 | } 46 | ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/stockfishonline/StockfishOnlineEngine.java: -------------------------------------------------------------------------------- ```java 1 | /* 2 | * Copyright (c) 2025 Broadcom, Inc. or its affiliates 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.broadcom.tanzu.demos.mcp.chess.stockfishonline; 18 | 19 | import com.broadcom.tanzu.demos.mcp.chess.ChessEngine; 20 | import io.github.wolfraam.chessgame.ChessGame; 21 | import io.github.wolfraam.chessgame.move.Move; 22 | import io.github.wolfraam.chessgame.notation.NotationType; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import java.util.Optional; 27 | import java.util.regex.Pattern; 28 | 29 | class StockfishOnlineEngine implements ChessEngine { 30 | private final Logger logger = LoggerFactory.getLogger(StockfishOnlineEngine.class); 31 | private final Pattern bestmovePattern = Pattern.compile("bestmove\\s(\\S+)"); 32 | private final StockfishOnline api; 33 | 34 | StockfishOnlineEngine(StockfishOnline api) { 35 | this.api = api; 36 | } 37 | 38 | public Optional<Move> getNextMove(ChessGame game) { 39 | final var fen = game.getFen(); 40 | logger.atDebug().log("Using Stockfish.online to guess next move using FEN: {}", fen); 41 | final var resp = api.getNextMove(fen, 3); 42 | if (resp == null || !resp.success()) { 43 | logger.atWarn().log("No next move found using Stockfish.online using FEN: {}", fen); 44 | return Optional.empty(); 45 | } 46 | 47 | final var matcher = bestmovePattern.matcher(resp.bestmove()); 48 | if (!matcher.find()) { 49 | logger.atWarn().log("Unable to read best move from Stockfish.online using FEN '{}': {}", fen, resp.bestmove()); 50 | return Optional.empty(); 51 | } 52 | 53 | final var bestMove = matcher.group(1); 54 | final var nextMove = game.getMove(NotationType.UCI, bestMove); 55 | logger.atInfo().log("Found next move with Stockfish.online using FEN '{}': {}", fen, nextMove); 56 | return Optional.of(nextMove); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "Stockfish.online"; 62 | } 63 | } 64 | ``` -------------------------------------------------------------------------------- /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.3</version> 9 | <relativePath/> <!-- lookup parent from repository --> 10 | </parent> 11 | <groupId>com.broadcom.tanzu.demos</groupId> 12 | <artifactId>mcp-chess</artifactId> 13 | <version>1.1.0-SNAPSHOT</version> 14 | <name>MCP Server :: Chess</name> 15 | 16 | <properties> 17 | <java.version>21</java.version> 18 | <spring-ai.version>1.0.0-M6</spring-ai.version> 19 | </properties> 20 | 21 | <dependencies> 22 | <dependency> 23 | <groupId>org.springframework.ai</groupId> 24 | <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> 25 | </dependency> 26 | 27 | <dependency> 28 | <groupId>io.github.wolfraam</groupId> 29 | <artifactId>chessgame</artifactId> 30 | <version>2.3</version> 31 | </dependency> 32 | <dependency> 33 | <groupId>com.github.alexandreroman</groupId> 34 | <artifactId>chessimage</artifactId> 35 | <version>1.0.0</version> 36 | </dependency> 37 | 38 | <dependency> 39 | <groupId>org.apache.httpcomponents.client5</groupId> 40 | <artifactId>httpclient5</artifactId> 41 | </dependency> 42 | <dependency> 43 | <groupId>org.springframework.boot</groupId> 44 | <artifactId>spring-boot-starter-web</artifactId> 45 | </dependency> 46 | 47 | <!-- Workaround for a strange issue when using GitHub Actions: --> 48 | <!-- this dependency brought by spring-ai-mcp is somehow missing --> 49 | <dependency> 50 | <groupId>com.fasterxml</groupId> 51 | <artifactId>classmate</artifactId> 52 | </dependency> 53 | 54 | <dependency> 55 | <groupId>org.springframework.boot</groupId> 56 | <artifactId>spring-boot-starter-test</artifactId> 57 | <scope>test</scope> 58 | </dependency> 59 | <dependency> 60 | <groupId>org.mockito</groupId> 61 | <artifactId>mockito-junit-jupiter</artifactId> 62 | <scope>test</scope> 63 | </dependency> 64 | </dependencies> 65 | 66 | <dependencyManagement> 67 | <dependencies> 68 | <dependency> 69 | <groupId>org.springframework.ai</groupId> 70 | <artifactId>spring-ai-bom</artifactId> 71 | <version>${spring-ai.version}</version> 72 | <type>pom</type> 73 | <scope>import</scope> 74 | </dependency> 75 | </dependencies> 76 | </dependencyManagement> 77 | 78 | <repositories> 79 | <repository> 80 | <id>jitpack.io</id> 81 | <url>https://jitpack.io</url> 82 | </repository> 83 | </repositories> 84 | 85 | <build> 86 | <plugins> 87 | <plugin> 88 | <groupId>org.graalvm.buildtools</groupId> 89 | <artifactId>native-maven-plugin</artifactId> 90 | </plugin> 91 | <plugin> 92 | <groupId>org.springframework.boot</groupId> 93 | <artifactId>spring-boot-maven-plugin</artifactId> 94 | </plugin> 95 | </plugins> 96 | </build> 97 | </project> 98 | ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/ChessTools.java: -------------------------------------------------------------------------------- ```java 1 | /* 2 | * Copyright (c) 2025 Broadcom, Inc. or its affiliates 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.broadcom.tanzu.demos.mcp.chess; 18 | 19 | import com.fasterxml.jackson.annotation.JsonClassDescription; 20 | import com.fasterxml.jackson.annotation.JsonInclude; 21 | import com.fasterxml.jackson.annotation.JsonPropertyDescription; 22 | import io.github.wolfraam.chessgame.ChessGame; 23 | import io.github.wolfraam.chessgame.notation.NotationType; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.ai.tool.annotation.Tool; 27 | import org.springframework.ai.tool.annotation.ToolParam; 28 | import org.springframework.stereotype.Component; 29 | 30 | @Component 31 | class ChessTools { 32 | private final Logger logger = LoggerFactory.getLogger(ChessTools.class); 33 | private final ChessEngine chessEngine; 34 | 35 | ChessTools(ChessEngine chessEngine) { 36 | this.chessEngine = chessEngine; 37 | } 38 | 39 | @Tool(name = "chess_guess_next_move", description = """ 40 | Guess the next move to play in a chess game. 41 | """) 42 | NextMove guessNextMove(@ToolParam(description = "Board state in Forsyth-Edwards Notation") String fen) { 43 | logger.atDebug().log("Guessing next move from FEN: {}", fen); 44 | final var game = new ChessGame(fen); 45 | final var resp = chessEngine.getNextMove(game) 46 | .map(move -> game.getNotation(NotationType.UCI, move)) 47 | .orElse(null); 48 | logger.atInfo().log("Guessed next move from FEN: {}=>{}", fen, resp); 49 | return new NextMove(fen, resp); 50 | } 51 | 52 | @Tool(name = "chess_is_legal_move", description = """ 53 | Check if a move is legal in a chess game. 54 | """) 55 | MoveLegality isLegalMove(@ToolParam(description = "Board state in Forsyth-Edwards Notation") String fen, 56 | @ToolParam(description = "Move in UCI format") String move) { 57 | logger.atDebug().log("Checking if the move {} is legal in FEN: {}", move, fen); 58 | final var game = new ChessGame(fen); 59 | final var resp = game.isLegalMove(game.getMove(NotationType.UCI, move)); 60 | logger.atInfo().log("Is move {} legal in FEN {}? {}", move, fen, resp ? "yes" : "no"); 61 | return new MoveLegality(fen, move, resp); 62 | } 63 | } 64 | 65 | @JsonClassDescription("A structure holding the legality of a move in a chess game") 66 | record MoveLegality( 67 | @JsonPropertyDescription("Board state in Forsyth-Edwards Notation (FEN) before the move") 68 | String fen, 69 | @JsonPropertyDescription("Move to check in UCI format (for instance: d2d3)") 70 | String move, 71 | @JsonPropertyDescription("Move legality: true if the move is legal in the current board state") 72 | boolean legal 73 | ) { 74 | } 75 | 76 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 77 | @JsonClassDescription("A structure holding a board state and the best move to play in a chess game") 78 | record NextMove( 79 | @JsonPropertyDescription("Board state in Forsyth-Edwards Notation (FEN)") 80 | String fen, 81 | @JsonPropertyDescription("Next move to play in UCI format (for instance: d2d3), if any") 82 | String nextMove 83 | ) { 84 | } 85 | ``` -------------------------------------------------------------------------------- /.github/workflows/build-and-release.yaml: -------------------------------------------------------------------------------- ```yaml 1 | name: Build and release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: write 15 | 16 | concurrency: 17 | group: ${{ github.workflow }} 18 | cancel-in-progress: true 19 | 20 | env: 21 | JAVA_VERSION: 21 22 | 23 | jobs: 24 | init: 25 | name: Initializing 26 | runs-on: ubuntu-latest 27 | outputs: 28 | artifact-id: ${{ steps.artifact-id.outputs.artifact-id }} 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Get artifact id 32 | id: artifact-id 33 | run: | 34 | ARTIFACT_ID=$(./mvnw -B org.apache.maven.plugins:maven-help-plugin:evaluate -Dexpression=project.artifactId -q -DforceStdout) 35 | echo artifact-id=$ARTIFACT_ID >> $GITHUB_OUTPUT 36 | 37 | build: 38 | name: Building on ${{ matrix.os }} 39 | needs: init 40 | runs-on: ${{ matrix.os }} 41 | strategy: 42 | matrix: 43 | os: [ macos-latest, windows-latest, ubuntu-latest ] 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: graalvm/setup-graalvm@v1 47 | with: 48 | java-version: ${{ env.JAVA_VERSION }} 49 | distribution: liberica 50 | cache: maven 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | native-image-job-reports: 'true' 53 | - name: Build and test on Windows 54 | if: runner.os == 'Windows' 55 | run: | 56 | .\mvnw -B -Pnative native:compile 57 | - name: Build and test 58 | if: runner.os != 'Windows' 59 | run: | 60 | ./mvnw -B -Pnative native:compile 61 | - uses: actions/upload-artifact@v4 62 | if: startsWith(github.ref, 'refs/tags/v') && runner.os == 'Windows' 63 | with: 64 | name: artifact-windows 65 | path: target/${{ needs.init.outputs.artifact-id }}.exe 66 | if-no-files-found: error 67 | compression-level: 0 68 | - uses: actions/upload-artifact@v4 69 | if: startsWith(github.ref, 'refs/tags/v') && runner.os == 'macOS' 70 | with: 71 | name: artifact-darwin 72 | path: target/${{ needs.init.outputs.artifact-id }} 73 | if-no-files-found: error 74 | compression-level: 0 75 | - uses: actions/upload-artifact@v4 76 | if: startsWith(github.ref, 'refs/tags/v') && runner.os == 'Linux' 77 | with: 78 | name: artifact-linux 79 | path: target/${{ needs.init.outputs.artifact-id }} 80 | if-no-files-found: error 81 | compression-level: 0 82 | 83 | release: 84 | name: Creating release 85 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 86 | needs: [ init, build ] 87 | runs-on: ubuntu-latest 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 90 | steps: 91 | - name: Download all artifacts 92 | uses: actions/download-artifact@v4 93 | with: 94 | path: artifacts 95 | - name: Display structure of downloaded files 96 | run: ls -R artifacts 97 | - uses: actions/create-release@v1 98 | id: create-release 99 | with: 100 | release_name: ${{ needs.init.outputs.artifact-id }}-${{ github.ref_name }} 101 | tag_name: ${{ github.ref_name }} 102 | draft: 'true' 103 | - uses: actions/upload-release-asset@v1 104 | with: 105 | upload_url: ${{ steps.create-release.outputs.upload_url }} 106 | asset_path: ./artifacts/artifact-windows/${{ needs.init.outputs.artifact-id }}.exe 107 | asset_name: ${{ needs.init.outputs.artifact-id }}-windows.exe 108 | asset_content_type: application/x-msdownload 109 | - uses: actions/upload-release-asset@v1 110 | with: 111 | upload_url: ${{ steps.create-release.outputs.upload_url }} 112 | asset_path: ./artifacts/artifact-darwin/${{ needs.init.outputs.artifact-id }} 113 | asset_name: ${{ needs.init.outputs.artifact-id }}-darwin 114 | asset_content_type: application/x-mach-binary 115 | - uses: actions/upload-release-asset@v1 116 | with: 117 | upload_url: ${{ steps.create-release.outputs.upload_url }} 118 | asset_path: ./artifacts/artifact-linux/${{ needs.init.outputs.artifact-id }} 119 | asset_name: ${{ needs.init.outputs.artifact-id }}-linux 120 | asset_content_type: application/x-executable 121 | ``` -------------------------------------------------------------------------------- /src/main/java/com/broadcom/tanzu/demos/mcp/chess/McpConfig.java: -------------------------------------------------------------------------------- ```java 1 | /* 2 | * Copyright (c) 2025 Broadcom, Inc. or its affiliates 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.broadcom.tanzu.demos.mcp.chess; 18 | 19 | import com.github.alexandreroman.chessimage.ChessRenderer; 20 | import io.modelcontextprotocol.server.McpServerFeatures; 21 | import io.modelcontextprotocol.spec.McpSchema; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | import org.springframework.ai.tool.ToolCallbackProvider; 25 | import org.springframework.ai.tool.annotation.ToolParam; 26 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 27 | import org.springframework.ai.util.json.schema.JsonSchemaGenerator; 28 | import org.springframework.aot.hint.MemberCategory; 29 | import org.springframework.aot.hint.annotation.RegisterReflection; 30 | import org.springframework.context.annotation.Bean; 31 | import org.springframework.context.annotation.Configuration; 32 | import org.springframework.util.ReflectionUtils; 33 | 34 | import java.io.ByteArrayOutputStream; 35 | import java.util.Base64; 36 | import java.util.Collections; 37 | import java.util.List; 38 | 39 | @Configuration(proxyBeanMethods = false) 40 | @RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_METHODS) 41 | class McpConfig { 42 | private final Logger logger = LoggerFactory.getLogger(McpConfig.class); 43 | 44 | @Bean 45 | ToolCallbackProvider chessToolsProvider(ChessTools chessTools) { 46 | // Register Spring AI tools as MCP tools. 47 | return MethodToolCallbackProvider.builder().toolObjects(chessTools).build(); 48 | } 49 | 50 | @Bean 51 | public List<McpServerFeatures.SyncPromptRegistration> prompts() { 52 | return List.of( 53 | new McpServerFeatures.SyncPromptRegistration(new McpSchema.Prompt( 54 | "start-a-new-game", 55 | "A prompt to start playing a chess game", 56 | Collections.emptyList()), req -> { 57 | 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.")); 58 | return new McpSchema.GetPromptResult("A message to start playing a chess game", List.of(m)); 59 | }) 60 | ); 61 | } 62 | 63 | @Bean 64 | List<McpServerFeatures.SyncToolRegistration> syncToolsReg() { 65 | // We're about to declare our own tool for generating board images. 66 | // We need to generate a JSON schema for the tool signature: 67 | // let's reuse utility classes from Spring AI with a dummy tool method. 68 | final var generateBoardImageMethod = ReflectionUtils.findMethod(McpConfig.class, "dummyGenerateBoardImage", String.class); 69 | assert generateBoardImageMethod != null; 70 | final var generateBoardImageInputSchema = JsonSchemaGenerator.generateForMethodInput(generateBoardImageMethod); 71 | 72 | // This is the MCP tool definition we need, including a JSON schema for the signature and a description. 73 | final var tool = new McpSchema.Tool("chess_generate_board_image", 74 | "Generate a board image in a chess game from a Forsyth-Edwards Notation (FEN).", 75 | generateBoardImageInputSchema); 76 | 77 | return List.of( 78 | new McpServerFeatures.SyncToolRegistration(tool, req -> { 79 | try { 80 | // Get FEN from the tool arguments. 81 | final var fen = (String) req.get("fen"); 82 | 83 | logger.atInfo().log("Rendering board to PNG image: {}", fen); 84 | final var out = new ByteArrayOutputStream(1024 * 4); 85 | new ChessRenderer().render(fen, out); 86 | 87 | logger.atInfo().log("Encoding PNG board image to base64: {}", fen); 88 | final var imgB64 = Base64.getEncoder().encodeToString(out.toByteArray()); 89 | 90 | // We take care of returning an image using the MCP schema: 91 | // Spring AI currently doesn't support this feature. 92 | final List<McpSchema.Content> contents = List.of( 93 | new McpSchema.ImageContent(Collections.singletonList(McpSchema.Role.USER), 94 | 1.0d, "image", imgB64, "image/png") 95 | ); 96 | return new McpSchema.CallToolResult(contents, false); 97 | } catch (Exception e) { 98 | final var msg = "Failed to generate board image: %s".formatted(e.getMessage()); 99 | return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(msg)), true); 100 | } 101 | }) 102 | ); 103 | } 104 | 105 | private void dummyGenerateBoardImage(@ToolParam(description = "Board state in Forsyth-Edwards Notation") String fen) { 106 | // This method does nothing on purpose. 107 | // We just want to generate a JSON schema from its arguments. 108 | } 109 | } 110 | ``` -------------------------------------------------------------------------------- /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 | ```