Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
rowYesRow index: 0 = top row, 1 = middle row, 2 = bottom row
colYesColumn index: 0 = left col, 1 = middle col, 2 = right col

Implementation Reference

  • 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)}`,
            },
          ],
        };
      }
    );
  • 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"),
    },
  • 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)}`,
            },
          ],
        };
      }
    );
  • 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 }] };
        }
      );
    }
  • 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");
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It discloses that the server alternates turns automatically and returns updated board, status, and legal moves. However, it does not specify error handling (e.g., if the cell is occupied or move is invalid), which is a minor gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description consists of two succinct sentences. The first sentence immediately states the action, and the second provides key coordinates and behavior. No unnecessary words, front-loaded with the core functionality.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema is provided, but the description lists the three return elements (board, status, legal moves). This is sufficient for an agent to understand the response shape. It does not explain the meaning of 'status' (e.g., win/draw/ongoing), but that is implied for a Tic-Tac-Toe game.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% coverage with descriptions for both row and col, including coordinate ranges and orientations. The tool description adds minimal extra information (repeating that rows/columns are 0-2). Given high schema coverage, the baseline is 3, and the description does not significantly enhance meaning.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool places the current player's mark at (row, col), specifies coordinate range (0-2) and orientation (top-left). This distinguishes it from sibling tools like ttt_get_legal_moves, ttt_get_state, and ttt_new_game, as it is the only move-making action.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explains that the server alternates turns automatically, which implies this is for making a move during a game. It does not explicitly state when not to use it (e.g., game not started, cell occupied), but the context of a turn-based game is clear. No alternatives are mentioned because it is the only move tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SrmTech-git/MCPArcade'

If you have feedback or need assistance with the MCP directory API, please join our Discord server