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]