Skip to main content
Glama
roku-dev-doc-mcp

roku-dev-doc-mcp

by artemisaiev

roku-dev-doc-mcp

A local Model Context Protocol server that serves Roku / BrightScript development documentation, mirrored from the docs/ folder of rokudev/dev-doc (branch v2.0) and indexed in SQLite with an FTS5 full-text index.

Read-only reference tooling: it never writes to the upstream repo, and needs no authentication for the public repo.

  • 600+ documents, ~6 MB of markdown, indexed in about a second

  • Offline after first sync — everything is served from a local SQLite file

  • Context-aware reads — large docs are returned as a section outline rather than dumping 90k tokens into your context window

Install

npm install -g .

Then point an MCP client at the roku-dev-doc-mcp command.

Claude Code

claude mcp add roku-docs -- roku-dev-doc-mcp

Claude Desktop / generic MCP config

If you use a Node version manager (nvm, fnm, asdf, volta), pin an absolute Node path rather than using the bare command:

{
  "mcpServers": {
    "roku-docs": {
      "command": "/absolute/path/to/node22/bin/node",
      "args": ["/absolute/path/to/roku-dev-doc-mcp/bin/roku-dev-doc-mcp.js"]
    }
  }
}

The bare command works only when #!/usr/bin/env node happens to resolve to Node >= 22:

{
  "mcpServers": {
    "roku-docs": {
      "command": "roku-dev-doc-mcp"
    }
  }
}

A GUI app does not inherit your shell's PATH. Claude Desktop compensates by building its own, which globs every ~/.nvm/versions/node/*/bin — so an old version sorting ahead of the current one wins, no matter what nvm alias default says. v16.20.2 sorts before v22.20.0, and the SQLite driver is a native module that cannot load on Node 16.

Symptom when this happens: the server dies immediately and Claude Desktop reports Server disconnected, with FATAL ERROR: Error::New napi_get_last_error_info in ~/Library/Logs/Claude/mcp-server-roku-docs.log. The entrypoint now checks the Node version first and prints a readable error instead, so check that log before assuming anything else is wrong.

On first run the index is empty, so the server kicks off a sync in the background — it connects immediately rather than blocking the client's startup, and tools report that the index is still building until it lands (about a second). To pre-warm it instead:

roku-dev-doc-mcp --sync

After that, the server refreshes itself in the background whenever the index is more than 7 days old. That refresh never blocks startup and never makes tools unavailable — the existing docs keep being served while it runs, so it is invisible to callers. Set ROKU_DOCS_MCP_MAX_AGE_DAYS to change the threshold, or 0 to disable it and rely on manual --sync only.

Related MCP server: papermc-docs-mcp

Using it in a project

You don't need to add anything to your project's CLAUDE.md. Adding the server is the whole setup.

The server sends its own usage guidance to the client during initialization, via the MCP instructions field (the INSTRUCTIONS constant in src/server.js, the single source of truth for that text). Any project that connects gets the rules automatically: prefer a lookup over a guess for ro* / if* symbols, SceneGraph node fields and defaults, BrightScript syntax, manifest keys and certification requirements — plus the folder map under Where things live.

CLAUDE.md is for what the server cannot know: your target Roku OS version, SceneGraph vs the legacy SDK, house patterns, directories to avoid. If you have such rules and want them shared across several Roku projects, reference one file rather than copying it, so it cannot drift:

@~/.claude/roku-project-conventions.md

Caveat. The MCP spec makes instructions optional for clients to honour. Claude Code and Claude Desktop both surface it; a third-party client may not. The individual tool descriptions are therefore written to stand on their own — on a client that ignores instructions the tools still work correctly, they just lose the "prefer a lookup over a guess" nudge.

Tools

Tool

Arguments

Purpose

search_roku_docs

query, limit?, prefix?

BM25-ranked full-text search with highlighted snippets

get_roku_doc

path, section?, full?

Read one page; large pages return an outline

list_roku_docs

prefix?, limit?

Browse the folder tree

refresh_roku_docs

force?

Re-sync from GitLab, returns a change summary

search_roku_docs accepts plain keywords as well as FTS5 syntax ("exact phrase", term*, AND/OR/NOT). A malformed query degrades to a plain keyword search rather than erroring.

get_roku_doc resolves paths leniently — roarray, roarray.md, and the full docs/REFERENCES/brightscript/components/roarray.md all work.

Where things live

Pass any of these as prefix to narrow a search:

Topic

prefix

Components (ro*)

docs/REFERENCES/brightscript/components

Interfaces (if*)

docs/REFERENCES/brightscript/interfaces

Events (ro*Event)

docs/REFERENCES/brightscript/events

Language / syntax

docs/REFERENCES/brightscript/language

SceneGraph nodes

docs/REFERENCES/scenegraph

Guides, tooling, debugging, Roku Pay

docs/DEVELOPER

Device / hardware specs

docs/SPECIFICATIONS

Components are ro*, interfaces are if* — searching the exact symbol name is usually the fastest route. Typical flow: search_roku_docs("Task control RUN") → get_roku_doc("docs/REFERENCES/scenegraph/control-nodes/task.md").

Large documents

Doc sizes are heavily skewed: most are a few KB, but the largest is ~354 KB (roughly 90k tokens). Anything over 12 KB returns a numbered section outline instead of the body:

This document is too large to return whole (346.5 KB, 76 sections). Call
`get_roku_doc` again with a `section` number or heading text from this outline:

  1.   Ingest specifications  (772 B)
  2.     MovieLabs  (644 B)
  3.   Roku content policies  (24 B)
  …

Then request a section by number (section: 24) or by heading text (section: "closed captions"). Search results for large docs also report which section best matches the query — so arriving from a search costs no extra round trip, you just pass that section straight to get_roku_doc. full: true forces the whole document.

12 KB (~3k tokens) is the measured break-even for this corpus: 80% of docs still return whole in a single call, the worst-case return drops from ~10.5k to ~3.2k tokens, and the high-frequency BrightScript component and interface pages (medians ~1.4 KB and ~3.3 KB) are unaffected. Tune it with ROKU_DOCS_MCP_MAX_DOC_BYTES if your context budget differs.

Resources

Docs are also exposed as MCP resources under docs://{path}, e.g. docs://docs/REFERENCES/brightscript/components/roarray.md, with path completion. Resources are the idiomatic mechanism for browsable read content; the tools above are better suited to search and refresh.

CLI

roku-dev-doc-mcp              # start the MCP server on stdio (default)
roku-dev-doc-mcp --sync       # sync the index and exit
roku-dev-doc-mcp --sync --force
roku-dev-doc-mcp --status     # show index status
roku-dev-doc-mcp --help

How the sync works

A sync runs when the index is empty at startup, when it is older than the staleness threshold at startup, or when you ask for one (refresh_roku_docs or --sync). All startup syncs are non-blocking. Only one sync runs at a time — concurrent callers share the same in-flight run.

  1. Read the head SHA of the v2.0 branch (GET /api/v4/projects/:project/repository/branches/v2.0, where GitLab returns it as commit.id). If it matches the last synced SHA and the index is non-empty, stop — nothing to do.

  2. Download the repo archive for that exact commit (GET /api/v4/projects/:project/repository/archive.tar.gz?sha=<sha>, ~1.8 MB, one request). Pinning to the SHA means a branch that moves mid-sync can't leave a recorded SHA that was never actually fetched.

  3. Stream it through gunzip + tar in memory, keeping only docs/**/*.md.

  4. Recompute each file's git blob SHA locally (sha1("blob <len>\0" + bytes)) and compare against the stored value. Unchanged files are skipped, so a re-sync only touches what actually moved and the change summary stays accurate.

  5. Delete rows no longer present upstream, then update the sync markers.

One HTTP request per sync (plus one cheap API call for the SHA check). The archive is never written to disk.

Archive throttling

GitLab rate-limits repository archive generation per project and answers a throttled request with 406 Not Acceptable, not 429. The cooldown runs to tens of seconds, so the archive fetch retries up to 5 times with backoff capped at 30s (honouring Retry-After when present), spanning roughly 50s in total. A short retry loop is not enough here: several attempts inside ~6s all fail while the identical request succeeds a minute later.

If every attempt fails the sync throws, the existing index is left untouched and keeps being served, and a background refresh logs the failure without taking the server down.

last_synced_at records when the index was last confirmed in sync with upstream, so it advances even when the SHA check finds nothing to download. Without that, an index past the staleness threshold whose upstream never moves would re-check on every single launch instead of once per interval.

Titles and frontmatter

Every upstream doc carries YAML frontmatter, and most have no # heading at all — so the frontmatter title is the primary source, with a first-heading and then filename fallback. Frontmatter is parsed out into title and excerpt columns and stripped from the stored content, so the full-text index holds prose rather than YAML keys.

Eight docs are frontmatter-only section landing pages upstream with no body; these are indexed by title and reported as such when read.

Configuration

Variable

Purpose

ROKU_DOCS_MCP_DATA_DIR

Override the data directory

GITLAB_TOKEN / GL_TOKEN

Raise the GitLab API rate limit (optional)

ROKU_DOCS_MCP_MAX_DOC_BYTES

Whole-doc size threshold (default 12000)

ROKU_DOCS_MCP_MAX_SECTION_BYTES

Hard ceiling on any one response (default 60000)

ROKU_DOCS_MCP_MAX_AGE_DAYS

Background refresh threshold in days (default 7, 0 disables)

The SQLite file lives in a per-user data directory, not in the install directory:

  • macOS — ~/Library/Application Support/roku-dev-doc-mcp/docs.sqlite

  • Linux — $XDG_DATA_HOME/roku-dev-doc-mcp/ or ~/.local/share/roku-dev-doc-mcp/

  • Windows — %LOCALAPPDATA%\roku-dev-doc-mcp\Data\

Only one unauthenticated GitLab API call is made per sync (the branch SHA check), against a 500/minute unauthenticated limit, so rate limiting is not a practical concern. GITLAB_TOKEN only matters behind a shared egress IP.

Development

npm install
npm test
roku-dev-doc-mcp/
├── bin/roku-dev-doc-mcp.js   # CLI entrypoint (shebang), arg handling only
├── src/
│   ├── server.js             # MCP server, tool + resource registration
│   ├── sync.js               # GitLab fetch, extract, frontmatter, upsert
│   ├── sections.js           # markdown section splitting for large docs
│   ├── db.js                 # SQLite schema, FTS5 triggers, queries
│   └── paths.js              # per-user data dir resolution
└── test/
    ├── parsing.test.js       # frontmatter, sections, query sanitising
    └── staleness.test.js     # background-refresh threshold logic

Plain Node.js ESM, no TypeScript, no build step. Requires Node >= 18.

License

The Unlicense — this software is released into the public domain. Do whatever you like with it: copy, modify, publish, sell, or redistribute, for any purpose, with no attribution required. See LICENSE.

Two things the dedication does not cover, since they were never mine to give away:

  • The documentation content itself. The docs this server mirrors belong to Roku and are reproduced unmodified from rokudev/dev-doc. The public-domain dedication applies to the code in this repository, not to the indexed text.

  • Dependencies. Each npm dependency keeps its own license — currently MIT (@modelcontextprotocol/sdk, better-sqlite3, zod), ISC (yaml), and Blue Oak 1.0.0 (tar). All permissive, but they are not public domain.

Available Tools

4 tools
get_roku_docRead a Roku docA
Read-only

Return the markdown content of a single documentation page by path. Documents larger than 11.7 KB are returned as a numbered section outline instead; pass section (a number or heading text) to read one of those sections. Search results for such documents already name the best-matching section, so you can request it directly without fetching the outline first.

Inputs: path (required), full, section.

list_roku_docsList Roku docsA
Read-only

Browse the documentation tree. With no prefix, lists the top-level sections and their document counts; with a prefix, lists the subfolders and documents beneath it.

Inputs: limit, prefix.

refresh_roku_docsRefresh Roku docsA
Idempotent

Re-sync the local index from rokudev/dev-doc@v2.0. Skips the download when the upstream branch has not moved since the last sync; pass force: true to re-index regardless. Returns a summary of what changed.

Inputs: force.

search_roku_docsSearch Roku docsA
Read-only

Full-text search across the Roku/BrightScript documentation, ranked by relevance. Supports plain keywords as well as FTS5 syntax ("exact phrase", term*, AND/OR/NOT). Returns the path, title, and a highlighted snippet for each hit — use get_roku_doc to read a result in full.

Inputs: query (required), limit, prefix.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.4/5.0

    Scored across 4 tools

    Disambiguation5/5

    Each tool has a distinct role: search (query), get (fetch by path), list (browse tree), refresh (sync index). There is no overlap or ambiguity between them.

    Naming Consistency5/5

    All tools follow a consistent verb_noun pattern with snake_case: search_, get_, list_, refresh_. The singular 'doc' for get_roku_doc is a natural contrast to the plural forms and matches the single-page return.

    Tool Count5/5

    Four tools is well-scoped for a documentation MCP server, covering discovery, retrieval, and maintenance without unnecessary sprawl or redundancy.

    Completeness5/5

    The tool surface fully covers the core workflow: browse (list), search (search), read (get), and keep the index current (refresh). There are no obvious gaps for a documentation access server.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers