No description
  • TypeScript 97.5%
  • JavaScript 2.5%
Find a file
Philip Washington Sorst bd91195cbb
All checks were successful
CI / ci (push) Successful in 10m2s
Latest artifact / latest (push) Successful in 14m42s
feat(sync): implement stat-based fast-path to skip unchanged files during sync
2026-08-18 01:52:37 +02:00
.forgejo/workflows ci: switch to pnpm/setup action for streamlined workflow management 2026-08-18 00:34:24 +02:00
docs feat(sync): implement stat-based fast-path to skip unchanged files during sync 2026-08-18 01:52:37 +02:00
src feat(sync): implement stat-based fast-path to skip unchanged files during sync 2026-08-18 01:52:37 +02:00
tests feat(sync): implement stat-based fast-path to skip unchanged files during sync 2026-08-18 01:52:37 +02:00
.gitignore feat(cli): add command-line interface for project configuration and indexing 2026-08-18 01:01:24 +02:00
AGENTS.md feat(sync): implement stat-based fast-path to skip unchanged files during sync 2026-08-18 01:52:37 +02:00
package.json initial commit 2026-08-17 23:36:23 +02:00
pnpm-lock.yaml initial commit 2026-08-17 23:36:23 +02:00
pnpm-workspace.yaml initial commit 2026-08-17 23:36:23 +02:00
README.md feat(cli): add command-line interface for project configuration and indexing 2026-08-18 01:01:24 +02:00
tsconfig.json initial commit 2026-08-17 23:36:23 +02:00
tsconfig.test.json initial commit 2026-08-17 23:36:23 +02:00
vitest.config.ts initial commit 2026-08-17 23:36:23 +02:00

project-docs.mcp

An MCP stdio server that makes a single project's documentation searchable through RAG. It indexes the project's README, standard root files, and everything under docs/, and exposes one tool: search_project_docs. The index lives inside the project at .cache/project-docs/index.sqlite (gitignored, derived data) and re-syncs itself on every call — editing a doc file simply makes the next search see it.

  • Embeddings: qwen/qwen3-embedding-4b via OpenRouter (no huge local model downloads).
  • Retrieval: hybrid vector search (sqlite-vec) + FTS5 keyword search, fused with Reciprocal Rank Fusion.
  • Reranking: qwen/qwen3-reranker-8b via OpenRouter, on by default, with graceful fallback to RRF ordering when the API fails.

Requirements

  • Node >= 24
  • OPENROUTER_API_KEY environment variable with real credits: embeddings cost ~$0.02/M input tokens and reranking is billed per request. Only changed files are ever re-embedded.

Running

Configure the server in your MCP client (e.g. for Claude Desktop or Cursor). The exact configuration depends on how the server is installed:

Installed as a package

With the package installed (npm i -g project-docs.mcp or listed in your client's package_manager), the project-docs-mcp binary is on PATH. If you build from this repo, a never-versioned tarball of the latest main is published to a moving latest release — With the package installed (npm i -g project-docs.mcp or listed in your client's package_manager), the project-docs-mcp binary is on PATH. If you build from this repo, a never-versioned tarball of the latest main is published to a moving latest release — download the artifact and install it directly with curl -fL --retry 5 -o /tmp/project-docs-mcp.tgz https://git.sorst.net/philipsorst/project-docs.mcp/releases/download/latest/project-docs-mcp.tgz && npm install -g /tmp/project-docs-mcp.tgz.

{
    "mcpServers": {
        "project-docs": {
            "command": "project-docs-mcp",
            "args": ["--project", "/absolute/path/to/your-repo"],
            "env": {"OPENROUTER_API_KEY": "sk-or-..."}
        }
    }
}

IntelliJ IDEA

IntelliJ launches MCP servers with the opened project's directory as the working directory, so --project can be omitted entirely — just the workspace being the project dir is sufficient. Keep .mcp.json in the project IntelliJ has open (it is auto-detected; you can also add it via Tools → MCP Servers → Add Server).

Installed as a package

With the package installed (npm i -g project-docs.mcp), the project-docs-mcp binary is on PATH and no arguments are needed:

{
    "mcpServers": {
        "project-docs": {
            "command": "project-docs-mcp",
            "env": {"OPENROUTER_API_KEY": "sk-or-..."}
        }
    }
}

From a repo checkout

If you use this repository directly as a checkout, build it once (pnpm install && pnpm build) and point the command at the built dist/index.js:

{
    "mcpServers": {
        "project-docs": {
            "command": "node",
            "args": [
                "/path/to/project-docs.mcp/dist/index.js"
            ],
            "env": {"OPENROUTER_API_KEY": "sk-or-..."}
        }
    }
}

Passing --project /absolute/path/to/your-repo explicitly works too and wins over the working directory.

Note (checkout only): the checkout must be built (pnpm build) — dist/index.js is gitignored — and node must be >= 24 and on PATH for IntelliJ's process launcher.

Options

Option Description
--project <path> Root of the project to index. Defaults to the current working directory — with an MCP client that launches the server with the project dir as cwd (e.g. IntelliJ), it can be omitted.
--db <path> Index database path. Defaults to <project>/.cache/project-docs/index.sqlite. Delete it to force a full reindex.
--no-rerank Disable the rerank stage (pure RRF ordering).
--help, -h Usage.

What gets indexed

  • Top-level standard files, matched case-insensitively: README.md, CONTRIBUTING.md, CHANGELOG.md, CODE_OF_CONDUCT.md, SECURITY.md, SUPPORT.md, CODEOWNERS. LICENSE is intentionally skipped.
  • The top-level docs/ directory (case-insensitive), recursively, for: markdown/text-like extensions .md, .markdown, .mdx, .rst, .txt, .adoc.

Hidden files/directories, node_modules, symlinks, binary content, and files over 1 MiB are skipped silently.

Tool: search_project_docs

{
    "query": "how do I configure the retry budget?",
    "limit": 5,
    "max_chars": 6000
}

Each call:

  1. Syncs the index from disk: new/changed files are chunked and re-embedded (batches of ≤16, 2 retries with backoff), removed files are dropped. Unchanged files cost nothing.
  2. Runs hybrid vector + FTS5 search fused with RRF, reranks the top candidates, and groups matching sections by file.
  3. Returns results bounded by the max_chars budget, with more_sections flags so an agent knows when a full file is worth opening itself.

Example response:

{
    "status": {
        "project": "/path/to/project",
        "indexed_files": 14,
        "files_added": 1,
        "files_updated": 0,
        "files_removed": 0,
        "chunks": 47,
        "model": "qwen/qwen3-embedding-4b",
        "reranker": "qwen/qwen3-reranker-8b",
        "db": "/path/to/project/.cache/project-docs/index.sqlite"
    },
    "results": [
        {
            "doc_id": "docs/configuration.md",
            "title": "docs/configuration.md",
            "score": 0.0328,
            "sections": [
                {
                    "heading": "Configuration > Retries",
                    "content": "The retry budget is configured with RETRY_LIMIT...\n\n[... truncated ...]"
                }
            ],
            "more_sections": true,
            "total_sections": 4,
            "shown_sections": 1
        }
    ]
}

Validation: an empty/whitespace-only query is rejected (isError: true), and a missing OPENROUTER_API_KEY fails loudly on first use rather than silently degrading.

Development

See AGENTS.md for the engineering contract. Quick start:

pnpm install
pnpm typecheck && pnpm typecheck:test && pnpm build && pnpm test
# live smoke tests against the real API (manual only):
OPENROUTER_API_KEY=... pnpm test -- tests/live.test.ts

Limitations

  • One project per server process (pass the path at startup); run multiple server instances for multiple projects.
  • The cache is a derived artifact kept in .cache/project-docs/ (gitignored): if embedding model constants change, the server refuses to open an old index (embedding model mismatch) until you delete index.sqlite.
  • No document-level ACLs or multi-user support — it mirrors your repo's file permissions.