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.
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
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" }
}
}
}
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 |
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♦.
{
"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"
}
}
{
"hole_cards": "AhKd",
"situation": "RFI",
"strategy": [
{ "action": "raise", "frequency": 1, "amount_bb": 3, "sizing_pot": 0.8 }
]
}
Always raise, to 3bb. Not much to discuss, so on to the flop.
{
"tool": "flop_tree",
"arguments": {
"board": "Qs7c2d",
"pot_type": "SRP",
"positions": { "hero": "BTN", "raiser": "BTN", "caller": "BB" },
"flop_version": "6max_RC_100bb_200NL"
}
}
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..." }
]
}
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 token is what you pass to flop_node, in its argument called
{
"tool": "flop_node",
"arguments": { "node": "<token of root/CHECK>", "hole_cards": "AhKd" }
}
{
"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 }
]
}
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.
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();
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.
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.
Real-time assistance (RTA) at real-money tables is prohibited.