A structure-aware chapter search MCP server.
Project description
chapter-mcp
A structure-aware chapter search MCP server and Python library.
chapter-mcp indexes only the folders you opt into, either via CLI --path
flags or a project-local .chapter-mcp/config.json, and returns structured
chapter results instead of raw line matches.
How chapters are created:
- Markdown files are split into heading sections
- Python files are split into top-level functions and classes
- Other text files are split into paragraphs
Result shape:
- chapter-returning tools group results by
categoryand thenfile - chapter entries are compact and omit
typeby default - chapter entries use
name,start_line,end_line, plus optionalcontent,snippet, orscore
The project is intentionally simple:
- search uses SQLite FTS5
- results are deterministic
- library mode and MCP mode share the same core implementation
- there is no built-in vector or semantic search
Tools
search(query, category?, limit=5, offset=0, include_snippet=False)- general FTS5 search over chapter names and content, returning grouped chapter references and optional snippets
search_chapter(query, category?, limit=5, offset=0)- FTS5 search over chapter names only, returning grouped chapter references
read_search(query, category?, offset=0)- reads the full chapter content for the ranked search match
read_chapter(chapter_name, file?, category?, count=5, offset=0, content_offset=0, content_limit?)- reads chapter content by exact chapter name, optionally sliced by content lines
list_chapters(category?, file?, count=5, offset=0)- lists grouped chapter names and line ranges without content
list_chapters_as_columns(category?, file?, count=5, offset=0, fields?)- lists chapters grouped by file, with compact rows ordered exactly like
columns
- lists chapters grouped by file, with compact rows ordered exactly like
list_files(category?, limit=100, offset=0)- lists indexed files and their metadata
stats()- returns index totals and category status
reindex(category?)- refreshes changed files
Install
uv sync
Usage
Pass --path one or more times to choose folders. Each folder basename becomes
the category:
uv run chapter-mcp --path docs
uv run chapter-mcp --path docs --path examples
Use category=path to set the category name explicitly:
uv run chapter-mcp --path knowledge=.serena/memories
If you prefer project-local config, add .chapter-mcp/config.json:
{
"paths": [
"code=src",
"tests=tests",
"readme=README.md"
]
}
Then run:
uv run chapter-mcp
Supported config fields:
rootoptional, defaults to the current working directorydboptional, defaults to<root>/.chapter-mcp/index.sqlite3pathsrequired when using configwatchoptional, defaults totruewatch_intervaloptional, defaults to1.0sync_startupoptional, defaults totrue
CLI flags override project config when both are present.
By default the server:
- uses the current working directory as the root
- stores the SQLite index at
.chapter-mcp/index.sqlite3 - runs startup indexing before accepting MCP connections
- watches configured folders for changes
Useful flags:
uv run chapter-mcp --path docs
uv run chapter-mcp --path docs --no-watch
uv run chapter-mcp --path docs --no-sync-startup
uv run chapter-mcp --path docs --watch-interval 0.5
Search Modes
Use search when you want general chapter lookup by content or title and only need compact references:
search("json content-type header")search("routing config")search("Find files by extension")search("json content-type header", include_snippet=True)
Use search_chapter when you want to find chapters by title only:
search_chapter("Find")search_chapter("Introduction")search_chapter("Routing")
search_chapter is useful when you know the section name or command/page title
you are looking for and want to avoid content-only matches.
Example grouped result shape:
{
"count": 2,
"results": [
{
"category": "docs",
"files": [
{
"file": "docs/curl.md",
"chapters": [
{
"name": "curl",
"start_line": 1,
"end_line": 8,
"snippet": "Use curl when sending a JSON Content-Type header..."
}
]
}
]
}
]
}
Partial chapter reads:
content_offsetskips that many lines from the beginning of the stored chapter contentcontent_limitreturns at most that many content lines after the offset- slicing is line-based within chapter content, not by absolute file line numbers
Example:
{
"count": 1,
"results": [
{
"category": "code",
"files": [
{
"file": "src/chapter_mcp/index.py",
"chapters": [
{
"name": "read_chapter",
"start_line": 373,
"end_line": 414,
"content": "def read_chapter(\n self,",
"content_offset": 0,
"content_total_lines": 12,
"content_truncated": true
}
]
}
]
}
]
}
Column mode for high-volume listing:
- allowed
fields:name,start_line,end_line - default
fields:name,start_line,end_line - results stay grouped by
categoryandfile - this costs a small amount of extra JSON overhead compared to a fully flat table
- the grouping is intentional because it avoids repeating file paths per row and keeps follow-up reads easier
Example:
{
"results": [
{
"category": "code",
"files": [
{
"file": "src/chapter_mcp/chunks.py",
"columns": ["name", "start_line", "end_line"],
"rows": [
["Chunk", 9, 15]
]
}
]
}
],
"truncated": false
}
Example with explicit fields:
{
"results": [
{
"category": "code",
"files": [
{
"file": "src/chapter_mcp/chunks.py",
"columns": ["name"],
"rows": [
["Chunk"]
]
}
]
}
],
"truncated": false
}
Use read_search when you want to open the best full chapter match directly:
read_search("json content-type header")read_search("Routing")read_search("Find files by extension", category="common")
Library Usage
from pathlib import Path
from chapter_mcp import ChapterIndex
index = ChapterIndex(
Path.cwd(),
Path(".chapter-mcp/index.sqlite3"),
category_paths=["docs", "examples"],
)
index.reindex()
print(index.search("install"))
print(index.search_chapter("Guide"))
print(index.read_search("install"))
index.close()
MCP Usage
uv run chapter-mcp \
--path instructions \
--path knowledge \
--sync-startup
Then call tools such as:
list_files()list_chapters()list_chapters_as_columns(fields=["name"])search("config handling", category="instructions", limit=3)search_chapter("Style guide", category="instructions", limit=3)read_search("config handling", category="instructions")read_chapter("Style guide", file="instructions/style-guide.md")read_chapter("Style guide", file="instructions/style-guide.md", content_offset=0, content_limit=20)
Workspace MCP config:
.codex/config.tomlconfigures the server for project-local Codex usage..chapter-mcp/config.jsondefines what a given project indexes..mcp.jsoncan stay generic and only describe how to launch the server.
Codex Setup
If project-local .codex/config.toml works in your Codex environment, prefer that.
If your Codex surface only reliably loads global MCP config, you can still keep folder selection project-specific by using one generic global server entry and storing the actual index configuration in each repo's .chapter-mcp/config.json.
Example global Codex config:
[mcp_servers.chapter-mcp]
command = "/opt/homebrew/bin/uv"
args = [
"run",
"python",
"-m",
"chapter_mcp",
"--root",
".",
]
cwd = "."
startup_timeout_sec = 20
required = false
With that setup, chapter-mcp starts in the current workspace and reads per-project index settings from .chapter-mcp/config.json automatically when no --path flags are passed directly.
Example project .chapter-mcp/config.json:
{
"paths": [
"code=src",
"tests=tests",
"readme=README.md"
]
}
Limitations
chapter-mcp is deliberately FTS5-only.
That means:
- literal phrasing matters more than with vector search
- broad conceptual queries may need better wording
- unrelated wording will not be matched semantically
- it does not do nearest-neighbor retrieval or semantic ranking
This tradeoff is intentional: the project favors simple, fast, stable chapter lookup over more complex semantic retrieval behavior.
If You Need Vector Search
If you actually need semantic/vector retrieval, evaluate a dedicated tool such
as txtai separately.
That can make sense when:
- users ask fuzzy conceptual questions
- wording often differs a lot from the indexed source text
- you want a real RAG or semantic retrieval workflow
chapter-mcp intentionally does not try to solve that problem.
MCP Inspector Example
npx @modelcontextprotocol/inspector \
uv run chapter-mcp \
--root /tmp/chapter-mcp-tldr \
--path common=pages/common \
--path instructions \
--sync-startup
Then in the Inspector Tools tab try:
stats()list_files()search("json content-type header", category="common", limit=3)search_chapter("curl", category="common", limit=3)read_search("json content-type header", category="common")
Optional TLDR Real-World Test
The repository does not commit the TLDR archive. To run the optional real-world test locally:
curl -L https://github.com/tldr-pages/tldr/archive/refs/heads/main.zip -o /tmp/tldr-main.zip
CHAPTER_MCP_TLDR_ZIP=/tmp/tldr-main.zip uv run pytest
If CHAPTER_MCP_TLDR_ZIP is not set, the TLDR-based test is skipped.
Development
uv run pytest
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chapter_mcp-0.1.0.tar.gz.
File metadata
- Download URL: chapter_mcp-0.1.0.tar.gz
- Upload date:
- Size: 126.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63f754a62beefcb15c99f6ea13eae33debd262543bf05aebfaa05ca8efd1cef9
|
|
| MD5 |
083ec8fd5e1d5928a27e04fb56f9d374
|
|
| BLAKE2b-256 |
d56a142e70f3b1d36b1aff71aaf396e6213a1d2db0dca701c44a8c0f7c91ca7a
|
Provenance
The following attestation bundles were made for chapter_mcp-0.1.0.tar.gz:
Publisher:
publish.yml on marcomq/chapter-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chapter_mcp-0.1.0.tar.gz -
Subject digest:
63f754a62beefcb15c99f6ea13eae33debd262543bf05aebfaa05ca8efd1cef9 - Sigstore transparency entry: 1630644895
- Sigstore integration time:
-
Permalink:
marcomq/chapter-mcp@aed5eaed46627866abe96d2a4a97eb8937278d7c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/marcomq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@aed5eaed46627866abe96d2a4a97eb8937278d7c -
Trigger Event:
release
-
Statement type:
File details
Details for the file chapter_mcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: chapter_mcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d01b8a882a252609b1acf62a4a43dc72b30daf293607b00f53a9f476d75193c0
|
|
| MD5 |
b591dc1bf398331bdc39fce626f7eb82
|
|
| BLAKE2b-256 |
00c6f5e93ee547e76578adabdf77d631dbbdcdc50322187366327cde50ba1f50
|
Provenance
The following attestation bundles were made for chapter_mcp-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on marcomq/chapter-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chapter_mcp-0.1.0-py3-none-any.whl -
Subject digest:
d01b8a882a252609b1acf62a4a43dc72b30daf293607b00f53a9f476d75193c0 - Sigstore transparency entry: 1630644925
- Sigstore integration time:
-
Permalink:
marcomq/chapter-mcp@aed5eaed46627866abe96d2a4a97eb8937278d7c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/marcomq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@aed5eaed46627866abe96d2a4a97eb8937278d7c -
Trigger Event:
release
-
Statement type: