Back to home

JohnXu22786

docs-retriever

doctrove: versioned library documentation retrieval MCP server for coding agents — zero runtime dependencies, installable as a dsh plugin bundle.

Stars
0
Language
JavaScript
Created
Aug 16, 2026
Updated
Aug 16, 2026

Introduction

简体中文

doctrove — Versioned library documentation retrieval (agent toolset)

doctrove is a versioned documentation retrieval plugin for coding agents: it maintains a "library documentation catalog index", letting agents fetch accurate, versioned, traceable API documentation snippets on demand while writing code — instead of guessing API usage from training memory — and thereby avoiding "APIs that don't exist in the docs", "outdated signatures", and "fabricated parameters".

  • Zero runtime dependencies: uses only Node.js built-ins (fetch, node:test), runs without installing any package;
  • Standard MCP stdio server: any MCP-capable client (dsh, Claude Code, Codex, opencode, etc.) can connect;
  • Built for dsh: ships a dsh bundle (cordis.patch.yml + a self-built bridge plugin), one-step integration via dsh plugin add, tools automatically appear in the model's tool list (mcp__doctrove__*);
  • Versioned: every entry carries multiple documentation volumes, supporting "latest stable / exact version / prefix version (4 → 4.21.x)" selection;
  • Scored, ranked results: entry retrieval and documentation snippets both carry 0–1 relevance scores and hit signals, so the model can verify "why it ranked first";
  • Offline-capable: ships with a built-in local demo index (data/index.json), runs without networking or remote sources;
  • Self-hostable remote index: the index is an open JSON format that can be hosted on any static hosting (a zero-dependency hosting script is included);
  • Smart caching: TTL + LRU in-memory cache, remote index and query results expire automatically per configuration, --no-cache disables it in one shot;
  • Graceful degradation: when the remote index is unreachable, falls back to the local index automatically, results are tagged with source so the agent can tell data provenance.

Quick start

Method A: connect directly from any MCP client

# Requires Node.js >= 18.17; no arguments means offline mode (built-in index)
node src/entry.js

Example config line using the official dsh bridge (also applies to Claude Code / Codex MCP config):

# dsh: insert into $DSH_HOME/profiles/<profile>/cordis.patch.yml
- insert:
    - id: mcp-doctrove
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        serverName: doctrove
        transport: stdio
        command: node
        args: ['/absolute/path/src/entry.js']

Once connected, the model sees 3 tools: catalog_lookup, catalog_releases, doc_extract (generic MCP clients see the bare names; in the dsh scenario they carry the mcp__doctrove__ prefix, see below).

Method B: install as a dsh plugin bundle (recommended)

This plugin is declared as a dsh bundle (the dsh.bundle field in package.json). Run the following in the plugin checkout directory:

dsh plugin --profile web add .
  • On first use it automatically initializes the web profile and adds this package to dsh.profile.bundles;
  • The doctrove/bridge plugin defined in cordis.patch.yml spawns this MCP server inside the dsh process, and after the handshake registers all tools into ctx.tools, no manual config changes needed;
  • Offline-capable: the built-in index is loaded by default; to use a remote index, configure args: ['--index-url', ...] on the bridge line (see below);
  • Uninstall: dsh plugin --profile web remove doctrove.

Installing in DSH

dsh plugin --profile demo add github:JohnXu22786/docs-retriever
  • demo is a dsh profile: it is created automatically on first use, and the package is added to dsh.profile.bundles;
  • The cordis.patch.yml inside the package defines the doctrove/bridge plugin, which starts this MCP server within the dsh process and registers all tools into ctx.tools after the handshake — no manual configuration needed;
  • Offline-capable out of the box: the built-in index is loaded by default; configure args: ['--index-url', ...] on the bridge line to use a remote index (see below);
  • Removal:
dsh plugin --profile demo remove doctrove

After installing, restart dsh, then in a session you can simply say:

"Write an Express 5 endpoint with :id route params and a JSON response — look up the exact route-param syntax first"

The corresponding tool call chain: mcp__doctrove__catalog_lookup (confirm express) → mcp__doctrove__doc_extract (id=express, focus=route parameters).

Note: dsh enables no MCP servers by default (every server command is trusted code executed outside the sandbox), and this plugin's bundle line is the "enable" action itself; only install plugins you trust.


dsh integration notes (how the pluginized harness loads it)

dsh uses the Cordis plugin framework, and the composition unit is a bundle: an npm package + a patch layer. The loading chain is:

package.json(dsh.bundle.patch → ./cordis.patch.yml)
  └─ one line in cordis.patch.yml: name: 'doctrove/bridge'
       └─ src/bridge/plugin.js(Cordis plugin, inject: ['tools'])
            ├─ spawns src/entry.js with Node itself(MCP server subprocess, stdio)
            ├─ completes the initialize / tools/list handshake
            └─ registers each tool as mcp__doctrove__<toolname> into ctx.tools
  • Tool interface: the model-visible tool names are mcp__<serverName>__<raw tool name>, serverName defaults to doctrove;
  • Events/skills: this plugin registers no events or skills; it exposes capabilities only through the ctx.tools tool interface (read-only tools, no side effects);
  • Lifecycle: the handshake and registration happen during the plugin's apply; on unload the subprocess is killed and all tools are deregistered automatically (registered via ctx.effect cleanup, no leftovers after hot reload/unload);
  • Two bridge options: the self-built bridge doctrove/bridge bundled with this package (zero-dependency, works out of the box) and the official @deepseek-ai/dsh-mcp-client config line (see examples/overlay-for-dsh.yml.example); tool naming and behavior are identical — pick either one, don't enable both;
  • Environment variables: dsh filters credential-like variables from MCP subprocess environments; the self-built bridge's subprocess inherits the host environment, so DOCTROVE_INDEX_URL and similar pass through, and can also be set explicitly with env: on the bridge line.

Common dsh issues

SymptomFix
Tools missing from the listCheck that the cordis.patch.yml line took effect (dsh --profile <name> --dump-config to inspect layers), confirm no startup log errors
Want a remote indexConfigure args: ['--index-url', 'https://your-index-url'] on the bridge line (directory root), or env: { DOCTROVE_INDEX_URL: '...' }
Want looser cachingConfigure args: ['--cache-ttl', '3600'] on the bridge line; use --no-cache for testing/debugging
pnpm >=10 rejects git-installed prepare scriptsThis plugin is pure JS with no build script, so it is not affected; install from checkout or tarball

Tool list (3 tools, all read-only)

ToolPurposeMain parameters
catalog_lookupSearch the doc catalog by name/description, return candidates with scores and hit signalsquery (required), limit
catalog_releasesList available and recommended versions of an entryid (required)
doc_extractExtract doc snippets for an entry/version/focus (relevance-ranked)id (required), version, focus, maxSections

catalog_lookup

Search the doc catalog. When unsure of a library's canonical id, call this first, then use the returned id with doc_extract.

// request
{ "query": "express", "limit": 5 }
// response (structuredContent summary)
{
  "results": [{
    "id": "express", "name": "Express", "summary": "Minimal web framework for Node.js",
    "score": 1.0, "matches": ["exact name match"],
    "versions": ["5.1.0", "4.21.2"], "latest": "5.1.0", "source": "local:.../data/index.json"
  }],
  "total": 1, "sources": ["local:.../data/index.json"]
}

catalog_releases

View an entry's version list and recommended version, useful for checking whether a target version is available (doc_extract supports the same version syntax).

{ "id": "express" }
// → { "id": "express", "name": "Express", "latest": "5.1.0",
//     "versions": ["5.1.0", "4.21.2"], "source": "local:..." }

doc_extract

Extract documentation. focus describes one concept at a time (e.g. "route parameters"); split cross-concept questions into multiple calls to avoid diluted results; version defaults to the latest stable release.

{ "id": "express", "version": "5", "focus": "wildcard" }
// → {
//     "id": "express", "name": "Express", "version": "5.1.0",
//     "releaseKind": "prefix", "releaseNote": "prefix match 5.x → latest 5.x release",
//     "sections": [{ "heading": "Wildcard routes", "score": 0.5, "matches": ["heading hit: 1 word"], ... }],
//     "source": "local:..."
//   }

Errors are always structured isError results, with error.code taking one of: validation / not-found / version / network / timeout / internal, and message carrying actionable hints (e.g. candidate versions when the requested one is unavailable). Parameter-validation failures are likewise folded into isError (rather than the protocol-level -32602), so the model sees a structured error code in one call and can self-correct.


Scoring and ranking algorithm

Entry retrieval (catalog_lookup)

Score = signal-tier score + popularity fine-tuning, both capped at 1.0:

SignalBase scoreNotes
Exact name match (case-insensitive)1.0name or id exactly equals the query
Exact alias match0.95e.g. query expressjs hits an alias
Name prefix match0.90e.g. query expr
Alias prefix match0.85
Name token overlap0.60–0.83proportional to hit tokens; ceiling deliberately below the alias-prefix tier to keep tier order invariant
Summary token overlap0.30–0.50when the name is completely unrelated
  • Popularity fine-tuning = (1 − raw) × min(0.1, log₁₀(popularity)/100), applied only within the headroom of the current signal tier, so "exact > alias > prefix > token overlap" can never be inverted by popularity;
  • Tokenization: English by word, Chinese per character (space-less languages);
  • Ties are broken by popularity, descending (stable sort).

Doc snippet ranking (doc_extract focus)

  • Snippet score = (2 × heading hit words + body hit words) / (2 × query words);
  • Heading hits count double the body; zero-hit snippets are filtered out; truncated past maxSections;
  • Without focus, snippets return in the index's original order.

Version selection (catalog_releases / doc_extract version)

latest / default → latest stable release (or latest prerelease when no stable exists); exact version → unique match (a v/V prefix is tolerated; build metadata such as +build.2 does not participate in comparison); prefix (5 / 5.1 / 5.1.x / 5.1.*) → latest release matching the prefix; prerelease identifiers compare per semver rules (rc.10 > rc.9); no match → version error with a candidate list attached.


Caching strategy

  • One TTL + LRU in-memory cache per process (default 256 entries, 600 s lifetime), caching: remote index fetches and query results; the local index itself is parsed once per process (static data);
  • TTL is configurable: --cache-ttl <sec> (0–86400, 0 = disabled), --no-cache is a shortcut for disabled;
  • Failure cooldown (negative caching): after a remote index fetch fails, a 30-second cooldown kicks in during which the plugin falls back to local and does not repeat the network request (avoiding a timeout wait on every query while the source is down); after the cooldown it retries automatically and heals itself once the source recovers. The cooldown timing is independent of the cache TTL (a --cache-ttl shorter than the cooldown does not cut it short); it does not apply under --no-cache / --cache-ttl 0 (every failure then really retries);
  • LRU evicts by access order; cache stats (hits/misses/evictions) are printed to stderr at exit with --debug;
  • Local-index cold start is free (synchronous read); after the first remote fetch, all queries hit the cache.

Offline mode and remote index

Offline mode (default)

Without --index-url the plugin is fully offline: it uses the built-in data/index.json (3 demo entries: Express 5.1/4.21 dual versions, Zod 3.24/3.23, Day.js 1.11, including a version-difference demo). The built-in index can be replaced with your own (--local-index <path>), see the format below.

Remote index

The index is an open JSON format hostable on any static HTTP service (GitHub Pages, object storage, intranet file servers all work):

index URL(--index-url / DOCTROVE_INDEX_URL,the URL of the directory containing index.json)
   └─ <url>/index.json   ← fetched by the plugin along this path

Minimal local hosting (zero dependencies, supports ETag conditional requests; by default listens on the local loopback only — change host yourself to expose on LAN):

node scripts/serve-index.mjs [dir] [port]   # default ./data, port 8730
node src/entry.js --index-url http://localhost:8730

Relationship between remote and local: remote first, local as fallback. When the remote fetch fails (offline/timeout/non-2xx/invalid format), the plugin degrades to the local index and keeps serving; every entry and result carries a source tag so the model can judge data freshness.

Index format specification

{
  "format": "doctrove-index@1",          // required, versioned format identifier
  "updatedAt": "2026-08-16T00:00:00.000Z",
  "entries": [{
    "id": "express",                      // required, canonical id (globally unique)
    "name": "Express",                    // required, display name
    "summary": "Minimal web framework for Node.js",
    "aliases": ["expressjs"],             // search aliases (array of strings)
    "homepage": "https://expressjs.com",
    "popularity": 1200,                   // popularity weight (scoring fine-tuning)
    "versions": ["5.1.0", "4.21.2"],      // required, available versions
    "volumes": {                          // required, version → documentation volume
      "5.1.0": {
        "summary": "highlights of this version (optional)",
        "sections": [{                    // required, doc snippets (elements must be non-array objects)
          "heading": "Route handlers",    // snippet title (2x ranking weight)
          "path": "https://expressjs.com/en/5x/api.html#app.METHOD",  // provenance link (optional)
          "body": "snippet body (may include code examples)"
        }]
      }
    }
  }]
}

Validation rules: format must be doctrove-index@1; entries must be an array; id/name non-empty and id unique; aliases must be an array of strings; every version in versions must have a matching volumes volume, and a volume's sections must be a valid array of objects. Invalid indexes are rejected (remote sources report network and degrade to local; local sources report config and exit).


Configuration reference

Precedence: command line > environment variables > config file > defaults.

SettingCLIEnvironment variableConfig file keyDefault
Remote index URL--index-url <url>DOCTROVE_INDEX_URLindexUrlnone (offline)
Local index path--local-index <path>DOCTROVE_LOCAL_INDEXlocalIndexbuilt-in data/index.json
Cache lifetime (s)--cache-ttl <sec> / --no-cacheDOCTROVE_CACHE_TTLcacheTtl600
Remote timeout (ms)--timeout-ms <ms>DOCTROVE_TIMEOUT_MStimeoutMs15000
Debug logging--debugDOCTROVE_DEBUGdebugfalse
Config file--config <path>DOCTROVE_CONFIGnone

The config file is JSON (example: examples/doctrove.config.example.json). All configuration is read-only: the plugin performs no writes and persists no local state. Empty-string environment variables count as unset (defaults apply); cacheTtl: 0 is a valid value (cache disabled).


Testing

node --test        # 105 cases: scoring/versions/cache/config/JSON-RPC/engine/e2e/index hosting

Coverage: scoring-ranking boundaries (tier order can never be inverted by popularity), version selection (latest/exact/prefix/prerelease/ build metadata), multi-source merge and degradation self-healing, failure cooldown, cache TTL/LRU, config precedence plus invalid values and empty strings, MCP protocol (uninitialized gate, version negotiation, error folding, conflicting messages), subprocess-level end-to-end (handshake + 3 tools + error paths + graceful exit), index hosting (ETag/304/traversal protection/symlink escape/malformed encodings).


Directory structure

src/
  entry.js            CLI entry: config → assembly → stdio MCP session
  core/               config (layered config), errors (unified error model), version
  vault/ttl.js        TTL + LRU in-memory cache
  catalog/            scoring (scoring/ranking), releases (version selection), store (catalog hub)
  supply/provider.js  data sources: LocalSource / RemoteSource + index validation
  protocol/           jsonrpc / engine (MCP session engine) / transport (stdio line protocol)
  tools/              registry (registry + parameter validation), definitions (3 tools)
  bridge/             plugin.js (dsh Cordis plugin), client.js (MCP stdio client)
data/index.json       built-in offline index (demo data, replaceable)
scripts/serve-index.mjs  zero-dependency index hosting script
test/                 105 test cases

License

MIT (see LICENSE).