MCP server & API

Let Claude sit down at the table: start games, guess, ask for hints, resign and read the scoreboard, remotely.

MCP 2025-06-18 · 2025-03-26 · 2024-11-05Streamable HTTP + stdioREST + OpenAPI 3.18 tools · 3 resources · 2 promptsno dependenciesv1.0.0
PLAY IN THE BROWSEROPENAPI.JSONSOURCE

Overview

The Mastermind server is one Node.js process with no dependencies. It serves the browser game, a REST API under /api, and an MCP server at /mcp using the Streamable HTTP transport, so any MCP client (Claude Code, Claude Desktop, claude.ai custom connectors, the MCP Inspector, your own agent) can connect over the network and play. A stdio bridge is included for clients that launch servers as local commands. Every player, human or agent, plays on the same scoreboard.

Codebreaker

new_game hides a random code; the agent calls guess until the key pegs are all black or the guesses run out. Lower guess counts score higher.

Codemaker

new_game with mode: "maker" and a secret: the server's Knuth minimax solver breaks the code and the transcript comes back at once. Points = guesses the solver needed.

Scoreboard

get_scores returns top scores, codemakers, per-player statistics and recent games; pass player for one player's own record.

Same catalog everywhere

The MCP tools and the REST routes are generated from one catalog with one validator: identical arguments, identical JSON, identical error codes.

Quick start

1. Run the server

git clone https://github.com/gextam/mastermind-web.git
cd mastermind-web
node server/server.js            # game http://localhost:8772/  ·  MCP http://localhost:8772/mcp

# or, on Windows, double-click start.cmd

Node.js 20 or newer, nothing to install. The data file server/data/state.json is created on first use.

2. Connect a client

Claude Code (remote, Streamable HTTP)

claude mcp add --transport http mastermind https://cocojs.com/games/mastermind/mcp
# with an API key configured on the server:
claude mcp add --transport http mastermind https://cocojs.com/games/mastermind/mcp --header "Authorization: Bearer YOUR_KEY"

Claude Desktop (stdio bridge in claude_desktop_config.json)

{
  "mcpServers": {
    "mastermind": {
      "command": "node",
      "args": [
        "/path/to/mastermind-web/server/mcp-stdio.js",
        "--url",
        "https://cocojs.com/games/mastermind/mcp"
      ]
    }
  }
}

The bridge forwards every message to the HTTP server so Desktop plays on the shared scoreboard. Pass --local instead of --url to play entirely in-process with a private data file (no server needed), or add --api-key=… when the server requires one.

claude.ai custom connector

Settings → Connectors → Add custom connector → URL https://cocojs.com/games/mastermind/mcp. The server is stateless and needs no OAuth; if you set an API key the connector cannot send it, so leave the key unset for claude.ai or put the server behind your own gateway.

MCP Inspector

npx @modelcontextprotocol/inspector
# transport: Streamable HTTP · URL: https://cocojs.com/games/mastermind/mcp

3. Say the word

“Play a game of Mastermind as Claude on the classic board. Read the rules first, think about which codes are still possible after every answer, and tell me the secret, your guess count, your score and your rank when you are done.”

There is also a ready-made prompt, play, that says exactly that (see prompts), and node tools/play-demo.js plays a game through the MCP endpoint from the command line so you can watch the transcript.

A game, call by call

agent → server tools/call rules (once; notation, boards, scoring) agent → server tools/call new_game {player, preset} → game_id, palette, guesses_left, message agent → server tools/call guess {game_id, "RROO"} → feedback {black, white}, history, guesses_left agent → server tools/call guess … → … (keep only codes consistent with every answer) server → agent status "won" | "lost", secret, score (the secret is revealed only when the game is over) agent → server tools/call get_scores {player} → rank, statistics, recent games

Games have no session: the game_id is the only handle, so an agent can stop and resume from any client. Unfinished games expire after 24 hours of inactivity; finished games remain readable through get_game and in the scoreboard.

Tools

Every tool returns the result both as a JSON text block and as structuredContent. Validation and game errors come back as a tool result with isError: true and a message such as Error (game_over): …; protocol-level problems (unknown tool, malformed request) are JSON-RPC errors.

ToolPurposeREST
rulesRules and notationGET /api/rules
new_gameStart a gamePOST /api/games
guessMake a guessPOST /api/games/{game_id}/guess
get_gameGame stateGET /api/games/{game_id}
list_gamesList gamesGET /api/games
hintHintPOST /api/games/{game_id}/hint
resignResignPOST /api/games/{game_id}/resign
get_scoresScoreboardGET /api/scores

rules GET /api/rulesread-only

The rules of Mastermind as played here: boards (presets), the colours and how to write a guess, how the black/white key pegs are counted, the scoring formula and a sound strategy. Read it once before your first game.

No arguments.

MCP call

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "rules",
    "arguments": {}
  }
}

REST equivalent

curl https://cocojs.com/games/mastermind/api/rules

Result (structuredContent / response body)

{
  "text": "# Mastermind\n\nThe codemaker hides a code of N coloured pegs; the codebreaker has a limited number of guesses to find it.\n… (full markdown continues)",
  "feedback": {
    "black": "right colour in the right position",
    "white": "right colour in the wrong position",
    "note": "key pegs are counts, not positions; each peg is matched at most once"
  },
  "presets": {
    "classic": {
      "label": "Classic",
      "pegs": 4,
      "colors": 6,
      "max_guesses": 10,
      "allow_duplicates": true,
      "allow_blanks": false,
      "code_space": 1296
    },
    "mini": {
      "label": "Mini",
      "pegs": 3,
      "colors": 6,
      "max_guesses": 8,
      "allow_duplicates": true,
      "allow_blanks": false,
      "code_space": 216
    },
    "unique": {
      "label": "No repeats",
      "pegs": 4,
      "colors": 6,
      "max_guesses": 10,
      "allow_duplicates": false,
      "allow_blanks": false,
      "code_space": 360
    },
    "blanks": {
      "label": "With blanks",
      "pegs": 4,
      "colors": 6,
      "max_guesses": 12,
      "allow_duplicates": true,
      "allow_blanks": true,
      "code_space": 2401
    },
    "super": {
      "label": "Super",
      "pegs": 5,
      "colors": 8,
      "max_guesses": 12,
      "allow_duplicates": true,
      "allow_blanks": false,
      "code_space": 32768
    },
    "grand": {
      "label": "Grand",
      "pegs": 6,
      "colors": 8,
      "max_guesses": 15,
      "allow_duplicates": true,
      "allow_blanks": false,
      "code_space": 262144
    }
  },
  "limits": {
    "pegs": [
      2,
      8
    ],
    "colors": [
      2,
      10
    ],
    "max_guesses": [
      1,
      30
    ],
    "player_name_length": 24
  },
  "palette": [
    {
      "id": 1,
      "name": "red",
      "letter": "R",
      "hex": "#e3261f"
    },
    {
      "id": 2,
      "name": "orange",
      "letter": "O",
      "hex": "#ff7f11"
    },
    {
      "id": 3,
      "name": "yellow",
      "letter": "Y",
      "hex": "#ffd42a"
    },
    {
      "id": 4,
      "name": "green",
      "letter": "G",
      "hex": "#21a83f"
    },
    {
      "id": 5,
      "name": "blue",
      "letter": "B",
      "hex": "#1f5fe0"
    },
    {
      "id": 6,
      "name": "purple",
      "letter": "P",
      "hex": "#7e35c4"
    },
    {
      "id": 7,
      "name": "white",
      "letter": "W",
      "hex": "#f4f4f4"
    },
    {
      "id": 8,
      "name": "black",
      "letter": "K",
      "hex": "#1b1b1f"
    },
    {
      "id": 9,
      "name": "cyan",
      "letter": "C",
      "hex": "#19c6d9"
    },
    {
      "id": 10,
      "name": "pink",
      "letter": "N",
      "hex": "#ff5fa8"
    }
  ],
  "blank": {
    "id": 0,
    "name": "empty",
    "letter": ".",
    "only_when": "allow_blanks is true"
  },
  "guess_formats": [
    "[\"red\",\"green\",\"blue\",\"yellow\"]",
    "\"RGBY\"",
    "\"1453\"",
    "\"red, green, blue, yellow\"",
    "[1,4,5,3]"
  ],
  "scoring": {
    "codebreaker": "round(1000 * (max_guesses + 1 - guesses_used - hints_used) / max_guesses * difficulty); difficulty = ln(code_space)/ln(1296); lost or resigned = 0",
    "codemaker": "points = guesses the solver needed (+1 if it failed)"
  }
}

new_game POST /api/games

Start a new game. Default: you are the codebreaker on the classic board (4 pegs, 6 colours, 10 guesses, repeats allowed) and the server hides a random code. Pass preset or the individual settings for other boards. With mode "maker" you supply the secret instead and the server's Knuth solver tries to break it; the game is returned already finished with the solver's transcript and your codemaker points. Returns the game_id you need for every other call, the palette, and a message telling you what to do next.

ArgumentTypeRequiredDescription
playerstringnoYour name on the scoreboard (default "Anonymous")
max length 24
modestringnobreaker: you guess the server's code (default). maker: you set the code, the server's solver breaks it.
one of breaker, maker
presetstringnoBoard preset: classic (4 pegs · 6 colours · 10 guesses); mini (3 pegs · 6 colours · 8 guesses); unique (4 pegs · 6 colours · no repeated colours); blanks (4 pegs · 6 colours + empty · 12 guesses); super (5 pegs · 8 colours · 12 guesses); grand (6 pegs · 8 colours · 15 guesses)
one of classic, mini, unique, blanks, super, grand
pegsintegernoPegs in the code (overrides the preset)
min 2 · max 8
colorsintegernoNumber of colours in play (the first N of the palette)
min 2 · max 10
max_guessesintegernoGuesses allowed
min 1 · max 30
allow_duplicatesbooleannoMay the code repeat a colour? (default true)
allow_blanksbooleannoMay the code contain empty positions? (default false)
secretstring | arraynomaker mode only: the code the solver must break. A code: an array of colour names, letters or numbers (["red","green","blue","yellow"], ["R","G","B","Y"], [1,4,5,3]) or a string of letters/digits ("RGBY", "1453"). Letters: R red, O orange, Y yellow, G green, B blue, P purple, W white, K black, C cyan, N pink; "." is empty when blanks are allowed.

MCP call

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "new_game",
    "arguments": {
      "player": "Claude",
      "preset": "classic"
    }
  }
}

REST equivalent

curl -X POST https://cocojs.com/games/mastermind/api/games \
  -H "Content-Type: application/json" \
  -d '{"player":"Claude","preset":"classic"}'

Result (structuredContent / response body)

{
  "game_id": "k83ppwt7b6mp",
  "player": "Claude",
  "mode": "breaker",
  "status": "playing",
  "finished": false,
  "config": {
    "preset": "classic",
    "pegs": 4,
    "colors": 6,
    "max_guesses": 10,
    "allow_duplicates": true,
    "allow_blanks": false,
    "code_space": 1296,
    "difficulty": 1
  },
  "palette": [
    {
      "id": 1,
      "name": "red",
      "letter": "R"
    },
    {
      "id": 2,
      "name": "orange",
      "letter": "O"
    },
    {
      "id": 3,
      "name": "yellow",
      "letter": "Y"
    },
    {
      "id": 4,
      "name": "green",
      "letter": "G"
    },
    {
      "id": 5,
      "name": "blue",
      "letter": "B"
    },
    {
      "id": 6,
      "name": "purple",
      "letter": "P"
    }
  ],
  "guesses_used": 0,
  "guesses_left": 10,
  "hints_used": 0,
  "history": [],
  "created_at": "2026-11-08T12:00:04.000Z",
  "updated_at": "2026-11-08T12:00:04.000Z",
  "message": "Guess 1 of 10 (10 left). Send a code of 4 colours with guess(game_id, guess); black = right colour in the right position, white = right colour in the wrong position."
}

guess POST /api/games/{game_id}/guess

Submit a guess for a game you are breaking. Returns the key pegs (black = right colour in the right position, white = right colour in the wrong position), the full history so far, guesses left, and when the game ends the secret and your score. A guess must have exactly as many pegs as the code.

ArgumentTypeRequiredDescription
game_idstringyesThe game_id returned by new_game
max length 40
guessstring | arrayyesA code: an array of colour names, letters or numbers (["red","green","blue","yellow"], ["R","G","B","Y"], [1,4,5,3]) or a string of letters/digits ("RGBY", "1453"). Letters: R red, O orange, Y yellow, G green, B blue, P purple, W white, K black, C cyan, N pink; "." is empty when blanks are allowed.

MCP call

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "guess",
    "arguments": {
      "game_id": "k83ppwt7b6mp",
      "guess": [
        "red",
        "red",
        "orange",
        "orange"
      ]
    }
  }
}

REST equivalent

curl -X POST https://cocojs.com/games/mastermind/api/games/k83ppwt7b6mp/guess \
  -H "Content-Type: application/json" \
  -d '{"guess":["red","red","orange","orange"]}'

Result (structuredContent / response body)

{
  "feedback": {
    "black": 0,
    "white": 1
  },
  "correct": false,
  "game_id": "k83ppwt7b6mp",
  "player": "Claude",
  "mode": "breaker",
  "status": "playing",
  "finished": false,
  "config": {
    "preset": "classic",
    "pegs": 4,
    "colors": 6,
    "max_guesses": 10,
    "allow_duplicates": true,
    "allow_blanks": false,
    "code_space": 1296,
    "difficulty": 1
  },
  "palette": [
    {
      "id": 1,
      "name": "red",
      "letter": "R"
    },
    {
      "id": 2,
      "name": "orange",
      "letter": "O"
    },
    {
      "id": 3,
      "name": "yellow",
      "letter": "Y"
    },
    {
      "id": 4,
      "name": "green",
      "letter": "G"
    },
    {
      "id": 5,
      "name": "blue",
      "letter": "B"
    },
    {
      "id": 6,
      "name": "purple",
      "letter": "P"
    }
  ],
  "guesses_used": 1,
  "guesses_left": 9,
  "hints_used": 0,
  "history": [
    {
      "turn": 1,
      "guess": [
        "red",
        "red",
        "orange",
        "orange"
      ],
      "letters": "RROO",
      "black": 0,
      "white": 1
    }
  ],
  "created_at": "2026-11-08T12:00:04.000Z",
  "updated_at": "2026-11-08T12:00:08.000Z",
  "message": "Guess 2 of 10 (9 left). Last answer: 0 black, 1 white. Send a code of 4 colours with guess(game_id, guess); black = right colour in the right position, white = right colour in the wrong position."
}

get_game GET /api/games/{game_id}read-only

The current state of a game: configuration, palette, every guess with its feedback, guesses left, and the secret and score once it is finished. Finished games stay readable (archived) after the live object is pruned.

ArgumentTypeRequiredDescription
game_idstringyesThe game_id returned by new_game
max length 40

MCP call

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_game",
    "arguments": {
      "game_id": "k83ppwt7b6mp"
    }
  }
}

REST equivalent

curl https://cocojs.com/games/mastermind/api/games/k83ppwt7b6mp

Result (structuredContent / response body)

{
  "game_id": "k83ppwt7b6mp",
  "player": "Claude",
  "mode": "breaker",
  "status": "won",
  "finished": true,
  "config": {
    "preset": "classic",
    "pegs": 4,
    "colors": 6,
    "max_guesses": 10,
    "allow_duplicates": true,
    "allow_blanks": false,
    "code_space": 1296,
    "difficulty": 1
  },
  "palette": [
    {
      "id": 1,
      "name": "red",
      "letter": "R"
    },
    {
      "id": 2,
      "name": "orange",
      "letter": "O"
    },
    {
      "id": 3,
      "name": "yellow",
      "letter": "Y"
    },
    {
      "id": 4,
      "name": "green",
      "letter": "G"
    },
    {
      "id": 5,
      "name": "blue",
      "letter": "B"
    },
    {
      "id": 6,
      "name": "purple",
      "letter": "P"
    }
  ],
  "guesses_used": 5,
  "guesses_left": 5,
  "hints_used": 1,
  "history": [
    {
      "turn": 1,
      "guess": [
        "red",
        "red",
        "orange",
        "orange"
      ],
      "letters": "RROO",
      "black": 0,
      "white": 1
    },
    {
      "turn": 2,
      "guess": [
        "orange",
        "yellow",
        "green",
        "green"
      ],
      "letters": "OYGG",
      "black": 1,
      "white": 1
    },
    {
      "turn": 3,
      "guess": [
        "green",
        "blue",
        "red",
        "green"
      ],
      "letters": "GBRG",
      "black": 0,
      "white": 1
    },
    {
      "turn": 4,
      "guess": [
        "orange",
        "purple",
        "yellow",
        "blue"
      ],
      "letters": "OPYB",
      "black": 0,
      "white": 2
    },
    {
      "turn": 5,
      "guess": [
        "purple",
        "orange",
        "green",
        "purple"
      ],
      "letters": "POGP",
      "black": 4,
      "white": 0
    }
  ],
  "created_at": "2026-11-08T12:00:04.000Z",
  "updated_at": "2026-11-08T12:00:28.000Z",
  "message": "Code broken in 5 guesses with 1 hint. Score 500.",
  "secret": {
    "colors": [
      "purple",
      "orange",
      "green",
      "purple"
    ],
    "letters": "POGP",
    "ids": [
      6,
      2,
      4,
      6
    ]
  },
  "score": 500,
  "duration_ms": 24000,
  "finished_at": "2026-11-08T12:00:28.000Z"
}

list_games GET /api/gamesread-only

List games on the server, newest first: by default the games still in progress; filter by player name or status (playing, won, lost, resigned, solved, unsolved, finished, all). Useful to resume a game whose id you lost.

ArgumentTypeRequiredDescription
playerstringnoOnly this player's games
max length 24
statusstringnoDefault playing
one of playing, finished, all, won, lost, resigned, solved, unsolved
limitintegernoDefault 20
min 1 · max 100

MCP call

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_games",
    "arguments": {
      "player": "Claude",
      "status": "playing"
    }
  }
}

REST equivalent

curl https://cocojs.com/games/mastermind/api/games?player=Claude&status=playing

Result (structuredContent / response body)

{
  "count": 1,
  "games": [
    {
      "game_id": "bgqwxpyvr7d5",
      "player": "Claude",
      "mode": "breaker",
      "status": "playing",
      "preset": "super",
      "pegs": 5,
      "colors": 8,
      "guesses_used": 0,
      "guesses_left": 12,
      "score": null,
      "created_at": "2026-11-08T12:00:32.000Z",
      "updated_at": "2026-11-08T12:00:32.000Z"
    }
  ]
}

hint POST /api/games/{game_id}/hint

Ask the server's solver for help in a game you are breaking: how many codes are still consistent with the feedback so far, a sample of them, and the guess it would play next. Each hint costs one guess in the score (it does not use up a turn).

ArgumentTypeRequiredDescription
game_idstringyesThe game_id returned by new_game
max length 40

MCP call

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "hint",
    "arguments": {
      "game_id": "k83ppwt7b6mp"
    }
  }
}

REST equivalent

curl -X POST https://cocojs.com/games/mastermind/api/games/k83ppwt7b6mp/hint

Result (structuredContent / response body)

{
  "game_id": "k83ppwt7b6mp",
  "remaining_codes": 256,
  "suggestion": {
    "colors": [
      "orange",
      "yellow",
      "green",
      "green"
    ],
    "letters": "OYGG",
    "ids": [
      2,
      3,
      4,
      4
    ]
  },
  "sample": [
    {
      "colors": [
        "blue",
        "orange",
        "green",
        "yellow"
      ],
      "letters": "BOGY",
      "ids": [
        5,
        2,
        4,
        3
      ]
    },
    {
      "colors": [
        "yellow",
        "purple",
        "blue",
        "red"
      ],
      "letters": "YPBR",
      "ids": [
        3,
        6,
        5,
        1
      ]
    },
    {
      "colors": [
        "blue",
        "orange",
        "green",
        "blue"
      ],
      "letters": "BOGB",
      "ids": [
        5,
        2,
        4,
        5
      ]
    },
    {
      "colors": [
        "yellow",
        "yellow",
        "red",
        "blue"
      ],
      "letters": "YYRB",
      "ids": [
        3,
        3,
        1,
        5
      ]
    },
    {
      "colors": [
        "blue",
        "purple",
        "red",
        "green"
      ],
      "letters": "BPRG",
      "ids": [
        5,
        6,
        1,
        4
      ]
    }
  ],
  "hints_used": 1,
  "note": "Each hint counts as one extra guess in the score."
}

resign POST /api/games/{game_id}/resigndestructive

Give up a game in progress. The secret is revealed, the game is recorded as resigned with a score of 0.

ArgumentTypeRequiredDescription
game_idstringyesThe game_id returned by new_game
max length 40

MCP call

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "resign",
    "arguments": {
      "game_id": "bgqwxpyvr7d5"
    }
  }
}

REST equivalent

curl -X POST https://cocojs.com/games/mastermind/api/games/bgqwxpyvr7d5/resign

Result (structuredContent / response body)

{
  "game_id": "bgqwxpyvr7d5",
  "player": "Claude",
  "mode": "breaker",
  "status": "resigned",
  "finished": true,
  "config": {
    "preset": "super",
    "pegs": 5,
    "colors": 8,
    "max_guesses": 12,
    "allow_duplicates": true,
    "allow_blanks": false,
    "code_space": 32768,
    "difficulty": 1.45
  },
  "palette": [
    {
      "id": 1,
      "name": "red",
      "letter": "R"
    },
    {
      "id": 2,
      "name": "orange",
      "letter": "O"
    },
    {
      "id": 3,
      "name": "yellow",
      "letter": "Y"
    },
    {
      "id": 4,
      "name": "green",
      "letter": "G"
    },
    {
      "id": 5,
      "name": "blue",
      "letter": "B"
    },
    {
      "id": 6,
      "name": "purple",
      "letter": "P"
    },
    {
      "id": 7,
      "name": "white",
      "letter": "W"
    },
    {
      "id": 8,
      "name": "black",
      "letter": "K"
    }
  ],
  "guesses_used": 0,
  "guesses_left": 12,
  "hints_used": 0,
  "history": [],
  "created_at": "2026-11-08T12:00:32.000Z",
  "updated_at": "2026-11-08T12:00:36.000Z",
  "message": "You resigned. The code was yellow, white, black, blue, green (YWKBG).",
  "secret": {
    "colors": [
      "yellow",
      "white",
      "black",
      "blue",
      "green"
    ],
    "letters": "YWKBG",
    "ids": [
      3,
      7,
      8,
      5,
      4
    ]
  },
  "score": 0,
  "duration_ms": 4000,
  "finished_at": "2026-11-08T12:00:36.000Z"
}

get_scores GET /api/scoresread-only

The scoreboard: top codebreaker scores, the strongest codemakers, per-player statistics (games, wins, win rate, average guesses, best score) and the most recent games. Pass player to get that player's own statistics and recent games as well.

ArgumentTypeRequiredDescription
playerstringnoAlso return this player's statistics and recent games
max length 24
limitintegernoRows per table (default 10)
min 1 · max 100
modestringnoOnly count games of this mode
one of breaker, maker

MCP call

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_scores",
    "arguments": {
      "player": "Claude",
      "limit": 10
    }
  }
}

REST equivalent

curl https://cocojs.com/games/mastermind/api/scores?player=Claude&limit=10

Result (structuredContent / response body)

{
  "generated_at": "2026-11-08T06:05:57.781Z",
  "totals": {
    "games": 4,
    "breaker_games": 3,
    "wins": 2,
    "losses": 0,
    "resigned": 1,
    "maker_games": 1,
    "players": 2
  },
  "top_scores": [
    {
      "rank": 1,
      "player": "Sophia",
      "score": 1000,
      "preset": "classic",
      "pegs": 4,
      "colors": 6,
      "guesses_used": 1,
      "hints_used": 0,
      "duration_ms": 4000,
      "finished_at": "2026-11-08T12:00:48.000Z",
      "game_id": "kcztnhrw49yg"
    },
    {
      "rank": 2,
      "player": "Claude",
      "score": 500,
      "preset": "classic",
      "pegs": 4,
      "colors": 6,
      "guesses_used": 5,
      "hints_used": 1,
      "duration_ms": 24000,
      "finished_at": "2026-11-08T12:00:28.000Z",
      "game_id": "k83ppwt7b6mp"
    }
  ],
  "codemakers": [
    {
      "rank": 1,
      "player": "Claude",
      "points": 4,
      "solver_guesses": 4,
      "solved": true,
      "preset": "classic",
      "secret": "RGBY",
      "finished_at": "2026-11-08T12:00:40.000Z",
      "game_id": "6w3hwdycdic2"
    }
  ],
  "players": [
    {
      "player": "Sophia",
      "games": 1,
      "wins": 1,
      "losses": 0,
      "resigned": 0,
      "win_rate": 100,
      "avg_guesses_when_won": 1,
      "best_score": 1000,
      "total_score": 1000,
      "maker_games": 0,
      "maker_points": 0,
      "last_played": "2026-11-08T12:00:48.000Z"
    },
    {
      "player": "Claude",
      "games": 3,
      "wins": 1,
      "losses": 0,
      "resigned": 1,
      "win_rate": 50,
      "avg_guesses_when_won": 5,
      "best_score": 500,
      "total_score": 500,
      "maker_games": 1,
      "maker_points": 4,
      "last_played": "2026-11-08T12:00:40.000Z"
    }
  ],
  "recent": [
    {
      "player": "Sophia",
      "mode": "breaker",
      "status": "won",
      "score": 1000,
      "preset": "classic",
      "guesses_used": 1,
      "finished_at": "2026-11-08T12:00:48.000Z",
      "game_id": "kcztnhrw49yg"
    },
    {
      "player": "Claude",
      "mode": "maker",
      "status": "solved",
      "score": 4,
      "preset": "classic",
      "guesses_used": 4,
      "finished_at": "2026-11-08T12:00:40.000Z",
      "game_id": "6w3hwdycdic2"
    },
    {
      "player": "Claude",
      "mode": "breaker",
      "status": "resigned",
      "score": 0,
      "preset": "super",
      "guesses_used": 0,
      "finished_at": "2026-11-08T12:00:36.000Z",
      "game_id": "bgqwxpyvr7d5"
    },
    {
      "player": "Claude",
      "mode": "breaker",
      "status": "won",
      "score": 500,
      "preset": "classic",
      "guesses_used": 5,
      "finished_at": "2026-11-08T12:00:28.000Z",
      "game_id": "k83ppwt7b6mp"
    }
  ],
  "player": {
    "player": "Claude",
    "games": 3,
    "wins": 1,
    "losses": 0,
    "resigned": 1,
    "win_rate": 50,
    "avg_guesses_when_won": 5,
    "best_score": 500,
    "total_score": 500,
    "maker_games": 1,
    "maker_points": 4,
    "last_played": "2026-11-08T12:00:40.000Z"
  },
  "player_games": [
    {
      "game_id": "6w3hwdycdic2",
      "mode": "maker",
      "status": "solved",
      "score": 4,
      "preset": "classic",
      "guesses_used": 4,
      "hints_used": 0,
      "duration_ms": 0,
      "finished_at": "2026-11-08T12:00:40.000Z",
      "secret": "RGBY",
      "history": [
        "RROO 1b0w",
        "RYGG 1b2w",
        "YRGB 0b4w",
        "RGBY 4b0w"
      ]
    },
    {
      "game_id": "bgqwxpyvr7d5",
      "mode": "breaker",
      "status": "resigned",
      "score": 0,
      "preset": "super",
      "guesses_used": 0,
      "hints_used": 0,
      "duration_ms": 4000,
      "finished_at": "2026-11-08T12:00:36.000Z",
      "secret": "YWKBG",
      "history": []
    },
    {
      "game_id": "k83ppwt7b6mp",
      "mode": "breaker",
      "status": "won",
      "score": 500,
      "preset": "classic",
      "guesses_used": 5,
      "hints_used": 1,
      "duration_ms": 24000,
      "finished_at": "2026-11-08T12:00:28.000Z",
      "secret": "POGP",
      "history": [
        "RROO 0b1w",
        "OYGG 1b1w",
        "GBRG 0b1w",
        "OPYB 0b2w",
        "POGP 4b0w"
      ]
    }
  ]
}

Resources & prompts

ResourceTypeContent
mastermind://rulestext/markdownRules, notation, boards and scoring (markdown)
mastermind://scoresapplication/jsonCurrent scoreboard (JSON)
mastermind://games/{game_id}application/jsonState of one game (JSON) (template)
PromptArgumentsPurpose
playplayer, presetPlay one complete game of Mastermind against the server and report the result
strategy—A short explanation of how to break a Mastermind code efficiently

REST API

Base path https://cocojs.com/games/mastermind/api. JSON in, JSON out; POST bodies are JSON objects; GET arguments go in the query string. Errors use one shape:

{
  "error": {
    "status": 400,
    "code": "invalid_colour",
    "message": "\"Z\" is not one of this game's colours: red (R), orange (O), yellow (Y), green (G), blue (B), purple (P)"
  }
}
EndpointToolNotes
GET /api—Index of endpoints and links
GET /api/health—Liveness, version, active game count
GET /api/tools—The MCP tool catalog, resources and prompts as JSON
GET /api/openapi.json—OpenAPI 3.1 document (static copy)
GET /api/rulesrulesread-only
POST /api/gamesnew_gamereturns 201
POST /api/games/{game_id}/guessguess
GET /api/games/{game_id}get_gameread-only
GET /api/gameslist_gamesread-only
POST /api/games/{game_id}/hinthint
POST /api/games/{game_id}/resignresign
GET /api/scoresget_scoresread-only
DELETE /api/games/{game_id}resignalias
StatusCodeMeaning
400invalid_guess, invalid_colour, invalid_config, invalid_argument, unknown_argument, missing_argument, missing_secret, invalid_jsonThe request is malformed; the message says exactly what to fix
401unauthorizedThe server requires Authorization: Bearer <key>
403forbidden_originBrowser Origin not allowed on /mcp
404not_foundUnknown game id or endpoint
409game_over, wrong_modeThe game is finished, or it is a codemaker game
413 / 429 / 503bad_body, rate_limited, busyBody too large, too many requests (Retry-After), too many games in progress

Notation & boards

A code is a list of colours. A guess (or a codemaker secret) may be sent as an array of names, letters or numbers, or as one string of letters or digits; the server echoes codes back as colors (names), letters and ids. A game uses the first colors entries of the palette:

1 red R2 orange O3 yellow Y4 green G5 blue B6 purple P7 white W8 black K9 cyan C10 pink N

With allow_blanks the extra symbol empty (0, .) may appear in codes.

PresetNamePegsColoursGuessesRepeatsCodes
classicClassic4610yes1,296
miniMini368yes216
uniqueNo repeats4610no360
blanksWith blanks46 + empty12yes2,401
superSuper5812yes32,768
grandGrand6815yes262,144
custom—2–82–101–30on/offhints and codemaker mode up to 2,000,000

Feedback. black = pegs of the right colour in the right position, white = pegs of the right colour in the wrong position. They are counts, not positions, and each secret peg and each guess peg is matched at most once: against the code RGBY, the guess RRRR earns 1 black and 0 white.

Scoring

Codebreaker: score = round(1000 × (max_guesses + 1 − guesses_used − hints_used) ÷ max_guesses × difficulty), where difficulty = ln(code_space) ÷ ln(1296) (classic = 1.00, super ≈ 1.45, grand ≈ 1.74). A classic code broken on the first guess scores 1000, on the fifth 600, on the tenth 100; a hint costs the same as one guess; lost or resigned games score 0.

Codemaker: points = the guesses the solver needed, +1 if it never broke the code. The solver is Knuth's minimax: on the classic board it needs at most five guesses, so a 5 is the best you can do there; bigger boards give it more to think about.

The scoreboard ranks codebreaker wins by score, then by time; players aggregates games, wins, win rate, average guesses on wins, best and total score; codemakers ranks by points.

Playing well

  1. Open with two pairs (RROO on the classic board). It is the opening that leaves the fewest possibilities in the worst case.
  2. Keep the consistent set. After each answer, discard every candidate code that would not have produced exactly that black/white answer against your guess. Only codes that survive every answer can be the secret.
  3. Guess a survivor, or better, the guess (survivor or not) whose worst-case answer leaves the smallest consistent set. That is Knuth's rule and it solves the classic board in five.
  4. Ask for a hint only when stuck: hint returns the size of the consistent set, a sample and the solver's next guess, but costs a guess in the score.
  5. Read message. Every response says what to do next and, at the end, the secret and your score.

Running a server

node server/server.js [--port=8772] [--host=0.0.0.0] [--data=server/data/state.json]
                      [--base-path=/games/mastermind] [--api-key=SECRET]
                      [--allowed-origins=https://a.example,https://b.example]
                      [--public-url=https://example.com/games/mastermind] [--no-static] [--quiet]

environment: PORT, HOST, MASTERMIND_DATA, MASTERMIND_BASE_PATH, MASTERMIND_API_KEY,
             MASTERMIND_ALLOWED_ORIGINS, MASTERMIND_PUBLIC_URL
ConcernBehaviour
AuthenticationNone by default. With --api-key, /mcp and the game routes require Authorization: Bearer <key> (or X-Api-Key); /api/health, /api/rules, /api/scores, /api/tools and the OpenAPI document stay public.
Origin policyBrowser requests to /mcp must come from the same host, from localhost, from the configured public URL or from --allowed-origins (DNS-rebinding protection). Non-browser clients send no Origin and are unaffected. CORS is open for the REST API.
Rate limitA token bucket per client address: bursts of 240 requests, refilling at 4 per second; excess requests get 429 with Retry-After. X-Forwarded-For is honoured when the connection comes from a private address (a reverse proxy).
PersistenceLive games and finished records are kept in one JSON file, written atomically and debounced. Finished games are pruned from memory after 6 hours (records stay); idle unfinished games expire after 24 hours. Records are capped at 20,000.
Reverse proxyMount under a prefix with --base-path (or MASTERMIND_BASE_PATH): the server strips it from every request, so ProxyPass /games/mastermind http://127.0.0.1:8772/games/mastermind in Apache (or location /games/mastermind/ { proxy_pass http://127.0.0.1:8772; } in nginx) just works. Set --public-url so the documents advertise the external address.
stdio bridgenode server/mcp-stdio.js --url https://cocojs.com/games/mastermind/mcp relays to a running server; --local runs the game in-process with --data (default server/data/state-stdio.json). Without --url it probes localhost:8772 and falls back to local play.

Protocol notes

Raw exchange

POST https://cocojs.com/games/mastermind/mcp
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}

→ 200 {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"prompts":{"listChanged":false},"logging":{}},"serverInfo":{"name":"mastermind","title":"Mastermind","version":"1.0.0"},"instructions":"…"}}

{"jsonrpc":"2.0","method":"notifications/initialized"}            → 202
{"jsonrpc":"2.0","id":2,"method":"tools/list"}                    → 200 {…"tools":[…8 tools…]}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"new_game","arguments":{"player":"Claude"}}}