cfunklabs-rag-react-docs 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfunklabs-rag-react-docs
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Retrieval-only MCP server over the indexed React documentation, with a prebuilt index downloaded on first run.
5
5
  Project-URL: Homepage, https://github.com/cfunklabs/rag-react-docs
6
6
  Project-URL: Repository, https://github.com/cfunklabs/rag-react-docs
@@ -13,6 +13,7 @@ Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Topic :: Software Development :: Documentation
15
15
  Requires-Python: >=3.14
16
+ Requires-Dist: certifi>=2024.0.0
16
17
  Requires-Dist: chromadb>=1.5.9
17
18
  Requires-Dist: mcp>=1.28.1
18
19
  Requires-Dist: platformdirs>=4.0.0
@@ -149,12 +150,19 @@ collection.
149
150
 
150
151
  In addition to the CLI, the retrieval pipeline is exposed as an [MCP](https://modelcontextprotocol.io/)
151
152
  server over stdio, so MCP clients (Cursor, Claude Desktop, etc.) can pull grounding context
152
- directly. It exposes a single **retrieval-only** tool:
153
+ directly. The server ships prescriptive metadata — a server-level instructions block plus a
154
+ richly documented tool — so a consuming LLM knows when to reach for it (any React 19.2 API,
155
+ hook, component, or pattern question) instead of relying on its own possibly-stale knowledge.
156
+ It exposes a single **retrieval-only** tool:
153
157
 
154
- - `search_docs(question, k?)` — embeds the question with the same model used at ingestion,
158
+ - `search_react_docs(question, k?)` — embeds the question with the same model used at ingestion,
155
159
  retrieves the most similar chunks from ChromaDB, and returns each chunk's `source` label,
156
160
  `content`, and retrieval `distance`. The client LLM generates the answer from those chunks,
157
161
  so no Anthropic key is needed to run the server.
162
+ - `question` should be a full natural-language question, not bare keywords.
163
+ - `k` defaults to `RAG_TOP_K` (5); use ~3 for a specific API lookup and ~8-10 for broad topics.
164
+ - `distance` is squared L2 over normalized embeddings, so **lower is more similar**. For this
165
+ corpus, `< ~1.0` is relevant and `> ~1.5` usually means off-topic / not covered.
158
166
 
159
167
  #### For end users (published package)
160
168
 
@@ -194,35 +202,72 @@ press Ctrl+C to stop.
194
202
  Run `uv run main.py` first — the dev server needs a populated collection. For interactive
195
203
  testing, launch the MCP Inspector with `uv run mcp dev mcp_server.py`.
196
204
 
197
- ### Publishing to PyPI
205
+ ### Releasing
198
206
 
199
207
  The published package (`cfunklabs-rag-react-docs`) contains only the retrieval + MCP server
200
208
  (the import package `rag_react_docs` under `src/`). Ingestion/query tooling and `src/utils/*`
201
209
  are dev-only and excluded from the wheel.
202
210
 
203
- Two artifacts get published: the Python package (to PyPI) and the prebuilt index (to a GitHub
204
- Release). They version independently — the index version is pinned as `INDEX_VERSION` in
205
- [src/rag_react_docs/config.py](src/rag_react_docs/config.py).
211
+ A release involves two independently-versioned artifacts:
206
212
 
207
- 1. Build and upload the index archive (after `uv run main.py` has populated `rag_datastore`):
213
+ - The **PyPI package**, published **automatically** by CI when you push a `v*` git tag. The
214
+ [.github/workflows/publish.yml](../.github/workflows/publish.yml) workflow builds the wheel/sdist
215
+ and uploads them to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/)
216
+ (OIDC) — no API tokens or secrets are involved.
217
+ - The **prebuilt index**, uploaded **manually** to a GitHub Release, and only when the corpus
218
+ changes. Its version is `INDEX_VERSION` in [src/rag_react_docs/config.py](src/rag_react_docs/config.py),
219
+ independent of the package version.
220
+
221
+ The `v*` tag drives PyPI and the `index-*` tag drives the index download; they never trigger each
222
+ other (the workflow filters `v*`).
223
+
224
+ #### Release checklist
225
+
226
+ **Step 1 - Increment the package version.** Bump the version in BOTH
227
+ [pyproject.toml](pyproject.toml) (`version`) and
228
+ [src/rag_react_docs/__init__.py](src/rag_react_docs/__init__.py) (`__version__`), keeping them in
229
+ sync. PyPI rejects re-uploads of an existing version, so this must change every release.
230
+
231
+ **Step 2 - Build and upload the index (only if the docs, chunking, or embeddings changed).** Most
232
+ code-only releases skip this step. If the index content changed, bump `REACT_VERSION` and/or
233
+ `INDEX_REVISION` in [src/rag_react_docs/config.py](src/rag_react_docs/config.py) first, then:
208
234
 
209
235
  ```bash
236
+ uv run main.py # repopulate rag_datastore (needs docs fetched + ANTHROPIC_API_KEY)
210
237
  uv run scripts/build_index_archive.py
211
238
  gh release create index-19-2-v1 dist/rag-index-19-2-v1.tar.gz dist/rag-index-19-2-v1.tar.gz.sha256
212
239
  ```
213
240
 
214
- 2. Build and publish the package (test on TestPyPI first):
241
+ Upload the index **before** publishing the package release, so the new package's `INDEX_URL`
242
+ resolves for end users on first run.
243
+
244
+ The index version follows the standard `index-<react-version>-v<incremental>` (e.g.
245
+ `index-19-2-v1`), composed from `REACT_VERSION` and `INDEX_REVISION`. Bump `REACT_VERSION` when
246
+ re-fetching the docs for a new React release, and bump `INDEX_REVISION` for re-chunk or
247
+ embedding-model changes within the same React version. Either bump changes the release tag/asset
248
+ name and the client cache path, so clients pull a fresh, compatible index instead of reusing a
249
+ stale cache.
250
+
251
+ **Step 3 - (Optional) Local build sanity check.** This verifies the wheel/sdist build; it is not
252
+ the publish mechanism (CI builds too). Artifacts land in the gitignored `dist/`.
215
253
 
216
254
  ```bash
217
- uv build # -> dist/ wheel + sdist (only rag_react_docs)
218
- uv publish --publish-url https://test.pypi.org/legacy/ # TestPyPI dry run
219
- uv publish # PyPI
255
+ uv build
220
256
  ```
221
257
 
222
- The index version follows the standard `index-<react-version>-v<incremental>` (e.g.
223
- `index-19-2-v1`), composed in [src/rag_react_docs/config.py](src/rag_react_docs/config.py) from
224
- `REACT_VERSION` and `INDEX_REVISION`. Bump `REACT_VERSION` when re-fetching the docs for a new
225
- React release, and bump `INDEX_REVISION` for re-chunk or embedding-model changes within the same
226
- React version. Either bump changes the release tag/asset name and cache path, so clients pull a
227
- fresh, compatible index instead of reusing a stale cache — re-release the archive under the new
228
- `index-<react-version>-v<incremental>` tag.
258
+ **Step 4 - Publish by tagging.** Commit the version bump, then tag and push. The `v*` tag triggers
259
+ CI, which builds and publishes to PyPI automatically:
260
+
261
+ ```bash
262
+ git commit -am "Release v0.1.3"
263
+ git tag -a v0.1.3 -m "Release v0.1.3"
264
+ git push origin main --tags
265
+ ```
266
+
267
+ After the workflow finishes, verify the new version at
268
+ [pypi.org/project/cfunklabs-rag-react-docs](https://pypi.org/project/cfunklabs-rag-react-docs/), and
269
+ (if you re-released the index) that the index asset URL returns `200`.
270
+
271
+ > Manual publishing (`uv publish`) is not the standard path: trusted publishing is configured for
272
+ > CI only, so a local upload would require a separate API token and bypass the pinned `pypi`
273
+ > environment. Prefer the tag-driven flow above.
@@ -129,12 +129,19 @@ collection.
129
129
 
130
130
  In addition to the CLI, the retrieval pipeline is exposed as an [MCP](https://modelcontextprotocol.io/)
131
131
  server over stdio, so MCP clients (Cursor, Claude Desktop, etc.) can pull grounding context
132
- directly. It exposes a single **retrieval-only** tool:
132
+ directly. The server ships prescriptive metadata — a server-level instructions block plus a
133
+ richly documented tool — so a consuming LLM knows when to reach for it (any React 19.2 API,
134
+ hook, component, or pattern question) instead of relying on its own possibly-stale knowledge.
135
+ It exposes a single **retrieval-only** tool:
133
136
 
134
- - `search_docs(question, k?)` — embeds the question with the same model used at ingestion,
137
+ - `search_react_docs(question, k?)` — embeds the question with the same model used at ingestion,
135
138
  retrieves the most similar chunks from ChromaDB, and returns each chunk's `source` label,
136
139
  `content`, and retrieval `distance`. The client LLM generates the answer from those chunks,
137
140
  so no Anthropic key is needed to run the server.
141
+ - `question` should be a full natural-language question, not bare keywords.
142
+ - `k` defaults to `RAG_TOP_K` (5); use ~3 for a specific API lookup and ~8-10 for broad topics.
143
+ - `distance` is squared L2 over normalized embeddings, so **lower is more similar**. For this
144
+ corpus, `< ~1.0` is relevant and `> ~1.5` usually means off-topic / not covered.
138
145
 
139
146
  #### For end users (published package)
140
147
 
@@ -174,35 +181,72 @@ press Ctrl+C to stop.
174
181
  Run `uv run main.py` first — the dev server needs a populated collection. For interactive
175
182
  testing, launch the MCP Inspector with `uv run mcp dev mcp_server.py`.
176
183
 
177
- ### Publishing to PyPI
184
+ ### Releasing
178
185
 
179
186
  The published package (`cfunklabs-rag-react-docs`) contains only the retrieval + MCP server
180
187
  (the import package `rag_react_docs` under `src/`). Ingestion/query tooling and `src/utils/*`
181
188
  are dev-only and excluded from the wheel.
182
189
 
183
- Two artifacts get published: the Python package (to PyPI) and the prebuilt index (to a GitHub
184
- Release). They version independently — the index version is pinned as `INDEX_VERSION` in
185
- [src/rag_react_docs/config.py](src/rag_react_docs/config.py).
190
+ A release involves two independently-versioned artifacts:
186
191
 
187
- 1. Build and upload the index archive (after `uv run main.py` has populated `rag_datastore`):
192
+ - The **PyPI package**, published **automatically** by CI when you push a `v*` git tag. The
193
+ [.github/workflows/publish.yml](../.github/workflows/publish.yml) workflow builds the wheel/sdist
194
+ and uploads them to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/)
195
+ (OIDC) — no API tokens or secrets are involved.
196
+ - The **prebuilt index**, uploaded **manually** to a GitHub Release, and only when the corpus
197
+ changes. Its version is `INDEX_VERSION` in [src/rag_react_docs/config.py](src/rag_react_docs/config.py),
198
+ independent of the package version.
199
+
200
+ The `v*` tag drives PyPI and the `index-*` tag drives the index download; they never trigger each
201
+ other (the workflow filters `v*`).
202
+
203
+ #### Release checklist
204
+
205
+ **Step 1 - Increment the package version.** Bump the version in BOTH
206
+ [pyproject.toml](pyproject.toml) (`version`) and
207
+ [src/rag_react_docs/__init__.py](src/rag_react_docs/__init__.py) (`__version__`), keeping them in
208
+ sync. PyPI rejects re-uploads of an existing version, so this must change every release.
209
+
210
+ **Step 2 - Build and upload the index (only if the docs, chunking, or embeddings changed).** Most
211
+ code-only releases skip this step. If the index content changed, bump `REACT_VERSION` and/or
212
+ `INDEX_REVISION` in [src/rag_react_docs/config.py](src/rag_react_docs/config.py) first, then:
188
213
 
189
214
  ```bash
215
+ uv run main.py # repopulate rag_datastore (needs docs fetched + ANTHROPIC_API_KEY)
190
216
  uv run scripts/build_index_archive.py
191
217
  gh release create index-19-2-v1 dist/rag-index-19-2-v1.tar.gz dist/rag-index-19-2-v1.tar.gz.sha256
192
218
  ```
193
219
 
194
- 2. Build and publish the package (test on TestPyPI first):
220
+ Upload the index **before** publishing the package release, so the new package's `INDEX_URL`
221
+ resolves for end users on first run.
222
+
223
+ The index version follows the standard `index-<react-version>-v<incremental>` (e.g.
224
+ `index-19-2-v1`), composed from `REACT_VERSION` and `INDEX_REVISION`. Bump `REACT_VERSION` when
225
+ re-fetching the docs for a new React release, and bump `INDEX_REVISION` for re-chunk or
226
+ embedding-model changes within the same React version. Either bump changes the release tag/asset
227
+ name and the client cache path, so clients pull a fresh, compatible index instead of reusing a
228
+ stale cache.
229
+
230
+ **Step 3 - (Optional) Local build sanity check.** This verifies the wheel/sdist build; it is not
231
+ the publish mechanism (CI builds too). Artifacts land in the gitignored `dist/`.
195
232
 
196
233
  ```bash
197
- uv build # -> dist/ wheel + sdist (only rag_react_docs)
198
- uv publish --publish-url https://test.pypi.org/legacy/ # TestPyPI dry run
199
- uv publish # PyPI
234
+ uv build
200
235
  ```
201
236
 
202
- The index version follows the standard `index-<react-version>-v<incremental>` (e.g.
203
- `index-19-2-v1`), composed in [src/rag_react_docs/config.py](src/rag_react_docs/config.py) from
204
- `REACT_VERSION` and `INDEX_REVISION`. Bump `REACT_VERSION` when re-fetching the docs for a new
205
- React release, and bump `INDEX_REVISION` for re-chunk or embedding-model changes within the same
206
- React version. Either bump changes the release tag/asset name and cache path, so clients pull a
207
- fresh, compatible index instead of reusing a stale cache — re-release the archive under the new
208
- `index-<react-version>-v<incremental>` tag.
237
+ **Step 4 - Publish by tagging.** Commit the version bump, then tag and push. The `v*` tag triggers
238
+ CI, which builds and publishes to PyPI automatically:
239
+
240
+ ```bash
241
+ git commit -am "Release v0.1.3"
242
+ git tag -a v0.1.3 -m "Release v0.1.3"
243
+ git push origin main --tags
244
+ ```
245
+
246
+ After the workflow finishes, verify the new version at
247
+ [pypi.org/project/cfunklabs-rag-react-docs](https://pypi.org/project/cfunklabs-rag-react-docs/), and
248
+ (if you re-released the index) that the index asset URL returns `200`.
249
+
250
+ > Manual publishing (`uv publish`) is not the standard path: trusted publishing is configured for
251
+ > CI only, so a local upload would require a separate API token and bypass the pinned `pypi`
252
+ > environment. Prefer the tag-driven flow above.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cfunklabs-rag-react-docs"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "Retrieval-only MCP server over the indexed React documentation, with a prebuilt index downloaded on first run."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -17,6 +17,7 @@ classifiers = [
17
17
  # Runtime deps for the published wheel: retrieval + MCP server only. The generation/ingestion
18
18
  # stack (langchain, langgraph, anthropic, ...) is dev-only and lives in [dependency-groups].
19
19
  dependencies = [
20
+ "certifi>=2024.0.0",
20
21
  "chromadb>=1.5.9",
21
22
  "mcp>=1.28.1",
22
23
  "platformdirs>=4.0.0",
@@ -5,4 +5,4 @@ ChromaDB index is downloaded from a GitHub Release on first run (see `datastore.
5
5
  users never run the ingestion pipeline themselves.
6
6
  """
7
7
 
8
- __version__ = "0.1.2"
8
+ __version__ = "0.1.4"
@@ -27,6 +27,10 @@ REACT_VERSION = "19-2"
27
27
  INDEX_REVISION = "v1"
28
28
  INDEX_VERSION = f"{REACT_VERSION}-{INDEX_REVISION}"
29
29
 
30
+ # Human-readable React version (e.g. "19.2") for user/LLM-facing strings like the tool
31
+ # description and server instructions. Derived from REACT_VERSION so there's one source of truth.
32
+ REACT_VERSION_LABEL = REACT_VERSION.replace("-", ".")
33
+
30
34
  # The prebuilt index is published as a GitHub Release asset. A sibling `<archive>.sha256` file
31
35
  # is fetched alongside it to verify the download before extraction.
32
36
  _DEFAULT_INDEX_URL = (
@@ -2,18 +2,21 @@
2
2
 
3
3
  The published package ships no vectors: the ~34 MB index lives as a GitHub Release asset and is
4
4
  fetched + cached the first time the server needs it. Subsequent runs read straight from the
5
- cache and work offline. Only the standard library is used for the download so the wheel stays
6
- dependency-light (no httpx/requests).
5
+ cache and work offline. The download uses urllib with an explicit certifi CA bundle so TLS
6
+ verification works even on interpreters that lack a configured system cert store (e.g. the
7
+ python.org macOS framework build), rather than relying on the ambient default SSL context.
7
8
  """
8
9
 
9
10
  import hashlib
10
11
  import os
11
12
  import shutil
13
+ import ssl
12
14
  import tarfile
13
15
  import tempfile
14
16
  import urllib.request
15
17
  from pathlib import Path
16
18
 
19
+ import certifi
17
20
  import chromadb
18
21
 
19
22
  from .config import COLLECTION_NAME, INDEX_URL, datastore_dir
@@ -24,9 +27,14 @@ from .config import COLLECTION_NAME, INDEX_URL, datastore_dir
24
27
  # looks valid on the next run.
25
28
  _MARKER = "chroma.sqlite3"
26
29
 
30
+ # Verify TLS against certifi's CA bundle instead of the interpreter default. Some Python builds
31
+ # (notably python.org macOS framework installs) ship without usable root certificates, which
32
+ # makes the default context fail with CERTIFICATE_VERIFY_FAILED on any HTTPS download.
33
+ _SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
34
+
27
35
 
28
36
  def _download(url: str, dest: Path) -> None:
29
- with urllib.request.urlopen(url) as response, open(dest, "wb") as out:
37
+ with urllib.request.urlopen(url, context=_SSL_CONTEXT) as response, open(dest, "wb") as out:
30
38
  shutil.copyfileobj(response, out)
31
39
 
32
40
 
@@ -46,7 +54,7 @@ def _verify_checksum(archive: Path, url: str) -> None:
46
54
  tolerated (some releases may not publish one) but a present-and-mismatched one is fatal.
47
55
  """
48
56
  try:
49
- with urllib.request.urlopen(url + ".sha256") as response:
57
+ with urllib.request.urlopen(url + ".sha256", context=_SSL_CONTEXT) as response:
50
58
  expected = response.read().decode().strip().split()[0]
51
59
  except Exception:
52
60
  return
@@ -0,0 +1,138 @@
1
+ """MCP server exposing the RAG retrieval pipeline over stdio.
2
+
3
+ Published as the `cfunklabs-rag-react-docs` console script (`uvx cfunklabs-rag-react-docs`). It
4
+ exposes a single `search_react_docs` tool that performs *retrieval only* against the downloaded
5
+ ChromaDB collection and returns the top-k chunks with their source/heading labels. The
6
+ consuming LLM (Cursor, Claude Desktop, etc.) ingests those chunks and generates its own
7
+ grounded answer, so no Anthropic API key or generation stack is needed server-side.
8
+ """
9
+
10
+ import sys
11
+ from typing import TypedDict
12
+
13
+ from mcp.server.fastmcp import FastMCP
14
+ from mcp.types import ToolAnnotations
15
+
16
+ from .config import DEFAULT_TOP_K, INDEX_URL, INDEX_VERSION, REACT_VERSION_LABEL
17
+ from .datastore import get_rag_collection
18
+ from .retrieval import retrieve_chunks
19
+
20
+
21
+ # Surfaced to MCP clients as the server's usage guidance. Kept prescriptive so a consuming LLM
22
+ # reaches for this tool instead of relying on its own (possibly stale) React knowledge.
23
+ SERVER_INSTRUCTIONS = f"""\
24
+ Semantic search over the official React documentation (React {REACT_VERSION_LABEL}, index \
25
+ {INDEX_VERSION}).
26
+
27
+ Use the `search_react_docs` tool whenever a task touches React itself -- hooks, built-in \
28
+ components, APIs, rendering/effects behavior, or idiomatic patterns -- instead of answering \
29
+ from the model's own training data, which may be stale or version-mismatched. It is the \
30
+ authoritative source for React {REACT_VERSION_LABEL} in this session.
31
+
32
+ The server is retrieval-only: `search_react_docs` returns ranked documentation chunks and the \
33
+ client composes the grounded answer. Always cite the `source` label of each chunk you rely on \
34
+ so the user can trace claims back to the docs."""
35
+
36
+
37
+ mcp = FastMCP("rag-react-docs", instructions=SERVER_INSTRUCTIONS)
38
+
39
+
40
+ class SearchResult(TypedDict):
41
+ """One retrieved documentation chunk.
42
+
43
+ - source: human-readable provenance label (file path > heading path) for citation
44
+ - content: the raw chunk text to ground an answer on
45
+ - distance: retrieval distance (squared L2 over normalized embeddings; lower is more similar)
46
+ """
47
+
48
+ source: str
49
+ content: str
50
+ distance: float | None
51
+
52
+
53
+ def _index_error() -> str | None:
54
+ """Return a human-readable reason the index is unavailable, or None if it's ready.
55
+
56
+ Distinguishes a real load/download failure (surfacing the underlying exception and the
57
+ URL it tried) from a genuinely empty collection, so callers report the actual cause rather
58
+ than a catch-all "index is empty" message.
59
+ """
60
+ try:
61
+ count = get_rag_collection().count()
62
+ except Exception as exc:
63
+ return (
64
+ f"Could not load the documentation index (downloaded from {INDEX_URL}): "
65
+ f"{type(exc).__name__}: {exc}"
66
+ )
67
+ if count == 0:
68
+ return "The documentation index loaded but contains no documents."
69
+ return None
70
+
71
+
72
+ # Passed as the tool `description` (an f-string, so version numbers interpolate -- a plain
73
+ # docstring can't). FastMCP uses this over the function docstring when both are present.
74
+ _SEARCH_DESCRIPTION = f"""\
75
+ Semantically search the official React documentation and return the most relevant chunks.
76
+
77
+ When to use: reach for this on ANY question about React itself -- hooks (`useState`, \
78
+ `useEffect`, ...), built-in components, APIs, rendering/effects/StrictMode behavior, migration, \
79
+ or idiomatic patterns. Prefer it over answering from memory: it indexes React \
80
+ {REACT_VERSION_LABEL} (index `{INDEX_VERSION}`), so it is more current and authoritative than \
81
+ the model's own training data. It does NOT cover unrelated topics or third-party libraries.
82
+
83
+ Args:
84
+ question: A full natural-language question, not bare keywords -- richer phrasing retrieves
85
+ better.
86
+ Good: "How do I run cleanup logic when a component unmounts with useEffect?"
87
+ Good: "What's the difference between useMemo and useCallback?"
88
+ Weak: "useEffect" (too terse; ambiguous intent)
89
+ k: How many chunks to return (default {DEFAULT_TOP_K}). Suggested by intent: ~3 for a
90
+ specific API/signature lookup, {DEFAULT_TOP_K} for a general question, ~8-10 for broad
91
+ or exploratory topics that likely span multiple doc pages.
92
+
93
+ Returns a list of results ordered by relevance (closest first). Each item has:
94
+ - source: human-readable provenance label (file path > heading path); cite this.
95
+ - content: the raw chunk text to ground an answer on.
96
+ - distance: retrieval distance -- squared L2 over normalized embeddings, so LOWER is more
97
+ similar. As a rough guide for this corpus: < ~1.0 is relevant, and > ~1.5 usually means
98
+ the question is off-topic or not covered (no strong match). If every result is above
99
+ ~1.5, prefer saying the docs don't cover it over guessing."""
100
+
101
+
102
+ @mcp.tool(
103
+ name="search_react_docs",
104
+ title="Search React Documentation",
105
+ description=_SEARCH_DESCRIPTION,
106
+ annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
107
+ )
108
+ def search_react_docs(question: str, k: int = DEFAULT_TOP_K) -> list[SearchResult]:
109
+ """Retrieve the top-k React-docs chunks for `question` (see tool description for guidance)."""
110
+ error = _index_error()
111
+ if error:
112
+ print(f"[rag-react-docs] {error}", file=sys.stderr)
113
+ return [{"source": "rag-react-docs", "content": error, "distance": None}]
114
+
115
+ return retrieve_chunks(question, k)
116
+
117
+
118
+ def main() -> None:
119
+ """Console-script entry point: start the MCP server on stdio."""
120
+ # Human-facing messages must go to stderr: the stdio transport reserves stdout for the
121
+ # JSON-RPC protocol, so anything printed there would corrupt the stream.
122
+ print(f"[rag-react-docs] MCP server starting on stdio (top_k={DEFAULT_TOP_K}).", file=sys.stderr)
123
+ print("[rag-react-docs] Ensuring documentation index is available...", file=sys.stderr)
124
+ error = _index_error()
125
+ if error:
126
+ print(f"[rag-react-docs] Warning: {error}", file=sys.stderr)
127
+ else:
128
+ print("[rag-react-docs] Index ready.", file=sys.stderr)
129
+
130
+ print("[rag-react-docs] Ready. Press Ctrl+C to stop.", file=sys.stderr)
131
+ try:
132
+ mcp.run()
133
+ except KeyboardInterrupt:
134
+ print("\n[rag-react-docs] Shutting down.", file=sys.stderr)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()
@@ -1,83 +0,0 @@
1
- """MCP server exposing the RAG retrieval pipeline over stdio.
2
-
3
- Published as the `cfunklabs-rag-react-docs` console script (`uvx cfunklabs-rag-react-docs`). It
4
- exposes a single `search_docs` tool that performs *retrieval only* against the downloaded
5
- ChromaDB collection and returns the top-k chunks with their source/heading labels. The
6
- consuming LLM (Cursor, Claude Desktop, etc.) ingests those chunks and generates its own
7
- grounded answer, so no Anthropic API key or generation stack is needed server-side.
8
- """
9
-
10
- import sys
11
-
12
- from mcp.server.fastmcp import FastMCP
13
-
14
- from .config import DEFAULT_TOP_K
15
- from .datastore import get_rag_collection
16
- from .retrieval import retrieve_chunks
17
-
18
-
19
- mcp = FastMCP("rag-react-docs")
20
-
21
-
22
- def _collection_is_empty() -> bool:
23
- try:
24
- return get_rag_collection().count() == 0
25
- except Exception:
26
- # Treat a missing/uninitialized/failed-download collection the same as an empty one.
27
- return True
28
-
29
-
30
- @mcp.tool()
31
- def search_docs(question: str, k: int = DEFAULT_TOP_K) -> list[dict]:
32
- """Search the indexed React documentation and return the most relevant chunks.
33
-
34
- Args:
35
- question: A natural-language question to search the React docs for.
36
- k: How many chunks to return (defaults to `DEFAULT_TOP_K`).
37
-
38
- Returns a list of results ordered by relevance. Each item has:
39
- - source: a human-readable provenance label (file path > heading path)
40
- - content: the raw chunk text to ground an answer on
41
- - distance: the retrieval distance (lower is more similar)
42
- """
43
- if _collection_is_empty():
44
- return [
45
- {
46
- "source": "rag-react-docs",
47
- "content": (
48
- "The documentation index is empty or could not be loaded. "
49
- "Check network access on first run so the index can be downloaded."
50
- ),
51
- "distance": None,
52
- }
53
- ]
54
-
55
- return retrieve_chunks(question, k)
56
-
57
-
58
- def main() -> None:
59
- """Console-script entry point: start the MCP server on stdio."""
60
- # Human-facing messages must go to stderr: the stdio transport reserves stdout for the
61
- # JSON-RPC protocol, so anything printed there would corrupt the stream.
62
- print(f"[rag-react-docs] MCP server starting on stdio (top_k={DEFAULT_TOP_K}).", file=sys.stderr)
63
- print("[rag-react-docs] Ensuring documentation index is available...", file=sys.stderr)
64
- try:
65
- if _collection_is_empty():
66
- print(
67
- "[rag-react-docs] Warning: index empty or unavailable -- check network access.",
68
- file=sys.stderr,
69
- )
70
- else:
71
- print("[rag-react-docs] Index ready.", file=sys.stderr)
72
- except Exception as exc: # pragma: no cover - defensive; _collection_is_empty swallows most
73
- print(f"[rag-react-docs] Warning: could not verify index: {exc}", file=sys.stderr)
74
-
75
- print("[rag-react-docs] Ready. Press Ctrl+C to stop.", file=sys.stderr)
76
- try:
77
- mcp.run()
78
- except KeyboardInterrupt:
79
- print("\n[rag-react-docs] Shutting down.", file=sys.stderr)
80
-
81
-
82
- if __name__ == "__main__":
83
- main()