Skip to main content
Glama

arma-mcp

An MCP server for Bohemia Interactive engine scripting: SQF commands, functions, event handlers, and config classnames — across Operation Flashpoint, Arma, Arma 2 and Arma 3.

Everything is served from a local index. The server never makes a network request; the only traffic is a scheduled, deliberately slow ingest.

What it knows

Source

Content

Where it comes from

BI Community Wiki

commands, functions, event handlers, per-game availability

MediaWiki API, ingested offline

Your Arma install

config classnames from the base game and every mod you have

PBOs read directly off disk

Classnames are generated on your machine, not shipped: the mods they come from carry licences that do not permit redistributing derived data (RHS is CC BY-NC-ND). npm run scan builds them from your own installation, which is what those licences do allow. See data/NOTICE.md.

Games

ofp, ofpe, arma1, arma2, arma2oa, arma3.

Take On Helicopters and VBS are excluded. They share the engine lineage but are not Arma titles; a page tagged for TKOH and an Arma title keeps its Arma tags and only loses the TKOH one.

Per-game availability is exact, not guessed. Every wiki page declares which games it applies to and the version of each that introduced it, so compare_games can answer "is remoteExec safe in Arma 2?" (no — Arma 3 1.50 onward) directly from the source.

Related MCP server: Arma Reforger MCP Server

Tools

Scripting documentation

Tool

Purpose

list_games

Games covered, with entry counts

search

Find commands/functions by name or by what they do

get_command

Full docs for one command: syntaxes, params, locality, examples

get_function

Full docs for one function (BIS_fnc_*)

get_event_handler

Docs for one event handler: arguments, locality, the command that adds it

compare_games

Availability and introduction version across all games

Discovery

Tool

Purpose

list_groups

Command groups with counts

browse_group

Everything in one group

added_in_version

What a given game version introduced

search_examples

Search the example code itself, not the prose

Config classnames

Tool

Purpose

search_classes

Find classnames by name or display name

get_class

One class: inheritance chain, scope, faction, source mod

find_subclasses

Everything descending from a base class

list_mods

Indexed mods with class counts

list_config_roots

CfgVehicles, CfgWeapons, ... with counts

Linting

Tool

Purpose

validate_sqf

Check a snippet: unknown commands, wrong game or version, global-effect and deprecation warnings

validate_sqf is the one that earns its keep in practice — it turns the corpus into a linter that catches "this command does not exist in Arma 2", "this was added in 2.14 but you target 2.10" (pass gameVersion) and "this has a global effect, so calling it on every client applies it N times".

Every tool is read-only and offline, and says so in its MCP annotations.

Game is a parameter, not a separate tool set — one wiki page documents a command across several games at once, so per-game tools would surface the same record many times and bloat the tool list.

Setup

Requires Node 22.13+ (it uses the built-in node:sqlite, so there is no native addon to rebuild when Node is upgraded).

npm ci
npm run index     # build data/index.sqlite from the tracked corpus (~1 min)
npm run build
npm run smoke     # verify every tool answers

The wiki corpus is committed, so no ingest is needed to get started. index also downloads the embedding model once (~23 MB) into data/models/; the server only ever loads it from there. Without it, search still works on its lexical arms and logs one line saying semantic search is off.

Classnames come from your own install:

npm run scan -- "C:/Program Files (x86)/Steam/steamapps/common/Arma 3"   # or set ARMA3_PATH
npm run index

scan accepts --curated to cover only the widely used mods (base game, CBA, CUP, RHS, ACE and friends) instead of everything installed. Both stay local. The scripting tools work without scan; the class tools report that no index is present until you run it.

Registering with an MCP client

Any client that takes an mcpServers entry:

{
  "mcpServers": {
    "arma-mcp": { "command": "node", "args": ["/path/to/arma3-mcp/dist/index.js"] }
  }
}

Claude Code: claude mcp add arma-mcp --scope user -- node /path/to/arma3-mcp/dist/index.js.

Use an absolute path to node if your client is launched from a GUI: those do not always inherit your shell PATH. Windows paths in a TOML config (Codex) must be single-quoted literal strings, or the backslashes become escapes.

Variable

Effect

ARMA_MCP_INDEX

Use an index from another location

ARMA_MCP_MODEL_DIR

Embedding model cache (default data/models)

ARMA_MCP_INTERVAL_MS

Ingest pacing between wiki requests (default 5000)

Retrieval

Hybrid, because pure vector search is the wrong tool for half of these queries.

  • Wiki entries — exact/prefix name matching, name-word coverage ("delete a vehicle" → deleteVehicle), FTS5, and vector similarity over the prose, fused with weighted Reciprocal Rank Fusion. An exact identifier always ranks first, arm weights shift with the query's shape (identifier or sentence), and a small prior favours entries documented across more games. Embeddings are all-MiniLM-L6-v2 running locally via Transformers.js (no API key).

  • Classnames — lexical only. rhs_2s1_tv and rhs_2s1_vmf are near-identical to an embedding model but mean different vehicles, so exact name and display-name matching is what discriminates. Embedding 100k identifiers would cost an hour of CPU to make results worse.

Arms are weighted rather than equal. A literal name hit is far stronger evidence than an FTS token overlap: searching T-72 tokenises to t + 72, which matches the unrelated OZM-72 mine while missing T-72B. Weighting lets the precise arm win.

Ranking is measured, not eyeballed: npm run eval scores 50 realistic queries from tests/search-eval.json (hit@1, hit@3, MRR), and the test suite enforces the recorded floor whenever a local index exists. Change SEARCH_TUNING in src/index/query.ts only with that check in hand.

Ingest, and being a good neighbour

community.bistudio.com is behind Cloudflare. Ordinary page requests get a JS challenge, but /wikidata/api.php answers JSON directly — so this project uses the sanctioned MediaWiki API and never scrapes rendered HTML.

Cloudflare returns 403 for a small number of specific pages. This was isolated by bisection, not guessed at: BIN fnc droneDestructionFX fails on its own, every time, while its neighbours succeed, and batch size, User-Agent, URL encoding and Node-vs-curl all make no difference. Because the API takes 50 titles per request, one such page fails its entire batch — which is what stalled early runs and looked, misleadingly, like rate limiting.

The ingest therefore:

  • paces requests at one per 5 seconds out of politeness (ARMA_MCP_INTERVAL_MS),

  • sends maxlag=5 so it yields when the wiki's replicas are lagging,

  • batches 50 titles per request, the anonymous limit — the whole corpus is ~100 requests, not ~4,700,

  • bisects a failed batch to isolate the page at fault, quarantines it, and carries on, so one unreachable page costs that page rather than the run,

  • checkpoints every batch, so an interrupted run resumes instead of restarting.

A full ingest is ~100 requests and completes in well under an hour. If a run stops early, run npm run ingest again — it picks up where it left off. npm run assemble rebuilds data/corpus.json from whatever checkpoints exist. The scheduled GitHub Action does the same weekly from a fresh runner, and the incremental path (npm run update) asks the wiki what changed since the last watermark and refetches only that — usually 2-5 requests.

If you want to cut the load by another order of magnitude, ask the wiki's administrators for the bot flag on an account: MediaWiki grants apihighlimits to that group, raising the cap from 50 to 500 titles per request and turning the full corpus into roughly 10 requests.

Data layout

data/corpus.json               wiki entries — tracked, diffable
data/classes.json              local scan — untracked (licensing; see NOTICE.md)
data/classes-curated.json.gz   curated local scan — untracked, same reason
data/index.sqlite              built artifact — untracked, `npm run index`
data/models/                   embedding model cache — untracked, fetched by `npm run index`
data/.ingest/                  resume checkpoints — untracked

The index is rebuilt locally rather than committed, which keeps git history clean. build-index prefers classes.json when present and otherwise falls back to the curated file; they are never merged, since that would double-count mods appearing in both.

Config parsing

Addon configs arrive in two forms and both are handled:

  • config.bin — Bohemia's rapified binary. 16-byte header, class bodies reached by explicit offsets.

  • config.cpp — plain text, roughly 7% of addons. Comments and preprocessor directives are stripped; macros are not expanded, so a class whose name comes from a macro is skipped rather than guessed at.

PBO members are read by seeking to the byte range needed, never by loading the archive: individual PBOs reach ~250 MB while the config inside is a few KB.

Compressed members use BI's LZSS variant, where the 12-bit back-reference is a distance back from the current output position rather than an absolute index into a space-filled ring buffer. Textbook LZSS appears to work — the first bytes of any file are literals either way — and then silently corrupts everything after, which is what made this worth pinning down.

Licensing

Licensed by component, because the data cannot be relicensed by us.

Component

Licence

src/, scripts/, tests/

MIT

data/corpus.json

Bohemia Interactive wiki terms — non-commercial, attributed

classnames

not distributed; generated from your own install

The wiki's general disclaimer explicitly permits integrating its content into derived works, but requires that those works "remain licensed non-commercial". MIT permits commercial use, so the corpus cannot be MIT — hence the split rather than a single repository licence.

The npm package therefore ships code only; you build the data locally from sources you are licensed to use. Full detail, including the per-mod licence table, is in data/NOTICE.md.

Wiki content belongs to Bohemia Interactive and the wiki's contributors, and every tool response links back to its source page. Classnames are read from your own installed game and mods and never leave the machine.

Related MCP Connectors

Related MCP Servers