ttt_get_legal_moves
Retrieve all empty squares on the tic-tac-toe board as (row, col) pairs to identify legal moves when needed separately.
Instructions
Returns all empty squares as (row, col) pairs. Legal moves are already included in every ttt_new_game/ttt_make_move response — only call this standalone if you need them in isolation.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/games/tictactoe.ts:211-227 (handler)Registration and handler for the 'ttt_get_legal_moves' tool. Registers the tool with an empty schema (no params), returns all empty squares as (row, col) pairs by calling getLegalMoves().
server.tool( "ttt_get_legal_moves", "Returns all empty squares as (row, col) pairs. Legal moves are already included in every ttt_new_game/ttt_make_move response — only call this standalone if you need them in isolation.", {}, async () => { if (game.status !== "in_progress") { return { content: [{ type: "text", text: `Game is over (${game.status}). No legal moves.` }], }; } const moves = getLegalMoves(game.board); const text = `Legal moves (${moves.length}): ` + moves.map(([r, c]) => `(row ${r}, col ${c})`).join(" "); return { content: [{ type: "text", text }] }; } ); - src/games/tictactoe.ts:64-72 (helper)Helper function getLegalMoves that iterates the 3x3 board and returns all empty cell coordinates as [row, col] pairs.
function getLegalMoves(board: Board): [number, number][] { const moves: [number, number][] = []; for (let r = 0; r < 3; r++) { for (let c = 0; c < 3; c++) { if (board[r][c] === null) moves.push([r, c]); } } return moves; } - src/games/tictactoe.ts:113-228 (registration)The registerTicTacToeTools function registers all tic-tac-toe tools (ttt_new_game, ttt_get_state, ttt_make_move, ttt_get_legal_moves) with the MCP server. This is the entry point for tool registration.
export function registerTicTacToeTools(server: McpServer): void { server.tool( "ttt_new_game", "Start a fresh Tic-Tac-Toe game. X always moves first. Optionally name the players so roles are explicit in every response.", { player_x_name: z .string() .optional() .default("X") .describe("Name for the X player (e.g. 'Claude', 'Human'). Defaults to 'X'."), player_o_name: z .string() .optional() .default("O") .describe("Name for the O player (e.g. 'Human', 'Claude'). Defaults to 'O'."), }, async ({ player_x_name, player_o_name }) => ({ content: [ { type: "text", text: ((): string => { game = newGameState(player_x_name ?? "X", player_o_name ?? "O"); return `New Tic-Tac-Toe game started.\n\n${renderState(game)}`; })(), }, ], }) ); server.tool( "ttt_get_state", "Get the current Tic-Tac-Toe board, status, player names, and legal moves.", {}, async () => ({ content: [{ type: "text", text: renderState(game) }], }) ); server.tool( "ttt_make_move", "Place the current player's mark at (row, col). Rows and columns are 0–2; top-left is row 0, col 0. The server alternates turns automatically. Returns updated board, status, and legal moves in one response.", { row: z.number().int().min(0).max(2).describe("Row index: 0 = top row, 1 = middle row, 2 = bottom row"), col: z.number().int().min(0).max(2).describe("Column index: 0 = left col, 1 = middle col, 2 = right col"), }, async ({ row, col }) => { if (game.status !== "in_progress") { return { content: [{ type: "text", text: `Game is already over (${game.status}). Call ttt_new_game to play again.` }], isError: true, }; } if (game.board[row][col] !== null) { const occupiedBy = game.board[row][col] as "X" | "O"; const occupiedByName = occupiedBy === "X" ? game.playerX : game.playerO; return { content: [ { type: "text", text: [ `Error: square_occupied`, `(row ${row}, col ${col}) is already taken by ${occupiedBy} (${occupiedByName}).`, ``, renderState(game), ].join("\n"), }, ], isError: true, }; } const placed = game.currentPlayer; const placedName = placed === "X" ? game.playerX : game.playerO; game.board[row][col] = placed; game.moveCount++; const result = checkWinner(game.board); if (result) { game.status = result.winner === "X" ? "x_wins" : "o_wins"; game.winningLine = result.line; } else if (game.moveCount === 9) { game.status = "draw"; } else { game.currentPlayer = placed === "X" ? "O" : "X"; } return { content: [ { type: "text", text: `${placedName} (${placed}) placed at row ${row}, col ${col}.\n\n${renderState(game)}`, }, ], }; } ); server.tool( "ttt_get_legal_moves", "Returns all empty squares as (row, col) pairs. Legal moves are already included in every ttt_new_game/ttt_make_move response — only call this standalone if you need them in isolation.", {}, async () => { if (game.status !== "in_progress") { return { content: [{ type: "text", text: `Game is over (${game.status}). No legal moves.` }], }; } const moves = getLegalMoves(game.board); const text = `Legal moves (${moves.length}): ` + moves.map(([r, c]) => `(row ${r}, col ${c})`).join(" "); return { content: [{ type: "text", text }] }; } ); }