ttt_make_move
Place the current player's mark at specified coordinates (0-2) and receive the updated board, game status, and legal moves in one response.
Instructions
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.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| row | Yes | Row index: 0 = top row, 1 = middle row, 2 = bottom row | |
| col | Yes | Column index: 0 = left col, 1 = middle col, 2 = right col |
Implementation Reference
- src/games/tictactoe.ts:158-209 (handler)The ttt_make_move tool handler function. It validates the game is in progress, checks if the target square is empty, places the current player's mark, checks for winner/draw, switches turns, and returns the updated board state.
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)}`, }, ], }; } ); - src/games/tictactoe.ts:154-157 (schema)Input schema for ttt_make_move: row (0-2) and col (0-2) as integers.
{ 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"), }, - src/games/tictactoe.ts:151-209 (registration)Registration of the ttt_make_move tool on the MCP server via server.tool() with name, description, schema, and handler.
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)}`, }, ], }; } ); - src/games/tictactoe.ts:113-228 (registration)The registerTicTacToeTools function which registers all Tic-Tac-Toe tools (including ttt_make_move) on the MCP server. Called from src/index.ts line 16.
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 }] }; } ); } - src/games/tictactoe.ts:74-110 (helper)The renderState helper function used by the handler to format board output. Also checkWinner (lines 39-62) and getLegalMoves (lines 64-72) are helpers used by the handler.
function renderState(g: GameState): string { const cell = (c: Cell) => (c === null ? " " : c); const lines = [ ` col 0 col 1 col 2`, `row 0 ${cell(g.board[0][0])} | ${cell(g.board[0][1])} | ${cell(g.board[0][2])}`, ` -----+-------+-----`, `row 1 ${cell(g.board[1][0])} | ${cell(g.board[1][1])} | ${cell(g.board[1][2])}`, ` -----+-------+-----`, `row 2 ${cell(g.board[2][0])} | ${cell(g.board[2][1])} | ${cell(g.board[2][2])}`, ``, `Players: X = ${g.playerX} | O = ${g.playerO}`, `Status: ${g.status}`, ]; if (g.status === "in_progress") { const currentName = g.currentPlayer === "X" ? g.playerX : g.playerO; lines.push(`Next move: ${g.currentPlayer} (${currentName})`); const legal = getLegalMoves(g.board) .map(([r, c]) => `(row ${r}, col ${c})`) .join(" "); lines.push(`Legal moves: ${legal}`); } else if (g.status === "x_wins" || g.status === "o_wins") { const winnerName = g.status === "x_wins" ? g.playerX : g.playerO; const winnerMark = g.status === "x_wins" ? "X" : "O"; lines.push(`Winner: ${winnerMark} (${winnerName})`); if (g.winningLine) { const wl = g.winningLine.map(([r, c]) => `(row ${r}, col ${c})`).join(", "); lines.push(`Winning line: ${wl}`); } } else if (g.status === "draw") { lines.push("Result: It's a draw!"); } lines.push(`\n[Show the board above verbatim to the human player — they need the row/col labels to know where to move.]`); return lines.join("\n"); }