Skip to content

Commit 8d834be

Browse files
authored
Fa 2 allowing login (#7)
* feat: adding login func to mcp server * docs: updating README * docs: adding steps for creating lichess account and API key * chore: fixing link to chess UI * chore: fixing incorrect types * [FA-3] enable starting game and playing (#2) * feat: adding game start and play functionality * docs: updating documentation * docs: updating documentation * docs: fixing issue with markdwon image * docs: formatting readme * docs: removing trailing semi-colon * docs: making prompt block-quote * chore: removing trailing slash * Fa 3 enable starting game (#6) * feat: adding game start and play functionality * docs: updating documentation * docs: updating documentation * docs: fixing issue with markdwon image * docs: formatting readme * docs: removing trailing semi-colon * docs: making prompt block-quote * chore: removing trailing slash * [FA-9] improving responses from LLM with board representation (#4) * feat: adding string representation of the board * feat: fixing issues with running module in claude desktop * docs: updating readme * feat: adding schema for UI config * feat: reduced the bot level to 3 * docs: adding video to readme * docs: removing video * chore: fixing pre-commit issues
1 parent f9f431a commit 8d834be

File tree

7 files changed

+167
-48
lines changed

7 files changed

+167
-48
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repos:
1414
files: "\\.(py|txt|yaml|json|md|toml|lock|cfg|html|sh|js|yml)$"
1515
- id: end-of-file-fixer
1616
- id: check-added-large-files
17-
args: ["--maxkb=1000"]
17+
args: ["--maxkb=50000"]
1818
- id: check-case-conflict
1919
- id: requirements-txt-fixer
2020

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
An LLM agent built using Model Context Protocol to play online games
66

7-
# 🏃 How do I get started?
8-
If you haven't already done so, please read [DEVELOPMENT.md](DEVELOPMENT.md) for instructions on how to set up your virtual environment using Poetry.
9-
107
# Pre-requisites
118

129
- `uv` installable via brew.
@@ -29,14 +26,18 @@ make project-setup
2926
1. Install server in Claude Desktop:
3027

3128
```bash
32-
uv run mcp install agent_uno/server.py
29+
cd agent_uno
30+
```
31+
32+
```bash
33+
uv run mcp install server.py
3334
```
3435

3536
2. Interact with MCP server with Claude Desktop.
3637

3738
Example prompt:
3839

39-
> Can you please log into the Chess API with the following API key ************ and then create a game. Once the game has been created the opponent will make the first move. Can you use the state to determine what an optimal next move will be and then make your own move playing continuously back and forth until completion? Please use the UCI chess standard for your moves, e.g., e2e4.
40+
> Can you please log into the Chess API with the following API key ************ and then create a game. Once the game has been created the opponent will make the first move. Can you use the previous moves and the layout of the board to determine what an optimal next move will be and then make your own move playing continuously back and forth until completion? Please use the UCI chess standard for your moves, e.g., e2e4.
4041
4142
> [!NOTE]
4243
> If you face issues with server starting in the Claude desktop this could be because of the relative path for the `command` in the server config. This will need to be changed to the absolute path to `uv` on your machine in this case. See [GH issue](https://github.com/cline/cline/issues/1160) for more details.

agent_uno/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Core functionality for the MCP server."""

agent_uno/core/schemas.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""MCP endpoint schemas."""
2+
3+
import datetime
4+
from typing import Optional
5+
6+
from pydantic import BaseModel, Field
7+
8+
9+
class AccountInfo(BaseModel):
10+
"""The account info of a LiChess user."""
11+
12+
id: str
13+
username: str
14+
perfs: dict[str, dict[str, int | bool]]
15+
created_at: datetime.datetime = Field(alias="createdAt")
16+
seen_at: datetime.datetime = Field(alias="seenAt")
17+
play_time: dict[str, int] = Field(alias="playTime")
18+
url: str
19+
count: dict[str, int]
20+
followable: bool
21+
following: bool
22+
blocking: bool
23+
24+
25+
class CreatedGame(BaseModel):
26+
"""The response of creating a new game."""
27+
28+
id: str
29+
rated: bool
30+
variant: dict[str, str]
31+
fen: str
32+
turns: int
33+
source: str
34+
speed: str
35+
perf: str
36+
created_at: datetime.datetime = Field(alias="createdAt")
37+
status: dict[str, str | int]
38+
player: str
39+
full_id: str = Field(alias="fullId")
40+
41+
42+
class UIConfig(BaseModel):
43+
"""The UI configuration of a game."""
44+
45+
url: str
46+
47+
48+
class State(BaseModel):
49+
"""A representation of the game state."""
50+
51+
type: str
52+
moves: str
53+
wtime: datetime.datetime
54+
btime: datetime.datetime
55+
winc: int
56+
binc: int
57+
status: str
58+
59+
60+
class CurrentState(BaseModel):
61+
"""The current state of a game."""
62+
63+
id: str
64+
variant: dict[str, str]
65+
speed: str
66+
perf: dict[str, str]
67+
rated: bool
68+
created_at: datetime.datetime = Field(alias="createdAt")
69+
white: dict[str, int]
70+
black: dict[str, Optional[str | bool | int]]
71+
initial_fen: str = Field(alias="initialFen")
72+
type: str
73+
state: State

agent_uno/server.py

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,46 @@
55
# 2. Agent is able to start a game
66
# 3. Agent is able to play the game by getting the current state and making moves.
77
"""
8-
import datetime
9-
from typing import cast
8+
from collections.abc import Callable
9+
from typing import Any, cast
1010

1111
from berserk import Client, TokenSession # type: ignore [import-not-found]
12+
from chess import Board # type: ignore [import-not-found]
13+
from core.schemas import ( # type: ignore [import-not-found]
14+
AccountInfo,
15+
CreatedGame,
16+
CurrentState,
17+
UIConfig,
18+
)
1219
from mcp.server.fastmcp import FastMCP # type: ignore [import-not-found]
13-
from pydantic import BaseModel, Field
1420

21+
mcp = FastMCP("chess-mcp", dependencies=["berserk", "python-chess"])
1522

16-
class AccountInfo(BaseModel):
17-
"""The account info of a LiChess user."""
23+
BOT_LEVEL = 3
1824

19-
id: str
20-
username: str
21-
perfs: dict[str, dict[str, int | bool]]
22-
created_at: datetime.datetime = Field(alias="createdAt")
23-
seen_at: datetime.datetime = Field(alias="seenAt")
24-
play_time: dict[str, int] = Field(alias="playTime")
25-
url: str
26-
count: dict[str, int]
27-
followable: bool
28-
following: bool
29-
blocking: bool
25+
session_state = {}
3026

3127

32-
class CreatedGame(BaseModel):
33-
"""The response of creating a new game."""
28+
def client_is_set_handler(func: Callable[[], Any]) -> Callable[[], Any]:
29+
"""A decorator to check if the client is set."""
3430

35-
id: str
36-
rated: bool
37-
variant: dict[str, str]
38-
fen: str
39-
turns: int
40-
source: str
41-
speed: str
42-
perf: str
43-
created_at: datetime.datetime = Field(alias="createdAt")
44-
status: dict[str, str | int]
45-
player: str
46-
full_id: str = Field(alias="fullId")
31+
def is_set_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Any:
32+
if "client" not in session_state:
33+
raise Exception("Client is not set. You need to log in first.")
34+
return func(*args, **kwargs)
4735

36+
return is_set_wrapper
4837

49-
mcp = FastMCP("chess-mcp", dependencies=["berserk"])
5038

51-
session_state = {}
39+
def id_is_set_handler(func: Callable[[], Any]) -> Callable[[], Any]:
40+
"""A decorator to check if the ID is set."""
41+
42+
def is_set_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Any:
43+
if "id" not in session_state:
44+
raise Exception("ID is not set. You need to start a game first.")
45+
return func(*args, **kwargs)
46+
47+
return is_set_wrapper
5248

5349

5450
@mcp.tool(description="Login to LiChess.") # type: ignore
@@ -62,40 +58,63 @@ async def login(api_key: str) -> None:
6258
session_state["client"] = Client(session)
6359

6460

61+
@client_is_set_handler
6562
@mcp.tool(description="Get account info.") # type: ignore
6663
async def get_account_info() -> AccountInfo:
6764
"""Get the account info of the logged in user."""
6865
return AccountInfo(**session_state["client"].account.get())
6966

7067

68+
@client_is_set_handler
7169
@mcp.tool(description="Create a new game.") # type: ignore
72-
async def create_game() -> str:
70+
async def create_game() -> UIConfig:
7371
"""An endpoint for creating a new game."""
7472
response = CreatedGame(
75-
**session_state["client"].challenges.create_ai(color="black")
73+
**session_state["client"].challenges.create_ai(color="black", level=BOT_LEVEL)
7674
)
77-
7875
session_state["id"] = response.id
7976

80-
return f"You can view the game taking place here: https://lichess-org.github.io/api-demo/#!/game/{response.id}"
77+
return UIConfig(url=f"https://lichess-org.github.io/api-demo/#!/game/{response.id}")
8178

8279

80+
@client_is_set_handler
81+
@id_is_set_handler
8382
@mcp.tool(description="End game.") # type: ignore
8483
async def end_game() -> None:
8584
"""End the current game."""
8685
session_state["client"].board.resign_game(session_state["id"])
8786

8887

89-
@mcp.tool(description="Get the current game state.") # type: ignore
90-
async def get_game_state() -> str:
88+
@client_is_set_handler
89+
@id_is_set_handler
90+
async def get_game_state() -> CurrentState:
9191
"""Get the current game state."""
92-
current_state = next(
93-
session_state["client"].board.stream_game_state(session_state["id"])
92+
return CurrentState(
93+
**next(session_state["client"].board.stream_game_state(session_state["id"]))
9494
)
95-
return cast(str, current_state["state"]["moves"])
9695

9796

97+
@mcp.tool(description="Get all previous moves in the match.") # type: ignore
98+
async def get_previous_moves() -> list[str]:
99+
"""Get all previous moves in the match."""
100+
current_state = await get_game_state()
101+
return cast(list[str], current_state.state.moves.split())
102+
103+
104+
@client_is_set_handler
105+
@id_is_set_handler
98106
@mcp.tool(description="Make a move.") # type: ignore
99107
async def make_move(move: str) -> None:
100108
"""Make a move in the current game."""
101109
session_state["client"].board.make_move(session_state["id"], move)
110+
111+
112+
@mcp.tool(description="Get the current board.") # type: ignore
113+
async def get_board() -> str:
114+
"""An endpoint for getting the current board as an ASCII representation."""
115+
board = Board()
116+
117+
for move in await get_previous_moves():
118+
board.push_uci(move)
119+
120+
return cast(str, board.__str__())

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ description = "An LLM agent built using Model Context Protocol to play online ga
55
authors = [{ name = "Fuzzy Labs", email = "info@fuzzylabs.ai" }]
66
readme = "README.md"
77
requires-python = ">=3.12,<4.0"
8-
dependencies = ["berserk>=0.13.2", "mcp[cli]>=1.6.0", "pydantic>=2.11.1"]
8+
dependencies = [
9+
"berserk>=0.13.2",
10+
"mcp[cli]>=1.6.0",
11+
"pydantic>=2.11.1",
12+
"python-chess>=1.999",
13+
]
914

1015
[dependency-groups]
1116
dev = ["pytest>=7.2.0", "pytest-cov>=4.0.0", "licensecheck>=2024.1.2"]

uv.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)