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
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.
| Tool | Purpose | REST |
|---|---|---|
rules | Rules and notation | GET /api/rules |
new_game | Start a game | POST /api/games |
guess | Make a guess | POST /api/games/{game_id}/guess |
get_game | Game state | GET /api/games/{game_id} |
list_games | List games | GET /api/games |
hint | Hint | POST /api/games/{game_id}/hint |
resign | Resign | POST /api/games/{game_id}/resign |
get_scores | Scoreboard | GET /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/rulesResult (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.
| Argument | Type | Required | Description |
|---|---|---|---|
player | string | no | Your name on the scoreboard (default "Anonymous") max length 24 |
mode | string | no | breaker: you guess the server's code (default). maker: you set the code, the server's solver breaks it. one of breaker, maker |
preset | string | no | Board 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 |
pegs | integer | no | Pegs in the code (overrides the preset) min 2 · max 8 |
colors | integer | no | Number of colours in play (the first N of the palette) min 2 · max 10 |
max_guesses | integer | no | Guesses allowed min 1 · max 30 |
allow_duplicates | boolean | no | May the code repeat a colour? (default true) |
allow_blanks | boolean | no | May the code contain empty positions? (default false) |
secret | string | array | no | maker 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.
| Argument | Type | Required | Description |
|---|---|---|---|
game_id | string | yes | The game_id returned by new_game max length 40 |
guess | string | array | yes | 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": "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.
| Argument | Type | Required | Description |
|---|---|---|---|
game_id | string | yes | The 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/k83ppwt7b6mpResult (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.
| Argument | Type | Required | Description |
|---|---|---|---|
player | string | no | Only this player's games max length 24 |
status | string | no | Default playing one of playing, finished, all, won, lost, resigned, solved, unsolved |
limit | integer | no | Default 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=playingResult (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).
| Argument | Type | Required | Description |
|---|---|---|---|
game_id | string | yes | The 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/hintResult (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.
| Argument | Type | Required | Description |
|---|---|---|---|
game_id | string | yes | The 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/resignResult (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.
| Argument | Type | Required | Description |
|---|---|---|---|
player | string | no | Also return this player's statistics and recent games max length 24 |
limit | integer | no | Rows per table (default 10) min 1 · max 100 |
mode | string | no | Only 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=10Result (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
| Resource | Type | Content |
|---|---|---|
mastermind://rules | text/markdown | Rules, notation, boards and scoring (markdown) |
mastermind://scores | application/json | Current scoreboard (JSON) |
mastermind://games/{game_id} | application/json | State of one game (JSON) (template) |
| Prompt | Arguments | Purpose |
|---|---|---|
play | player, preset | Play 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)"
}
}
| Endpoint | Tool | Notes |
|---|---|---|
| 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/rules | rules | read-only |
| POST /api/games | new_game | returns 201 |
| POST /api/games/{game_id}/guess | guess | |
| GET /api/games/{game_id} | get_game | read-only |
| GET /api/games | list_games | read-only |
| POST /api/games/{game_id}/hint | hint | |
| POST /api/games/{game_id}/resign | resign | |
| GET /api/scores | get_scores | read-only |
| DELETE /api/games/{game_id} | resign | alias |
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_guess, invalid_colour, invalid_config, invalid_argument, unknown_argument, missing_argument, missing_secret, invalid_json | The request is malformed; the message says exactly what to fix |
| 401 | unauthorized | The server requires Authorization: Bearer <key> |
| 403 | forbidden_origin | Browser Origin not allowed on /mcp |
| 404 | not_found | Unknown game id or endpoint |
| 409 | game_over, wrong_mode | The game is finished, or it is a codemaker game |
| 413 / 429 / 503 | bad_body, rate_limited, busy | Body 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.
| Preset | Name | Pegs | Colours | Guesses | Repeats | Codes |
|---|---|---|---|---|---|---|
classic | Classic | 4 | 6 | 10 | yes | 1,296 |
mini | Mini | 3 | 6 | 8 | yes | 216 |
unique | No repeats | 4 | 6 | 10 | no | 360 |
blanks | With blanks | 4 | 6 + empty | 12 | yes | 2,401 |
super | Super | 5 | 8 | 12 | yes | 32,768 |
grand | Grand | 6 | 8 | 15 | yes | 262,144 |
| custom | — | 2–8 | 2–10 | 1–30 | on/off | hints 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
- Open with two pairs (
RROOon the classic board). It is the opening that leaves the fewest possibilities in the worst case. - 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.
- 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.
- Ask for a hint only when stuck:
hintreturns the size of the consistent set, a sample and the solver's next guess, but costs a guess in the score. - 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
| Concern | Behaviour |
|---|---|
| Authentication | None 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 policy | Browser 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 limit | A 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). |
| Persistence | Live 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 proxy | Mount 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 bridge | node 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
- Protocol versions
2025-06-18,2025-03-26,2024-11-05; the server echoes a supported requested version, otherwise answers with the newest. - Streamable HTTP, stateless: no
Mcp-Session-Idis issued or required.POST /mcpanswers withapplication/json(ortext/event-streamwhen that is the only accepted type). Notifications and client responses get202.GET /mcpreturns405(no server-initiated stream);DELETE /mcpis accepted and a no-op. JSON-RPC batches are processed in order. - Capabilities:
tools,resources(with templates, no subscriptions),prompts,logging. Tool results carrystructuredContentplus a text block; tool annotations mark read-only and destructive tools. - Errors:
-32700parse,-32600invalid request,-32601unknown method,-32602unknown tool / bad params,-32002unknown resource.
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"}}}