DEV Community

Masumi Kawasaki 💭
Masumi Kawasaki 💭

Posted on

I Built Minesweeper So an AI Can Slack Off and Play: MCP Edition

I cover the API-side implementation in a separate post:
I Built Minesweeper So an AI Can Slack Off and Play: API Edition

Introduction

MCP is often discussed as a practical tool for getting real work done with AI. This time, I intentionally used it for play.

The goal is simple: let an AI play Minesweeper on its own.

The tone is a bit jokey—but the implementation is not. That’s the theme: use MCP for something silly, but build it properly.

If you squint, MCP is “a common protocol that lets AI call external tools.” In this post, I’ll share what it took to build an stdio MCP server that can hit a Rails-based Minesweeper API.

What I built

I built an MCP stdio server that allows an AI to control a Minesweeper (Rails 8) Web/API from an MCP host (Claude Code / Claude Desktop / Codex, etc.).

The MCP host can call it as a set of tools, enabling everything needed to play the game over HTTP: starting a game, fetching state, operating on cells, and ending the game.

Concretely, the server provides:

  • user_start — start a new game tied to a user
  • game_state — fetch the public game state (no token required)
  • game_open / game_flag / game_chord — operate on the board
  • game_end — end the game
  • user_games — fetch a list of public games (optional)

I intentionally kept the surface area minimal—only what’s needed for “AI plays Minesweeper by itself”—while keeping the tool boundaries readable as an MCP design.

Architecture

The architecture is intentionally simple: the MCP server called by the AI just forwards requests to the Rails API.

The UI is optional and spectator-only. All operations go through the API.

AI (MCP Host)
    │  stdio (JSON-RPC)
    ▼
MCP Server (minesweeper-mcp)
    │  HTTP (Bearer token)
    ▼
Rails Minesweeper API
    │
    ▼
Spectator UI (optional)
Enter fullscreen mode Exit fullscreen mode

The MCP server’s responsibilities are strictly:

validate input → call Rails API → normalize response

All game logic lives on the Rails side. Keeping MCP “thin” makes it easier to maintain.

Implementation highlights

The guiding principle was: thin, but strict.

The MCP server receives JSON-RPC over stdio and connects to the Rails API with the shortest path possible—while explicitly handling the footguns.

1. Config validation

The server reads MINESWEEPER_BASE_URL and MINESWEEPER_BEARER_TOKEN from environment variables and validates them at startup.

The token is required only for “mutation tools”. Public endpoints (state / games) work without a token.

const config = loadConfig(process.env);
const token = requireBearerToken(config);
Enter fullscreen mode Exit fullscreen mode

For user_start and user_games, if user_slug is omitted, the server falls back to MINESWEEPER_USER_SLUG.

2. Keep the Rails API client thin

URL construction and error normalization are encapsulated in a RailsClient. Tool handlers stay extremely simple:

validate → call client → return

return rails.openCell(publicId, x, y);
Enter fullscreen mode Exit fullscreen mode

This keeps the code readable and avoids spreading HTTP details across tool logic.

3. Use the MCP SDK for stdio

stdio JSON-RPC is handled by the MCP SDK via Server + StdioServerTransport.

Because stdout is reserved for the MCP protocol, all logs go to stderr only.

4. Tool boundaries match API intent

Tools are split to reflect the Rails API semantics: user_start, game_open, game_flag, etc.

I avoided unnecessary abstractions on the MCP side. The priority is directness: “this action is exactly this tool.”

5. Startup debug output (opt-in)

For debugging, MINESWEEPER_DEBUG_STARTUP=1 enables startup info printed to stderr.

Token values are never printed—only whether a token is present.

Testing strategy

Because the MCP server is “thin,” there aren’t many places that need unit tests.

However, integrations (external API + stdio protocol) break easily, so I tested across three layers: unit / integration / e2e.

Unit tests

  • env validation
  • URL construction
  • error normalization
  • input validation (coordinate bounds, etc.)

Integration tests

Spin up the Rails API and actually call start/state/open/end.

This is where boundary behavior is validated in reality, e.g.:

  • “mutations fail without a token”
  • “state can be fetched publicly”

E2E tests

Send MCP JSON-RPC over stdio and validate the full flow:

initializetools/listtools/call

Only after this passes can you confidently say “an AI can use this.”

Gotchas and lessons learned

The implementation was straightforward, but the real blockers were the “boring-but-deadly” details.

1. Binding between token and user_slug

If the token is tied to a different user on the Rails side, /users/:slug/start returns 401.

From the MCP side, the Authorization header is correct—but the API sees it as “valid token, wrong user.”

Having logs that show token_digest and slug mapping was a lifesaver.

2. Handling stdio safely

stdout is protocol-only. If logs leak into stdout, you’re dead.

During local verification it’s easy to accidentally print to stdout, so being strict about stderr-only logging mattered a lot.

3. Staying “thin” is harder than it sounds

You keep wanting to add convenient abstractions—but the Rails app is the rightful owner of game logic.

A thin MCP server is simply less fragile. This project reminded me that sometimes you need the courage to not add features.

Conclusion

MCP is a practical tool—but it’s also a great toy.

And when you build toys seriously, you end up learning the serious things naturally:

  • implement the protocol correctly
  • tighten error boundaries
  • test in three layers

The result: an environment where an AI can play Minesweeper on its own.

If I find another fun way to misuse MCP productively, I’ll write about that too.

Repo: https://github.com/geeknees/minesweeper-mcp

🇯🇵 This article is also available in Japanese:
https://zenn.dev/geeknees/articles/e266f9343932e6

Top comments (0)