{
  "name": "Chessfolio",
  "description": "Two capability sets over one MCP door. Personal, PAT-authenticated tools for the token owner's own chess data — stats, rating progress, games (list, detail, PGN attachment and review requests), problem opening lines, the latest weekly report, puzzles, and a personal study collection separate from played games (upload a PGN to study, list it, request review, delete it again). Five narrow writes across those tools: attach_pgn and upload_study_game each accept exactly one validated PGN — for an exact owned game id, and for a new study-collection row — request_game_review / request_study_analysis each queue (or wait for) an engine review of one owned game or study game, and delete_study_game removes one game (and its notes) from the token owner's own study collection, never touching the underlying analysed game record or any other user. None can edit results, ratings or metadata, and none fetches URLs or accepts files. Plus six PUBLIC, token-less tools: three tournament tools reading public Chess-Results data, an ECF rating calculator, and two tools reading a curated classic-games library (no per-ply data — prose and provenance only). Each tool's `auth` field says which it is.",
  "version": "1.5.0",
  "homepage": "https://chessfolio.io",
  "documentation": "https://chessfolio.io/developers",
  "auth": {
    "type": "bearer",
    "description": "Personal tools need Authorization: Bearer <token> — create a personal access token at chessfolio.io → Settings → API access. The public tools (auth: \"none\") need no token."
  },
  "remotes": [
    {
      "transport": "streamable-http",
      "endpoint": "https://chessfolio.io/api/mcp"
    }
  ],
  "tools": [
    {
      "name": "get_profile",
      "description": "The authenticated user's Chessfolio profile: display name, linked platform usernames, connected sources with sync freshness (last_synced_at), and total game count. Read-only.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      }
    },
    {
      "name": "get_chess_stats",
      "description": "Aggregate statistics over the user's games for a chosen window: win/draw/loss by colour, online vs over-the-board comparison, performance rating, records and streaks, recent form, weekday performance, opponent-strength breakdown, game-length breakdown, top openings, monthly form and average accuracy. Same compute as the chessfolio.io dashboard.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "range": {
            "type": "string",
            "enum": [
              "30d",
              "90d",
              "1y",
              "all"
            ],
            "description": "Time window: 30d, 90d, 1y (default) or all."
          },
          "providers": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "chesscom",
                "lichess",
                "ecf"
              ]
            },
            "description": "Restrict to sources: chesscom, lichess and/or ecf (OTB). Omit for all."
          },
          "timeClasses": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "bullet",
                "blitz",
                "rapid",
                "standard"
              ]
            },
            "description": "Restrict to time-control buckets: bullet, blitz, rapid, standard. Omit for all."
          },
          "opening": {
            "type": "string",
            "description": "Opening-family prefix filter, e.g. 'Sicilian'."
          }
        },
        "additionalProperties": false
      }
    },
    {
      "name": "get_rating_progress",
      "description": "Rating series per provider and time control (including ECF over-the-board), each with start/end/delta over the window. Series longer than 60 points are evenly downsampled (first and last always kept) — pointsDownsampled says when. The window's opening value is seeded from the latest pre-window rating so deltas match the dashboard.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "range": {
            "type": "string",
            "enum": [
              "30d",
              "90d",
              "1y",
              "all"
            ],
            "description": "Time window: 30d, 90d, 1y (default) or all."
          },
          "providers": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "chesscom",
                "lichess",
                "ecf"
              ]
            },
            "description": "Restrict to sources: chesscom, lichess and/or ecf (OTB). Omit for all."
          },
          "timeClasses": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "bullet",
                "blitz",
                "rapid",
                "standard"
              ]
            },
            "description": "Restrict to time-control buckets: bullet, blitz, rapid, standard. Omit for all."
          }
        },
        "additionalProperties": false
      }
    },
    {
      "name": "list_games",
      "description": "Paged list of the user's games (50 per page) across chess.com, Lichess, ECF (OTB), manual and PGN imports, with filters matching the chessfolio.io games library: provider, result, colour, time class, date range, move-count range, opening, opponent, opening-line prefix (lineMoves, as returned by get_problem_lines) and free-text search. accuracy is the effective accuracy (platform value preferred, chessfolio review value as fallback — accuracySource says which).",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer",
            "description": "1-based page number (50 games per page)."
          },
          "sort": {
            "type": "string",
            "enum": [
              "date-desc",
              "date-asc",
              "accuracy-desc",
              "accuracy-asc"
            ],
            "description": "Sort order. Default date-desc."
          },
          "providers": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "chesscom",
                "lichess",
                "ecf",
                "manual",
                "pgn"
              ]
            },
            "description": "Sources to include. Omit for all."
          },
          "results": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "win",
                "draw",
                "loss"
              ]
            },
            "description": "Filter by result."
          },
          "colours": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "white",
                "black"
              ]
            },
            "description": "Filter by the user's colour."
          },
          "timeClasses": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "ultraBullet",
                "bullet",
                "blitz",
                "rapid",
                "classical",
                "daily",
                "correspondence",
                "standard"
              ]
            },
            "description": "Raw time classes to include."
          },
          "from": {
            "type": "string",
            "description": "Inclusive lower date bound, YYYY-MM-DD."
          },
          "to": {
            "type": "string",
            "description": "Inclusive upper date bound, YYYY-MM-DD."
          },
          "movesMin": {
            "type": "integer",
            "description": "Minimum full-move count."
          },
          "movesMax": {
            "type": "integer",
            "description": "Maximum full-move count."
          },
          "opening": {
            "type": "string",
            "description": "Opening-name prefix filter."
          },
          "opponent": {
            "type": "string",
            "description": "Opponent-name contains filter."
          },
          "lineMoves": {
            "type": "string",
            "description": "SAN opening-line prefix (use a lineMoves value from get_problem_lines)."
          },
          "q": {
            "type": "string",
            "description": "Free-text search over opponent, opening, event and ECO."
          }
        },
        "additionalProperties": false
      }
    },
    {
      "name": "get_game",
      "description": "One of the user's games in full: the same summary fields as list_games plus its moves and, where a Chessfolio review exists, a review summary. `movesSource` says what the moves are — `pgn` when the full game is stored (reviewed or PGN-attached games), `opening-only` when only the recorded opening line is known, or null for an OTB/online game with no attached PGN, which carries no moves at all (the endpoint never fabricates moves). `review` is present only when a Chessfolio analysis exists: accuracy for both colours and for the user, the key critical moments and a move-classification histogram (no per-ply detail). Read-only. `id` is a game id exactly as returned by list_games.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The game id, as returned by list_games."
          }
        },
        "additionalProperties": false,
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "get_game_analysis",
      "description": "Move-by-move engine analysis of one of the user's reviewed games (the deterministic detail behind get_game's review summary). For each ply: the position before and after (FEN), the played move and the engine's best move in SAN and UCI, evaluations before and after, centipawn loss, the classification (book, brilliant, best, great, good, inaccuracy, mistake, miss, blunder), the principal variation, whether the position was forced, the only-move margin, clock/think-time where the game carries clock data, the game `phase` (opening, middlegame or endgame by the lichess-divider-v1 piece-count rule, with book exit as the opening boundary), and `concepts`: deterministic tactical and positional tags derived only from the stored analysis (the ply's classification included), the game's clock readings and legal-move replay, never from a language model. Detector set v1: hanging_piece, missed_capture, missed_mate, allowed_mate, fork, pin, back_rank, forcing_move_missed, opening_principle, time_trouble. Each tag carries its evidence (squares, pieces and the capturing or best move, or the rule and clock bucket where the tag is not about one move), and an empty concepts list means no detector could support a claim, not that the move was sound; a stored position that will not parse, or a detector that fails, yields an empty list too. Every move returned is replayed and checked for legality first, and a stored line that will not replay is truncated at its last legal move (principalVariationTruncated says when). Paginated over plies (40 at a time, 120 maximum): pass fromPly to continue, or side='user' for just the user's own moves. Nothing is invented: `availability` lists what this deployment cannot populate and why (human difficulty is not modelled; threat detection is partial, limited to the concepts listed; clocks are absent on games stored before clock capture and on over-the-board imports). Read-only: it serves the stored review and never starts engine work, so an unreviewed game returns analysed=false and tells you to call request_game_review.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The exact Chessfolio game id, as returned by list_games."
          },
          "fromPly": {
            "type": "integer",
            "minimum": 1,
            "description": "First ply to return, 1-based (ply 1 is White's first move). Use the nextFromPly value from a previous response to page. Default 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 120,
            "description": "How many plies to return, 1 to 120. Default 40."
          },
          "side": {
            "type": "string",
            "enum": [
              "both",
              "user"
            ],
            "description": "'user' returns only the user's own moves — usually what you want when coaching. Default 'both'."
          }
        },
        "additionalProperties": false,
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "get_critical_moments",
      "description": "The teachable positions from one of the user's reviewed games, so you do not have to read every ply to find what matters. Each moment carries the position (FEN) and side to move, the played move and the engine's best move in SAN and UCI, the continuation the engine wanted, and a REFUTATION: the engine's own best line from the position the played move actually produced, i.e. how it should have been punished. Also: severity, the evaluation swing, a templated plain-English summary of the facts (summarySource says 'template'; no language model wrote it), progressive hints that narrow without ever naming the move, a training question, and acceptable answers (the best move, plus any alternative within 0.25 pawns of it). Each moment also carries `phase` (opening, middlegame or endgame, lichess-divider-v1) and `concepts`, the same deterministic v1 detector set as get_game_analysis (hanging_piece, missed_capture, missed_mate, allowed_mate, fork, pin, back_rank, forcing_move_missed, opening_principle, time_trouble), each with its evidence, so a coach can say 'left the bishop en prise to a fork' from facts rather than guesswork. A moment whose ply is missing from the stored per-ply analysis reports phase null and an empty concepts list, because there is then no position to divide or replay. Evaluations come in two frames: evalBefore/evalAfter are raw engine output and are SIDE-TO-MOVE relative, so evalAfter belongs to the OPPONENT and reads as its own opposite; evalBeforeMover/evalAfterMover restate both from the mover's side, which is the frame the summary narrates and the frame you want when quoting a number to a player. `lossReconciles` says whether centipawnLoss equals the mover-frame difference on that ply; it does not when the ±500 cap bit or the loss was forced to zero. Every move is legality-checked before it is returned. `matchesKnownWeakness` is always null, not false: cross-game recurring-weakness detection is not built yet, so an absent match must not be read as a checked-and-clear one. Read-only; an unreviewed game returns analysed=false.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The exact Chessfolio game id, as returned by list_games."
          },
          "side": {
            "type": "string",
            "enum": [
              "both",
              "user"
            ],
            "description": "'user' returns only the user's own moments. Default 'both'."
          },
          "minSeverity": {
            "type": "string",
            "enum": [
              "moderate",
              "major",
              "critical"
            ],
            "description": "Drop moments below this severity. Brilliancies are exempt — they are not a milder mistake. Omit for everything."
          },
          "minCentipawnLoss": {
            "type": "integer",
            "description": "Also surface any ply conceding at least this many centipawns (100 = one pawn), even where the engine recorded no moment. The engine's own gate is a 15% win-probability loss, which in a decided or quiet position can skip a 1.5-pawn error entirely — so this ADDS moments rather than filtering them, and each carries source='derived'."
          },
          "maxMoments": {
            "type": "integer",
            "description": "Cap the number returned (1-40, default 40). When more match than fit, the most severe are kept and returned in ply order — `matching` and `truncated` report what was left out."
          }
        },
        "additionalProperties": false,
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "attach_pgn",
      "description": "Attach one complete PGN to an existing game in the authenticated user's Chessfolio library. Use an exact game `id` returned by list_games; this tool never guesses which row a score belongs to. It accepts PGN text only — no URL, path, binary file or metadata update — containing one parseable game with at least one move (maximum 100,000 characters; maximum 20 attachments per hour). Comments, annotations and side variations are removed and only parser-generated canonical PGN crosses the storage boundary. Only the attachment pointer changes: results, ratings and game metadata are untouched. The attachment makes the row reviewable but does not start engine analysis. Calls are idempotent: if the row already has moves, its existing PGN is kept and the response says alreadyAttached=true.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The exact Chessfolio game id, as returned by list_games."
          },
          "pgn": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100000,
            "description": "One complete PGN, including movetext. Headers are strongly recommended for checking the match."
          }
        },
        "additionalProperties": false,
        "required": [
          "id",
          "pgn"
        ]
      }
    },
    {
      "name": "request_game_review",
      "description": "Ask Chessfolio to run its engine review over one of the authenticated user's games. Use an exact game `id` returned by list_games. The game must already have moves — attach one first with attach_pgn if it does not. Analysis is queued and usually takes 20-40 seconds, so the default is to return immediately with status 'queued'; call this tool again with the same id to collect the finished review. Pass `wait` (seconds, 0-45; values outside that range are rejected) to have the server wait for it instead and return the review in one call. A game that has already been reviewed returns its existing review immediately with alreadyReviewed=true and costs nothing against the hourly limit — repeat calls are safe and free, and keep working even once the limit is exhausted, because the limit gates newly queued analysis only. Maximum 20 newly queued reviews per hour. Re-analysing an already-reviewed game is not offered.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The exact Chessfolio game id, as returned by list_games."
          },
          "wait": {
            "type": "integer",
            "minimum": 0,
            "maximum": 45,
            "description": "Seconds to wait for a queued analysis before giving up and returning 'queued'. Whole seconds from 0 to 45; anything outside that range is rejected, not clamped. Default 0 (return immediately)."
          }
        },
        "additionalProperties": false,
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "get_problem_lines",
      "description": "The 'lines that keep hurting': per-colour opening lines (6–24 plies) where the user's score drops at least 8 points below their colour baseline over at least 5 games, ranked by a struggle index of (drop × log2 of games). Each line carries its SAN prefix (lineMoves) for drill-through into list_games, plus a ready-made games URL. OTB (ECF) games carry no move lists and are excluded — hasLineData says whether any analysable games exist in the window.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "range": {
            "type": "string",
            "enum": [
              "30d",
              "90d",
              "1y",
              "all"
            ],
            "description": "Time window: 30d, 90d, 1y (default) or all."
          },
          "providers": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "chesscom",
                "lichess",
                "ecf"
              ]
            },
            "description": "Restrict to sources: chesscom, lichess and/or ecf (OTB). Omit for all."
          },
          "timeClasses": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "bullet",
                "blitz",
                "rapid",
                "standard"
              ]
            },
            "description": "Restrict to time-control buckets: bullet, blitz, rapid, standard. Omit for all."
          }
        },
        "additionalProperties": false
      }
    },
    {
      "name": "get_weekly_report",
      "description": "The user's most recent Friday weekly report as its frozen payload: per-source sections (chess.com / Lichess / OTB) with games, rating lines, best win and toughest defeat, plus puzzles — exactly what the email and the public share page render, with the share URL included. Returns a clear message when no report exists yet (reports generate on Fridays for weeks with activity).",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      }
    },
    {
      "name": "get_puzzle_stats",
      "description": "Cross-source puzzle activity and ratings: 12-week solve summary with win rate, volume bucketed to suit the window (day/week/month), and rating series for Lichess puzzles and chess.com tactics (chess.com is a peak-only rating — their API exposes no current value). Rating series longer than 120 rows are evenly downsampled, first and last kept (dataDownsampled says when).",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "range": {
            "type": "string",
            "enum": [
              "30d",
              "90d",
              "1y",
              "all"
            ],
            "description": "Time window: 30d, 90d, 1y (default) or all."
          }
        },
        "additionalProperties": false
      }
    },
    {
      "name": "list_study_games",
      "description": "The authenticated user's personal study collection — every game saved via upload_study_game, newest first, up to 200 rows. Each entry carries id, source, white/black names, event, year, result and when it was saved. This is a separate collection from the user's own played games (list_games): study games are things the user wants to STUDY, not games the user played, so there is no colour or accuracy-for-the-user field here. Call request_study_analysis with an id to have Chessfolio engine-review one. Read-only.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      }
    },
    {
      "name": "upload_study_game",
      "description": "Paste one complete PGN into the authenticated user's personal study collection — for studying somebody else's game (a classic, a friend's game, one found online), not for the user's own played games (use attach_pgn against an existing list_games row for those). Validated like every PGN door on this api: exactly one parseable game, at least one move, legal throughout, no set-position (FEN/SetUp) games; maximum 100,000 characters; maximum 20 uploads per hour. Any prose in `{...}` comments is extracted and kept as editable per-ply study notes the user can read and edit in the web study viewer at /study — unlike attach_pgn, comments here are not simply discarded — though only the canonical, comment-free mainline is ever analysed or sent for engine review, and this tool surface has no way to read or write those notes itself. The collection is deduplicated on the MOVES, not the file: re-uploading a game whose headers, comments or clock tags differ from one already in the collection lands on that same row (alreadyInCollection=true) rather than creating a duplicate — its analysis is kept, and any new comments fill plies that don't already have a note; the old upload's own clock tags and other comment-only data are not merged in and are not stored anywhere. Separately, and independently of collection membership, Chessfolio's backing store reuses analysis whenever the full canonical text (headers and clocks included) byte-matches a game already analysed anywhere in Chessfolio's store — for example one copied from get_library_game — so request_study_analysis can return that analysis for free, no new engine time. Returns an id for request_study_analysis and list_study_games.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "pgn": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100000,
            "description": "One complete PGN, including movetext. Headers are strongly recommended (white/black/event/date), though only the movetext is required."
          }
        },
        "additionalProperties": false,
        "required": [
          "pgn"
        ]
      }
    },
    {
      "name": "request_study_analysis",
      "description": "Ask Chessfolio to run its engine review over one of the authenticated user's study games (as saved by upload_study_game). Spends from the SAME shared request_review allowance as request_game_review — 20 newly queued analyses per hour, atomic, shared across both the games surface and the study collection, because both doors ultimately queue work on one concurrency-1 engine rather than owning a ceiling each. A study game whose analysis already exists (including one that content-addressed onto an already-analysed api game) returns complete immediately, at zero cost against the limit — repeat calls are safe and free. Analysis is queued and usually takes 20-40 seconds, so the default is to return immediately with status 'queued'; call this tool again with the same id to collect the finished review, or pass wait (seconds, 0-45; values outside that range are rejected) to have the server wait for it instead. There is no accuracy 'for the user' here: a study game is somebody else's game, so only white and black accuracies are reported, never a colour-attributed one.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The study game id, as returned by upload_study_game or list_study_games."
          },
          "wait": {
            "type": "integer",
            "minimum": 0,
            "maximum": 45,
            "description": "Seconds to wait for a queued analysis before giving up and returning 'queued'. Whole seconds from 0 to 45; anything outside that range is rejected, not clamped. Default 0 (return immediately)."
          }
        },
        "additionalProperties": false,
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "delete_study_game",
      "description": "Delete ONE game from the authenticated user's personal study collection, including its per-ply study notes (they cascade with the row). This cannot be undone — there is no recycle bin, and re-uploading the PGN later creates a fresh entry with a new id. What that means for notes: any note typed or edited directly in Chessfolio's study viewer is gone for good — it lived only on the deleted row. But a note that came from a `{...}` comment embedded in the PGN itself is not gone in the same sense: it lives in the PGN text, not the row, so re-uploading that same PGN re-seeds it as a fresh note on the new entry. Scope is deliberately narrow: it removes the study-collection row only, so it does NOT delete the underlying analysed game record in Chessfolio's backing store (a library game or another copy of the same game keeps its analysis) and it cannot touch any other user's data. `id` must come from list_study_games (or upload_study_game); an unknown id, an id belonging to somebody else and a malformed id all return the same not-found — the three are indistinguishable by design. Deleting a game the user PLAYED is not offered anywhere on this surface: list_games rows cannot be deleted, only study-collection rows can.",
      "auth": "bearer",
      "input_schema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The study game id to delete, as returned by list_study_games or upload_study_game."
          }
        },
        "additionalProperties": false,
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "list_library_games",
      "description": "Public, token-less listing of Chessfolio's curated classic-games library — around 50 published historical and instructive games, each with editorial commentary, a source citation and full engine analysis behind it. Returns one card per game (slug, title, white, black, event, year, result, ECO) so an agent can browse and pick one. Call get_library_game with a slug from this list for the full entry, including its editorial essay and canonical PGN. No token needed; no personal scope.",
      "auth": "none",
      "input_schema": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      }
    },
    {
      "name": "get_library_game",
      "description": "Public, token-less full entry for one published library game: the editorial essay, its source citation, the canonical PGN, and both players' overall engine accuracy — but deliberately NO per-ply data (no move evaluations, no critical moments, no depth): the library surface is prose and provenance, not an analysis feed, by hard design exclusion. To study the game move by move, paste its PGN into your own collection with upload_study_game — because the PGN is byte-identical to the one already analysed here, the analysis comes back at zero engine cost via content-addressed dedupe onto the already-analysed row. slug comes from list_library_games.",
      "auth": "none",
      "input_schema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "maxLength": 80,
            "description": "The library game's slug, as returned by list_library_games."
          }
        },
        "additionalProperties": false,
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "get_tournament_state",
      "description": "Public, read-only snapshot of any Chess-Results tournament — no token needed (source: chess-results.com). Returns the seeded player list, published round pairings with results, current standings, and any not-paired / requested-bye / withdrawal notes, plus a snapshotAt timestamp. Treat it as a point-in-time scrape that MAY BE STALE, not a live feed — always read snapshotAt. tnr is the Chess-Results tournament number (e.g. 651260) or a full chess-results.com tournament URL.",
      "auth": "none",
      "input_schema": {
        "type": "object",
        "properties": {
          "tnr": {
            "type": "string",
            "description": "Chess-Results tournament number (e.g. 651260) or a full chess-results.com tournament URL."
          }
        },
        "additionalProperties": false,
        "required": [
          "tnr"
        ]
      }
    },
    {
      "name": "estimate_round1",
      "description": "Public, read-only ESTIMATE of Round-1 pairings for a Chess-Results tournament — no token needed (source: chess-results.com). This is an ESTIMATE derived from the seed list, NOT the official pairing: the arbiter's real draw can differ. The opponent estimate is more reliable than colour — Round-1 colours hinge on the initial-colour draw, so treat the estimated colour as a coin-flip. The underlying snapshot may be stale (see staleWarning in the payload). Optional target returns just one player's board by case-insensitive name. tnr is the Chess-Results tournament number (e.g. 651260) or a full URL.",
      "auth": "none",
      "input_schema": {
        "type": "object",
        "properties": {
          "tnr": {
            "type": "string",
            "description": "Chess-Results tournament number (e.g. 651260) or a full chess-results.com tournament URL."
          },
          "target": {
            "type": "string",
            "description": "Optional player name (case-insensitive) to return just their estimated board."
          }
        },
        "additionalProperties": false,
        "required": [
          "tnr"
        ]
      }
    },
    {
      "name": "estimate_pairings",
      "description": "Public, read-only ESTIMATE of the next round's pairings for a Chess-Results Swiss tournament — no token needed (source: chess-results.com). It runs the real FIDE Dutch pairing engine (bbpPairings) over the live standings, but it is an ESTIMATE, NOT the official pairing: the arbiter's Swiss-Manager draw can legitimately differ (accelerated pairings, custom settings, manual corrections). The older manual seeded-Swiss method is a teaching aid, not the target. Colours follow each player's prior-round colour history. Optional round picks which round to estimate (defaults to the next unplayed round). Optional target spotlights one player by case-insensitive name and returns their board plus a what-if scenario table. tnr is the Chess-Results tournament number (e.g. 651260) or a full chess-results.com tournament URL.",
      "auth": "none",
      "input_schema": {
        "type": "object",
        "properties": {
          "tnr": {
            "type": "string",
            "description": "Chess-Results tournament number (e.g. 651260) or a full chess-results.com tournament URL."
          },
          "round": {
            "type": "integer",
            "description": "round to estimate; defaults to the next unplayed round"
          },
          "target": {
            "type": "string",
            "description": "a player name to spotlight; returns their board + a what-if scenario table"
          }
        },
        "additionalProperties": false,
        "required": [
          "tnr"
        ]
      }
    },
    {
      "name": "calculate_ecf_rating_change",
      "description": "Public, token-less ECF rating calculator: work out what a set of results does to an English Chess Federation rating, using the ECF's published K Rating algorithm (V4, August 2020). Returns the new rating plus a per-game audit trail — rating difference, Elo difference-table offset, score offset and increment for each game — so the answer can be checked by hand. It is DETERMINISTIC ARITHMETIC, NOT AN OFFICIAL ECF FIGURE: the ECF rates a whole monthly cycle against one Old Rating carried in from the previous cycle, which is not always the rating published for the month the games were played, and it uses the opponent ratings held for that cycle. K Rating only, so it does not apply to new or partially-rated players (under 10 rated games), who are rated by the P (performance) algorithm instead.",
      "auth": "none",
      "input_schema": {
        "type": "object",
        "properties": {
          "currentRating": {
            "type": "integer",
            "description": "The player's ECF rating before these games, e.g. 1650. Four-digit scale (2020 onwards), not an old-style three-digit grade."
          },
          "games": {
            "type": "string",
            "description": "The games, as \"opponent rating then result\", comma-separated: \"1750 win, 1700 draw, 1600 loss\". w/d/l, 1/=/0 and +/-  also work, as do 1750=w and 1750:d. Up to 100 games. Pass the whole month together: the 700-point monthly cap is a per-month rule."
          },
          "age": {
            "type": "integer",
            "description": "The player's age in years. Only the under-18 boundary matters: a junior who is GAINING rating moves at K = 40 instead of 20, which roughly doubles the change. Omitted means treat as an adult."
          },
          "gamesThisMonth": {
            "type": "integer",
            "description": "Every rated game the player played in the rating month, if more than the ones listed here. The ECF caps a month's movement at 700 points by scaling K down, and that cap divides by this number. Omit when the games listed are all of them."
          }
        },
        "additionalProperties": false,
        "required": [
          "currentRating",
          "games"
        ]
      }
    }
  ]
}