Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/mcp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: MCP server

on:
pull_request:
paths:
- 'tools/mcp/**'
workflow_call: {}

permissions:
contents: read

jobs:
test:
name: Python tests (${{ matrix.python-version }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.13']
defaults:
run:
working-directory: tools/mcp
steps:
- name: Checkout current PR
uses: actions/checkout@v7
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: tools/mcp/pyproject.toml
- name: Install with dev extras
run: pip install -e '.[dev]'
- name: Run pytest
run: pytest
- name: Run the stdio smoke test
run: python smoke_test.py
1 change: 1 addition & 0 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ exclude:
- Rakefile
- README_WRITING.markdown
- README.markdown
- tools
- util
- vendor
- WORKFLOW.md
Expand Down
9 changes: 9 additions & 0 deletions tools/mcp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.venv/
.sample-corpus/
__pycache__/
.pytest_cache/
*.egg-info/
dist/
build/
.coverage
htmlcov/
164 changes: 164 additions & 0 deletions tools/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# OpenVox Docs MCP server

[![MCP server](https://github.com/OpenVoxProject/openvox-docs/actions/workflows/mcp.yml/badge.svg)](https://github.com/OpenVoxProject/openvox-docs/actions/workflows/mcp.yml)

A local [Model Context Protocol](https://modelcontextprotocol.io/) server that
exposes the OpenVox documentation to MCP-aware tools (Claude Code, Cursor, Claude
Desktop, …) so an assistant can search and read the docs in-workflow.

It is the local, self-hosted counterpart to a hosted "Ask AI" widget: no third
party, no API key, no usage quota, and queries never leave your machine.

![Claude Code answering a question by calling the openvox-docs MCP server](demo/demo.gif)

> Recorded with [VHS](https://github.com/charmbracelet/vhs) from
> [`demo/demo.tape`](demo/demo.tape); regenerate with `vhs tools/mcp/demo/demo.tape`.

## How it works

The server's corpus is the two machine-readable files the docs site publishes
(see the repository README's "LLM-friendly documentation files" section):

- [`llms.txt`](https://docs.openvoxproject.org/llms.txt) — the index of pages,
grouped by project.
- [`llms-full.txt`](https://docs.openvoxproject.org/llms-full.txt) — the full
text of every current ("latest") page, including the generated reference pages
(configuration, function, type, man pages).

On first use it fetches both from `https://docs.openvoxproject.org`, caches them
under `~/.cache/openvox-docs-mcp/` with conditional requests (so refreshes are
cheap and it still works offline from the last copy), parses them into one
document per page, and builds a BM25 keyword index.

## Tools

| Tool | Purpose |
|------|---------|
| `list_projects` | List the documentation projects (openvox, openvox-server, …). |
| `list_docs(project?)` | List pages as `{project, title, url}`, optionally filtered to one project. |
| `search_docs(query, project?, limit=5)` | BM25 keyword search; returns `{title, url, project, score, snippet}`. |
| `get_doc(ref, max_chars=40000)` | Full text of one page, resolved by URL, URL path, or exact title. The body is capped at `max_chars` (the single-page function/type references are very large); raise it to fetch more. |
| `refresh_corpus()` | Force a re-fetch of the llms files and rebuild the index. |

## Install

Requires Python 3.10+.

```console
cd tools/mcp
python -m venv .venv && . .venv/bin/activate
pip install -e .
```

## Register with an MCP client

All clients launch the same stdio entry point. Use the **absolute path** to the
installed executable — `tools/mcp/.venv/bin/openvox-docs-mcp` after the install
above (substitute your checkout path). Each client's config also accepts an `env`
block if you want to set the variables from [Configuration](#configuration)
(for example `OPENVOX_DOCS_SOURCE`).

### Claude Code

```console
claude mcp add openvox-docs -- /path/to/tools/mcp/.venv/bin/openvox-docs-mcp
```

Or add it to a project-scoped `.mcp.json`:

```json
{
"mcpServers": {
"openvox-docs": {
"command": "/path/to/tools/mcp/.venv/bin/openvox-docs-mcp"
}
}
}
```

### Cursor

Add it to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project). Cursor
uses the same `mcpServers` schema as Claude Code:

```json
{
"mcpServers": {
"openvox-docs": {
"command": "/path/to/tools/mcp/.venv/bin/openvox-docs-mcp"
}
}
}
```

### GitHub Copilot (VS Code)

Add it to `.vscode/mcp.json` in your workspace (or run **MCP: Add Server** from
the Command Palette). VS Code uses a top-level `servers` key, and stdio is the
default for a `command`:

```json
{
"servers": {
"openvox-docs": {
"type": "stdio",
"command": "/path/to/tools/mcp/.venv/bin/openvox-docs-mcp"
}
}
}
```

Or from the command line:

```console
code --add-mcp '{"name":"openvox-docs","command":"/path/to/tools/mcp/.venv/bin/openvox-docs-mcp"}'
```

### Codex CLI

Add it to `~/.codex/config.toml` (or a project-scoped `.codex/config.toml`).
Codex uses a `mcp_servers` TOML table:

```toml
[mcp_servers.openvox-docs]
command = "/path/to/tools/mcp/.venv/bin/openvox-docs-mcp"
```

## Testing

Install the dev extras, then run the suite:

```console
pip install -e '.[dev]'
pytest
```

`pytest` reports coverage and fails under 90% (configured in `pyproject.toml`).
The unit tests cover corpus parsing, remote fetch/caching (offline fallback and
304 reuse), BM25 ranking, and every tool. For an end-to-end check that launches
the server over stdio and exercises all five tools against a throwaway corpus
(network- and model-free):

```console
python smoke_test.py
```

## Configuration

The server is configured entirely through environment variables:

| Variable | Default | Effect |
|----------|---------|--------|
| `OPENVOX_DOCS_BASE_URL` | `https://docs.openvoxproject.org` | Site to fetch the llms files from. |
| `OPENVOX_DOCS_SOURCE` | _(unset)_ | Read `llms.txt` / `llms-full.txt` from a local directory instead of fetching. Point it at a Jekyll `_site/` build for offline or pre-release use. |

For example, to run against a local build of this repo:

```console
bundle exec jekyll build
OPENVOX_DOCS_SOURCE="$PWD/_site" openvox-docs-mcp
```

> **Note:** until the `llms.txt` / `llms-full.txt` feature is deployed to the live
> site, set `OPENVOX_DOCS_SOURCE` to a local `_site/` build — the live URLs will
> 404 otherwise.
Binary file added tools/mcp/demo/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions tools/mcp/demo/demo.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# VHS tape: the OpenVox docs MCP server used live from Claude Code.
#
# Prereqs:
# - vhs installed (https://github.com/charmbracelet/vhs)
# - the server registered with Claude Code (see ../README.md), e.g. pointing at
# a local corpus via OPENVOX_DOCS_SOURCE
# - the mcp__openvox-docs__* tools allowed (so the run isn't blocked on a
# permission prompt)
#
# Render from the repo root:
# vhs tools/mcp/demo/demo.tape

Output tools/mcp/demo/demo.gif

Require claude

Set Shell zsh
Set FontSize 16
Set Width 1200
Set Height 860
Set Padding 18
Set Theme "Catppuccin Mocha"
Set Framerate 12
Set TypingSpeed 50ms

# Quietly land in the repo root with a clean screen.
Hide
Type "cd /Users/michaelharp/projects/openvox-docs && clear"
Enter
Sleep 1s
Show

Type "claude"
Sleep 1s
Enter
Sleep 8s

Type@35ms "Using the openvox-docs MCP server's search_docs tool, find the OpenVoxDB PostgreSQL configuration page. Reply with only the page title and URL on one line."
Sleep 1s
Enter

# Wait for the model to call search_docs and write its (one-line) answer.
Sleep 35s

# Hold on the finished answer.
Sleep 3s
5 changes: 5 additions & 0 deletions tools/mcp/openvox_docs_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Local MCP server exposing the OpenVox documentation corpus."""

from .server import main, mcp

__all__ = ["main", "mcp"]
4 changes: 4 additions & 0 deletions tools/mcp/openvox_docs_mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .server import main

if __name__ == "__main__":
main()
Loading
Loading