API & MCP
Everything this site does is an HTTP call you can make too. You can list the games, start one, make moves, and ask the same computer you play here for its move. It answers in JSON, it's free, and there's no key or anything to sign up for.
The API lives under https://tictactoe.fun/api/v1. The whole contract is
/api/v1/openapi.yaml, an OpenAPI document you can read
or point a client generator at. This page is the short version.
If you'd rather have an AI assistant play, skip to MCP.
Limits
-
bestandanalyzeon Connect Four and Ultimate Tic-Tac-Toe run a search, and the server runs only a few at a time. If they all stay busy for four seconds, or too many searches are already waiting, you get a 503 with the codebusy, andRetry-Aftersays how many seconds to wait. - In MCP, those same searches spend from a session's budget, and the server holds only so many sessions at once. The MCP section below has both figures.
-
POST /api/v1/roomsanswers a 503 with the coderoom_capwhen the server already has as many rooms open as it holds.
Try it with curl
A game is the moves played so far. You send the list, and the server plays it through the same engine this site runs and tells you where the game stands. There's nothing to keep between calls but the list.
A move is a square, row then column, counted from 0 at the top left. On a
3×3 board 1.1 is the middle and 0.2 the top right corner. In a URL the
moves go in moves, separated by commas, and no moves at all is the empty
board:
moves=, the empty board.Each step below draws the board its moves reach.
Start with the list of games. Each one has an id you'll use in every other
call. Here you see just the first game.
curl https://tictactoe.fun/api/v1/games
{"games": [{"id": "ttt", "name": "Tic-Tac-Toe", "players": 2}]}
See the moves
legal tells you where a list of moves leaves the game. With no moves, that's
the empty board:
curl 'https://tictactoe.fun/api/v1/games/ttt/legal?moves='
{"game": "ttt", "start": 0, "moves": [],
"board": [["", "", ""], ["", "", ""], ["", "", ""]],
"turn": "X", "over": false,
"legal": [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]]}
board is the rows from the top, and legal lists every move you can make,
written the way you send one back. In JSON a move is [row, column].
Make a move
To play, post the moves so far and the one to add. X takes the middle:
curl -X POST https://tictactoe.fun/api/v1/games/ttt/moves -d '{"moves": [], "move": [1, 1]}'
{"game": "ttt", "start": 0, "moves": [[1, 1]],
"board": [["", "", ""], ["", "X", ""], ["", "", ""]],
"turn": "O", "over": false}
moves=1.1, X in the middle. The next few steps ask about this board.The answer's moves has your move on the end. Send it back unchanged next
time, and you never have to track the game yourself.
over is always there. While the game goes on, turn says who moves next.
Once it's over, result is the winner, X or O, or draw. Here X finishes
the top row:
moves=0.0,1.0,0.1,1.1, X to move with two in the top row.curl -X POST https://tictactoe.fun/api/v1/games/ttt/moves -d '{"moves": [[0, 0], [1, 0], [0, 1], [1, 1]], "move": [0, 2]}'
{"game": "ttt", "moves": [[0, 0], [1, 0], [0, 1], [1, 1], [0, 2]],
"over": true, "result": "X", "legal": []}
moves=0.0,1.0,0.1,1.1,0.2, three in the top row, so X has won.Ask for the best move
curl 'https://tictactoe.fun/api/v1/games/ttt/best?moves=1.1'
{"game": "ttt", "moves": [[1, 1]], "move": [0, 0], "source": "table"}
source says where the answer came from. table and book are proven, so
that's perfect play. search and mcts are strong, but they don't prove
anything, and random is the easy level's pick.
For a weaker opponent, add level=easy or level=medium. Pass a seed and
you get the same move every time. Leave it out and the server picks one and
tells you which.
curl 'https://tictactoe.fun/api/v1/games/ttt/best?moves=1.1&level=easy&seed=7'
{"game": "ttt", "moves": [[1, 1]], "move": [0, 1], "source": "random",
"level": "easy", "seed": 7}
Rate every move
analyze rates each legal move for the player making it: the outcome with
best play from both sides, in how many moves the game has left, and best
on the ones worth playing. Here you see just the first two moves.
curl 'https://tictactoe.fun/api/v1/games/ttt/analyze?moves=1.1'
{"game": "ttt", "moves": [[1, 1]], "turn": "O", "evaluations": [
{"move": [0, 0], "value": 0, "outcome": "draw", "in": 8, "best": true},
{"move": [0, 1], "value": -5, "outcome": "loss", "in": 6}
]}
Other games
Every game draws its board as rows from the top, and a move is always the
square it plays. In Connect Four that's the square your piece lands on, so the
first move in the middle column is 5.3, the bottom row. A few games need a
little more:
- In Three Men's Morris, once all six pieces are down a move takes a piece from
one point to another:
0.0>1.1in a URL,[[0, 0], [1, 1]]in JSON. The answer'sphasesays which kind of move comes next. - In Wild Tic-Tac-Toe you pick your mark:
1.1=O, or[1, 1, "O"]. - In Ultimate the board is the whole 9×9, and
playInsays which small board the next move has to go in, ornullwhen it can go anywhere. - Treblecross comes in nine board lengths. Its document,
/api/v1/games/treblecross, lists them asstarts, andstart=0picks the first.
Connect Four and Ultimate
These two aren't in a table, so the computer can't look the answer up. In
Connect Four it searches ten moves ahead, and in Ultimate it plays out
thousands of random games from the position and picks the move those games
went through most often. Either way you get a strong move rather than a proven one, and source
tells you which kind you got. Here each player has dropped a piece in the
middle column:
curl 'https://tictactoe.fun/api/v1/games/connect4/best?moves=5.3,4.3'
{"game": "connect4", "moves": [[5, 3], [4, 3]], "move": [3, 3],
"source": "search", "depth": 10}
moves=5.3,4.3, one piece each in the middle column.depth is how many moves ahead the search looked. An Ultimate answer says
mcts instead, with iterations, the number of games it played.
A new version of the engine can answer these differently, so they're cached
for an hour rather than for good. Each one carries an ETag. Ask again with
it in If-None-Match and you get a 304 until the engine changes.
Errors
Anything that isn't a success comes back in one shape. When a move is the
problem, at says which one, counting from 0:
curl -X POST https://tictactoe.fun/api/v1/games/ttt/moves -d '{"moves": [[1, 1]], "move": [1, 1]}'
{"error": {"code": "occupied", "message": "move 1, 1.1, was refused: engine: cell occupied", "at": 1}}
Switch on code, which stays the same. message is written for people and
can change. A room's socket and the MCP tools use the same codes, so you can handle a
refusal one way wherever you meet it.
bad_move- A move isn't written as one, or has the wrong shape for the position.
atsays which move. illegal_move- That move isn't on this board, or this position doesn't allow it.
occupied- The square is taken, or in Ultimate, it's not on a board you were sent to.
game_over- The game has ended, so there's no move to make or recommend.
unknown_game- No game has that id.
bad_body- The body isn't the JSON object the contract describes.
bad_startstartisn't one of the game's openings.bad_argument- An MCP tool argument is outside the values it takes.
not_found- Nothing is served at that path.
no_room- No live room has that code.
bad_levellevelisn't easy, medium or hard, or theseedisn't one it takes.bad_query- The query has a parameter that endpoint doesn't read, or doesn't parse.
method_not_allowed- The path doesn't take that method.
Allowlists the ones it does. not_your_turn- In a room, it's the other player's move.
not_owner- Only the player who opened the room can start another round.
game_not_over- Another round can start only once this one has ended.
room_cap- The server has as many rooms open as it can hold. Try again after
Retry-After. budget_spent- This MCP session has spent its expensive calls. A new session starts fresh.
analysis_disabled- The server has switched off searches for this game for now. Games answered from a table still answer.
busy- Every search the server runs at once is taken. Try again after
Retry-After. session_cap- The server has as many MCP sessions open as it holds. Try again after
Retry-After. Open sessions keep working. no_solver- The rules run, but nothing here can recommend a move.
host_failure- The engine failed on the server. Nothing was wrong with your request.
server_failure- The room failed on the server. Nothing was wrong with your message.
MCP
https://tictactoe.fun/mcp is the same engine over the Model Context
Protocol, on Streamable HTTP. Point an AI assistant at it and it can list the
games, play one, and ask the engine for the answer. There's no key here
either.
To add it to Claude Code:
claude mcp add --transport http tictactoe https://tictactoe.fun/mcp
A client that reads a JSON file, like a project's .mcp.json, takes it this
way:
{
"mcpServers": {
"tictactoe": {"type": "http", "url": "https://tictactoe.fun/mcp"}
}
}
On claude.ai, add the same address as a custom connector under Settings, then Connectors.
There's no sign-in, and nothing about you is kept. Privacy says what the endpoint does with a call, and Terms covers using it.
Tools
list_gameslists every game, with its id and board.get_gameis one game in full: its board, and its rules as markdown.new_gameopens a game and draws the board. Passfirst: "o"and the engine plays first, andlevelto open against easy, medium or hard.legal_moveslists the moves you can play in a position.make_moveplays one more move and draws the board after it.best_moveis the engine's move and what proved it. It takes the samelevelandseedas the API.analyzerates every legal move.render_boarddraws the position as text, with the name of each square and every legal move.
None of the tools change anything on the server. The game lives in the list of moves you pass.
Each game is also a resource, game://{id}, with its rules.
The prompt play_a_game sets up a whole game and has the model score itself
against the engine afterwards.
A game is the moves played so far, the same as in the API. Every result hands
back start and moves. Pass them to the next call as they are. A move is
[row, column] from the top left, and every result lists the legal ones.
Every result carries a budget. A session gets 200 expensive calls: a
Connect Four or Ultimate answer spends one, unless it's at easy, and
everything else is free. An answer refused as busy spends nothing.
The server holds 1,000 sessions at once. Past that, a new session gets a 503
with the code session_cap and a Retry-After, and every open session keeps
working.
A board in the chat
In Claude, ChatGPT and any other chat that supports MCP Apps, new_game
opens a board right in the conversation. Click a square to play, and the
computer answers at the level you pick, or the one the chat asked
new_game for. The board tells the chat where the
game stands after every move, so you can ask about the position or pick the
game up in words. A chat without MCP Apps plays the same game from the text
board.
Every move on the board is a tool call like any other, so a Connect Four or Ultimate game above easy spends from that budget.
An example
After X takes the middle, a model asks for O's best move:
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "best_move", "arguments": {"game": "ttt", "moves": [[1, 1]]}}}
The result's structured content:
{"game": "ttt", "start": 0, "moves": [[1, 1]], "move": [0, 0], "play": "a1",
"source": "table", "proven": true, "budget": {"limit": 200, "remaining": 200}}