#
tokens: 9595/50000 18/18 files
lines: off (toggle) GitHub
raw markdown copy
# 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) &amp;#36;today.year Broadcom, Inc. or its affiliates&#10;&#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10;     http://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;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"

```