lcu-mcp
# lcu-mcp
[](https://m8ven.ai/mcp/triggered0-lcu-mcp-191f9c)
[](https://www.npmjs.com/package/lcu-mcp)
[](LICENSE)
[](https://nodejs.org)
[](https://github.com/Triggered0/lcu-mcp/actions/workflows/ci.yml)
An [MCP](https://modelcontextprotocol.io) server that exposes a running League of Legends client to any MCP host — the LCU REST API, live WAMP events & recording, client DOM and CDP console, and OpenAPI schema introspection over stdio.
Ask your assistant what queue you are in, watch champ select unfold event by event, inspect the client's DOM, or drive the client itself — without writing a line of glue code.
## Contents
- [How it works](#how-it-works)
- [Requirements](#requirements)
- [Installation](#installation)
- [Registering with an MCP host](#registering-with-an-mcp-host)
- [Quick start](#quick-start)
- [Tools](#tools)
- [Configuration](#configuration)
- [Enabling DOM access](#enabling-dom-access)
- [Security](#security)
- [Development](#development)
- [Contributing](#contributing)
- [Troubleshooting](#troubleshooting)
- [Privacy](#privacy)
- [Disclaimer](#disclaimer)
- [License](#license)
## How it works
Two independent subsystems run inside one Node process:
- **`LcuClient`** reads the client's lockfile to discover the port and password, then talks REST over HTTPS with Riot's root CA pinned, and holds a WebSocket tap on `OnJsonApiEvent` that feeds an in-process ring buffer.
- **`CdpClient`** attaches to the client's Chrome DevTools Protocol endpoint (exposed by [Pengu Loader](https://pengu.lol)) for DOM queries and JavaScript evaluation.
Both connect lazily and survive client restarts — the lockfile port changes on every launch, so the directory is watched rather than the file. Events are polled rather than pushed, because MCP has no server-to-client push.
## Requirements
| | |
|---|---|
| **Node.js** | >= 24 (ESM, no build step) |
| **League of Legends** | Running. The lockfile at `C:\Riot Games\League of Legends\lockfile` supplies the port and password. |
| **Pengu Loader** | Optional — required **only** for `lol_dom_query` and `lol_eval`. Everything else works without it. |
Windows only in practice: the default lockfile path and the Pengu integration are Windows-specific.
## Installation
### Via `npx` (Recommended, zero install)
Run directly with `npx`:
```bash
npx -y lcu-mcp
```
### From source
```bash
git clone https://github.com/Triggered0/lcu-mcp.git
cd lcu-mcp
npm install
```
Runtime dependencies are exactly three: `@modelcontextprotocol/sdk`, `zod`, and `ws`.
## Registering with an MCP host
### Claude Code
```bash
# Recommended: via npx
claude mcp add lcu --scope user -- npx -y lcu-mcp
# Or from a local clone:
claude mcp add lcu --scope user -- node C:\path\to\lcu-mcp\src\index.js
```
### Any host that reads `.mcp.json`
```json
{
"mcpServers": {
"lcu": {
"command": "npx",
"args": ["-y", "lcu-mcp"]
}
}
}
```
Or from a local repository clone:
```json
{
"mcpServers": {
"lcu": {
"command": "node",
"args": ["C:\\path\\to\\lcu-mcp\\src\\index.js"],
"env": { "LCU_MCP_CONFIG": "C:\\path\\to\\lcu-mcp\\config\\allowlist.json" }
}
}
}
```
`LCU_MCP_CONFIG` is optional; without it the server looks for `config/allowlist.json` relative to its working directory, and falls back to built-in defaults if that file does not exist.
## Quick start
Start the League client, then ask your assistant in plain language. A few things that work with no further setup:
| Ask | What runs |
|---|---|
| *"Is the LCU connection healthy?"* | `lol_status` |
| *"Who am I logged in as?"* | `lol_get("/lol-summoner/v1/current-summoner")` |
| *"What am I doing in the client right now?"* | `lol_get("/lol-gameflow/v1/gameflow-phase")` |
| *"Which champion is id 157?"* | `lol_static(kind="champions", ids=[157])` |
| *"Watch champ select and tell me what happens."* | `lol_events_start(["/lol-champ-select/"])`, then `lol_events_poll` |
| *"What does the lobby endpoint accept?"* | `lol_schema("/lol-lobby/v2/lobby")` |
| *"Accept the ready check."* | `lol_request("POST", "/lol-matchmaking/v1/ready-check/accept")` — needs a [write allowlist](#configuration) entry |
| *"Screenshot the client."* | `lol_cdp_screenshot` — needs [Pengu Loader](#enabling-dom-access) |
| *"What is my ranked winrate and recent roles?"* | `lol_analytics_player` |
| *"Summarize my last 5 matches concisely."* | `lol_analytics_match_history(count=5)` |
| *"Analyze damage and objectives in my last game."* | `lol_analytics_match_detail` |
| *"What is our team damage mix in champ select?"* | `lol_analytics_champ_select_scout` |
| *"Set my summoner spells to Flash and Ignite."* | `lol_workflow_spells_set(spell1="flash", spell2="ignite")` |
| *"Swap champion with ARAM bench."* | `lol_workflow_champ_select_bench(champion="AramBenchChamp")` |
| *"How much essence do my loot shards yield?"* | `lol_analytics_loot_summary` |
Nothing here needs a Riot API key or an internet connection: every call goes to `127.0.0.1`.
## Tools
| Tool | Purpose |
|---|---|
| `lol_status` | Per-subsystem health, resolved LCU port, configured CDP port, whether `allowEval` is on |
| `lol_get(path)` | GET any LCU path |
| `lol_request(method, path, body?)` | Any verb, subject to the write allowlist |
| `lol_endpoints(filter?)` | List the curated endpoint table |
| `lol_static(kind, ids?, query?, fields?, limit?, offset?, refresh?)` | Resolve champion/item/perk/spell/map/queue ids to names from the client's local game data |
| `lol_events_start(filters?)` | Open the WebSocket tap and begin buffering |
| `lol_events_poll(since?, limit?, filter?)` | Drain the ring buffer |
| `lol_events_stop()` | Close the tap |
| `lol_dom_query(selector, all?, props?)` | Query the client DOM |
| `lol_eval(expression, awaitPromise?)` | Evaluate JavaScript in the page |
| `lol_wamp_record_start(uris?, restart?)` | Record LCU WAMP traffic on an independent socket |
| `lol_wamp_record_dump(uri?, since?, until?, kinds?, limit?, cursor?)` | Dump the recorded timeline and per-URI stats |
| `lol_wamp_record_stop()` | Close the recorder socket |
| `lol_cdp_console_start()` | Begin buffering client console output |
| `lol_cdp_console_tail(since?, until?, cursor?, limit?, level?, targetId?, text?)` | Read buffered console entries |
| `lol_cdp_console_stop()` | Stop and discard the console buffer |
| `lol_cdp_network_start()` | Begin buffering the HTTP requests the client UI makes |
| `lol_cdp_network_tail(since?, until?, cursor?, limit?, urlContains?, method?, status?, minStatus?, type?, failedOnly?, targetId?)` | Read buffered requests |
| `lol_cdp_network_body(requestId)` | Fetch one response body from the live renderer |
| `lol_cdp_network_summary(since?, until?, urlContains?, method?)` | Aggregate requests by method and url |
| `lol_cdp_network_stop()` | Stop and discard the request buffer |
| `lol_logs_tail(target?, lines?, session?, level?, search?)` | Tail the most recent lines of a client, UX, or game log |
| `lol_logs_watch_start(target?)` | Begin streaming newly appended log lines into a ring buffer |
| `lol_logs_watch_poll(cursor?, limit?, level?, search?)` | Poll newly streamed log entries |
| `lol_logs_watch_stop()` | Stop and discard the log watcher buffer |
| `lol_logs_sessions(target?, limit?)` | List active and historical log files on disk |
| `lol_game_all(format?)` | Fetch full real-time live game state from in-match game engine (summary or raw) |
| `lol_game_stats()` | Check match status, game clock, mode, and map terrain from the live game engine |
| `lol_game_player(name?)` | Fetch real-time stats, abilities, items, and runes for the active or named player |
| `lol_game_events(afterId?)` | Retrieve in-game events (kills, objectives, aces) with incremental cursor support |
| `lol_restart_ux(waitForReady?, timeoutSeconds?)` | Safely restart client CEF renderers with readiness polling |
| `lol_launch_client(executablePath?, pollIntervalMs?, timeoutSeconds?)` | Launch Riot Client / League of Legends client and wait for LCU readiness |
| `lol_cdp_targets()` | List all active CDP debugging targets (pages, popups, workers) |
| `lol_cdp_screenshot(targetId?, format?, quality?, savePath?)` | Capture client screenshot via CDP (returns MCP image + disk save) |
| `lol_cdp_performance()` | CEF performance metrics, JS heap memory usage, DOM node counts, and leak warnings |
| `lol_cdp_dom_tree(includeOverlaysOnly?, maxDepth?)` | Inspect UI modal hierarchy, viewport routes, and click-blocking transparent overlays |
| `lol_cdp_storage(storageType?, filter?, limit?, parseJson?)` | Inspect localStorage/sessionStorage keys and feature flags with credential redaction |
| `lol_cdp_network_bottlenecks(thresholdMs?, limit?, includeInitiators?)` | Identify slow requests, P50/P90/P99 endpoint latencies, and failed assets |
| `lol_forensics_anomaly_detect(windowSeconds?, severityFilter?)` | Scan across all streams for crash signatures, HTTP error clusters, and console bursts |
| `lol_forensics_export_har(limit?, savePath?)` | Export captured HTTP network traffic to standard HAR 1.2 archive with redaction |
| `lol_schema(path?, method?, model?, refresh?)` | Query internal LCU OpenAPI/Swagger v2 schemas and models |
| `lol_forensics_correlate(since?, until?, limit?, sources?, uriPrefix?, levels?, networkFailedOnly?, logLevel?, format?)` | Correlate telemetry across all 5 streams (WAMP, CDP console, CDP network, disk logs, live game) on a shared time axis |
| `lol_forensics_bundle(since?, until?, limit?, sources?, includeLogTail?, format?)` | Generate an end-to-end diagnostic snapshot combining system status, active timeline streams, and disk log fallbacks |
| `lol_workflow_matchmaking_accept()` | Accept matchmaking ready check if active and unaccepted |
| `lol_workflow_champ_select(champion, type?, completed?)` | Pick, hover, or ban champion by name or numeric ID in active champion select |
| `lol_workflow_champ_select_bench(champion)` | Swap champion with available ARAM bench champion by name or ID |
| `lol_workflow_spells_set(spell1, spell2?)` | Set summoner spells (Flash, Ignite, Smite, Teleport, etc.) by name or ID in champion select |
| `lol_workflow_runes_set(primaryStyleId, subStyleId, selectedPerkIds, name?, replace?)` | Configure, update, and activate a rune/perk page |
| `lol_workflow_lobby(queueId, startMatchmaking?)` | Create game lobby for a queue (e.g. 420 Ranked Solo, 450 ARAM) and optionally start matchmaking |
| `lol_workflow_lobby_invite(toSummonerPuuids)` | Invite players to current lobby party by summoner PUUIDs |
| `lol_workflow_play_again()` | Recreate game lobby from post-game End of Game screen |
| `lol_workflow_honor(target, honorCategory?)` | Vote for teammate on post-game honor ballot ("COOL", "SHOTCALLER", "HEART") |
| `lol_analytics_player(summonerName?, puuid?)` | Aggregate player identity, ranked tiers, recent winrates, and role breakdown |
| `lol_analytics_match_history(summonerName?, puuid?, count?)` | Token-efficient compact match history rows |
| `lol_analytics_match_detail(gameId?)` | Deep post-game breakdown (objectives, damage share, gold, KDA, team stats) |
| `lol_analytics_champ_select_scout()` | Champ select composition scout (ally/enemy roles, champions, AP/AD damage mix) |
| `lol_analytics_live_combat()` | Real-time live in-game combat telemetry, lane differentials, objective clock |
| `lol_analytics_loot_summary()` | Calculate total Blue and Orange Essence yields from champion and skin shards |
| `lol_workflow_loot_disenchant(lootId, count?)` | Disenchant champion or skin shards for essence |
| `lol_chat_send(message, conversationId?)` | Send chat message into active champion select, lobby, or chat |
| `lol_chat_status(availability?, statusMessage?)` | Update summoner presence status message and availability |
**`lol_status` first.** When anything else fails it tells you which half is down — a closed client looks nothing like a missing Pengu install.
**Ids come back raw.** Champ select, the gameflow, and match history all speak in numbers — `championId: 157`, `perk: 8008`, `queueId: 420`. `lol_static` resolves them to `Yasuo`, `Lethal Tempo`, and `Ranked Solo/Duo` from documents the client already serves locally, so no Data Dragon, no API key, and no call leaves `127.0.0.1`. It projects to `{id, name}` and pages at 50 entries by default — `items` alone is 868 entries and 667 KB raw — so widen it deliberately with `fields`, `limit`, and `offset`.
**Events are polled.** `lol_events_poll` returns a `cursor`; pass it back as `since` next time. A non-zero `dropped` means the ring buffer wrapped and that many events were lost after your cursor. Entries with `truncated: true` had their `data` clipped at 4 KB — re-fetch the full body with `lol_get` on the entry's `uri`.
**The client only emits when state changes.** Sitting idle on the home screen it can stay silent indefinitely; navigating the UI or entering a lobby produces bursts. An empty poll usually means nothing happened, not that the tap is broken — check `running` and `lol_status` to tell the two apart.
**Diagnosing a missing event.** `lol_wamp_record_*` runs on its own WAMP socket
outside the client renderer, so it proves what the LCU actually emitted and
when. Read it together with `lol_cdp_console_tail` and a `lol_eval` probe to
separate three cases: the LCU never emitted, it emitted but the page never
received, or the page received and mishandled. Start both recorders *before*
the thing you want to observe — they only hold what arrived after they started.
**Three views, one story.** `lol_wamp_record_*` shows what the LCU pushed,
`lol_cdp_console_*` shows what the page said, and `lol_cdp_network_*` shows what
the page asked for. Each entry names the code that issued the request, so "why
did the client call this" has an answer rather than a guess. A 404 arrives as an
ordinary response, not a transport failure — reach for `minStatus: 400` when you
want everything that went wrong, and `failedOnly` only for connections that never
completed. Response bodies are fetched on demand with `lol_cdp_network_body`, not
buffered.
**Disk logs for historical and game-engine forensics.** `lol_logs_tail` and
`lol_logs_sessions` inspect `LeagueClient.log`, `LeagueClientUx.log`, and `GameLogs`
(`r3dlog.txt`) directly on disk without requiring an active WebSocket or Pengu debugger.
For live monitoring across client actions, `lol_logs_watch_start` streams newly appended
lines from EOF into a dedicated ring buffer. All lines undergo ingest-time credential
scrubbing and Riot auth token redaction.
**In-match game engine telemetry (`lol_game_*`).** When League enters a live match
(loading screen, Summoner's Rift, ARAM, Practice Tool, or TFT), the game engine hosts
an internal HTTPS server on `127.0.0.1:2999/liveclientdata`. `lol_game_all` provides a
complete, token-efficient projection of match clock, team comparisons, scores, items,
and vital stats (or full raw JSON via `format: 'raw'`). `lol_game_events` tracks combat
and objective kills incrementally using `afterId`. If no match is currently running,
tools report a clear indication rather than connection failures.
**Unified multi-stream forensics & diagnostic snapshot (`lol_forensics_*`).**
Complex client bugs often span multiple architectural layers — for example, a champion select lock-in failure might involve an LCU REST 400 error, a frontend exception in CEF, an unfulfilled WAMP gameflow state change, and a diagnostic log entry in `LeagueClient.log`.
- **`lol_forensics_correlate`** merges events chronologically onto a unified time axis from all 5 telemetry streams:
- `wamp`: LCU WebSocket events emitted by backend microservices.
- `cdp`: Frontend console logs, warnings, errors, and unhandled page exceptions.
- `network`: CEF HTTP requests and responses, status codes, round-trip durations, and network dropouts.
- `logs`: Live disk log lines streamed from `LeagueClient.log` or `LeagueClientUx.log`.
- `game`: In-match combat, objective, and gameflow events from the live game engine.
Supported parameters:
- `sources`: Restrict correlation to specific streams (`wamp`, `cdp`, `network`, `logs`, `game`).
- `since` & `until`: Timestamp filtering bounds in epoch ms or relative clock ts.
- `limit`: Maximum total events returned across streams (default 100, max 1000).
- `uriPrefix`: Filter WAMP events by URI prefix (e.g. `/lol-champ-select/`).
- `levels`: Filter CDP console entries by level (`['error', 'warning', 'info', 'log', 'debug']`).
- `networkFailedOnly`: Filter CDP network requests to only failed or aborted connections.
- `logLevel`: Filter live disk log entries by log level (e.g. `ERROR`, `WARN`, `INFO`).
- `format`: Output format: `'narrative'` (default chronological human/LLM-readable log), `'events'` (interleaved JSON array), or `'summary'` (aggregated event and error metrics).
- **`lol_forensics_bundle`** is a one-stop diagnostic snapshot tool for triaging client issues, generating bug reports, or feeding a comprehensive post-mortem to an LLM:
- Inspects real-time system status across LCU REST, CEF remote debugging, live game engine, and all 4 background stream watchers.
- Compiles telemetry metrics and error tallies.
- Generates the chronological multi-stream timeline narrative.
- Automatically falls back to reading the last 50 lines of `LeagueClient.log` from disk when the live log watcher is unstarted or empty (`includeLogTail: true`), guaranteeing diagnostic context even when recorders were not pre-armed.
- Sanitizes all lockfile passwords, Riot authentication tokens, and session credentials using deep secret redaction.
- Supported parameters: `since`, `until`, `limit` (default 200, max 2000), `sources` (stream filtering), `includeLogTail` (fallback to disk log tail, default `true`), and `format` (`'markdown'` for a ready-to-paste triage report or `'json'` for structured tooling).
- **`lol_forensics_anomaly_detect`**: Scans across all active background streams (`wamp`, `cdp`, `network`, `logs`) for crash signatures, HTTP 5xx error bursts, and frontend exception spikes, returning an overall health verdict (`HEALTHY`, `DEGRADED`, `CRITICAL`), root-cause hypotheses, and anomaly timestamps. Supports `windowSeconds` and `severityFilter` (`CRITICAL`, `DEGRADED`, `ALL`).
- **`lol_forensics_export_har`**: Exports captured HTTP/HTTPS network traffic from the active network tailer into a standard HAR 1.2 archive. Automatically redacts sensitive authorization headers, bearer tokens, and session cookies. Can return the HAR JSON structure directly or save it to disk via `savePath`.
**Deep CEF diagnostics & UI inspection.** When diagnosing client frontend lag, memory leaks, unclickable buttons, or client UI state:
- **`lol_cdp_performance`**: Queries CEF DevTools performance metrics. Normalizes JS heap memory (`jsHeapUsedMb`, `jsHeapTotalMb`), utilization ratios, DOM node counts, and style recalculation counts, raising warnings when memory pressure thresholds are breached (>250MB heap or >15,000 DOM nodes).
- **`lol_cdp_network_bottlenecks`**: Analyzes buffered network traffic to rank slowest HTTP calls, calculate P50, P90, and P99 latencies per normalized endpoint pattern (e.g. `/lol-champ-select/v1/session`), group failed asset loads (image 404s, failed script plugins), and correlate initiator script stack traces.
- **`lol_cdp_dom_tree`**: Analyzes the client's live DOM modal stack, visible viewports/plugins (`rcp-fe-lol-*`), and identifies invisible/transparent full-screen backdrop overlays (`opacity: 0` with active pointer events) that frequently cause "frozen UI" states or unclickable buttons.
- **`lol_cdp_storage`**: Inspects client `localStorage` and `sessionStorage` in the CEF context. Features case-insensitive substring key/value filtering, structured JSON parsing, and automatic multi-tier redaction of Riot auth tokens, session passwords, and sensitive cookies.
**Workflow macro automation (`lol_workflow_*`).** High-level client automation needs multi-step orchestration across REST endpoints, active session discovery, and static data catalogs. Instead of 4–8 separate round-trip tool calls with manual state inspection, each macro inspects its preconditions and then issues the one mutation that follows from them. Rollback is limited to what a call created itself — `lol_workflow_lobby` closes a lobby it opened if the matchmaking search then fails — and otherwise a macro that fails part-way leaves the client where it got to. Either way the error names the failing call and the state the client is left in.
Macros mutate the client, so **every write they send goes through the [write allowlist](#configuration)**, exactly like `lol_request`. The shipped `config/allowlist.json` permits all of them; delete a line to disable the corresponding macro, and the refusal will name the line that would re-enable it.
- **`lol_workflow_matchmaking_accept`**: One-call match acceptance. Verifies that a matchmaking ready check is actively in progress (`'InProgress'`) before posting acceptance. Idempotent if already accepted (`playerResponse: 'Accepted'`); returns state without throwing when no check is in progress. A closed client is reported as an error, not as "no ready check".
- **`lol_workflow_champ_select`**: Pick, hover, or ban champions in active champion select. Resolves champions by human-friendly name (e.g. `"Aatrox"`, `"Yasuo"`) or numeric ID via local static game data, identifies the local player's active action cell, and executes a hover (`completed: false`) or lock-in (`completed: true`, default). An exact name wins; a partial name that matches more than one champion is refused with the candidates listed, because a lock-in cannot be undone. Safely detects if champion select is inactive or if no eligible action is currently pending for the player.
- **`lol_workflow_champ_select_bench`**: Swap active champion with an available champion on the ARAM bench by name or ID. Safely resolves bench champions, checks session state, and swaps without obsolete reroll mechanics.
- **`lol_workflow_spells_set`**: Set summoner spells in active champion select by name (e.g. `"flash"`, `"ignite"`, `"smite"`, `"teleport"`) or numeric ID. Updates `spell1` and optionally `spell2` simultaneously.
- **`lol_workflow_runes_set`**: Set, replace, and activate rune pages. With `replace: true` (default) it reuses an editable page **whose name matches `name`** and overwrites it; otherwise it creates a new editable page. It never overwrites a page the user named something else. Accepts primary and secondary style IDs along with an array of perk IDs, ensuring the resulting page is immediately activated.
- **`lol_workflow_lobby`**: Create game lobbies and optionally trigger queue search. Sets up custom or matchmade lobbies by queue ID (e.g. `420` for Ranked Solo/Duo, `440` for Ranked Flex, `450` for ARAM). A no-op if the client is already in the requested queue; if it is in a lobby on a *different* queue, that lobby is replaced. Optionally dispatches matchmaking search (`startMatchmaking: true`) within the same operation. If that search fails and the call had created the lobby from nothing, the lobby is closed again; if it had replaced an existing lobby, the new one is kept, because the old party cannot be restored and no lobby at all is the worse outcome.
- **`lol_workflow_lobby_invite`**: Dispatch invitations to summoner PUUIDs to join the current party lobby.
- **`lol_workflow_play_again`**: Recreate previous game lobby from the End of Game or post-match screen with party settings preserved.
- **`lol_workflow_honor`**: Submit an honor vote for an eligible teammate on the post-game honor ballot by name or summonerId with badge category (`"COOL"`, `"SHOTCALLER"`, `"HEART"`).
**Player & Match Analytics (`lol_analytics_*`).** Read-only, token-efficient performance scouting and post-match telemetry:
- **`lol_analytics_player`**: Aggregates summoner identity, ranked tiers and LP (Solo/Duo, Flex, Arena), win rates, and recent match role tendencies in a single compact JSON summary.
- **`lol_analytics_match_history`**: Returns compact, token-efficient match rows (champion name, outcome, KDA, CS, duration, queue, timestamp) without bloating host LLM context windows.
- **`lol_analytics_match_detail`**: Deep post-game breakdown analyzing baron/dragon/herald/tower objectives, individual damage shares, gold earned, and combat metrics for any historical match.
- **`lol_analytics_champ_select_scout`**: Evaluates active champion select draft composition, assessing allied and enemy champion picks, assigned roles, bans, and team magic vs. physical (AP vs. AD) damage distribution.
- **`lol_analytics_live_combat`**: Connects to the in-match game engine (`127.0.0.1:2999`) to calculate real-time combat telemetry: current gold and CS differentials vs lane opponents, team gold leads, KDA pacing, and live match clock.
**Loot Economy & Crafting (`lol_analytics_loot_summary`, `lol_workflow_loot_disenchant`).**
- **`lol_analytics_loot_summary`**: Scans player inventory and tallies Blue Essence yield from champion shards and Orange Essence yield from skin, ward, and emote shards.
- **`lol_workflow_loot_disenchant`**: Disenchants a specified number of shards for a given loot ID via the client crafting recipe endpoint. Subject to write allowlist.
**In-Client Chat & Status (`lol_chat_*`).**
- **`lol_chat_send`**: Dispatches chat messages directly into active champion select, lobby, or custom conversation channels without requiring manual conversation discovery.
- **`lol_chat_status`**: Updates player chat availability (`chat`, `away`, `dnd`, `mobile`) and custom status message strings visible to friends.
**Filters are URI prefixes applied at ingest.** The unfiltered firehose fills the buffer quickly, so pass something like `["/lol-champ-select/", "/lol-gameflow/"]` unless you genuinely want everything.
## Configuration
`config/allowlist.json`:
```json
{
"allowEval": true,
"cdpPort": 8888,
"eventBufferSize": 1000,
"writeAllowlist": [
"POST /lol-matchmaking/v1/ready-check/accept",
"PATCH /lol-champ-select/v1/session/actions/*"
]
}
```
The shipped file also carries the remaining lines the `lol_workflow_*` macros need — `POST /lol-lobby/v2/lobby`, `POST /lol-lobby/v2/lobby/matchmaking/search`, `POST /lol-perks/v1/pages` and `PUT /lol-perks/v1/pages/*` — plus `DELETE /lol-lobby/v2/lobby`, which `lol_workflow_lobby` uses only to close a lobby it just opened. Drop that line and the macro still works; it simply reports the lobby it could not clean up.
| Key | Default | Meaning |
|---|---|---|
| `allowEval` | `true` | Whether `lol_eval` may run JavaScript in the page |
| `cdpPort` | `8888` | Pengu Loader's remote debugging port |
| `eventBufferSize` | `1000` | Ring buffer capacity; oldest entries are evicted first |
| `writeAllowlist` | `[]` | Which mutating requests `lol_request` and the `lol_workflow_*` macros may send |
| `wampRecordBufferSize` | `20000` | Recorder timeline entry count |
| `wampRecordMaxBytes` | `67108864` | Recorder byte budget; evicts on whichever fills first |
| `wampRecordPayloadCap` | `512` | Per-payload truncation for the recorder |
| `wampRecordFullPayloadUris` | `["/lol-gameflow/v1/gameflow-phase"]` | URI prefixes exempt from the payload cap |
| `wampRecordFile` | `null` | Optional NDJSON path the timeline is appended to |
| `cdpConsoleBufferSize` | `5000` | Console tailer entry count |
| `cdpNetworkBufferSize` | `5000` | Network tailer entry count; about 50 minutes at the client's measured request rate |
| `logWatchBufferSize` | `5000` | Disk log watcher ring buffer capacity |
| `logsDir` | `null` | Optional custom logs directory override (defaults to auto-detected `Logs/`) |
| `liveGamePort` | `2999` | Live Client Data API port hosted by the League of Legends game engine |
Allowlist matching rules:
- An entry is `METHOD path`. The method is compared case-insensitively, the path **case-sensitively**.
- `GET` and `HEAD` are always allowed and need no entry.
- `*` matches a single path segment without crossing `/`: `/a/b/*` matches `/a/b/c` (e.g. `PATCH /lol-champ-select/v1/session/actions/*`), and infix `/a/*/c` matches `/a/b/c` (e.g. `POST /lol-loot/v1/recipes/*/craft` and `POST /lol-chat/v1/conversations/*/messages`), but does not match across multiple `/` delimiters.
- A refused call returns the exact config line that would permit it, and the request is never sent.
## Enabling DOM access
`lol_dom_query` and `lol_eval` need the client's CEF remote debugging port, which Riot's build only opens through Pengu Loader — an externally added `--remote-debugging-port` flag is ignored.
Pengu's config is plain `key=value` text, one pair per line — not JSON, not INI. In `C:\Program Files\Pengu Loader\config`, set:
```
RemoteDebuggingPort=8888
```
Then restart the client UX so CEF picks the port up:
```
POST /riotclient/kill-and-restart-ux
```
This leaves a live game untouched. Until it happens, both tools fail with these exact instructions rather than a bare `ECONNREFUSED`.
## Security
- **TLS verification stays on.** The LCU's self-signed certificate is validated against Riot's root CA, vendored at `certs/riotgames.pem`. The server never sets `rejectUnauthorized: false`.
- **The password never leaves the process.** It is held only to build the `Authorization` header — no tool returns it, nothing logs it, and error text is scrubbed of it before it reaches the host. CDP target URLs embed it too, so they are redacted before any tool returns them.
- **`lol_eval` bypasses the write allowlist by construction.** The client page can `fetch` any LCU endpoint from its own origin, so evaluated JavaScript can do anything the client can. This is accepted, not fixed: it is gated by the `allowEval` flag, whose state `lol_status` reports.
> **Treat the write allowlist as a guardrail against mistakes, not as a security boundary — while `allowEval` is `true` it can be bypassed.** Set `allowEval` to `false` for a real boundary. `lol_dom_query` keeps working, because it injects the selector as data rather than as code.
## Development
```bash
npm test # unit tests via node:test — no League client needed
npm run smoke # live end-to-end check against a running client
npm start # run the server on stdio
```
`npm run smoke` prints one line per stage and exits 1 if any stage fails. It is never run in CI. The event stage waits for real delivery and reports three outcomes: `PASS` when events arrived, `SKIP` when the tap connected but an idle client sent nothing, and `FAIL` when the tap could not connect.
```
bin/lcu-mcp.js # npx entry point
certs/ # Riot's root CA, pinned for TLS verification
config/ # default allowlist.json
scripts/ # lint.mjs (syntax gate) and smoke.mjs (live check)
src/
index.js # stdio transport, context wiring, tool registration
config.js # config loading and validation
allowlist.js # pure write-allowlist matching
redact.js # strip passwords from URLs and strings
backoff.js # shared reconnect schedule
clock.js # injectable time source, so tests never sleep
lcu/
lockfile.js # parse, read, and watch the lockfile
client.js # REST with the pinned CA
buffer.js # ring buffer with cursor and drop accounting
ingest.js # pure ingest policy: prefix filters, truncation
events.js # WebSocket tap with backoff reconnect
recorder.js # independent WAMP socket for forensic recording
ndjson.js # append recorded frames to disk
timeline.js # query, filter, and page a recorded timeline
schema.js # fetch and dereference the OpenAPI document
static.js # lazy per-kind cache over the local game data documents
cdp/
discover.js # probe the debugging port, pick and redact the target
client.js # attach, evaluate, DOM query
console.js # buffered console tailer on its own socket
network.js # buffered HTTP request tailer on its own socket
logs/
parser.js # parse log lines and redact credentials/tokens
sessions.js # locate League log folders and active/past sessions
reader.js # backward chunk reader from EOF
watcher.js # live tailing and timeline buffering
game/
client.js # HTTPS client to in-match engine on port 2999
summary.js # token-efficient projection of allgamedata
forensics/
correlate.js # merge all 5 telemetry streams onto a unified time axis
bundle.js # one-stop diagnostic snapshot across all subsystems
workflow/
matchmaking.js # ready check accept and status inspection
champ_select.js # champion resolution, action detection, lock-in/hover
runes.js # rune page mutation, creation, and activation
lobby.js # lobby creation and queue search dispatch
tools/ # one module per tool group
tests/ # one test file per source module
```
## Contributing
Pull requests are welcome. The house rules are short: write the test first, keep the runtime dependency list at three, and never let a password reach a buffer, a log, or a tool return. `npm run lint && npm test` must be clean before you open the PR — see [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow.
Found a security issue? Please do not open a public issue — use [private vulnerability reporting](https://github.com/Triggered0/lcu-mcp/security/advisories/new) instead, as described in [SECURITY.md](SECURITY.md).
## Troubleshooting
| Symptom | Cause |
|---|---|
| `League client is not running: no lockfile at ...` | The client is closed, or installed somewhere other than the default path. |
| Every CDP tool fails with a Pengu hint | Pengu Loader is not active, or `RemoteDebuggingPort` is unset. Follow [Enabling DOM access](#enabling-dom-access). |
| `no "page" target` | CDP is reachable but the UX is still starting. Retry once the client is visible. |
| `lol_events_poll` returns nothing | Usually an idle client, not a fault. Navigate the UI and poll again; check `running` in the response. |
| A write is refused | The verb and path are not on the allowlist. The error message contains the exact line to add. |
| TLS errors on every REST call | The vendored CA is wrong or stale. Fix the PEM — never disable verification. |
## Privacy
lcu-mcp runs locally, talks only to `127.0.0.1`, and collects nothing. What it reads from the client flows to the MCP host you connected — see [PRIVACY.md](PRIVACY.md).
## Disclaimer
lcu-mcp is not endorsed by Riot Games and does not reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games and all associated properties are trademarks or registered trademarks of Riot Games, Inc.
This project uses the client's own local API. You are responsible for how you use it; automating gameplay may violate Riot's Terms of Service.
## License
[MIT](LICENSE) © Triggered
TDQS
Scored across 61 tools
Many tools have overlapping diagnostic or monitoring purposes (e.g., lol_events_start vs lol_wamp_record_start, lol_cdp_network_summary vs lol_cdp_network_bottlenecks, multiple game/analytics tools). Descriptions explicitly guide selection with cross-references, so boundaries are mostly clear, but the sheer number of similar alternatives creates realistic misselection risk.
All tool names use a consistent lol_ prefix and snake_case, with predictable category markers (cdp, logs, wamp, workflow, analytics, forensics, game). The pattern is not strictly verb_noun throughout (e.g., lol_get, lol_status, lol_schema), but it remains highly readable and mostly uniform.
61 tools is an extreme mismatch for a single MCP server, far exceeding the 25+ threshold that already indicates excessive breadth. While each tool may have a niche, the count forces agents to navigate an unwieldy surface with many related start/stop/poll variants.
The tool surface is very comprehensive, covering LCU REST, CDP, WAMP, disk logs, live game telemetry, forensics, and automation workflows. Only minor gaps exist (e.g., some CRUD operations rely on the generic lol_request), but agents have workarounds and no major domain area is unaddressed.