cfunklabs-rag-react-docs 0.1.3__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.
- {cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/PKG-INFO +63 -19
- {cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/README.md +62 -18
- {cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/pyproject.toml +1 -1
- {cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/src/rag_react_docs/__init__.py +1 -1
- {cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/src/rag_react_docs/config.py +4 -0
- cfunklabs_rag_react_docs-0.1.4/src/rag_react_docs/server.py +138 -0
- cfunklabs_rag_react_docs-0.1.3/src/rag_react_docs/server.py +0 -82
- {cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/.gitignore +0 -0
- {cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/src/rag_react_docs/datastore.py +0 -0
- {cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/src/rag_react_docs/retrieval.py +0 -0
- {cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/src/rag_react_docs/source_label.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cfunklabs-rag-react-docs
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
@@ -150,12 +150,19 @@ collection.
|
|
|
150
150
|
|
|
151
151
|
In addition to the CLI, the retrieval pipeline is exposed as an [MCP](https://modelcontextprotocol.io/)
|
|
152
152
|
server over stdio, so MCP clients (Cursor, Claude Desktop, etc.) can pull grounding context
|
|
153
|
-
directly.
|
|
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:
|
|
154
157
|
|
|
155
|
-
- `
|
|
158
|
+
- `search_react_docs(question, k?)` — embeds the question with the same model used at ingestion,
|
|
156
159
|
retrieves the most similar chunks from ChromaDB, and returns each chunk's `source` label,
|
|
157
160
|
`content`, and retrieval `distance`. The client LLM generates the answer from those chunks,
|
|
158
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.
|
|
159
166
|
|
|
160
167
|
#### For end users (published package)
|
|
161
168
|
|
|
@@ -195,35 +202,72 @@ press Ctrl+C to stop.
|
|
|
195
202
|
Run `uv run main.py` first — the dev server needs a populated collection. For interactive
|
|
196
203
|
testing, launch the MCP Inspector with `uv run mcp dev mcp_server.py`.
|
|
197
204
|
|
|
198
|
-
###
|
|
205
|
+
### Releasing
|
|
199
206
|
|
|
200
207
|
The published package (`cfunklabs-rag-react-docs`) contains only the retrieval + MCP server
|
|
201
208
|
(the import package `rag_react_docs` under `src/`). Ingestion/query tooling and `src/utils/*`
|
|
202
209
|
are dev-only and excluded from the wheel.
|
|
203
210
|
|
|
204
|
-
|
|
205
|
-
Release). They version independently — the index version is pinned as `INDEX_VERSION` in
|
|
206
|
-
[src/rag_react_docs/config.py](src/rag_react_docs/config.py).
|
|
211
|
+
A release involves two independently-versioned artifacts:
|
|
207
212
|
|
|
208
|
-
|
|
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:
|
|
209
234
|
|
|
210
235
|
```bash
|
|
236
|
+
uv run main.py # repopulate rag_datastore (needs docs fetched + ANTHROPIC_API_KEY)
|
|
211
237
|
uv run scripts/build_index_archive.py
|
|
212
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
|
|
213
239
|
```
|
|
214
240
|
|
|
215
|
-
|
|
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/`.
|
|
216
253
|
|
|
217
254
|
```bash
|
|
218
|
-
uv build
|
|
219
|
-
uv publish --publish-url https://test.pypi.org/legacy/ # TestPyPI dry run
|
|
220
|
-
uv publish # PyPI
|
|
255
|
+
uv build
|
|
221
256
|
```
|
|
222
257
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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.
|
|
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
|
-
- `
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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.
|
{cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/src/rag_react_docs/config.py
RENAMED
|
@@ -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 = (
|
|
@@ -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,82 +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, INDEX_URL
|
|
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 _index_error() -> str | None:
|
|
23
|
-
"""Return a human-readable reason the index is unavailable, or None if it's ready.
|
|
24
|
-
|
|
25
|
-
Distinguishes a real load/download failure (surfacing the underlying exception and the
|
|
26
|
-
URL it tried) from a genuinely empty collection, so callers report the actual cause rather
|
|
27
|
-
than a catch-all "index is empty" message.
|
|
28
|
-
"""
|
|
29
|
-
try:
|
|
30
|
-
count = get_rag_collection().count()
|
|
31
|
-
except Exception as exc:
|
|
32
|
-
return (
|
|
33
|
-
f"Could not load the documentation index (downloaded from {INDEX_URL}): "
|
|
34
|
-
f"{type(exc).__name__}: {exc}"
|
|
35
|
-
)
|
|
36
|
-
if count == 0:
|
|
37
|
-
return "The documentation index loaded but contains no documents."
|
|
38
|
-
return None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@mcp.tool()
|
|
42
|
-
def search_docs(question: str, k: int = DEFAULT_TOP_K) -> list[dict]:
|
|
43
|
-
"""Search the indexed React documentation and return the most relevant chunks.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
question: A natural-language question to search the React docs for.
|
|
47
|
-
k: How many chunks to return (defaults to `DEFAULT_TOP_K`).
|
|
48
|
-
|
|
49
|
-
Returns a list of results ordered by relevance. Each item has:
|
|
50
|
-
- source: a human-readable provenance label (file path > heading path)
|
|
51
|
-
- content: the raw chunk text to ground an answer on
|
|
52
|
-
- distance: the retrieval distance (lower is more similar)
|
|
53
|
-
"""
|
|
54
|
-
error = _index_error()
|
|
55
|
-
if error:
|
|
56
|
-
print(f"[rag-react-docs] {error}", file=sys.stderr)
|
|
57
|
-
return [{"source": "rag-react-docs", "content": error, "distance": None}]
|
|
58
|
-
|
|
59
|
-
return retrieve_chunks(question, k)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def main() -> None:
|
|
63
|
-
"""Console-script entry point: start the MCP server on stdio."""
|
|
64
|
-
# Human-facing messages must go to stderr: the stdio transport reserves stdout for the
|
|
65
|
-
# JSON-RPC protocol, so anything printed there would corrupt the stream.
|
|
66
|
-
print(f"[rag-react-docs] MCP server starting on stdio (top_k={DEFAULT_TOP_K}).", file=sys.stderr)
|
|
67
|
-
print("[rag-react-docs] Ensuring documentation index is available...", file=sys.stderr)
|
|
68
|
-
error = _index_error()
|
|
69
|
-
if error:
|
|
70
|
-
print(f"[rag-react-docs] Warning: {error}", file=sys.stderr)
|
|
71
|
-
else:
|
|
72
|
-
print("[rag-react-docs] Index ready.", file=sys.stderr)
|
|
73
|
-
|
|
74
|
-
print("[rag-react-docs] Ready. Press Ctrl+C to stop.", file=sys.stderr)
|
|
75
|
-
try:
|
|
76
|
-
mcp.run()
|
|
77
|
-
except KeyboardInterrupt:
|
|
78
|
-
print("\n[rag-react-docs] Shutting down.", file=sys.stderr)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if __name__ == "__main__":
|
|
82
|
-
main()
|
|
File without changes
|
{cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/src/rag_react_docs/datastore.py
RENAMED
|
File without changes
|
{cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/src/rag_react_docs/retrieval.py
RENAMED
|
File without changes
|
{cfunklabs_rag_react_docs-0.1.3 → cfunklabs_rag_react_docs-0.1.4}/src/rag_react_docs/source_label.py
RENAMED
|
File without changes
|