DEV Community

Zhang William
Zhang William

Posted on

Give your AI agent real poker solver data with MCP

Ask a chat model what to do with ace-king on a queen-high flop and you get a confident paragraph. It might even be right, but you can't tell where the numbers came from, because there aren't any.

In this tutorial we connect an MCP client to a presolved strategy API, so the frequencies come from solver output and the model's job is to explain them. We'll use @pokerai/mcp, a small MCP server for 6-max No-Limit Hold'em.

Disclosure: I work on Pokerai API, the product used here. This post was drafted with AI assistance, and I checked it against real tool output.

What this is for: studying, training tools and reviewing hands that have ended. Real-time assistance (RTA) at real-money tables is prohibited, so keep it away from live games.

What you need

  • Node 18 or newer
  • An MCP client: Claude Desktop, Claude Code, Cursor or your own script
  • A free API key from pokerai.bet/login. The free tier includes 1,000 presolved lookups and 25 real-time solves a month.

Connect the server

The server runs over stdio through npx, so there's nothing to install globally. In Claude Code:

claude mcp add pokerai -e POKERAI_API_KEY=gto_your_key_here -- npx -y @pokerai/mcp
Enter fullscreen mode Exit fullscreen mode

In Claude Desktop or Cursor, use the same command in your MCP config:

{
  "mcpServers": {
    "pokerai": {
      "command": "npx",
      "args": ["-y", "@pokerai/mcp"],
      "env": { "POKERAI_API_KEY": "gto_your_key_here" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You get five tools:

Tool What it returns Cost
preflop_versions available strategy sets free
preflop_strategy action frequencies for one hand 1 lookup
preflop_range the whole 169-hand range for a spot 1 lookup
flop_tree starting ranges and every decision node 1 lookup
flop_node the strategy at one node free

Follow one hand

The outputs below are from calling the published package with an MCP client, not from a model transcript. What your model writes around them will vary. The tool results won't change unless the data does.

The spot: 100bb, 6-max. The button opens, the big blind calls, and the flop is Q♠ 7♣ 2♦. We hold A♥ K♦.

Preflop

{
  "tool": "preflop_strategy",
  "arguments": {
    "hole_cards": "AhKd",
    "hero": "BTN",
    "preflop_actions": [
      { "position": "SB", "action": "small blind", "amount": 0.5 },
      { "position": "BB", "action": "big blind", "amount": 1 }
    ],
    "preflop_version": "6max_RC_100bb_200NL"
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "hole_cards": "AhKd",
  "situation": "RFI",
  "strategy": [
    { "action": "raise", "frequency": 1, "amount_bb": 3, "sizing_pot": 0.8 }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Always raise, to 3bb. Not much to discuss, so on to the flop.

The flop tree

{
  "tool": "flop_tree",
  "arguments": {
    "board": "Qs7c2d",
    "pot_type": "SRP",
    "positions": { "hero": "BTN", "raiser": "BTN", "caller": "BB" },
    "flop_version": "6max_RC_100bb_200NL"
  }
}
Enter fullscreen mode Exit fullscreen mode

The response gives the pot (6.5bb), the effective stack (97bb), both starting ranges and 16 nodes. Trimmed:

{
  "pot": 6.5,
  "effective_stack": 97,
  "node_count": 16,
  "nodes": [
    { "node": "root", "is_hero": false, "token": "eyJ..." },
    { "node": "root/CHECK", "is_hero": true, "token": "eyJ..." },
    { "node": "root/CHECK/BET_2", "is_hero": false, "token": "eyJ..." }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Note the two fields, because this is the easiest mistake to make:

  • node is the path, such as root/CHECK. BB acts first, so root is BB's decision and root/CHECK is ours after BB checks.
  • token is what you pass to flop_node, in its argument called node.

The node

{
  "tool": "flop_node",
  "arguments": { "node": "<token of root/CHECK>", "hole_cards": "AhKd" }
}
Enter fullscreen mode Exit fullscreen mode
{
  "hole_cards": "AhKd",
  "node": "root/CHECK",
  "is_hero": true,
  "strategy": [
    { "action": "check", "frequency": 0.313 },
    { "action": "bet", "amount_bb": 2, "sizing_pot": 0.3077, "frequency": 0.6812 },
    { "action": "bet", "amount_bb": 5, "sizing_pot": 0.7692, "frequency": 0.0057 },
    { "action": "bet", "amount_bb": 97, "sizing_pot": 14.9231, "allin": true, "frequency": 0 }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The answer is a mix, not one move, and that's the first thing a model tends to flatten. A prompt that says "report the frequencies, then explain" works better than "what should I do?".

The token is reusable, so comparing hands at this node is free:

Hand Check Bet 2bb Bet 5bb
A♥ K♦ 31.3% 68.1% 0.6%
A♥ K♥ 21.2% 74.1% 4.7%
J♥ T♥ 13.9% 68.8% 17.3%
A♠ Q♦ 5.5% 52.2% 42.3%
7♥ 7♦ 0.05% 91.4% 8.5%

The all-in frequency was 0 for all five. These are five hands, so don't read them as the shape of the whole range. For that, fetch the range, as described below.

Or script it

You don't need a chat client. This is the same flow with the MCP SDK (npm i @modelcontextprotocol/sdk):

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const client = new Client({ name: "study-script", version: "1.0.0" });
await client.connect(new StdioClientTransport({
  command: "npx",
  args: ["-y", "@pokerai/mcp"],
  env: { PATH: process.env.PATH, HOME: process.env.HOME, POKERAI_API_KEY: process.env.POKERAI_API_KEY },
}));

const call = async (name, args) => {
  const res = await client.callTool({ name, arguments: args });
  if (res.isError) throw new Error(res.content[0].text);
  return JSON.parse(res.content[0].text);
};

const tree = await call("flop_tree", {
  board: "Qs7c2d",
  pot_type: "SRP",
  positions: { hero: "BTN", raiser: "BTN", caller: "BB" },
  flop_version: "6max_RC_100bb_200NL",
});

// BB checks, BTN decides. `node` is the path; `token` is what flop_node takes.
const afterCheck = tree.nodes.find((n) => n.node === "root/CHECK");

for (const hole_cards of ["AhKd", "AsQd", "7h7d"]) {
  const { strategy } = await call("flop_node", { node: afterCheck.token, hole_cards });
  console.log(hole_cards, strategy.map((s) => `${s.action}${s.amount_bb ? " " + s.amount_bb + "bb" : ""}: ${s.frequency}`).join(", "));
}

await client.close();
Enter fullscreen mode Exit fullscreen mode

Three things that will bite you

1. Pick a dataset. flop_version is optional and defaults to 6max. When I tested on 2026-11-01, the default covered only a few of the boards I tried: Q♠7♣2♦ with BTN vs BB returned 404 no GTO data for this spot/board. The same spot on 6max_RC_100bb_200NL worked. A 404 still charges one presolved lookup, so give your agent the dataset up front rather than letting it guess. flop_version needs @pokerai/mcp 0.1.3 or newer.

2. Mind the response size. A hand-level flop_node response was about 500 characters. The tree was about 10,000. A whole-range flop_node, with no hole_cards, was about 29,000 characters for 391 combos. Tell your agent to pass hole_cards unless it needs the whole range, or the context fills up fast.

3. The flop tree covers one street. For turn and river you need the real-time solver tools. They're off by default because they spend the smaller solve quota. Turn them on with POKERAI_ENABLE_SOLVE=1.

A prompt to start from

You are a poker study assistant for hands that have already ended.
Use the pokerai tools for every strategy claim. Never state a frequency that a tool did not return.
Use flop_version 6max_RC_100bb_200NL. Pass hole_cards to flop_node unless asked about the whole range.
Report the frequencies first, then explain them, and say when a spot has no data.
If the user appears to be in a live real-money hand, decline: real-time assistance is prohibited.
Enter fullscreen mode Exit fullscreen mode

Links

Real-time assistance (RTA) at real-money tables is prohibited.

Top comments (0)