{
  "$schema": "https://agentskills.io/schemas/skills-index-v0.2.json",
  "name": "Chessfolio Skills",
  "description": "Owner-scoped Chessfolio MCP skills: personal chess-data reads (including a cross-game weakness profile and durable coaching state) plus nine narrowly scoped writes (PAT bearer auth). None of the nine accepts a URL, a path or an uploaded file: what they take is PGN text, ids, labels and numbers sent in the call itself, and none can edit results, ratings or metadata, or touch another user's data. 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 and request_study_analysis each queue (or wait for) an engine review of one owned game or study game; delete_study_game removes one game and its notes from the token holder's own study collection, leaving the underlying analysed game record and every other user untouched; set_training_focus creates, retitles or resolves one of at most three active training focuses, and never deletes one; add_coaching_entry appends one entry to an append-only coaching log, every game or position reference proved owned first and no entry's text ever edited, with two kinds of edit to an existing row: flipping one owned, OPEN assignment to completed or skipped when it is given that assignment's id, and, for a result naming a saved position, stamping that position's last-quizzed time and appending to its quiz history, which is how a quiz is recorded; save_position saves one position, copied from the owner's own review when a game is named, up to 500 per user; remove_saved_position deletes one saved position, the only coaching row an agent can delete. Those four coaching writes share one allowance of 60 an hour. Plus six public no-token tools: tournament lookups, an ECF rating calculator, and a classic-games library. All nine writes act only on an exact game, study-collection row or coaching row owned by the token holder, and the user can delete any coaching row from their dashboard.",
  "skills": [
    {
      "name": "get_profile",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_chess_stats",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_rating_progress",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "list_games",
      "type": "api",
      "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).",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_game",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_game_analysis",
      "type": "api",
      "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) together with winProbabilityBefore and winProbabilityLoss — the pair the classifier actually judged, so a move can carry a large centipawnLoss and still be `good` when the position was already decided, and these two numbers say why rather than leaving the label looking wrong — 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, side='user' for just the user's own moves, or verbosity='compact' to drop the position and engine-line reconstruction fields when sweeping a whole game. 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; win probability is absent on analyses stored before the api recorded it, and is never recomputed from the published evaluations to fill the gap). Every boolean in `availability` is about this deployment's coverage, never a finding about the game — `availability.legend` says so in the payload. 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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_critical_moments",
      "type": "api",
      "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. `winProbabilityBefore`/`winProbabilityLoss` carry the pair `classification` was judged on, which is why a moment can pass a centipawn filter while its label stays mild: the thresholds widen as a position becomes decided. Every move is legality-checked before it is returned. `matchesKnownWeakness` lists which of the user's current top weaknesses (get_weakness_profile, default 12-month window) each of their OWN moments exhibits, matched on phase, clock bucket, piece moved, move kind and concept tags. It has three states and collapsing them into two is the error to avoid: null means no profile was supplied for this request (the usual cause being no reviewed game with stored features in the window, though a profile read that failed degrades to the same value), so nothing was checked at all; an empty array on one of their own moments means it was checked and nothing matched; a populated array is a match. With a profile loaded, an opponent's moment always reports an empty array, and there it means not-applicable rather than checked-and-clear, as does a moment whose ply is missing from the per-ply analysis. The `availability.matchesKnownWeakness` flag beside it says only whether a profile was loaded, which makes it a statement about THIS USER and the odd one out in a block of coverage flags; `availability.legend` sorts the rest into the ones this deployment fixes and the ones that turn on what this game carries. Read-only; an unreviewed game returns analysed=false.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "attach_pgn",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "request_game_review",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_problem_lines",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_weekly_report",
      "type": "api",
      "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).",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_puzzle_stats",
      "type": "api",
      "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).",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "list_study_games",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "upload_study_game",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "request_study_analysis",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "delete_study_game",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_weakness_profile",
      "type": "api",
      "description": "A ranked list of what is going wrong across the user's reviewed games in a window (range, default 1y): the cross-game diagnosis to run BEFORE picking a game to work on. Each reviewed game's OWN moves (never the opponent's) are bucketed on phase, clock pressure (scramble at 20 seconds or less left after the move, pressed at 60 or less, comfortable above, unknown with none stored), piece (SAN letters K, Q, R, B, N and P for pawn moves), move kind (capture, check, quiet, pawn_push, castle), concept tag (the v1 detector ids get_critical_moments reports) and colour, plus two per-game conversion buckets: conversion:winning (win probability reached 80%) and conversion:lost (fell to 20% or less). Move buckets report moves, inaccuracies, mistakes, misses, blunders and an error rate per 100 moves. Conversion buckets report a SUCCESS rate over games that had the situation AND a stored result, so `games` is not 'games I was winning': a game whose result Chessfolio does not hold is in neither `games` nor `won`. Buckets clearing five reviewed games are ranked by a struggle index, and the top N (1 to 10, default 5) come back as `weaknesses`: a stable id such as concept:hanging_piece or clock:scramble, a templated label, the numbers, a `delta` against the previous window of equal length (compareToPrevious, default true; range=all has none), and up to three `evidence` entries — a list_games game id, a ply, a templated fact — ready for get_critical_moments. That index is THREE scores, because the buckets are three different kinds of thing. Phase, clock, piece, kind and colour partition the user's moves, every move sitting in exactly one bucket of each, so those are scored on how far the bucket's error rate sits ABOVE the user's own overall rate, times log2 of the bucket's moves. Concept tags are not a partition: a move carries none, one or several, and most tags only ever attach to a move that was already a mistake, which makes a rate measured within the tag 100 by construction and tells you nothing. So a concept is measured over the WHOLE window instead. Its `moves` is every user ply in the window, its `ratePer100` reads as 'this habit costs me N errors per 100 moves', a tag that never fired reports errors 0 and rate 0 over that same full move count, and it is ranked only once it accounts for at least a QUARTER of every error in the window, scored as that rate times log2 of the window's moves. A concept's five-game floor likewise counts the games where the tag landed on an error, not the games it fired in. Conversion buckets score (1 minus the success rate) times log2 of games. Read `delta` against its own bucket: on a move bucket it is a change in an ERROR rate, so positive means worse; on a conversion bucket a change in a SUCCESS rate, so positive means better. `avgCollapsePly` averages every collapse in the bucket, including games the user went on to win: it says where the game turned, never which move lost it. Buckets under the floor go to `lowSample` with counts and no rank, so 'not enough data' never reads as 'not a problem'; `byDimension` carries every known bucket's counts either way. Honesty rules: nothing is claimed on fewer than five reviewed games, this window or the previous one, so a delta against three games comes back null; a partition bucket at or below the user's own overall rate scores 0, as does a concept tag holding less than a quarter of the errors, so a player whose errors are evenly spread across their tags gets no concept in the list at all — read a short list of low-scoring partition buckets as 'nothing stands out', because over a few dozen games some bucket always sits a point or two above the overall rate by chance; clock:unknown and concept:time_trouble are counted in byDimension but reach neither `weaknesses` nor `lowSample`, missing clock data being a gap in the record rather than a habit, and the time-trouble tag being exactly the clock:pressed and clock:scramble errors under a second name, so ranking it would count the same errors twice; over-the-board (ECF) games carry no moves and are excluded; and only games with a stored Chessfolio review count (`reviewedGames` says how many), so hasReviewData=false means nothing reviewed falls in this window: call request_game_review and try again. No engine runs and no position is replayed: it aggregates features stored when each review landed, every string templated from them (summarySource: 'template'; no language model wrote it). Read-only. Counts against the 10-a-minute weakness-profile allowance, its own bucket, shared with GET /api/v1/me/weakness-profile and separate from the door's shared ceiling: one call aggregates the whole window and, unless compareToPrevious is false, the window before it, so it is not priced as an ordinary read.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "get_coaching_state",
      "type": "api",
      "description": "What you and this user have agreed to work on, read in one call: make this your FIRST call in any coaching session. Returns the user's ACTIVE training focuses (at most three), each with the profile bucket snapshot taken when it was set (`baseline`), the same bucket from the current 90-day get_weakness_profile window (`now`), and a `trend` of improving / flat / worse (a move of at least 10% either way) or unknown when either side is missing; the trend is arithmetic over those two numbers and nothing more, with no minimum-games floor on either side: unlike get_weakness_profile, which claims nothing on fewer than five reviewed games, a baseline or a current reading here can each rest on as little as one, so a focus can flip to worse or improving on the strength of a single game. Also the last 20 coaching entries of every kind, newest first; the assignments still `open`; how many positions are saved; and `lastSessionAt`, the time of the most recent `session` entry. Resolved focuses (done or dropped) are not returned. Every string is templated from stored facts (`summarySource: \"template\"`): nothing on this response was written by a language model on the server, and a user with no coaching rows gets empty lists, not an error. Read-only; spends only the shared 120/minute limit.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "set_training_focus",
      "type": "api",
      "description": "Create, retitle or resolve ONE training focus for the authenticated user: a short statement of what you and the user are working on, e.g. \"Stop hanging pieces\". With no `id` it creates a focus (`title` required, 1-120 characters). Pass `weaknessId` (an id exactly as returned by get_weakness_profile, such as concept:hanging_piece or clock:scramble) to snapshot that bucket's current 90-day figure as `baseline`, which is what makes get_coaching_state's trend possible; without one the trend is always unknown. With an `id` (from get_coaching_state) it retitles (`title`) and/or resolves the focus: `status` done or dropped stamps `resolvedAt`; `status` active re-opens one. At most THREE focuses can be active at once: a fourth returns an error naming the three so you can resolve one first. Resolving is the only way through, because this tool never deletes. Scope: it touches one coaching_focus row owned by the token holder and nothing else, so no games, no study collection, no entries and no other user. Rows are attributed to the token that wrote them (`createdBy: agent:<token prefix>`), and the user can delete any focus from their dashboard at any time. Counts against the 60-per-hour coaching-write allowance shared by set_training_focus, add_coaching_entry, save_position and remove_saved_position.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "add_coaching_entry",
      "type": "api",
      "description": "Append ONE entry to the user's coaching log: a `reflection` (what the user said or noticed), a `note` (yours), an `assignment` (homework, which starts `open` unless you pass `status`), a `result` (how a quiz or assignment went) or a `session` (a summary of this session; get_coaching_state's lastSessionAt reads the newest of these). `body` is 1-2000 characters of plain text. Optionally file it against a focus (`focusId`) and pin it to evidence with `gameId` (one of the user's own games from list_games), `ply` (with gameId), `fen` (validated and normalised) and/or `positionId` (a saved position from list_saved_positions); every ref is checked before anything is stored, and a ref the user does not own is rejected, never stored. RECORDING A QUIZ: `kind: \"result\"` with `positionId` and `correct: true|false` also stamps that position's lastQuizzedAt and appends to its quizResults, which is what list_saved_positions' dueForQuiz reads; there is no separate quiz tool. `status` (open / completed / skipped) is accepted on assignments only, at creation. CLOSING AN ASSIGNMENT: a `result` or `note` entry carrying `assignmentId` (from get_coaching_state's openAssignments) and `assignmentStatus` completed or skipped flips that ONE owned, OPEN assignment's status and nothing else; its text is never edited, it is never deleted, and an assignment that is not the user's own or not open is rejected. Beyond that single status flip, entries are APPEND-ONLY from this surface: nothing here edits an existing entry's text or deletes an entry, and the user can delete any entry from their dashboard. Never touches games, the study collection or another user's data. Counts against the shared 60-per-hour coaching-write allowance.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "save_position",
      "type": "api",
      "description": "Save ONE position for the user to come back to: a FEN with a `label` (1-80 characters, e.g. \"Knight fork you missed vs X\") and an optional `why` (up to 500). Two ways in. (1) `gameId` + `ply`: the FEN, the engine's best move and the acceptable answers (best move plus any alternative within 0.25 pawns, the same rule get_critical_moments uses) are COPIED from the user's stored review of that ply; a fen passed alongside gameId is rejected, not trusted. The game must be the user's own (list_games) and already reviewed (request_game_review first). (2) A bare `fen`: validated and normalised; `bestMove` is null and `acceptableAnswers` is empty, because Chessfolio runs no engine here and will not invent an answer. Saving the same FEN from the same source again returns the existing row with `alreadySaved: true` rather than a duplicate; a bare FEN and the same FEN via a game are different sources. Optionally file it against a focus (`focusId`). At most 500 saved positions per user: the 501st is rejected until one is removed. Touches one coaching_positions row owned by the token holder and nothing else; the user can delete any saved position from their dashboard. Counts against the shared 60-per-hour coaching-write allowance.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "list_saved_positions",
      "type": "api",
      "description": "The user's saved positions, newest first, 25 per page (`page`), optionally filtered to one focus (`focusId`) or to those `dueForQuiz`: never quizzed, or whose most recent quiz result was wrong. Each carries the FEN and side to move, the label and why, where it came from (`source`: the game id and ply, or null for a bare FEN), `bestMove` and `acceptableAnswers` (SAN) where the position was saved from a reviewed ply, and its quiz history, so you can run a quiz without playing chess yourself: show the FEN, ask for a move, compare the answer to acceptableAnswers, then record it with add_coaching_entry (kind result, positionId, correct). A position saved from a bare FEN has no acceptable answers, so Chessfolio cannot mark it right or wrong. Read-only; `total` counts the rows matching the filter.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "remove_saved_position",
      "type": "api",
      "description": "Delete ONE saved position owned by the authenticated user, by the exact `id` from list_saved_positions. This cannot be undone. Scope is deliberately narrow: only that coaching_positions row goes; the game it was taken from, the user's review, any coaching entry that referenced it (its refs are left as they were) and every other user are untouched. An unknown id, an id belonging to somebody else and a malformed id all return the same not-found, indistinguishable by design. This is the only coaching row an agent can delete: focuses are resolved with set_training_focus, and entries cannot be removed from this surface at all; the user deletes those from their dashboard. Counts against the shared 60-per-hour coaching-write allowance.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "bearer"
    },
    {
      "name": "list_library_games",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "none"
    },
    {
      "name": "get_library_game",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "none"
    },
    {
      "name": "get_tournament_state",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "none"
    },
    {
      "name": "estimate_round1",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "none"
    },
    {
      "name": "estimate_pairings",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "none"
    },
    {
      "name": "calculate_ecf_rating_change",
      "type": "api",
      "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.",
      "transport": "mcp",
      "endpoint": "https://chessfolio.io/api/mcp",
      "auth": "none"
    }
  ]
}