@deepsweet/mdn 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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright (c) 2026–present Kir Belevich
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
13
+ all 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
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ Offline-first [MDN Web Docs](https://developer.mozilla.org/) RAG-MCP server ready for semantic search with hybrid vector (1024-d) and full‑text (BM25) retrieval.
2
+
3
+ ## Example
4
+
5
+ ![example screenshot](example.webp)
6
+
7
+ ## Content
8
+
9
+ The dataset covers the core MDN documentation sections, including:
10
+
11
+ - Web API
12
+ - JavaScript
13
+ - HTML
14
+ - CSS
15
+ - SVG
16
+ - HTTP
17
+
18
+ See [dataset repo](https://huggingface.co/datasets/deepsweet/mdn) on HuggigFace for more details.
19
+
20
+ ## Usage
21
+
22
+ ### 1. Download dataset and embedding model
23
+
24
+ ```sh
25
+ npx @deepsweet/mdn@latest download
26
+ ```
27
+
28
+ Both [dataset](https://huggingface.co/datasets/deepsweet/mdn) (\~260 MB) and the [embedding model GGUF file](https://huggingface.co/deepsweet/bge-m3-GGUF-Q4_K_M) (\~438 MB) will be downloaded directly from HugginFace and stored in its default cache location (typically `~/.cache/huggingface/`), just like the `hf download` command does.
29
+
30
+ ### 2. Setup RAG-MCP server
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "mdn": {
36
+ "command": "npx",
37
+ "args": [
38
+ "@deepsweet/mdn",
39
+ "server"
40
+ ],
41
+ "env": {}
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ The `stdio` server will spawn [llama.cpp](https://github.com/ggml-org/llama.cpp) under the hood, load the embedding model (~655 MB RAM/VRAM), and query the dataset – all on demand.
48
+
49
+ ## Settings
50
+
51
+ | Env variable | Default value | Description |
52
+ |----------------------------|-----------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
53
+ | `MDN_DATASET_PATH` | HuggingFace cache | Custom dataset directory path |
54
+ | `MDN_DATASET_LOCALE` | `en-us` | Dataset language, currently `en-us` only |
55
+ | `MDN_MODEL_PATH` | HuggingFace cache | Custom model file path |
56
+ | `MDN_MODEL_TTL` | `1800` | For how long llama.cpp with embedding model should be kept loaded in memory, in seconds; `0` to prevent unloading |
57
+ | `MDN_QUERY_DESCRIPTION` | `Natural language query for hybrid vector and full-text search` | Custom search query description in case your LLM does a poor job asking the MCP tool |
58
+ | `MDN_SEARCH_RESULTS_LIMIT` | `3` | Total search results limit |
59
+
60
+ ## To do
61
+
62
+ - [ ] figure out a better query description so that LLM doesn't over-generate keywords
63
+ - [ ] add more dataset [translations](https://github.com/mdn/translated-content/tree/main/files/)
64
+ - [ ] automatically update and upload the dataset artifacts monthly with GitHub Actions
65
+
66
+ ## License
67
+
68
+ The RAG-MCP server itself and the processing scripts are available under MIT.
package/dist/index.js ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/env.ts
4
+ import { z } from "zod";
5
+ var env = z.object({
6
+ MDN_DATASET_PATH: z.string().optional(),
7
+ MDN_DATASET_LOCALE: z.enum(["en-us"]).default("en-us"),
8
+ MDN_MODEL_PATH: z.string().optional(),
9
+ MDN_MODEL_TTL: z.number().default(1800),
10
+ MDN_QUERY_DESCRIPTION: z.string().default("Natural language query for hybrid vector and full-text search"),
11
+ MDN_SEARCH_RESULTS_LIMIT: z.coerce.number().default(3)
12
+ }).parse(process.env);
13
+
14
+ // src/huggingface.ts
15
+ import fs from "node:fs/promises";
16
+ import path from "path";
17
+ import { getHFHubCachePath, getRepoFolderName, scanCachedRepo, snapshotDownload } from "@huggingface/hub";
18
+
19
+ // src/utils.ts
20
+ var getTableName = (locale) => {
21
+ return `mdn-${locale}`;
22
+ };
23
+
24
+ // src/huggingface.ts
25
+ var DATASET_REPO = "deepsweet/mdn";
26
+ var MODEL_REPO = "deepsweet/bge-m3-GGUF-Q4_K_M";
27
+ var MODEL_FILE = "bge-m3-GGUF-Q4_K_M.gguf";
28
+ var replaceSymlinksWithHardlinks = async (dir) => {
29
+ const entries = await fs.readdir(dir, { withFileTypes: true });
30
+ for (const entry of entries) {
31
+ const fullPath = path.join(dir, entry.name);
32
+ if (entry.isSymbolicLink()) {
33
+ const target = await fs.readlink(fullPath);
34
+ const sourcePath = path.resolve(dir, target);
35
+ await fs.unlink(fullPath);
36
+ await fs.link(sourcePath, fullPath);
37
+ continue;
38
+ }
39
+ if (entry.isDirectory()) {
40
+ await replaceSymlinksWithHardlinks(fullPath);
41
+ }
42
+ }
43
+ };
44
+ var getLatestCachedRepoRevision = async (name, type) => {
45
+ const cachePath = getHFHubCachePath();
46
+ const repoFolderName = getRepoFolderName({ name, type });
47
+ const repoPath = path.join(cachePath, repoFolderName);
48
+ const repo = await scanCachedRepo(repoPath);
49
+ if (repo.revisions.length === 0) {
50
+ throw new Error("Unable to get model path, it needs to be downloaded first");
51
+ }
52
+ const latestRevision = repo.revisions.reduce((latest, current) => {
53
+ if (current.lastModifiedAt > latest.lastModifiedAt) {
54
+ return current;
55
+ }
56
+ return latest;
57
+ });
58
+ return latestRevision.path;
59
+ };
60
+ var downloadDataset = async (locale) => {
61
+ const tableName = getTableName(locale);
62
+ const dirPath = await snapshotDownload({
63
+ repo: `datasets/${DATASET_REPO}`,
64
+ path: `data/${tableName}.lance`
65
+ });
66
+ const dataPath = path.join(dirPath, "data");
67
+ await replaceSymlinksWithHardlinks(dataPath);
68
+ };
69
+ var getDatasetPath = async () => {
70
+ if (process.env.MDN_DATASET_PATH != null) {
71
+ return process.env.MDN_DATASET_PATH;
72
+ }
73
+ const latestRevisionPath = await getLatestCachedRepoRevision(DATASET_REPO, "dataset");
74
+ const datasetPath = path.join(latestRevisionPath, "data");
75
+ return datasetPath;
76
+ };
77
+ var downloadModel = async () => {
78
+ await snapshotDownload({
79
+ repo: MODEL_REPO
80
+ });
81
+ };
82
+ var getModelPath = async () => {
83
+ if (process.env.MDN_MODEL_PATH != null) {
84
+ return process.env.MDN_MODEL_PATH;
85
+ }
86
+ const latestRevisionPath = await getLatestCachedRepoRevision(MODEL_REPO, "model");
87
+ const modelPath = path.join(latestRevisionPath, MODEL_FILE);
88
+ return modelPath;
89
+ };
90
+
91
+ // src/server.ts
92
+ import lancedb2 from "@lancedb/lancedb";
93
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
94
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
95
+ import { z as z2 } from "zod";
96
+
97
+ // src/llama.ts
98
+ import { getLlama } from "node-llama-cpp";
99
+ var MAX_TOKENS = 8192;
100
+ var getLlamaContext = async (modelPath) => {
101
+ const llama = await getLlama();
102
+ const model = await llama.loadModel({ modelPath });
103
+ const context = await model.createEmbeddingContext({
104
+ contextSize: MAX_TOKENS,
105
+ batchSize: MAX_TOKENS,
106
+ threads: 0
107
+ });
108
+ return context;
109
+ };
110
+
111
+ // src/query.ts
112
+ import lancedb from "@lancedb/lancedb";
113
+
114
+ // src/vectorize.ts
115
+ var vectorize = async (context, text) => {
116
+ const embedding = await context.getEmbeddingFor(text);
117
+ const vector = embedding.vector;
118
+ return vector;
119
+ };
120
+
121
+ // src/query.ts
122
+ var queryHybrid = async (llamaContext, table, reranker, text) => {
123
+ const vector = await vectorize(llamaContext, text);
124
+ const results = await table.query().nearestTo(vector).fullTextSearch(text).rerank(reranker).limit(env.MDN_SEARCH_RESULTS_LIMIT).toArray();
125
+ return results;
126
+ };
127
+ var createReranker = async () => {
128
+ const reranker = await lancedb.rerankers.RRFReranker.create();
129
+ return reranker;
130
+ };
131
+
132
+ // package.json
133
+ var name = "@deepsweet/mdn";
134
+ var version = "0.1.0";
135
+
136
+ // src/server.ts
137
+ var startMcpServer = async () => {
138
+ const datasetPath = await getDatasetPath();
139
+ const db = await lancedb2.connect(datasetPath);
140
+ const reranker = await createReranker();
141
+ const server = new McpServer({ name, version });
142
+ const tableName = getTableName(env.MDN_DATASET_LOCALE);
143
+ const table = await db.openTable(tableName);
144
+ const modelPath = await getModelPath();
145
+ const llamaTtl = env.MDN_MODEL_TTL * 1000;
146
+ let llamaContext = null;
147
+ let llamaTimeout = null;
148
+ server.registerTool("MDN", {
149
+ description: "Reference documentation for Web API, JavaScript, HTML, CSS, SVG and HTTP",
150
+ inputSchema: z2.object({
151
+ query: z2.string().describe(env.MDN_QUERY_DESCRIPTION)
152
+ })
153
+ }, async ({ query }) => {
154
+ llamaContext ??= await getLlamaContext(modelPath);
155
+ if (env.MDN_MODEL_TTL > 0) {
156
+ if (llamaTimeout !== null) {
157
+ clearTimeout(llamaTimeout);
158
+ }
159
+ llamaTimeout = setTimeout(() => {
160
+ llamaTimeout = null;
161
+ llamaContext?.dispose().catch(console.error);
162
+ llamaContext = null;
163
+ }, llamaTtl);
164
+ }
165
+ const results = await queryHybrid(llamaContext, table, reranker, query);
166
+ return {
167
+ content: results.map((result) => ({
168
+ type: "text",
169
+ text: result.text
170
+ }))
171
+ };
172
+ });
173
+ const transport = new StdioServerTransport;
174
+ await server.connect(transport);
175
+ };
176
+
177
+ // src/index.ts
178
+ switch (process.argv[2]) {
179
+ case "download": {
180
+ const locale = process.argv[3] ?? env.MDN_DATASET_LOCALE;
181
+ await downloadDataset(locale);
182
+ await downloadModel();
183
+ break;
184
+ }
185
+ case "server": {
186
+ await startMcpServer();
187
+ break;
188
+ }
189
+ default: {
190
+ console.error('Unknown or missing command, use "download" or "server"');
191
+ process.exit(1);
192
+ }
193
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@deepsweet/mdn",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "bin": "dist/index.js",
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist/"
12
+ ],
13
+ "dependencies": {
14
+ "@huggingface/hub": "^2.11.0",
15
+ "@lancedb/lancedb": "0.27.2-beta.1",
16
+ "@modelcontextprotocol/sdk": "^1.29.0",
17
+ "apache-arrow": "18.1.0",
18
+ "node-llama-cpp": "^3.18.1",
19
+ "zod": "^4.3.6"
20
+ },
21
+ "devDependencies": {
22
+ "@huggingface/tokenizers": "^0.1.3",
23
+ "@stylistic/eslint-plugin": "^5.10.0",
24
+ "@types/bun": "^1.3.11",
25
+ "dedent": "^1.7.2",
26
+ "eslint": "^10.1.0",
27
+ "eslint-plugin-import": "^2.32.0",
28
+ "eslint-plugin-perfectionist": "^5.7.0",
29
+ "globals": "^17.4.0",
30
+ "gray-matter": "^4.0.3",
31
+ "marked": "^17.0.5",
32
+ "rimraf": "^6.1.3",
33
+ "typescript": "^6.0.2",
34
+ "typescript-eslint": "^8.58.0"
35
+ },
36
+ "scripts": {
37
+ "chunk": "bun scripts/chunk.ts",
38
+ "ingest": "bun scripts/ingest.ts",
39
+ "query": "bun scripts/query.ts",
40
+ "check": "tsc --noEmit && eslint --cache scripts/ src/",
41
+ "dist": "bun build --format esm --target node --packages external --outdir dist/ src/index.ts"
42
+ },
43
+ "license": "MIT"
44
+ }