Docs MCP server: search and cited answers from your own docs
Serene Docs Search is a documentation search engine on SereneDB: it turns your docs into one index that a search box, Ask AI and a docs MCP server for agents all read. It pulls Markdown, HTML or PDF from a Git repo, a folder, a website or an S3-compatible bucket. Full-text BM25 needs no model; hybrid search, cited answers and the MCP server are switches you turn on.
npm install @serenedb/docs-search-react@latestStart with BM25 and no model. Turn on hybrid, Ask AI or MCP when you need them
Serene Docs Search ranks heading sections with BM25 while the reader types, with stemming, stopwords, synonyms, a typo pass and highlighted snippets, and no model at all. Hybrid adds an embedding per section and fuses both rankings with RRF. Ask AI and the MCP server read the same index. The results below come from serenedb.com's own docs index.
create ind
- 1Sql › Statements › CREATE INDEXCREATE INDEXCREATE INDEX The CREATE INDEX statement constructs an index on the specified column(s) of the specified table.bm25
- 2Sql › IndexesCREATE INDEXCREATE INDEXbm25
- 3Sql › Statements › CREATE INDEXCREATE INDEXbm25
- 4The State of Serene, July 2026CREATE INDEX stopped blocking your writesCREATE INDEX ... USING inverted(...) no longer blocks writes. Inserts, updates and deletes run against the table for the whole build and the index…bm25
- 5Sql › IndexesCREATE INDEX and DROP INDEXCREATE INDEX DROP INDEX Indexes are created with CREATE INDEX and removed with DROP INDEX. The index type is selected with the USING clause (USING…bm25
- 1Sql › Statements › CREATE INDEXCREATE INDEXCREATE INDEX The CREATE INDEX statement constructs an index on the specified column(s) of the specified table.bm25 · rrf
- 2Sql › IndexesCREATE INDEXCREATE INDEXbm25 · rrf
- 3Sql › Statements › CREATE INDEXCREATE INDEXbm25 · rrf
- 4The State of Serene, July 2026CREATE INDEX stopped blocking your writesCREATE INDEX ... USING inverted(...) no longer blocks writes. Inserts, updates and deletes run against the table for the whole build and the index…bm25 · rrf
- 5Sql › IndexesCREATE INDEX and DROP INDEXCREATE INDEX DROP INDEX Indexes are created with CREATE INDEX and removed with DROP INDEX. The index type is selected with the USING clause (USING…bm25 · rrf
serch synonims
- 1Sql › Functions › Search › Tokenizers › expand_solr_synonymsexpand_solr_synonymsThe expand_solr_synonyms template expands tokens through a synonyms map written in Apache Solr synonyms-file format, supplied inline via the required…bm25
- 2Cookbook › Search › SynonymsSynonyms…laptop under notebook. CREATE TEXT SEARCH DICTIONARY syn AS split_text(case := 'lower') | expand_solr_synonyms('tv, television, telly laptop =>…bm25
- 3Sql › Functions › Search › Tokenizer FunctionsFunctionsEach signature lists the value first and then the template's options with their defaults; a required option has none. Option semantics, constraints…bm25
- 4Sql › Functions › Search › Tokenizers › generate_shinglesPhrase searchts_phrase, phraseto_tsquery, the quoted phrases of to_tsquery and ## work on a shingle column, and a phrase the column indexes as one shingle is a…bm25
- 5Sql › Functions › Search › Full-Text Search Functionsts_phrase(text[, gap, text, ...][, slop := N])ts_phrase(text[, gap, text, ...][, slop := N]) Match a run of tokens in their indexed order, optionally separated by token gaps. Parameter · Type ·…bm25
- 1Sql › Functions › Search › Tokenizers › expand_solr_synonymsexpand_solr_synonymsThe expand_solr_synonyms template expands tokens through a synonyms map written in Apache Solr synonyms-file format, supplied inline via the required…bm25 · rrf
- 2Cookbook › Search › SynonymsSynonyms…laptop under notebook. CREATE TEXT SEARCH DICTIONARY syn AS split_text(case := 'lower') | expand_solr_synonyms('tv, television, telly laptop =>…bm25 · rrf
- 3Sql › Functions › Search › Tokenizer FunctionsFunctionsEach signature lists the value first and then the template's options with their defaults; a required option has none. Option semantics, constraints…bm25 · rrf
- 4Sql › Functions › Search › Tokenizers › generate_shinglesPhrase searchts_phrase, phraseto_tsquery, the quoted phrases of to_tsquery and ## work on a shingle column, and a phrase the column indexes as one shingle is a…bm25 · rrf
- 5Sql › Functions › Search › Full-Text Search Functionsts_phrase(text[, gap, text, ...][, slop := N])ts_phrase(text[, gap, text, ...][, slop := N]) Match a run of tokens in their indexed order, optionally separated by token gaps. Parameter · Type ·…bm25 · rrf
make new rows searchable sooner
- 1Cookbook › Search › Search over Iceberg: insert → searchableSearch over Iceberg: insert → searchable…is the source of truth; SereneDB keeps a derived search index over it. Any engine — Spark, BigQuery, Flink, SereneDB itself — writes rows to the…bm25
- 2Cookbook › Search › One Search BoxUpdates stay searchableBecause the catch-all is a stored generated column, an UPDATE to any source column recomputes it and the index follows — no reindex step, no drift.…bm25
- 3Sql › Functions › Bitstring Functionsset_bit(bitstring, index, new_value)set_bit(bitstring, index, new_value) Sets the nth bit in bitstring to newvalue; the first (leftmost) bit is indexed 0. Returns a new bitstring.…bm25
- 4Sql › SQL IntroductionCreating a New TableYou can create a new table by specifying the table name, along with all column names and their types: CREATE TABLE weather ( city VARCHAR, temp_lo…bm25
- 5The State of Serene, September 2026More SQL and a new parser…biggest update yet, about 1,200 changes in one go. New SQL you can use today: MATCH_RECOGNIZE, row pattern matching from SQL:2016. Sessions, funnels…bm25
- 1The State of Serene, September 2026More SQL and a new parser…biggest update yet, about 1,200 changes in one go. New SQL you can use today: MATCH_RECOGNIZE, row pattern matching from SQL:2016. Sessions, funnels…bm25 · rrf
- 2Clients › Langchain serenedb › FAQ and TroubleshootingWhy don't rows I just added show up in search results?SereneDB's inverted index is eventually consistent. A row written to the base table is invisible to full-text queries, and to vector queries routed…AI suggested
- 3Sql › Statements › VACUUMRefreshing — REFRESH_*REFRESH_* Inverted indexes are eventually consistent: rows you INSERT, UPDATE or DELETE may not be visible to queries until the index is refreshed.…AI suggested
- 4Cookbook › Search › Search over Iceberg: insert → searchableSearch over Iceberg: insert → searchable…is the source of truth; SereneDB keeps a derived search index over it. Any engine — Spark, BigQuery, Flink, SereneDB itself — writes rows to the…bm25 · rrf
- 5Cookbook › Search › One Search BoxUpdates stay searchableBecause the catch-all is a stored generated column, an UPDATE to any source column recomputes it and the index follows — no reindex step, no drift.…bm25 · rrf
In SereneDB, the CREATE INDEX statement constructs an index on specified column(s) or expressions of a table [1].
… For full-text, vector, or geospatial search, you can specify USING inverted [1, 5].
In SereneDB, text search synonyms are configured using text search dictionaries that combine a tokenizer with a synonym expansion filter in a pipeline [2].
… expand_solr_synonyms: Expands surface words based on inline rules in Apache Solr format [1, 2].
- [1] expand_solr_synonyms
- [2] Synonyms
In SereneDB, inverted indexes are eventually consistent [2, 3].
… Use VACUUM (REFRESH_TABLE) or VACUUM (REFRESH_INDEX) to publish pending writes to readers [3, 6].
- [2] Why don't rows I just added show up in search results?
- [3] Refreshing — REFRESH_*
- [6] Background intervals
claude mcp add --transport http serenedb-docs https://api.serenedb.com/mcp
From a Git repo to one SereneDB index, three ways to read it
Serene Docs Search runs as two containers: SereneDB and the search backend. The backend pulls your source, splits pages into heading sections, writes them into one inverted index and serves /v1/search and /v1/ask. The widget, Ask AI and the optional MCP service (port 7710) all go through it. Hover a stage, or a column inside the index, to find its line.
Markdown in a repository
The backend watches the branch with a cheap ls-remote and syncs on a new commit. Markdown splits at headings h1–h4 by default, so a hit links to its section.
A live site
The crawler starts at a URL and follows the sitemap and a link depth. HTML is scoped with CSS selectors such as article or main, and URL mapping points results at the published pages.
A folder or an S3-compatible bucket
Formats: Markdown and MDX by default, plus HTML, reStructuredText, plain text, Jupyter notebooks and PDF when you list them. A custom endpoint reaches R2 or MinIO, and bucket credentials stay in the backend's environment.
Two containers
The configurator generates serene-search.config.json and a docker-compose.yml with a random SERENE_SEARCH_TOKEN for the admin routes. Search, health and Ask AI stay public, so the website bundle carries no secret.
One inverted index per corpus
BM25 (k1 1.2, b 0.4) over the heading, the heading trail and the section body, with an analyzer that lower-cases, drops stopwords, splits code identifiers, applies Solr-format synonyms when the config lists them, and stems. Hybrid mode adds an ivf column with cosine distance.
A React component or one script tag
Search as you type: complete words must match and the last one counts as a prefix. Enter jumps to the section anchor. Below, the statement the backend runs for “create ind” and its three rows.
Answers with citations
The model gets a seeded search, then calls search_docs and read_section itself for up to four rounds and writes a final answer with [n] citations. Answer and embedding providers are set independently: any OpenAI-compatible API or Ollama.
The same index for agents
An optional container serves MCP over Streamable HTTP on port 7710. It calls the backend's public API, so agents see what readers see.
Four switches, one index
run one, two, three or all fourPrefix on the last word, stemming, synonyms, a typo pass with did you mean, snippets.
One embedding per section, fused with BM25 by weighted reciprocal rank fusion.
A seeded search, up to four rounds of tool calls, [n] citations, multi-turn.
search_docs, read_section and docs_health over Streamable HTTP.
From a Git repo to ⌘K in five steps
Configure, start two containers, let the first sync build the index, then add the widget. Steps 3 and 4 are the SQL the backend runs inside SereneDB, run on SereneDB 26.11.1 on 2026-11-03.
{
"version": 1,
"source": { "type": "git", "url": "https://github.com/acme/docs",
"branch": "main", "subdir": ["docs"] },
"content": {
"extensions": [".md", ".mdx"],
"markdown": { "mode": "split" }
},
"search": { "type": "fulltext" },
"sync": { "mode": "commits", "interval": "1h" }
}docker compose up -d curl http://localhost:7700/v1/health
CREATE TEXT SEARCH DICTIONARY serene_docs_sections_dict AS split_text(case := 'lower') | remove_stopwords(['the', 'a', 'an', 'this', 'these', 'those']) | split_text_csv('_') | expand_solr_synonyms('db, database k8s => kubernetes') | stem_words('en') WITH (frequency, position, norm); SELECT ts_lexize('serene_docs_sections_dict', 'Creating DB indexes on k8s'); -- {creat,databas,db,index,on,kubernet} CREATE TABLE serene_docs_sections ( id VARCHAR PRIMARY KEY, url VARCHAR, title VARCHAR, trail VARCHAR, body VARCHAR, embedding FLOAT[4]); CREATE INDEX serene_docs_sections_idx ON serene_docs_sections USING inverted (id, title serene_docs_sections_dict, trail serene_docs_sections_dict, body serene_docs_sections_dict, embedding ivf (metric = 'cosine')) INCLUDE (url) WITH (optimize_top_k = 'bm25(1.2, 0.4)');
SELECT title, url, round(BM25(serene_docs_sections_idx.tableoid, 1.2, 0.4), 2) AS score FROM serene_docs_sections_idx WHERE (title @@ (plainto_tsquery('create') ^ 2) OR trail @@ plainto_tsquery('create') OR body @@ plainto_tsquery('create')) AND (title @@ (ts_starts_with('ind') ^ 2) OR trail @@ ts_starts_with('ind') OR body @@ ts_starts_with('ind')) ORDER BY score DESC, id;
npm install @serenedb/docs-search-react@latest import { SereneDocsSearch } from "@serenedb/docs-search-react"; import "@serenedb/docs-search-react/styles.css"; <SereneDocsSearch backendUrl="https://search.example.com" />
What teams build on it
Open-source project docs
Markdown in a Git repository, split at headings, synced on each commit. Full-text BM25 needs no model and no API key, and the widget gives readers ⌘K with section links.
Full-text search →SELECT title, url, round(BM25(serene_docs_sections_idx.tableoid, 1.2, 0.4), 2) AS score FROM serene_docs_sections_idx WHERE (title @@ (plainto_tsquery('create') ^ 2) OR trail @@ plainto_tsquery('create') OR body @@ plainto_tsquery('create')) AND (title @@ (ts_starts_with('ind') ^ 2) OR trail @@ ts_starts_with('ind') OR body @@ ts_starts_with('ind')) ORDER BY score DESC, id;
| title | url | score |
|---|---|---|
| CREATE INDEX | /docs/sql/statements/create_index#create-index | 5.99 |
| CREATE INDEX … USING inverted | /docs/sql/statements/create_index/inverted | 5.85 |
| CREATE INDEX stopped blocking your writes | /blog/state-of-serene-2026-07#create-index-stopped-blocking-your-writes | 4.21 |
Internal knowledge base
Exported pages in a folder or an S3-compatible bucket: Markdown, HTML, PDF. Hybrid search catches the question asked in other words, and Ask AI answers from the indexed sections with citations.
RAG database →WITH fused AS ( SELECT id, RANK() OVER (ORDER BY s DESC) AS rank, 1.0 AS w, 1 AS is_lex FROM (SELECT id, BM25(serene_docs_sections_idx.tableoid, 1.2, 0.4) AS s FROM serene_docs_sections_idx WHERE title @@ plainto_tsquery('new rows missing search') OR body @@ (plainto_tsquery('new') || plainto_tsquery('rows') || plainto_tsquery('missing')) ORDER BY s DESC LIMIT 50) lex UNION ALL SELECT id, RANK() OVER (ORDER BY dist) AS rank, 0.7 AS w, 0 AS is_lex FROM (SELECT id, cosine_distance(embedding, [0.25, 0.92, 0.1, 0.05]::FLOAT[4]) AS dist FROM serene_docs_sections_idx WHERE embedding IS NOT NULL ORDER BY dist LIMIT 50) vec ), ranked AS ( SELECT id, SUM(w / (60 + rank)) AS rrf, MAX(is_lex) AS lex_hit FROM fused GROUP BY id ORDER BY rrf DESC, id LIMIT 4 ) SELECT t.title, round(r.rrf, 4) AS rrf, CASE WHEN r.lex_hit = 0 THEN 'AI suggested' ELSE 'bm25 + vector' END AS badge FROM ranked r JOIN serene_docs_sections t ON t.id = r.id ORDER BY r.rrf DESC, t.id;
| title | rrf | badge |
|---|---|---|
| Why don't rows I just added show up in search results? | 0.0277 | bm25 + vector |
| Refreshing — REFRESH_* | 0.0276 | bm25 + vector |
| CREATE INDEX stopped blocking your writes | 0.0111 | AI suggested |
| CREATE INDEX … USING inverted | 0.0109 | AI suggested |
Docs for AI agents over MCP
The optional MCP service gives Claude Code, Codex, Cursor or your own agent three tools over the same index. Try it on SereneDB's own docs first.
Database for AI agents →claude mcp add --transport http serenedb-docs https://api.serenedb.com/mcp
| tool | arguments | returns |
|---|---|---|
| search_docs | query, limit | numbered sections: title, url, snippet |
| read_section | url | the docs page that holds the section |
| docs_health | – | connectivity, index size, search type |
A docs MCP server lets an AI agent search and read your documentation as tools. Serene Docs Search's optional MCP service exposes search_docs, read_section and docs_health over the same index the search box uses.
A search engine for a product's or project's documentation: it indexes the docs and returns the sections that answer a query, ranked, while the reader types. Serene Docs Search splits Markdown, HTML or PDF pages into heading sections, ranks them by BM25 in one SereneDB index and returns each match with a snippet and a link to its heading. Hybrid search, Ask AI with cited answers and the docs MCP server read the same index.
Run the MCP service, then claude mcp add --transport http <name> https://<host>/mcp or codex mcp add <name> --url https://<host>/mcp. To try it on SereneDB's own docs: claude mcp add --transport http serenedb-docs https://api.serenedb.com/mcp.
Yes. A Git source can be limited to subdirectories, content.exclude drops globs, and HTML can be scoped with CSS selectors. The MCP service reads only what the backend indexed.
Serene Docs Search is Apache-2.0 and self-hosted: the widget, the backend, the index and any model keys stay on your infrastructure, and your own backend indexes your repo, folder, site or bucket. The source is on GitHub.
serenedb.com/docs, a Docusaurus site, runs it: its search box is this widget over the site's own index. The widget is a React component or a single script tag that renders into any HTML page, and the backend indexes either the Markdown sources or the built HTML, so no generator-specific plugin is involved.
No. Full-text BM25 runs with no model. Hybrid search needs an embeddings provider (OpenAI-compatible or Ollama), and Ask AI needs a chat model; each is a separate switch.
Git, a mounted folder, a website crawl with sitemap, or an S3-compatible bucket (R2 and MinIO through a custom endpoint). Formats: Markdown and MDX by default, plus HTML, reStructuredText, plain text, Jupyter notebooks and PDF when you list them.
Git sources check the branch for a new commit, any source can be polled on an interval, and CI can call POST /v1/reindex. Snapshots hash every section, so unchanged sections are skipped and deleted ones pruned. Inside SereneDB, the backend publishes each sync with VACUUM (REFRESH_TABLE).
From sections of your index: the model searches and reads them through tools and cites them as [n]. The default instruction tells it to answer from the indexed docs only and to say when it is unsure. See RAG database for retrieval for LLM answers in general.
Get started with SereneDB
Open the configurator, start two containers and add the widget. Full-text search needs no model and no API key.