Skip to main content
Glama
README.md
# OpenDota MCP

A dependency-free [OpenDota API](https://docs.opendota.com/) client for MCP-compatible assistants and the terminal. It runs locally over **stdio**, uses **Node.js 20 or later**, and needs no API key for public endpoints.

- Four MCP tools: endpoint discovery, endpoint details, API calls, and Steam ID conversion.
- CLI commands for matches, players, heroes, and generic GET requests.
- A bundled endpoint catalog for offline discovery.
- Request pacing, timeouts, bounded GET retries, and API-key redaction in errors.
- Explicit POST operations for replay parsing and player refresh.

This is an independent community client. It is not an official OpenDota or Valve product.

## Install from GitHub

Clone the repository:

```sh
git clone https://github.com/HFUBlackRabbit/opendota-mcp.git
cd opendota-mcp
node server.mjs --help
node server.mjs --version
```

There are no runtime or development npm dependencies to install. `npm start` launches the MCP server; `npm test` runs the local test suite. The package is marked `private` to prevent accidental npm registry publication; GitHub cloning and local use work normally.

## Connect an MCP client

Use an absolute path to the cloned `server.mjs`. If a desktop client cannot find `node`, use an absolute path to your Node.js executable as `command`.

For clients using JSON configuration, see [examples/mcp.json](examples/mcp.json):

```json
{
  "mcpServers": {
    "opendota": {
      "command": "node",
      "args": ["/absolute/path/to/opendota-mcp/server.mjs"]
    }
  }
}
```

For Codex, merge the following section into your user configuration. Do not replace unrelated settings. See [examples/codex.toml](examples/codex.toml).

```toml
[mcp_servers.opendota]
command = "node"
args = ["/absolute/path/to/opendota-mcp/server.mjs"]
```

Use Windows paths in the form supported by your client, such as `C:/tools/opendota-mcp/server.mjs`. The server does not bind an HTTP port. Without CLI arguments it waits for newline-delimited JSON-RPC messages on stdin and writes protocol messages to stdout.

### Tools

| Tool | Purpose |
|---|---|
| `opendota_find_endpoints` | Search the bundled catalog with `query` and `limit` |
| `opendota_endpoint_info` | Read an endpoint's description and parameters using `path` and optional `method` |
| `opendota_api_call` | Make a GET or an allowed POST using `path`, `params`, optional `fields`, and `max_chars` |
| `opendota_account_id` | Convert a Steam32 or SteamID64 value to OpenDota `account_id` |

Start by finding an endpoint, then inspect its parameters. A compact API call looks like this:

```json
{
  "path": "/players/1/matches",
  "params": {
    "limit": 5,
    "project": ["kills", "deaths"]
  },
  "fields": ["match_id", "kills", "deaths"],
  "max_chars": 40000
}
```

`fields` selects top-level properties of an object or of every element in an array **after** the API response arrives. It does not project fields inside `players`. Use upstream `project`/`limit` when the endpoint supports them.

MCP output defaults to 40,000 characters, with a configurable limit of 1,000–200,000. Larger results become a JSON object with `truncated`, `total_chars`, `hint`, and `preview`; that preview is not a complete match. The CLI emits the full JSON response, which is useful for replay analysis that requires all ten players.

## Use the CLI

Commands that contact the API require network access. IDs below illustrate the syntax; choose a player or match whose public data is available.

```sh
node server.mjs heroes
node server.mjs heroes stats
node server.mjs player 1
node server.mjs player 1 recentMatches
node server.mjs player 1 heroes -p having=10
node server.mjs match 271145478
node server.mjs search Dendi
node server.mjs endpoints "players matches"
node server.mjs info "/players/{account_id}/matches"
node server.mjs account-id "76561197960265729"
```

Generic GET requests accept paths relative to the API base: `/heroes`, not a full URL, `/api` prefix, or query string. Repeat a parameter for arrays:

```sh
node server.mjs get /players/1/matches -p limit=5 -p project=kills -p project=deaths --compact
```

`project=kills,deaths` is not equivalent to two `project` parameters. SteamID64 values must be passed as strings in JSON to preserve their precision. The `player`, `refresh`, and `account-id` commands accept Steam32 or SteamID64.

CLI API results are JSON on stdout; failures are written to stderr with a nonzero exit code. `--help` and `--version` return plain text. The package exposes both `opendota` and `opendota-mcp` executable aliases when installed as a local package.

### Operations that change upstream state

These commands send POST requests and are never automatically retried:

```sh
# Request replay parsing. This creates an upstream job.
node server.mjs parse 271145478

# Use job.jobId from the previous response, not the match ID.
node server.mjs job 12345

# Request an upstream player-data refresh.
node server.mjs refresh 1
```

Only POST `/request/{match_id}` and `/players/{account_id}/refresh` are allowed. A parse request counts as **10 calls toward OpenDota rate limits**, but one call for billing. Check [OpenDota's current limits and terms](https://www.opendota.com/api-keys) before large workloads.

GET `/request/{jobId}` may return a job or `null`. A disappeared job does not prove successful parsing: fetch the match again and inspect its parsed fields. Replay availability and public player history are controlled upstream.

## Configuration and credentials

| Environment variable | Default | Purpose |
|---|---|---|
| `OPENDOTA_API_KEY` | unset | Optional OpenDota key |
| `OPENDOTA_BASE_URL` | `https://api.opendota.com/api` | Trusted API base; CLI override: `--base-url` |
| `OPENDOTA_TIMEOUT_MS` | `30000` | Per-attempt timeout; `--timeout-ms` |
| `OPENDOTA_INTERVAL_MS` | `1100` | Interval per rate-limit budget unit in one process; `--interval-ms` |
| `OPENDOTA_RETRIES` | `2` | Additional GET attempts; `--retries` |

Supply credentials using your process environment or MCP client's secret configuration. `.env` files are not loaded automatically. The key is sent as OpenDota's `api_key` query parameter; passing it through CLI arguments or tool `params` is rejected.

Set `OPENDOTA_BASE_URL` only to a trusted server: the configured API key is sent to that base URL. Error output redacts the configured key, but that does not make arbitrary upstream content or HTTP logs safe to publish. Remove credentials and private data before attaching reports.

The queue is local to one process. Each GET or player refresh uses one interval; a replay-parse POST reserves ten intervals before the next request. Multiple CLI processes or MCP instances do not share a rate-limit budget. GET retries cover HTTP 429/502/503/504 and network timeouts/errors. `Retry-After` is respected; requests are not retried early when the requested delay exceeds the retry budget. POST is not retried.

## Data limitations and catalog provenance

- Player IDs, match IDs, and parse-job IDs are different identifiers.
- `recentMatches` returns a limited recent sample; use `matches` with filters and pagination for history.
- Player aggregates usually apply `significant=1`. Use `significant=0` when appropriate for nonstandard modes, and separate Turbo from ranked or tournament data.
- Amateur league team summaries can be empty even when direct match IDs are available. Consult `/leagues/{league_id}/matchIds` for the league's catalog; a missing parse or absent field is not a zero statistic.
- `/health` is an API endpoint; OpenDota's service `/status` is outside the API base and is not a relative CLI API route.

`endpoints.json` is a local OpenAPI-derived snapshot, retrieved **2026-10-19**, API version **31.1.0**. Endpoint discovery works offline and does not guarantee every route or field remains available on the live service. Sources: [API specification](https://github.com/odota/core/blob/master/svc/api/spec.ts), [player parameters](https://github.com/odota/core/blob/master/svc/api/requests/playerParams.ts), and [OpenDota documentation](https://docs.opendota.com/). See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for attribution.

## Development and verification

```sh
npm test
npm run pack:check
```

The Node test suite uses local HTTP fixtures and child processes. It checks CLI behavior, MCP initialization/tool calls, filtering, output truncation, retries, and credential redaction without calling the public API. The GitHub Actions workflow is configured to run it on Node.js 22 and 24 on Linux and Windows.

The repository also contains a small [Postman simulation](postman/simulations/opendota.sim.yaml). It is optional and is not a runtime dependency. Fixtures are synthetic contract examples, not recorded player data. To exercise the real HTTP client against it, install Postman CLI 1.75.0 or later and run:

```sh
postman simulation run postman/simulations/opendota.sim.yaml --port 4900 --no-history --output ndjson
```

After its `listening` event, run the checker in a separate terminal. It accepts only a loopback base URL and replaces any inherited API key with empty or synthetic test values:

```sh
node scripts/verify-simulation.mjs --base-url http://127.0.0.1:4900/opendota/api
```

Stop the simulation with Ctrl+C after checking. See [docs/verification.md](docs/verification.md) for the expected request counts and recorded evidence. Mocked checks establish behavior against those fixtures, not live OpenDota availability or completeness. No GitHub Actions step submits replay-parse or player-refresh requests to the public service.

## License

MIT. See [LICENSE](LICENSE). OpenDota's source material is attributed separately in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).