From a32a8d89cdf0bb9bb44b48136921da8ab4416897 Mon Sep 17 00:00:00 2001 From: Scott Clare Date: Fri, 4 Apr 2025 15:34:54 +0100 Subject: [PATCH 1/9] feat: adding dockerfile for running server with stdio --- Dockerfile | 37 +++++++++++++++++ agent_uno/__init__.py | 1 - pyproject.toml | 3 ++ src/agent_uno/__init__.py | 12 ++++++ src/agent_uno/__main__.py | 5 +++ {agent_uno => src/agent_uno}/core/__init__.py | 0 .../agent_uno}/core/exceptions.py | 0 {agent_uno => src/agent_uno}/core/schemas.py | 0 {agent_uno => src/agent_uno}/server.py | 40 +++++++++++-------- uv.lock | 4 +- 10 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 Dockerfile delete mode 100644 agent_uno/__init__.py create mode 100644 src/agent_uno/__init__.py create mode 100644 src/agent_uno/__main__.py rename {agent_uno => src/agent_uno}/core/__init__.py (100%) rename {agent_uno => src/agent_uno}/core/exceptions.py (100%) rename {agent_uno => src/agent_uno}/core/schemas.py (100%) rename {agent_uno => src/agent_uno}/server.py (83%) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c8b28cf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# Use a Python image with uv pre-installed +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv + +# Install the project into `/app` +WORKDIR /app + +# Enable bytecode compilation +ENV UV_COMPILE_BYTECODE=1 + +# Copy from the cache instead of linking since it's a mounted volume +ENV UV_LINK_MODE=copy + +# Install the project's dependencies using the lockfile and settings +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project --no-dev --no-editable + +# Then, add the rest of the project source code and install it +# Installing separately from its dependencies allows optimal layer caching +ADD . /app +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev --no-editable + +FROM python:3.12-slim-bookworm + +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY --from=uv --chown=app:app /app/.venv /app/.venv + +# Place executables in the environment at the front of the path +ENV PATH="/app/.venv/bin:$PATH" + +# when running the container, add --db-path and a bind mount to the host's db file +ENTRYPOINT ["agent-uno"] diff --git a/agent_uno/__init__.py b/agent_uno/__init__.py deleted file mode 100644 index 2a09ba4..0000000 --- a/agent_uno/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Top-level package for agent_uno.""" # noqa: N999 diff --git a/pyproject.toml b/pyproject.toml index 3cd6e61..994e52a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,9 @@ dependencies = [ "python-dotenv (>=1.1.0,<2.0.0)", ] +[project.scripts] +agent-uno = "agent_uno:main" + [dependency-groups] dev = ["pytest>=7.2.0", "pytest-cov>=4.0.0", "licensecheck>=2024.1.2"] diff --git a/src/agent_uno/__init__.py b/src/agent_uno/__init__.py new file mode 100644 index 0000000..114ff0c --- /dev/null +++ b/src/agent_uno/__init__.py @@ -0,0 +1,12 @@ +"""Top-level package for agent_uno.""" # noqa: N999 + +from .server import mcp_server + + +def main() -> None: + """Main function to run the MCP server.""" + mcp_server.run() + + +if __name__ == "__main__": + main() diff --git a/src/agent_uno/__main__.py b/src/agent_uno/__main__.py new file mode 100644 index 0000000..f25ee12 --- /dev/null +++ b/src/agent_uno/__main__.py @@ -0,0 +1,5 @@ +"""Main entry point for the Agent UNO project.""" + +from agent_uno import main + +main() diff --git a/agent_uno/core/__init__.py b/src/agent_uno/core/__init__.py similarity index 100% rename from agent_uno/core/__init__.py rename to src/agent_uno/core/__init__.py diff --git a/agent_uno/core/exceptions.py b/src/agent_uno/core/exceptions.py similarity index 100% rename from agent_uno/core/exceptions.py rename to src/agent_uno/core/exceptions.py diff --git a/agent_uno/core/schemas.py b/src/agent_uno/core/schemas.py similarity index 100% rename from agent_uno/core/schemas.py rename to src/agent_uno/core/schemas.py diff --git a/agent_uno/server.py b/src/agent_uno/server.py similarity index 83% rename from agent_uno/server.py rename to src/agent_uno/server.py index 45e1bce..477bf50 100644 --- a/agent_uno/server.py +++ b/src/agent_uno/server.py @@ -6,14 +6,18 @@ # 3. Agent is able to play the game by getting the current state and making moves. """ +import logging import os from collections.abc import Callable from typing import Any, Optional, cast from berserk import Client, TokenSession, exceptions from chess import Board -from core.exceptions import GameNotStartedError, MissingSessionStateError -from core.schemas import ( +from dotenv import load_dotenv +from mcp.server.fastmcp import FastMCP + +from agent_uno.core.exceptions import GameNotStartedError, MissingSessionStateError +from agent_uno.core.schemas import ( AccountInfo, BoardRepresentation, CreatedGameAI, @@ -22,12 +26,11 @@ GameStateMsg, UIConfig, ) -from dotenv import load_dotenv -from mcp.server.fastmcp import FastMCP load_dotenv() -mcp = FastMCP("chess-mcp", dependencies=["berserk", "python-chess"]) +logger = logging.getLogger(__name__) +mcp_server = FastMCP("chess-mcp", dependencies=["berserk", "python-chess"]) BOT_LEVEL = 3 LICHESS_ADDRESS = "https://lichess.org" @@ -61,7 +64,7 @@ def is_set_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Any: return is_set_wrapper -@mcp.tool(description="Login to LiChess.") # type: ignore +@mcp_server.tool(description="Login to LiChess.") # type: ignore async def login() -> None: """Login to LiChess using the provided API key. @@ -74,30 +77,34 @@ async def login() -> None: "API_KEY not found in environment variables. Please set it in your .env" ) session = TokenSession(api_key) + logger.info("Logged into LiChess with API key.") SESSION_STATE["client"] = Client(session) @client_is_set_handler -@mcp.tool(description="Get account info.") # type: ignore +@mcp_server.tool(description="Get account info.") # type: ignore async def get_account_info() -> AccountInfo: """Get the account info of the logged in user.""" return AccountInfo(**SESSION_STATE["client"].account.get()) @client_is_set_handler -@mcp.tool(description="Create a new game against an AI.") # type: ignore +@mcp_server.tool(description="Create a new game against an AI.") # type: ignore async def create_game_against_ai(level: int = BOT_LEVEL) -> UIConfig: """An endpoint for creating a new game.""" response = CreatedGameAI( **SESSION_STATE["client"].challenges.create_ai(color=COLOR, level=level) ) + + logger.info("Created game against Stockfish AI.") + SESSION_STATE["id"] = response.id return UIConfig(url=f"{LICHESS_ADDRESS}/{response.id}") @client_is_set_handler -@mcp.tool(description="Create a new game against a person.") # type: ignore +@mcp_server.tool(description="Create a new game against a person.") # type: ignore async def create_game_against_person(username: str) -> UIConfig: """An endpoint for creating a new game.""" response = CreatedGamePerson( @@ -105,13 +112,15 @@ async def create_game_against_person(username: str) -> UIConfig: color=COLOR, username=username, rated=False ) ) + logger.info(f"Created game against {username}.") + SESSION_STATE["id"] = response.id return UIConfig(url=f"{LICHESS_ADDRESS}/{response.id}") @client_is_set_handler -@mcp.tool(description="Whether the opponent had made there first move or not.") # type: ignore +@mcp_server.tool(description="Whether the opponent had made there first move or not.") # type: ignore async def is_opponent_turn() -> GameStateMsg: """Whether the opponent had made there first move or not.""" moves = await get_previous_moves() @@ -128,10 +137,11 @@ async def is_opponent_turn() -> GameStateMsg: @client_is_set_handler @id_is_set_handler -@mcp.tool(description="End game.") # type: ignore +@mcp_server.tool(description="End game.") # type: ignore async def end_game() -> None: """End the current game.""" SESSION_STATE["client"].board.resign_game(SESSION_STATE["id"]) + logger.info("Game ended.") @client_is_set_handler @@ -145,7 +155,7 @@ async def get_game_state() -> CurrentState: @client_is_set_handler @id_is_set_handler -@mcp.tool(description="Make a move.") # type: ignore +@mcp_server.tool(description="Make a move.") # type: ignore async def make_move(move: str) -> None: """Make a move in the current game.""" SESSION_STATE["client"].board.make_move(SESSION_STATE["id"], move) @@ -172,7 +182,7 @@ async def get_board() -> str: return cast(str, board.__str__()) -@mcp.tool( +@mcp_server.tool( description="""Get the current board as an ASCII representation and all previous moves.""" ) # type: ignore @@ -182,7 +192,3 @@ async def get_board_representation() -> BoardRepresentation | GameStateMsg: previous_moves = await get_previous_moves() return BoardRepresentation(board=board, previous_moves=previous_moves) - - -if __name__ == "__main__": - mcp.run() diff --git a/uv.lock b/uv.lock index c3a8ab7..e7d9bfc 100644 --- a/uv.lock +++ b/uv.lock @@ -10,8 +10,8 @@ dependencies = [ { name = "berserk" }, { name = "mcp", extra = ["cli"] }, { name = "pydantic" }, - { name = "python-dotenv" }, { name = "python-chess" }, + { name = "python-dotenv" }, ] [package.dev-dependencies] @@ -26,8 +26,8 @@ requires-dist = [ { name = "berserk", specifier = ">=0.13.2" }, { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }, { name = "pydantic", specifier = ">=2.11.1" }, - { name = "python-dotenv", specifier = ">=1.1.0,<2.0.0" }, { name = "python-chess", specifier = ">=1.999" }, + { name = "python-dotenv", specifier = ">=1.1.0,<2.0.0" }, ] [package.metadata.requires-dev] From 56ed662b81f7decc31fe2301522c821e1cdd62f2 Mon Sep 17 00:00:00 2001 From: Scott Clare Date: Fri, 4 Apr 2025 15:38:08 +0100 Subject: [PATCH 2/9] docs: updating documentation --- README.md | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a9b2391..4308dfb 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,33 @@ make project-setup # Quick Start -1. Install server in Claude Desktop: - -```bash -cd agent_uno -``` - -```bash -uv run mcp install server.py -``` +1. Add server config in client environment: + +
+Docker (Recommended) +
+{ + "mcpServers": { + "mcp-chess": { + "command": "docker", + "args": ["run", "-i", "--rm", "mcp-chess"] + } + } +} +
+ +
+uv +
+{ + "mcpServers": { + "mcp-chess": { + "command": "uv", + "args": ["run", "mcp-chess"] + } + } +} +
> [!TIP] > The above command updates the MCP config in Claude desktop. Whilst Claude Desktop only supports `stdio` transport the server can be directly executed with communication via `sse` transport and HTTP requests. For documentation on how to do this see [direct_execution.md](docs/direct_execution.md). From 41dd42c4ae9f6c6a8305bf52d20743874a7bec78 Mon Sep 17 00:00:00 2001 From: Scott Clare Date: Fri, 4 Apr 2025 15:45:41 +0100 Subject: [PATCH 3/9] feat: allowing for option of transport type --- docs/direct_execution.md | 4 ++-- pyproject.toml | 1 + src/agent_uno/__init__.py | 12 ++++++++++-- uv.lock | 2 ++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/direct_execution.md b/docs/direct_execution.md index de9fec5..1b54a25 100644 --- a/docs/direct_execution.md +++ b/docs/direct_execution.md @@ -5,7 +5,7 @@ Currently testing of endpoints can be performed with the following command: ```bash -uv run mcp dev server.py +uv run mcp dev src/agent_uno/server.py:mcp_server ``` This will start a local server running `Inspector` than can be used to interact with the MCP tools. @@ -17,7 +17,7 @@ This will start a local server running `Inspector` than can be used to interact You can run the server directly with the following command: ```bash -uv run mcp run server.py --transport sse +uv run agent-uno --transport sse ``` > [!NOTE] diff --git a/pyproject.toml b/pyproject.toml index 994e52a..f4c7126 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ readme = "README.md" requires-python = ">=3.12,<4.0" dependencies = [ "berserk>=0.13.2", + "click>=8.1.8", "mcp[cli]>=1.6.0", "pydantic>=2.11.1", "python-chess>=1.999", diff --git a/src/agent_uno/__init__.py b/src/agent_uno/__init__.py index 114ff0c..4261e60 100644 --- a/src/agent_uno/__init__.py +++ b/src/agent_uno/__init__.py @@ -1,11 +1,19 @@ """Top-level package for agent_uno.""" # noqa: N999 +from typing import Literal + +import click + from .server import mcp_server +Transport = Literal["stdio", "sse"] + -def main() -> None: +@click.command() # type: ignore +@click.option("--transport", default="stdio", help="The MCP transport type.") # type: ignore +def main(transport: Transport) -> None: """Main function to run the MCP server.""" - mcp_server.run() + mcp_server.run(transport=transport) if __name__ == "__main__": diff --git a/uv.lock b/uv.lock index e7d9bfc..87ba856 100644 --- a/uv.lock +++ b/uv.lock @@ -8,6 +8,7 @@ version = "0.0.1" source = { editable = "." } dependencies = [ { name = "berserk" }, + { name = "click" }, { name = "mcp", extra = ["cli"] }, { name = "pydantic" }, { name = "python-chess" }, @@ -24,6 +25,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "berserk", specifier = ">=0.13.2" }, + { name = "click", specifier = ">=8.1.8" }, { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }, { name = "pydantic", specifier = ">=2.11.1" }, { name = "python-chess", specifier = ">=1.999" }, From 04d324c278d900e323dc56fd1fef46b410413201 Mon Sep 17 00:00:00 2001 From: Scott Clare Date: Fri, 4 Apr 2025 16:04:58 +0100 Subject: [PATCH 4/9] docs: updating markdown dropdowns to have code blocks as content --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4308dfb..4389659 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ make project-setup
Docker (Recommended)
+ { "mcpServers": { "mcp-chess": { @@ -37,11 +38,13 @@ make project-setup } } } +
uv
+ { "mcpServers": { "mcp-chess": { @@ -50,6 +53,7 @@ make project-setup } } } +
> [!TIP] From 864a7dd9d09f617ba4bf4d83ae274076f2ceee7d Mon Sep 17 00:00:00 2001 From: Scott Clare Date: Fri, 4 Apr 2025 16:06:12 +0100 Subject: [PATCH 5/9] docs: updating markdown dropdowns to have code blocks as content --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4389659..98f4941 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ make project-setup
Docker (Recommended)
- +```json { "mcpServers": { "mcp-chess": { @@ -38,13 +38,13 @@ make project-setup } } } - +```
uv
- +``` { "mcpServers": { "mcp-chess": { @@ -53,7 +53,7 @@ make project-setup } } } - +```
> [!TIP] From a09f7c9ab9ed107a2f4f476eefedb88915fa9dce Mon Sep 17 00:00:00 2001 From: Scott Clare Date: Fri, 4 Apr 2025 16:07:06 +0100 Subject: [PATCH 6/9] docs: updating markdown dropdowns to have code blocks as content --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 98f4941..0541844 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ make project-setup
Docker (Recommended) -
```json { "mcpServers": { @@ -43,7 +42,6 @@ make project-setup
uv -
``` { "mcpServers": { From f3d4a5847f732ebb3445fe6b4e71af89c3fce4f6 Mon Sep 17 00:00:00 2001 From: Scott Clare Date: Fri, 4 Apr 2025 16:08:01 +0100 Subject: [PATCH 7/9] docs: updating markdown dropdowns to have code blocks as content --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0541844..1ba881b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ make project-setup
Docker (Recommended) + ```json { "mcpServers": { @@ -42,7 +43,8 @@ make project-setup
uv -``` + +```json { "mcpServers": { "mcp-chess": { From dd7547074482363d208771991ec6f40761473614 Mon Sep 17 00:00:00 2001 From: Scott Clare Date: Mon, 7 Apr 2025 13:04:20 +0100 Subject: [PATCH 8/9] docs: updating documentation on how to run via uv and docker --- README.md | 20 +++++++++++--------- src/agent_uno/server.py | 9 ++++----- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b140fc8..3e04f7b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ make project-setup
Docker (Recommended) +```bash +docker build -t mcp-chess . +``` + ```json { "mcpServers": { @@ -44,16 +48,14 @@ make project-setup
uv -```json -{ - "mcpServers": { - "mcp-chess": { - "command": "uv", - "args": ["run", "mcp-chess"] - } - } -} +```bash +cd src/agent_uno ``` + +```python +uv run mcp install server.py:mcp_server +``` +
> [!TIP] diff --git a/src/agent_uno/server.py b/src/agent_uno/server.py index 2a9ebcb..05f77cf 100644 --- a/src/agent_uno/server.py +++ b/src/agent_uno/server.py @@ -13,11 +13,8 @@ from berserk import Client, TokenSession, exceptions from chess import Board -from dotenv import load_dotenv -from mcp.server.fastmcp import FastMCP - -from agent_uno.core.exceptions import GameNotStartedError, MissingSessionStateError -from agent_uno.core.schemas import ( +from core.exceptions import GameNotStartedError, MissingSessionStateError +from core.schemas import ( AccountInfo, BoardRepresentation, CreatedGameAI, @@ -26,6 +23,8 @@ GameStateMsg, UIConfig, ) +from dotenv import load_dotenv +from mcp.server.fastmcp import FastMCP load_dotenv() From c5d4a1f4bf7ee88d946a0289400d70b63a2b3759 Mon Sep 17 00:00:00 2001 From: Scott Clare Date: Mon, 7 Apr 2025 13:18:58 +0100 Subject: [PATCH 9/9] merge: fixing merge conflicts --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 1dc61f4..62e9909 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,10 @@ make project-setup ## Quick Start -<<<<<<< HEAD -1. Add server config in client environment: +### 1. Add server config in client environment:
Docker (Recommended) -======= -### 1. Install server in Claude Desktop: ->>>>>>> main ```bash docker build -t mcp-chess .