grounded-rag-mcp 0.1.0

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/chunking.d.ts +17 -0
  4. package/dist/chunking.js +31 -0
  5. package/dist/chunking.js.map +1 -0
  6. package/dist/collection.d.ts +21 -0
  7. package/dist/collection.js +72 -0
  8. package/dist/collection.js.map +1 -0
  9. package/dist/config.d.ts +20 -0
  10. package/dist/config.js +10 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/embeddings.d.ts +26 -0
  13. package/dist/embeddings.js +47 -0
  14. package/dist/embeddings.js.map +1 -0
  15. package/dist/eval.d.ts +19 -0
  16. package/dist/eval.js +42 -0
  17. package/dist/eval.js.map +1 -0
  18. package/dist/grounding.d.ts +13 -0
  19. package/dist/grounding.js +19 -0
  20. package/dist/grounding.js.map +1 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +19 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/ingest.d.ts +12 -0
  25. package/dist/ingest.js +33 -0
  26. package/dist/ingest.js.map +1 -0
  27. package/dist/retrieval/bm25.d.ts +21 -0
  28. package/dist/retrieval/bm25.js +69 -0
  29. package/dist/retrieval/bm25.js.map +1 -0
  30. package/dist/retrieval/fusion.d.ts +27 -0
  31. package/dist/retrieval/fusion.js +70 -0
  32. package/dist/retrieval/fusion.js.map +1 -0
  33. package/dist/server.d.ts +13 -0
  34. package/dist/server.js +170 -0
  35. package/dist/server.js.map +1 -0
  36. package/dist/stores/memory.d.ts +18 -0
  37. package/dist/stores/memory.js +45 -0
  38. package/dist/stores/memory.js.map +1 -0
  39. package/dist/text.d.ts +8 -0
  40. package/dist/text.js +12 -0
  41. package/dist/text.js.map +1 -0
  42. package/dist/version.d.ts +2 -0
  43. package/dist/version.js +3 -0
  44. package/dist/version.js.map +1 -0
  45. package/package.json +54 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chetan C
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # grounded-rag-mcp (TypeScript)
2
+
3
+ An **MCP server that gives any LLM host grounded, cited retrieval over your own documents** — hybrid retrieval (BM25 + dense), reranking-ready, citations, and a built-in eval harness. The TypeScript/Node twin of the [Python package](https://pypi.org/project/grounded-rag-mcp/).
4
+
5
+ > Point it at documents. Your MCP host (Claude Desktop, an IDE, a custom agent) can then `search` and `answer` over them — grounded in the real text, with citations, and an honest "not in the documents" path.
6
+
7
+ ## Why
8
+
9
+ Most RAG-over-MCP examples are toys. This one is built production-flavored:
10
+
11
+ - **Hybrid retrieval** — BM25 (exact terms) + dense (semantics), fused with Reciprocal Rank Fusion.
12
+ - **Grounding + citations** — answers cite sources; if the answer isn't in the docs, it says so.
13
+ - **Built-in eval** — measure retrieval quality (recall@k, MRR, hit-rate), not just vibes.
14
+ - **Zero-dependency default** — a deterministic hashing embedder runs with nothing extra.
15
+ - **Strict TypeScript**, ESM, tested, CI on Node 18/20/22.
16
+
17
+ ## Status
18
+
19
+ Built in public, phase by phase.
20
+
21
+ - [x] Phase 0 — scaffold, packaging, CI
22
+ - [x] Phase 1 — core retrieval (chunk → embed → BM25 + dense → RRF)
23
+ - [x] Phase 2 — MCP server (stdio) with `ingest` / `search`
24
+ - [x] Phase 3 — grounding + `answer` (via MCP sampling)
25
+ - [x] Phase 4 — eval, resource + prompt, docs
26
+ - [ ] Phase 5 — publish to npm
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install grounded-rag-mcp
32
+ # or run without installing:
33
+ npx grounded-rag-mcp
34
+ ```
35
+
36
+ ## Tools
37
+
38
+ | Tool | What it does |
39
+ |---|---|
40
+ | `ingest_documents` | Chunk, embed, and index files or raw text into a named collection |
41
+ | `search` | Hybrid / dense / bm25 retrieval, per-stage scores |
42
+ | `answer` | Grounded, cited answer via MCP sampling; refuses when nothing is found |
43
+ | `list_collections` | List collections and chunk counts |
44
+ | `evaluate_retrieval` | hitRate / mrr / recallAtK on labeled cases |
45
+
46
+ Also exposes a resource (`rag://collections`) and a prompt (`grounded_answer`).
47
+
48
+ ## Use it with an MCP host (e.g. Claude Desktop)
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "grounded-rag": {
54
+ "command": "npx",
55
+ "args": ["-y", "grounded-rag-mcp"]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ npm install
65
+ npm run lint && npm run format:check && npm run typecheck && npm run build && npm test
66
+ ```
67
+
68
+ ## License
69
+
70
+ MIT © Chetan C
@@ -0,0 +1,17 @@
1
+ /** Split documents into overlapping, word-based chunks. */
2
+ import type { ChunkingConfig } from "./config.js";
3
+ /** A retrievable unit of text plus where it came from (for citations). */
4
+ export interface Chunk {
5
+ chunkId: string;
6
+ text: string;
7
+ source: string;
8
+ }
9
+ /**
10
+ * Split one document into overlapping, word-based chunks.
11
+ *
12
+ * Word-based (not character-based) so we never cut a word in half, and overlapping so a
13
+ * fact straddling a boundary still lives wholly inside at least one chunk. Chunk size is
14
+ * the single highest-impact retrieval knob: too large blurs the embedding across topics,
15
+ * too small strips the context needed to answer.
16
+ */
17
+ export declare function chunkDocument(text: string, source: string, config?: ChunkingConfig): Chunk[];
@@ -0,0 +1,31 @@
1
+ /** Split documents into overlapping, word-based chunks. */
2
+ import { DEFAULT_CHUNKING } from "./config.js";
3
+ /**
4
+ * Split one document into overlapping, word-based chunks.
5
+ *
6
+ * Word-based (not character-based) so we never cut a word in half, and overlapping so a
7
+ * fact straddling a boundary still lives wholly inside at least one chunk. Chunk size is
8
+ * the single highest-impact retrieval knob: too large blurs the embedding across topics,
9
+ * too small strips the context needed to answer.
10
+ */
11
+ export function chunkDocument(text, source, config = DEFAULT_CHUNKING) {
12
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
13
+ if (words.length === 0)
14
+ return [];
15
+ const step = Math.max(1, config.size - config.overlap);
16
+ const chunks = [];
17
+ for (let start = 0; start < words.length; start += step) {
18
+ const window = words.slice(start, start + config.size);
19
+ if (window.length === 0)
20
+ break;
21
+ chunks.push({
22
+ chunkId: `${source}#${chunks.length}`,
23
+ text: window.join(" "),
24
+ source,
25
+ });
26
+ if (start + config.size >= words.length)
27
+ break; // reached the end; avoid a trailing dup
28
+ }
29
+ return chunks;
30
+ }
31
+ //# sourceMappingURL=chunking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunking.js","sourceRoot":"","sources":["../src/chunking.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAG3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAS/C;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAY,EACZ,MAAc,EACd,SAAyB,gBAAgB;IAEzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC5D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAElC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IACvD,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,IAAI,EAAE,CAAC;QACxD,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QACvD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM;QAC/B,MAAM,CAAC,IAAI,CAAC;YACV,OAAO,EAAE,GAAG,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE;YACrC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;YACtB,MAAM;SACP,CAAC,CAAC;QACH,IAAI,KAAK,GAAG,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM;YAAE,MAAM,CAAC,wCAAwC;IAC1F,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,21 @@
1
+ /** A Collection ties chunks + embedder + vector store + BM25 into one retrievable unit. */
2
+ import type { Chunk } from "./chunking.js";
3
+ import type { RetrievalConfig } from "./config.js";
4
+ import type { Embedder } from "./embeddings.js";
5
+ /** A retrieval hit: the chunk, its final score, and per-stage scores for debugging. */
6
+ export interface RetrievedChunk {
7
+ chunk: Chunk;
8
+ score: number;
9
+ stageScores: Record<string, number>;
10
+ }
11
+ export declare class Collection {
12
+ readonly name: string;
13
+ private embedder;
14
+ private chunks;
15
+ private store;
16
+ private bm25;
17
+ constructor(name: string, embedder: Embedder);
18
+ add(chunks: Chunk[]): void;
19
+ get numChunks(): number;
20
+ retrieve(query: string, config?: Partial<RetrievalConfig>): RetrievedChunk[];
21
+ }
@@ -0,0 +1,72 @@
1
+ /** A Collection ties chunks + embedder + vector store + BM25 into one retrievable unit. */
2
+ import { DEFAULT_RETRIEVAL } from "./config.js";
3
+ import { BM25Index } from "./retrieval/bm25.js";
4
+ import { reciprocalRankFusion, topKIds, weightedSumFusion } from "./retrieval/fusion.js";
5
+ import { InMemoryVectorStore } from "./stores/memory.js";
6
+ export class Collection {
7
+ name;
8
+ embedder;
9
+ chunks = new Map();
10
+ store;
11
+ bm25 = new BM25Index();
12
+ constructor(name, embedder) {
13
+ this.name = name;
14
+ this.embedder = embedder;
15
+ this.store = new InMemoryVectorStore(embedder.dim);
16
+ }
17
+ add(chunks) {
18
+ if (chunks.length === 0)
19
+ return;
20
+ for (const chunk of chunks)
21
+ this.chunks.set(chunk.chunkId, chunk);
22
+ const vectors = this.embedder.embed(chunks.map((c) => c.text));
23
+ this.store.add(chunks.map((c) => c.chunkId), vectors);
24
+ // BM25 is rebuilt over ALL chunks (index-time and query-time must agree).
25
+ const allIds = [...this.chunks.keys()];
26
+ this.bm25.build(allIds, allIds.map((id) => this.chunks.get(id)?.text ?? ""));
27
+ }
28
+ get numChunks() {
29
+ return this.chunks.size;
30
+ }
31
+ retrieve(query, config = {}) {
32
+ const cfg = { ...DEFAULT_RETRIEVAL, ...config };
33
+ if (this.chunks.size === 0)
34
+ return [];
35
+ const pool = cfg.candidatePool;
36
+ const queryVec = this.embedder.embed([query])[0];
37
+ if (queryVec === undefined)
38
+ return [];
39
+ const dense = new Map(this.store.search(queryVec, pool));
40
+ const bm25 = new Map(this.bm25.search(query, pool));
41
+ let fused;
42
+ if (cfg.mode === "dense") {
43
+ fused = dense;
44
+ }
45
+ else if (cfg.mode === "bm25") {
46
+ fused = bm25;
47
+ }
48
+ else if (cfg.fusion === "weighted") {
49
+ fused = weightedSumFusion([dense, bm25], [0.5, 0.5]);
50
+ }
51
+ else {
52
+ fused = reciprocalRankFusion([topKIds(dense, pool), topKIds(bm25, pool)], cfg.rrfK);
53
+ }
54
+ const results = [];
55
+ for (const chunkId of topKIds(fused, cfg.topK)) {
56
+ const chunk = this.chunks.get(chunkId);
57
+ if (chunk === undefined)
58
+ continue;
59
+ results.push({
60
+ chunk,
61
+ score: fused.get(chunkId) ?? 0,
62
+ stageScores: {
63
+ dense: dense.get(chunkId) ?? 0,
64
+ bm25: bm25.get(chunkId) ?? 0,
65
+ fused: fused.get(chunkId) ?? 0,
66
+ },
67
+ });
68
+ }
69
+ return results;
70
+ }
71
+ }
72
+ //# sourceMappingURL=collection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.js","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /** Configuration types and defaults for chunking and retrieval. */
2
+ export type RetrievalMode = "hybrid" | "dense" | "bm25";
3
+ export type FusionMethod = "rrf" | "weighted";
4
+ export interface ChunkingConfig {
5
+ /** Chunk size in words. */
6
+ size: number;
7
+ /** Overlap in words between consecutive chunks. */
8
+ overlap: number;
9
+ }
10
+ export declare const DEFAULT_CHUNKING: ChunkingConfig;
11
+ export interface RetrievalConfig {
12
+ topK: number;
13
+ mode: RetrievalMode;
14
+ fusion: FusionMethod;
15
+ /** RRF dampening constant. */
16
+ rrfK: number;
17
+ /** Per-retriever candidates fed into fusion. */
18
+ candidatePool: number;
19
+ }
20
+ export declare const DEFAULT_RETRIEVAL: RetrievalConfig;
package/dist/config.js ADDED
@@ -0,0 +1,10 @@
1
+ /** Configuration types and defaults for chunking and retrieval. */
2
+ export const DEFAULT_CHUNKING = { size: 200, overlap: 40 };
3
+ export const DEFAULT_RETRIEVAL = {
4
+ topK: 5,
5
+ mode: "hybrid",
6
+ fusion: "rrf",
7
+ rrfK: 60,
8
+ candidatePool: 20,
9
+ };
10
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,mEAAmE;AAYnE,MAAM,CAAC,MAAM,gBAAgB,GAAmB,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;AAY3E,MAAM,CAAC,MAAM,iBAAiB,GAAoB;IAChD,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,QAAQ;IACd,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,EAAE;IACR,aAAa,EAAE,EAAE;CAClB,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Embedders: turn text into vectors.
3
+ *
4
+ * The default `HashingEmbedder` is dependency-free and deterministic (the hashing trick),
5
+ * so the package installs and runs with nothing extra — great for wiring, tests, and a
6
+ * zero-setup quickstart. It is semantically weak (it matches words, not meaning); a
7
+ * transformers.js-backed semantic embedder is a planned optional backend.
8
+ *
9
+ * Coding to the `Embedder` interface keeps the rest of the system ignorant of which
10
+ * embedder is in use — the pluggable-backend design decision.
11
+ */
12
+ export interface Embedder {
13
+ readonly dim: number;
14
+ embed(texts: string[]): number[][];
15
+ }
16
+ export declare class HashingEmbedder implements Embedder {
17
+ readonly dim: number;
18
+ constructor(dim?: number);
19
+ /**
20
+ * Stable hash (not a per-process-randomized one) so the same token always maps to the
21
+ * same bucket across processes — this matters the moment an index is persisted and
22
+ * reloaded.
23
+ */
24
+ private bucket;
25
+ embed(texts: string[]): number[][];
26
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Embedders: turn text into vectors.
3
+ *
4
+ * The default `HashingEmbedder` is dependency-free and deterministic (the hashing trick),
5
+ * so the package installs and runs with nothing extra — great for wiring, tests, and a
6
+ * zero-setup quickstart. It is semantically weak (it matches words, not meaning); a
7
+ * transformers.js-backed semantic embedder is a planned optional backend.
8
+ *
9
+ * Coding to the `Embedder` interface keeps the rest of the system ignorant of which
10
+ * embedder is in use — the pluggable-backend design decision.
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ import { tokenize } from "./text.js";
14
+ /** Scale a vector to unit length so a dot product equals cosine similarity. */
15
+ function l2normalize(vec) {
16
+ let norm = Math.sqrt(vec.reduce((sum, x) => sum + x * x, 0));
17
+ if (norm === 0)
18
+ norm = 1; // guard against divide-by-zero on empty text
19
+ return vec.map((x) => x / norm);
20
+ }
21
+ export class HashingEmbedder {
22
+ dim;
23
+ constructor(dim = 256) {
24
+ this.dim = dim;
25
+ }
26
+ /**
27
+ * Stable hash (not a per-process-randomized one) so the same token always maps to the
28
+ * same bucket across processes — this matters the moment an index is persisted and
29
+ * reloaded.
30
+ */
31
+ bucket(token) {
32
+ const digest = createHash("md5").update(token).digest();
33
+ const value = digest.readBigUInt64LE(0);
34
+ return Number(value % BigInt(this.dim));
35
+ }
36
+ embed(texts) {
37
+ return texts.map((text) => {
38
+ const vec = new Array(this.dim).fill(0);
39
+ for (const token of tokenize(text)) {
40
+ const b = this.bucket(token);
41
+ vec[b] = (vec[b] ?? 0) + 1;
42
+ }
43
+ return l2normalize(vec);
44
+ });
45
+ }
46
+ }
47
+ //# sourceMappingURL=embeddings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embeddings.js","sourceRoot":"","sources":["../src/embeddings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAOrC,+EAA+E;AAC/E,SAAS,WAAW,CAAC,GAAa;IAChC,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7D,IAAI,IAAI,KAAK,CAAC;QAAE,IAAI,GAAG,CAAC,CAAC,CAAC,6CAA6C;IACvE,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,OAAO,eAAe;IACjB,GAAG,CAAS;IAErB,YAAY,GAAG,GAAG,GAAG;QACnB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,KAAa;QAC1B,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;QACxD,MAAM,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACxC,OAAO,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,KAAe;QACnB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACxB,MAAM,GAAG,GAAG,IAAI,KAAK,CAAS,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAChD,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC7B,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YAC7B,CAAC;YACD,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
package/dist/eval.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /** Retrieval evaluation: measure quality with numbers, not vibes. */
2
+ import type { Collection } from "./collection.js";
3
+ import type { RetrievalMode } from "./config.js";
4
+ export interface EvalCase {
5
+ query: string;
6
+ relevantSources: string[];
7
+ }
8
+ export interface EvalMetrics {
9
+ cases: number;
10
+ hitRate: number;
11
+ mrr: number;
12
+ recallAtK: number;
13
+ topK: number;
14
+ }
15
+ /**
16
+ * hit_rate (any relevant in top-k), MRR (rank of first relevant), recall@k (fraction of a
17
+ * query's relevant sources found), averaged over cases — so retrieval quality is trackable.
18
+ */
19
+ export declare function evaluate(collection: Collection, cases: EvalCase[], topK?: number, mode?: RetrievalMode): EvalMetrics;
package/dist/eval.js ADDED
@@ -0,0 +1,42 @@
1
+ /** Retrieval evaluation: measure quality with numbers, not vibes. */
2
+ const round4 = (x) => Math.round(x * 1e4) / 1e4;
3
+ /**
4
+ * hit_rate (any relevant in top-k), MRR (rank of first relevant), recall@k (fraction of a
5
+ * query's relevant sources found), averaged over cases — so retrieval quality is trackable.
6
+ */
7
+ export function evaluate(collection, cases, topK = 5, mode = "hybrid") {
8
+ if (cases.length === 0) {
9
+ return { cases: 0, hitRate: 0, mrr: 0, recallAtK: 0, topK };
10
+ }
11
+ let hits = 0;
12
+ let reciprocalRanks = 0;
13
+ let recallSum = 0;
14
+ for (const testCase of cases) {
15
+ const results = collection.retrieve(testCase.query, { topK, mode });
16
+ const retrievedSources = results.map((r) => r.chunk.source);
17
+ const relevant = new Set(testCase.relevantSources);
18
+ let firstRank = 0;
19
+ for (let i = 0; i < retrievedSources.length; i++) {
20
+ const src = retrievedSources[i];
21
+ if (src !== undefined && relevant.has(src)) {
22
+ firstRank = i + 1;
23
+ break;
24
+ }
25
+ }
26
+ if (firstRank > 0) {
27
+ hits += 1;
28
+ reciprocalRanks += 1 / firstRank;
29
+ }
30
+ const found = [...relevant].filter((s) => retrievedSources.includes(s)).length;
31
+ recallSum += found / relevant.size;
32
+ }
33
+ const n = cases.length;
34
+ return {
35
+ cases: n,
36
+ hitRate: round4(hits / n),
37
+ mrr: round4(reciprocalRanks / n),
38
+ recallAtK: round4(recallSum / n),
39
+ topK,
40
+ };
41
+ }
42
+ //# sourceMappingURL=eval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval.js","sourceRoot":"","sources":["../src/eval.ts"],"names":[],"mappings":"AAAA,qEAAqE;AAkBrE,MAAM,MAAM,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AAEhE;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,UAAsB,EACtB,KAAiB,EACjB,IAAI,GAAG,CAAC,EACR,OAAsB,QAAQ;IAE9B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC;IAC9D,CAAC;IAED,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,MAAM,QAAQ,IAAI,KAAK,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACpE,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;QAEnD,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,gBAAgB,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACjD,MAAM,GAAG,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;YAChC,IAAI,GAAG,KAAK,SAAS,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3C,SAAS,GAAG,CAAC,GAAG,CAAC,CAAC;gBAClB,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAClB,IAAI,IAAI,CAAC,CAAC;YACV,eAAe,IAAI,CAAC,GAAG,SAAS,CAAC;QACnC,CAAC;QAED,MAAM,KAAK,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/E,SAAS,IAAI,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC;IACrC,CAAC;IAED,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,OAAO;QACL,KAAK,EAAE,CAAC;QACR,OAAO,EAAE,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;QACzB,GAAG,EAAE,MAAM,CAAC,eAAe,GAAG,CAAC,CAAC;QAChC,SAAS,EAAE,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC;QAChC,IAAI;KACL,CAAC;AACJ,CAAC"}
@@ -0,0 +1,13 @@
1
+ /** Grounding: turn retrieved chunks into cited context, and the honest "not found" message. */
2
+ import type { RetrievedChunk } from "./collection.js";
3
+ export declare const NOT_FOUND = "I couldn't find an answer to that in the provided documents.";
4
+ export declare const GROUNDING_SYSTEM_PROMPT: string;
5
+ export interface Citation {
6
+ ref: number;
7
+ source: string;
8
+ chunkId: string;
9
+ score: number;
10
+ }
11
+ export declare function buildCitations(hits: RetrievedChunk[]): Citation[];
12
+ /** Number each passage and label it with its source, so the model can cite [n]. */
13
+ export declare function buildContext(hits: RetrievedChunk[]): string;
@@ -0,0 +1,19 @@
1
+ /** Grounding: turn retrieved chunks into cited context, and the honest "not found" message. */
2
+ export const NOT_FOUND = "I couldn't find an answer to that in the provided documents.";
3
+ export const GROUNDING_SYSTEM_PROMPT = "You are a retrieval QA assistant. Answer the question using ONLY the numbered context " +
4
+ "passages provided. Cite the passages you rely on with their bracket numbers, e.g. [1]. " +
5
+ "If the answer is not contained in the context, say you couldn't find it in the documents " +
6
+ "— never use outside knowledge and never invent details.";
7
+ export function buildCitations(hits) {
8
+ return hits.map((h, i) => ({
9
+ ref: i + 1,
10
+ source: h.chunk.source,
11
+ chunkId: h.chunk.chunkId,
12
+ score: Math.round(h.score * 1e6) / 1e6,
13
+ }));
14
+ }
15
+ /** Number each passage and label it with its source, so the model can cite [n]. */
16
+ export function buildContext(hits) {
17
+ return hits.map((h, i) => `[${i + 1}] (source: ${h.chunk.source})\n${h.chunk.text}`).join("\n\n");
18
+ }
19
+ //# sourceMappingURL=grounding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grounding.js","sourceRoot":"","sources":["../src/grounding.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAI/F,MAAM,CAAC,MAAM,SAAS,GAAG,8DAA8D,CAAC;AAExF,MAAM,CAAC,MAAM,uBAAuB,GAClC,wFAAwF;IACxF,yFAAyF;IACzF,2FAA2F;IAC3F,yDAAyD,CAAC;AAS5D,MAAM,UAAU,cAAc,CAAC,IAAsB;IACnD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACzB,GAAG,EAAE,CAAC,GAAG,CAAC;QACV,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM;QACtB,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO;QACxB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG;KACvC,CAAC,CAAC,CAAC;AACN,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,YAAY,CAAC,IAAsB;IACjD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,KAAK,CAAC,MAAM,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACpG,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point: launch the MCP server over stdio (the transport local hosts use).
4
+ *
5
+ * We log only to stderr — for an stdio MCP server, stdout is the JSON-RPC channel and must
6
+ * never carry stray text.
7
+ */
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { createServer } from "./server.js";
10
+ async function main() {
11
+ const server = createServer();
12
+ const transport = new StdioServerTransport();
13
+ await server.connect(transport);
14
+ }
15
+ main().catch((err) => {
16
+ process.stderr.write(`grounded-rag-mcp failed to start: ${String(err)}\n`);
17
+ process.exit(1);
18
+ });
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC3E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ /** Load documents from disk or raw strings into chunks. */
2
+ import { type Chunk } from "./chunking.js";
3
+ import { type ChunkingConfig } from "./config.js";
4
+ /**
5
+ * Read text files (and directories of them) and chunk their contents.
6
+ *
7
+ * v1 handles plain text and markdown — the formats that need no parsing dependency.
8
+ * Richer parsing (PDF, HTML) is a pluggable loader layer, kept out of v1 on purpose.
9
+ */
10
+ export declare function loadPaths(paths: string[], config?: ChunkingConfig): Chunk[];
11
+ /** Chunk raw in-memory strings (each becomes its own source `text:N`). */
12
+ export declare function loadTexts(texts: string[], config?: ChunkingConfig): Chunk[];
package/dist/ingest.js ADDED
@@ -0,0 +1,33 @@
1
+ /** Load documents from disk or raw strings into chunks. */
2
+ import { readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { extname, join } from "node:path";
4
+ import { chunkDocument } from "./chunking.js";
5
+ import { DEFAULT_CHUNKING } from "./config.js";
6
+ const SUPPORTED = new Set([".txt", ".md", ".markdown"]);
7
+ function walk(path) {
8
+ const stat = statSync(path);
9
+ if (stat.isDirectory()) {
10
+ return readdirSync(path).flatMap((entry) => walk(join(path, entry)));
11
+ }
12
+ return SUPPORTED.has(extname(path).toLowerCase()) ? [path] : [];
13
+ }
14
+ /**
15
+ * Read text files (and directories of them) and chunk their contents.
16
+ *
17
+ * v1 handles plain text and markdown — the formats that need no parsing dependency.
18
+ * Richer parsing (PDF, HTML) is a pluggable loader layer, kept out of v1 on purpose.
19
+ */
20
+ export function loadPaths(paths, config = DEFAULT_CHUNKING) {
21
+ const files = paths.flatMap(walk);
22
+ const chunks = [];
23
+ for (const file of files) {
24
+ const text = readFileSync(file, "utf-8");
25
+ chunks.push(...chunkDocument(text, file, config));
26
+ }
27
+ return chunks;
28
+ }
29
+ /** Chunk raw in-memory strings (each becomes its own source `text:N`). */
30
+ export function loadTexts(texts, config = DEFAULT_CHUNKING) {
31
+ return texts.flatMap((text, i) => chunkDocument(text, `text:${i}`, config));
32
+ }
33
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../src/ingest.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAE3D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,aAAa,EAAc,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAuB,MAAM,aAAa,CAAC;AAEpE,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC;AAExD,SAAS,IAAI,CAAC,IAAY;IACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QACvB,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAClE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,KAAe,EAAE,SAAyB,gBAAgB;IAClF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,SAAS,CAAC,KAAe,EAAE,SAAyB,gBAAgB;IAClF,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * BM25 keyword retrieval, implemented from scratch (no extra dependency).
3
+ *
4
+ * Scores a document per query term via IDF (rare terms matter more), term-frequency
5
+ * saturation (`k1` — the 2nd occurrence helps a lot, the 20th barely more), and length
6
+ * normalization (`b` — long docs shouldn't win just for being long). Captures exact-term
7
+ * matches (names, codes, acronyms) that dense/semantic search misses.
8
+ */
9
+ export declare class BM25Index {
10
+ private readonly k1;
11
+ private readonly b;
12
+ private ids;
13
+ private docs;
14
+ private docFreq;
15
+ private avgLen;
16
+ constructor(k1?: number, b?: number);
17
+ /** (Re)build from all chunks — cheap at local scale; rebuilt on every ingest. */
18
+ build(ids: string[], texts: string[]): void;
19
+ private idf;
20
+ search(query: string, topK: number): Array<[string, number]>;
21
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * BM25 keyword retrieval, implemented from scratch (no extra dependency).
3
+ *
4
+ * Scores a document per query term via IDF (rare terms matter more), term-frequency
5
+ * saturation (`k1` — the 2nd occurrence helps a lot, the 20th barely more), and length
6
+ * normalization (`b` — long docs shouldn't win just for being long). Captures exact-term
7
+ * matches (names, codes, acronyms) that dense/semantic search misses.
8
+ */
9
+ import { tokenize } from "../text.js";
10
+ export class BM25Index {
11
+ k1;
12
+ b;
13
+ ids = [];
14
+ docs = [];
15
+ docFreq = new Map();
16
+ avgLen = 0;
17
+ constructor(k1 = 1.5, b = 0.75) {
18
+ this.k1 = k1;
19
+ this.b = b;
20
+ }
21
+ /** (Re)build from all chunks — cheap at local scale; rebuilt on every ingest. */
22
+ build(ids, texts) {
23
+ this.ids = ids;
24
+ this.docs = texts.map((t) => tokenize(t));
25
+ this.docFreq = new Map();
26
+ for (const tokens of this.docs) {
27
+ for (const term of new Set(tokens)) {
28
+ this.docFreq.set(term, (this.docFreq.get(term) ?? 0) + 1);
29
+ }
30
+ }
31
+ const totalLen = this.docs.reduce((sum, d) => sum + d.length, 0);
32
+ this.avgLen = this.docs.length > 0 ? totalLen / this.docs.length : 0;
33
+ }
34
+ idf(term) {
35
+ const n = this.docs.length;
36
+ const df = this.docFreq.get(term) ?? 0;
37
+ // +0.5 smoothing; max(..., 0) keeps very common terms from going negative.
38
+ return Math.max(0, Math.log((n - df + 0.5) / (df + 0.5) + 1));
39
+ }
40
+ search(query, topK) {
41
+ if (this.docs.length === 0)
42
+ return [];
43
+ const queryTerms = tokenize(query);
44
+ const results = [];
45
+ for (let i = 0; i < this.ids.length; i++) {
46
+ const tokens = this.docs[i];
47
+ const id = this.ids[i];
48
+ if (tokens === undefined || id === undefined)
49
+ continue;
50
+ const counts = new Map();
51
+ for (const t of tokens)
52
+ counts.set(t, (counts.get(t) ?? 0) + 1);
53
+ const dl = tokens.length;
54
+ let score = 0;
55
+ for (const term of queryTerms) {
56
+ const tf = counts.get(term) ?? 0;
57
+ if (tf === 0)
58
+ continue;
59
+ const denom = tf + this.k1 * (1 - this.b + (this.b * dl) / (this.avgLen || 1));
60
+ score += this.idf(term) * ((tf * (this.k1 + 1)) / denom);
61
+ }
62
+ if (score > 0)
63
+ results.push([id, score]);
64
+ }
65
+ results.sort((a, b) => b[1] - a[1]);
66
+ return results.slice(0, topK);
67
+ }
68
+ }
69
+ //# sourceMappingURL=bm25.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bm25.js","sourceRoot":"","sources":["../../src/retrieval/bm25.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Combine results from multiple retrievers into one ranking.
3
+ *
4
+ * - Reciprocal Rank Fusion (RRF): combines by *rank*, so it never has to make an unbounded
5
+ * BM25 score and a 0-1 cosine score comparable. Robust, and the default.
6
+ * - Normalized weighted sum: combines by *score*, which first requires normalizing each
7
+ * retriever to [0, 1] — and that normalization has a sharp edge (see below).
8
+ *
9
+ * War story encoded as a test: the inverted min-max `(max - x) / (max - min)` maps the BEST
10
+ * score to 0 and the WORST to 1, silently pushing the most relevant documents to the bottom
11
+ * where they get truncated before the model ever sees them. `fusion.test.ts` asserts the
12
+ * correct direction so that inversion can never return unnoticed.
13
+ */
14
+ /** Fuse ranked ID lists. A doc's score is the sum of 1/(k + rank) across lists (rank is 1-based). */
15
+ export declare function reciprocalRankFusion(rankings: string[][], k?: number): Map<string, number>;
16
+ /**
17
+ * Rescale scores to [0, 1] preserving order: best -> 1, worst -> 0.
18
+ *
19
+ * CORRECT: norm(x) = (x - min) / (max - min).
20
+ * The inverted mistake, (max - x) / (max - min), flips the ranking. When all scores are
21
+ * equal (max === min), every item is equally relevant, so map to 1.
22
+ */
23
+ export declare function minMaxNormalize(scores: Map<string, number>): Map<string, number>;
24
+ /** Normalize each retriever's scores to [0, 1], then take a weighted sum. */
25
+ export declare function weightedSumFusion(scoreMaps: Array<Map<string, number>>, weights: number[]): Map<string, number>;
26
+ /** The k highest-scoring IDs, best first. */
27
+ export declare function topKIds(scores: Map<string, number>, k: number): string[];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Combine results from multiple retrievers into one ranking.
3
+ *
4
+ * - Reciprocal Rank Fusion (RRF): combines by *rank*, so it never has to make an unbounded
5
+ * BM25 score and a 0-1 cosine score comparable. Robust, and the default.
6
+ * - Normalized weighted sum: combines by *score*, which first requires normalizing each
7
+ * retriever to [0, 1] — and that normalization has a sharp edge (see below).
8
+ *
9
+ * War story encoded as a test: the inverted min-max `(max - x) / (max - min)` maps the BEST
10
+ * score to 0 and the WORST to 1, silently pushing the most relevant documents to the bottom
11
+ * where they get truncated before the model ever sees them. `fusion.test.ts` asserts the
12
+ * correct direction so that inversion can never return unnoticed.
13
+ */
14
+ /** Fuse ranked ID lists. A doc's score is the sum of 1/(k + rank) across lists (rank is 1-based). */
15
+ export function reciprocalRankFusion(rankings, k = 60) {
16
+ const scores = new Map();
17
+ for (const ranking of rankings) {
18
+ ranking.forEach((docId, position) => {
19
+ const rank = position + 1;
20
+ scores.set(docId, (scores.get(docId) ?? 0) + 1 / (k + rank));
21
+ });
22
+ }
23
+ return scores;
24
+ }
25
+ /**
26
+ * Rescale scores to [0, 1] preserving order: best -> 1, worst -> 0.
27
+ *
28
+ * CORRECT: norm(x) = (x - min) / (max - min).
29
+ * The inverted mistake, (max - x) / (max - min), flips the ranking. When all scores are
30
+ * equal (max === min), every item is equally relevant, so map to 1.
31
+ */
32
+ export function minMaxNormalize(scores) {
33
+ const out = new Map();
34
+ if (scores.size === 0)
35
+ return out;
36
+ const values = [...scores.values()];
37
+ const lo = Math.min(...values);
38
+ const hi = Math.max(...values);
39
+ if (hi === lo) {
40
+ for (const key of scores.keys())
41
+ out.set(key, 1);
42
+ return out;
43
+ }
44
+ const span = hi - lo;
45
+ for (const [key, value] of scores)
46
+ out.set(key, (value - lo) / span);
47
+ return out;
48
+ }
49
+ /** Normalize each retriever's scores to [0, 1], then take a weighted sum. */
50
+ export function weightedSumFusion(scoreMaps, weights) {
51
+ if (scoreMaps.length !== weights.length) {
52
+ throw new Error("scoreMaps and weights length mismatch");
53
+ }
54
+ const fused = new Map();
55
+ scoreMaps.forEach((scoreMap, i) => {
56
+ const weight = weights[i] ?? 0;
57
+ for (const [docId, value] of minMaxNormalize(scoreMap)) {
58
+ fused.set(docId, (fused.get(docId) ?? 0) + weight * value);
59
+ }
60
+ });
61
+ return fused;
62
+ }
63
+ /** The k highest-scoring IDs, best first. */
64
+ export function topKIds(scores, k) {
65
+ return [...scores.entries()]
66
+ .sort((a, b) => b[1] - a[1])
67
+ .slice(0, k)
68
+ .map(([id]) => id);
69
+ }
70
+ //# sourceMappingURL=fusion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fusion.js","sourceRoot":"","sources":["../../src/retrieval/fusion.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * MCP server: exposes the retrieval engine as MCP tools, resources, and prompts.
3
+ *
4
+ * Built on the MCP TypeScript SDK (`McpServer`). Each tool's zod `inputSchema` becomes the
5
+ * JSON Schema the host LLM reads to decide when and how to call it, so the descriptions are
6
+ * part of the interface, not just docs.
7
+ *
8
+ * `createServer()` returns a fresh instance with its own collection registry — so tests can
9
+ * spin up isolated servers instead of sharing global state.
10
+ */
11
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { type Embedder } from "./embeddings.js";
13
+ export declare function createServer(embedder?: Embedder): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,170 @@
1
+ /**
2
+ * MCP server: exposes the retrieval engine as MCP tools, resources, and prompts.
3
+ *
4
+ * Built on the MCP TypeScript SDK (`McpServer`). Each tool's zod `inputSchema` becomes the
5
+ * JSON Schema the host LLM reads to decide when and how to call it, so the descriptions are
6
+ * part of the interface, not just docs.
7
+ *
8
+ * `createServer()` returns a fresh instance with its own collection registry — so tests can
9
+ * spin up isolated servers instead of sharing global state.
10
+ */
11
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { z } from "zod";
13
+ import { Collection } from "./collection.js";
14
+ import { HashingEmbedder } from "./embeddings.js";
15
+ import { evaluate } from "./eval.js";
16
+ import * as grounding from "./grounding.js";
17
+ import { loadPaths, loadTexts } from "./ingest.js";
18
+ import { VERSION } from "./version.js";
19
+ function text(payload) {
20
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
21
+ }
22
+ export function createServer(embedder = new HashingEmbedder(512)) {
23
+ const collections = new Map();
24
+ const getOrCreate = (name) => {
25
+ let col = collections.get(name);
26
+ if (col === undefined) {
27
+ col = new Collection(name, embedder);
28
+ collections.set(name, col);
29
+ }
30
+ return col;
31
+ };
32
+ const mcp = new McpServer({ name: "grounded-rag-mcp", version: VERSION });
33
+ mcp.registerTool("ingest_documents", {
34
+ description: "Ingest documents into a named collection so they can be searched. Provide `paths` " +
35
+ "(files/dirs of .txt/.md) and/or `texts` (raw strings). Returns chunks added and total.",
36
+ inputSchema: {
37
+ collection: z.string().default("default"),
38
+ paths: z.array(z.string()).optional(),
39
+ texts: z.array(z.string()).optional(),
40
+ chunkSize: z.number().int().min(1).default(200),
41
+ chunkOverlap: z.number().int().min(0).default(40),
42
+ },
43
+ }, async ({ collection, paths, texts, chunkSize, chunkOverlap }) => {
44
+ if ((paths === undefined || paths.length === 0) &&
45
+ (texts === undefined || texts.length === 0)) {
46
+ throw new Error("Provide `paths` and/or `texts` to ingest.");
47
+ }
48
+ const cfg = { size: chunkSize, overlap: chunkOverlap };
49
+ const chunks = [
50
+ ...(paths ? loadPaths(paths, cfg) : []),
51
+ ...(texts ? loadTexts(texts, cfg) : []),
52
+ ];
53
+ const col = getOrCreate(collection);
54
+ col.add(chunks);
55
+ return text({ collection, chunksAdded: chunks.length, totalChunks: col.numChunks });
56
+ });
57
+ mcp.registerTool("search", {
58
+ description: "Search a collection and return the most relevant chunks, each with its source and " +
59
+ "per-stage scores. mode: hybrid (default) | dense | bm25. Empty result = not in the docs.",
60
+ inputSchema: {
61
+ query: z.string(),
62
+ collection: z.string().default("default"),
63
+ topK: z.number().int().min(1).default(5),
64
+ mode: z.enum(["hybrid", "dense", "bm25"]).default("hybrid"),
65
+ },
66
+ }, async ({ query, collection, topK, mode }) => {
67
+ const col = collections.get(collection);
68
+ if (col === undefined)
69
+ return text([]);
70
+ const hits = col.retrieve(query, { topK, mode: mode });
71
+ return text(hits.map((h) => ({
72
+ text: h.chunk.text,
73
+ source: h.chunk.source,
74
+ chunkId: h.chunk.chunkId,
75
+ score: Math.round(h.score * 1e6) / 1e6,
76
+ stageScores: h.stageScores,
77
+ })));
78
+ });
79
+ mcp.registerTool("answer", {
80
+ description: "Answer a question grounded in a collection, with citations. Retrieves relevant " +
81
+ "passages and asks the host's model (via MCP sampling) to answer using only those, " +
82
+ "citing them. Refuses (grounded=false) when nothing relevant is found.",
83
+ inputSchema: {
84
+ query: z.string(),
85
+ collection: z.string().default("default"),
86
+ topK: z.number().int().min(1).default(5),
87
+ },
88
+ }, async ({ query, collection, topK }) => {
89
+ const col = collections.get(collection);
90
+ const hits = col ? col.retrieve(query, { topK }) : [];
91
+ if (hits.length === 0) {
92
+ return text({ grounded: false, answer: grounding.NOT_FOUND, citations: [] });
93
+ }
94
+ const citations = grounding.buildCitations(hits);
95
+ const context = grounding.buildContext(hits);
96
+ try {
97
+ const result = await mcp.server.createMessage({
98
+ messages: [
99
+ {
100
+ role: "user",
101
+ content: {
102
+ type: "text",
103
+ text: `Context passages:\n\n${context}\n\nQuestion: ${query}`,
104
+ },
105
+ },
106
+ ],
107
+ maxTokens: 512,
108
+ systemPrompt: grounding.GROUNDING_SYSTEM_PROMPT,
109
+ temperature: 0,
110
+ });
111
+ const answerText = result.content.type === "text" ? result.content.text : "";
112
+ return text({ grounded: true, answer: answerText, citations });
113
+ }
114
+ catch {
115
+ // Host has no sampling — hand back grounded context so it can answer itself.
116
+ return text({
117
+ grounded: true,
118
+ answer: null,
119
+ citations,
120
+ context,
121
+ note: "Host has no sampling; compose the answer from `context`, citing sources.",
122
+ });
123
+ }
124
+ });
125
+ mcp.registerTool("list_collections", { description: "List all ingested collections and how many chunks each contains." }, async () => text([...collections.entries()].map(([name, col]) => ({ name, chunks: col.numChunks }))));
126
+ mcp.registerTool("evaluate_retrieval", {
127
+ description: "Measure retrieval quality on labeled cases: hitRate, mrr, recallAtK. Each case is " +
128
+ "{query, relevantSources}. Use it to quantify quality and catch regressions.",
129
+ inputSchema: {
130
+ cases: z.array(z.object({ query: z.string(), relevantSources: z.array(z.string()) })),
131
+ collection: z.string().default("default"),
132
+ topK: z.number().int().min(1).default(5),
133
+ mode: z.enum(["hybrid", "dense", "bm25"]).default("hybrid"),
134
+ },
135
+ }, async ({ cases, collection, topK, mode }) => {
136
+ const col = collections.get(collection);
137
+ if (col === undefined)
138
+ return text({ cases: 0, hitRate: 0, mrr: 0, recallAtK: 0, topK });
139
+ return text(evaluate(col, cases, topK, mode));
140
+ });
141
+ mcp.registerResource("collections", "rag://collections", {
142
+ title: "Collections",
143
+ description: "Ingested collections and chunk counts",
144
+ mimeType: "application/json",
145
+ }, async (uri) => ({
146
+ contents: [
147
+ {
148
+ uri: uri.href,
149
+ text: JSON.stringify([...collections.entries()].map(([name, col]) => ({ name, chunks: col.numChunks }))),
150
+ },
151
+ ],
152
+ }));
153
+ mcp.registerPrompt("grounded_answer", {
154
+ title: "Grounded answer",
155
+ description: "Instructs strict, cited, grounded answering.",
156
+ argsSchema: { question: z.string() },
157
+ }, ({ question }) => ({
158
+ messages: [
159
+ {
160
+ role: "user",
161
+ content: {
162
+ type: "text",
163
+ text: `${grounding.GROUNDING_SYSTEM_PROMPT}\n\nQuestion: ${question}`,
164
+ },
165
+ },
166
+ ],
167
+ }));
168
+ return mcp;
169
+ }
170
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /** A dependency-free, in-memory vector store (dense retrieval by exact cosine). */
2
+ export declare class InMemoryVectorStore {
3
+ readonly dim: number;
4
+ private ids;
5
+ private matrix;
6
+ constructor(dim: number);
7
+ add(ids: string[], vectors: number[][]): void;
8
+ /**
9
+ * Return [chunkId, cosine] for the top-k nearest vectors.
10
+ *
11
+ * Vectors are pre-normalized (the embedder does it), so a dot product IS the cosine.
12
+ * Zero-similarity hits are dropped: a 0 cosine means no shared signal at all, and
13
+ * surfacing it would feed the model irrelevant context and invite an ungrounded answer.
14
+ * Exact O(n) search is fine to tens of thousands of chunks; swap in an ANN index beyond.
15
+ */
16
+ search(query: number[], topK: number): Array<[string, number]>;
17
+ get size(): number;
18
+ }
@@ -0,0 +1,45 @@
1
+ /** A dependency-free, in-memory vector store (dense retrieval by exact cosine). */
2
+ export class InMemoryVectorStore {
3
+ dim;
4
+ ids = [];
5
+ matrix = [];
6
+ constructor(dim) {
7
+ this.dim = dim;
8
+ }
9
+ add(ids, vectors) {
10
+ if (ids.length !== vectors.length) {
11
+ throw new Error("ids and vectors length mismatch");
12
+ }
13
+ this.ids.push(...ids);
14
+ this.matrix.push(...vectors);
15
+ }
16
+ /**
17
+ * Return [chunkId, cosine] for the top-k nearest vectors.
18
+ *
19
+ * Vectors are pre-normalized (the embedder does it), so a dot product IS the cosine.
20
+ * Zero-similarity hits are dropped: a 0 cosine means no shared signal at all, and
21
+ * surfacing it would feed the model irrelevant context and invite an ungrounded answer.
22
+ * Exact O(n) search is fine to tens of thousands of chunks; swap in an ANN index beyond.
23
+ */
24
+ search(query, topK) {
25
+ const sims = [];
26
+ for (let i = 0; i < this.ids.length; i++) {
27
+ const row = this.matrix[i];
28
+ const id = this.ids[i];
29
+ if (row === undefined || id === undefined)
30
+ continue;
31
+ let dot = 0;
32
+ for (let j = 0; j < row.length; j++) {
33
+ dot += (row[j] ?? 0) * (query[j] ?? 0);
34
+ }
35
+ if (dot > 0)
36
+ sims.push([id, dot]);
37
+ }
38
+ sims.sort((a, b) => b[1] - a[1]);
39
+ return sims.slice(0, topK);
40
+ }
41
+ get size() {
42
+ return this.ids.length;
43
+ }
44
+ }
45
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAEnF,MAAM,OAAO,mBAAmB;IACrB,GAAG,CAAS;IACb,GAAG,GAAa,EAAE,CAAC;IACnB,MAAM,GAAe,EAAE,CAAC;IAEhC,YAAY,GAAW;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED,GAAG,CAAC,GAAa,EAAE,OAAmB;QACpC,IAAI,GAAG,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;IAC/B,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,KAAe,EAAE,IAAY;QAClC,MAAM,IAAI,GAA4B,EAAE,CAAC;QACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACzC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACvB,IAAI,GAAG,KAAK,SAAS,IAAI,EAAE,KAAK,SAAS;gBAAE,SAAS;YACpD,IAAI,GAAG,GAAG,CAAC,CAAC;YACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACpC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YACzC,CAAC;YACD,IAAI,GAAG,GAAG,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QACpC,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;IACzB,CAAC;CACF"}
package/dist/text.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /** Shared text utilities: one tokenizer used by BM25 and the hashing embedder. */
2
+ /**
3
+ * Lowercase and split into alphanumeric tokens.
4
+ *
5
+ * One shared tokenizer means index-time and query-time tokenization can never drift
6
+ * apart — a classic and hard-to-spot source of retrieval bugs.
7
+ */
8
+ export declare function tokenize(text: string): string[];
package/dist/text.js ADDED
@@ -0,0 +1,12 @@
1
+ /** Shared text utilities: one tokenizer used by BM25 and the hashing embedder. */
2
+ const TOKEN_RE = /[a-z0-9]+/g;
3
+ /**
4
+ * Lowercase and split into alphanumeric tokens.
5
+ *
6
+ * One shared tokenizer means index-time and query-time tokenization can never drift
7
+ * apart — a classic and hard-to-spot source of retrieval bugs.
8
+ */
9
+ export function tokenize(text) {
10
+ return text.toLowerCase().match(TOKEN_RE) ?? [];
11
+ }
12
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAElF,MAAM,QAAQ,GAAG,YAAY,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;AAClD,CAAC"}
@@ -0,0 +1,2 @@
1
+ /** The package version, kept in one place and asserted by the smoke test. */
2
+ export declare const VERSION = "0.1.0";
@@ -0,0 +1,3 @@
1
+ /** The package version, kept in one place and asserted by the smoke test. */
2
+ export const VERSION = "0.1.0";
3
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "grounded-rag-mcp",
3
+ "version": "0.1.0",
4
+ "description": "An MCP server that gives any LLM host grounded, cited retrieval over your own documents — hybrid BM25 + dense retrieval, cross-encoder-ready reranking, and built-in eval.",
5
+ "type": "module",
6
+ "bin": {
7
+ "grounded-rag-mcp": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "vitest run",
21
+ "lint": "eslint .",
22
+ "format": "prettier --write .",
23
+ "format:check": "prettier --check ."
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "rag",
28
+ "retrieval",
29
+ "llm",
30
+ "embeddings",
31
+ "hybrid-search",
32
+ "reranking",
33
+ "model-context-protocol"
34
+ ],
35
+ "author": "Chetan C",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/chetan1521/grounded-rag-mcp-ts.git"
40
+ },
41
+ "homepage": "https://github.com/chetan1521/grounded-rag-mcp-ts",
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.30.0",
44
+ "zod": "^3.23.8"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.7.0",
48
+ "eslint": "^9.12.0",
49
+ "prettier": "^3.3.3",
50
+ "typescript": "^5.6.2",
51
+ "typescript-eslint": "^8.8.0",
52
+ "vitest": "^2.1.2"
53
+ }
54
+ }