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/README.md b/README.md index 51d1851..62e9909 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,40 @@ make project-setup ## Quick Start -### 1. Install server in Claude Desktop: +### 1. Add server config in client environment: + +
+Docker (Recommended) ```bash -cd agent_uno +docker build -t mcp-chess . +``` + +```json +{ + "mcpServers": { + "mcp-chess": { + "command": "docker", + "args": ["run", "-i", "--rm", "mcp-chess"] + } + } +} ``` +
+ +
+uv ```bash -uv run mcp install server.py +cd src/agent_uno ``` +```python +uv run mcp install server.py:mcp_server +``` + +
+ > [!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). 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/application_tests/tests.py b/application_tests/tests.py index c3d44fe..b476643 100644 --- a/application_tests/tests.py +++ b/application_tests/tests.py @@ -9,8 +9,8 @@ OKGREEN = "\033[92m" ENDC = "\033[0m" -COMMAND = "python" -ARGS = ["agent_uno/server.py"] +COMMAND = "uv" +ARGS = ["run", "agent-uno"] SERVER_PARAMS = StdioServerParameters( command=COMMAND, args=ARGS, @@ -88,7 +88,7 @@ { "name": "get_board_representation", "description": "Get the current board as an ASCII representation and all " - "previous\n moves.", + "previous moves.", "inputSchema": { "properties": {}, "title": "get_board_representationArguments", 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 d356282..a66c3e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,12 +7,16 @@ 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", "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..4261e60 --- /dev/null +++ b/src/agent_uno/__init__.py @@ -0,0 +1,20 @@ +"""Top-level package for agent_uno.""" # noqa: N999 + +from typing import Literal + +import click + +from .server import mcp_server + +Transport = Literal["stdio", "sse"] + + +@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(transport=transport) + + +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 87% rename from agent_uno/server.py rename to src/agent_uno/server.py index d6aa384..05f77cf 100644 --- a/agent_uno/server.py +++ b/src/agent_uno/server.py @@ -29,7 +29,7 @@ load_dotenv() logger = logging.getLogger(__name__) -mcp = FastMCP("chess-mcp", dependencies=["berserk", "python-chess"]) +mcp_server = FastMCP("chess-mcp", dependencies=["berserk", "python-chess"]) BOT_LEVEL = 3 LICHESS_ADDRESS = "https://lichess.org" @@ -63,7 +63,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. @@ -81,14 +81,14 @@ async def login() -> None: @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( @@ -103,7 +103,7 @@ async def create_game_against_ai(level: int = BOT_LEVEL) -> UIConfig: @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( @@ -119,7 +119,7 @@ async def create_game_against_person(username: str) -> UIConfig: @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() @@ -136,7 +136,7 @@ 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"]) @@ -154,7 +154,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) @@ -185,11 +185,11 @@ async def get_board() -> Board: return Board(fen) -@mcp.tool( - description="""Get the current board as an ASCII representation and all previous - moves.""" +@mcp_server.tool( + description="Get the current board as an ASCII representation and all previous " + "moves." ) # type: ignore -async def get_board_representation() -> BoardRepresentation | GameStateMsg: +async def get_board_representation() -> BoardRepresentation: """An endpoint for getting the current board as an ASCII representation.""" board = await get_board() previous_moves = await get_previous_moves() @@ -197,7 +197,3 @@ async def get_board_representation() -> BoardRepresentation | GameStateMsg: return BoardRepresentation( board=board.__str__(), previous_moves=previous_moves, check=board.is_check() ) - - -if __name__ == "__main__": - mcp.run() diff --git a/uv.lock b/uv.lock index c3a8ab7..87ba856 100644 --- a/uv.lock +++ b/uv.lock @@ -8,10 +8,11 @@ version = "0.0.1" source = { editable = "." } dependencies = [ { name = "berserk" }, + { name = "click" }, { name = "mcp", extra = ["cli"] }, { name = "pydantic" }, - { name = "python-dotenv" }, { name = "python-chess" }, + { name = "python-dotenv" }, ] [package.dev-dependencies] @@ -24,10 +25,11 @@ 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-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]