harmonyos-best-practices-mcp 0.1.0 → 0.2.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/README.md +32 -5
- package/dist/data.js +136 -0
- package/dist/index.js +93 -33
- package/dist/search.js +4 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HarmonyOS 最佳实践 MCP 服务器
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把鸿蒙应用开发最佳实践资料通过 MCP 暴露为检索工具,供 Claude Code / Cursor / Cline 等 MCP 客户端在开发时动态调用。
|
|
4
4
|
|
|
5
5
|
**文档(9MB)随包发布,装包即用、零配置。** 代码(8GB)不随包,按需配置本地路径(见下)。
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|------|------|
|
|
11
11
|
| `search_best_practices({query, limit?})` | 全文检索文档(中文友好),返回相关度排序的文档列表(含主题、是否有代码、代码仓库名) |
|
|
12
12
|
| `get_doc({name})` | 读取指定文档(docId)的完整 Markdown 正文 |
|
|
13
|
-
| `get_code_example({docName})` | 返回文档关联的参考代码:本地仓库绝对路径、远程 URL
|
|
13
|
+
| `get_code_example({docName})` | 返回文档关联的参考代码:本地仓库绝对路径、远程 URL、README 简介、入口 `.ets/.ts` 文件(带用途注释) |
|
|
14
14
|
| `list_by_topic({topic?})` | 按大类浏览(稳定性/性能/媒体/功耗/一多…);省略参数返回所有大类及文档数 |
|
|
15
15
|
|
|
16
16
|
## 安装(最终用户)
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
无需 clone 本仓库。直接用 npx 或全局安装:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
# 方式一:一次性运行(
|
|
22
|
-
npx harmonyos-best-practices-mcp
|
|
21
|
+
# 方式一:一次性运行(推荐,每次自动拉最新版)
|
|
22
|
+
npx -y harmonyos-best-practices-mcp
|
|
23
23
|
|
|
24
|
-
# 方式二:全局安装
|
|
24
|
+
# 方式二:全局安装(需手动更新)
|
|
25
25
|
npm install -g harmonyos-best-practices-mcp
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -90,6 +90,33 @@ Claude Code 配置加 env:
|
|
|
90
90
|
| `BP_INDEX` | 包内 `data/index.md` | 索引文件(一般无需改) |
|
|
91
91
|
| `BP_CODE_DIR` | 空 | 本地代码根目录;为空时 `get_code_example` 只给 URL |
|
|
92
92
|
|
|
93
|
+
## 更新
|
|
94
|
+
|
|
95
|
+
文档和服务器会持续更新(版本号见 `package.json`)。
|
|
96
|
+
|
|
97
|
+
**更新服务器**:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# npx -y 方式:无需手动操作,每次启动自动拉最新版
|
|
101
|
+
# 全局安装方式:手动更新
|
|
102
|
+
npm update -g harmonyos-best-practices-mcp
|
|
103
|
+
# 或锁定最新版
|
|
104
|
+
npm install -g harmonyos-best-practices-mcp@latest
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
更新后**重启 AI 客户端**(Claude Code / Cursor / Cline 等),让新进程加载新版 MCP。
|
|
108
|
+
|
|
109
|
+
**更新文档**:文档随包内置(`data/docs/`),更新 npm 包即同步更新 452 篇文档,无需单独操作。
|
|
110
|
+
|
|
111
|
+
**更新代码包**(仅当启用了本地代码读取):GitHub Release 有新版 `harmonyos-best-practices-code.tar.gz` 时,重新下载解压覆盖 `BP_CODE_DIR` 指向的目录即可。
|
|
112
|
+
|
|
113
|
+
**查看版本**:
|
|
114
|
+
```bash
|
|
115
|
+
npm view harmonyos-best-practices-mcp version # 最新发布版
|
|
116
|
+
npm ls -g harmonyos-best-practices-mcp # 本地已装版本
|
|
117
|
+
```
|
|
118
|
+
或看客户端 MCP 面板里服务器的 `version` 字段。
|
|
119
|
+
|
|
93
120
|
## 开发与发布(维护者)
|
|
94
121
|
|
|
95
122
|
```bash
|
package/dist/data.js
CHANGED
|
@@ -67,6 +67,8 @@ function parseIndex(indexFile) {
|
|
|
67
67
|
subtitle: "",
|
|
68
68
|
codeRefs: [],
|
|
69
69
|
hasCode: false,
|
|
70
|
+
readmeDigest: "",
|
|
71
|
+
headings: "",
|
|
70
72
|
};
|
|
71
73
|
continue;
|
|
72
74
|
}
|
|
@@ -134,6 +136,8 @@ function parseCrawlLog(docsDir, docs) {
|
|
|
134
136
|
subtitle,
|
|
135
137
|
codeRefs: [],
|
|
136
138
|
hasCode: false,
|
|
139
|
+
readmeDigest: "",
|
|
140
|
+
headings: "",
|
|
137
141
|
});
|
|
138
142
|
}
|
|
139
143
|
}
|
|
@@ -158,9 +162,141 @@ export function getStore() {
|
|
|
158
162
|
arr.push(meta.docId);
|
|
159
163
|
topics.set(meta.topic, arr);
|
|
160
164
|
}
|
|
165
|
+
// Preload a short README digest for each cloned repo, used for search scoring.
|
|
166
|
+
// Only read when codeDir is configured; otherwise leave empty (search still works on doc text).
|
|
167
|
+
if (codeDir) {
|
|
168
|
+
for (const meta of docs.values()) {
|
|
169
|
+
if (!meta.hasCode)
|
|
170
|
+
continue;
|
|
171
|
+
const digests = [];
|
|
172
|
+
for (const ref of meta.codeRefs) {
|
|
173
|
+
if (ref.status !== "cloned" || !ref.localPath)
|
|
174
|
+
continue;
|
|
175
|
+
const repoAbs = resolveCodePath(codeDir, ref.localPath);
|
|
176
|
+
const intro = readReadmeIntro(repoAbs);
|
|
177
|
+
if (intro)
|
|
178
|
+
digests.push(intro);
|
|
179
|
+
}
|
|
180
|
+
meta.readmeDigest = digests.join(" \n ");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Preload all markdown headings for each doc — rich section-level signal for search.
|
|
184
|
+
for (const meta of docs.values()) {
|
|
185
|
+
meta.headings = extractHeadings(docsDir, meta.docId);
|
|
186
|
+
}
|
|
161
187
|
_store = { docs, topics, docsDir, codeDir };
|
|
162
188
|
return _store;
|
|
163
189
|
}
|
|
190
|
+
/** Extract all markdown heading texts from a doc, joined by space. Light & full-text. */
|
|
191
|
+
export function extractHeadings(docsDir, docId) {
|
|
192
|
+
const file = path.join(docsDir, `${docId}.md`);
|
|
193
|
+
let text;
|
|
194
|
+
try {
|
|
195
|
+
text = fs.readFileSync(file, "utf8");
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return "";
|
|
199
|
+
}
|
|
200
|
+
const heads = [];
|
|
201
|
+
for (const line of text.split(/\r?\n/)) {
|
|
202
|
+
const m = line.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
203
|
+
if (m) {
|
|
204
|
+
// strip inline markdown noise (**bold**, `code`, links)
|
|
205
|
+
const clean = m[1]
|
|
206
|
+
.replace(/\*\*/g, "")
|
|
207
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
208
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
|
|
209
|
+
.trim();
|
|
210
|
+
if (clean)
|
|
211
|
+
heads.push(clean);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return heads.join(" ");
|
|
215
|
+
}
|
|
216
|
+
/** Read the intro section (project overview) of a repo's README.md, ~300 chars. */
|
|
217
|
+
export function readReadmeIntro(repoAbs) {
|
|
218
|
+
const file = path.join(repoAbs, "README.md");
|
|
219
|
+
let text;
|
|
220
|
+
try {
|
|
221
|
+
text = fs.readFileSync(file, "utf8");
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
// Try to isolate the "项目简介/Overview/简介" section: from that heading up to the
|
|
227
|
+
// next section (效果预览/使用说明/How to Use/Effect/工程目录...).
|
|
228
|
+
const startMatch = text.match(/(?:^|\n)#+\s*(项目简介|简介|Overview|Project Overview|介绍)\s*\n/i);
|
|
229
|
+
let body;
|
|
230
|
+
if (startMatch) {
|
|
231
|
+
const after = text.slice((startMatch.index ?? 0) + startMatch[0].length);
|
|
232
|
+
const endMatch = after.match(/\n#+\s*(效果预览|使用说明|工程目录|Project Directory|How to Use|Effect|具体实现|相关概念|目录结构)/i);
|
|
233
|
+
body = endMatch ? after.slice(0, endMatch.index) : after;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// No intro heading: take everything before the first ## section after the title.
|
|
237
|
+
const lines = text.split(/\r?\n/);
|
|
238
|
+
body = lines.slice(1, 40).join("\n");
|
|
239
|
+
}
|
|
240
|
+
const cleaned = body
|
|
241
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
242
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
|
|
243
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
|
|
244
|
+
.replace(/^#+\s*/gm, "")
|
|
245
|
+
.replace(/[*_`>]/g, "")
|
|
246
|
+
.replace(/\s+/g, " ")
|
|
247
|
+
.trim();
|
|
248
|
+
return cleaned.slice(0, 300);
|
|
249
|
+
}
|
|
250
|
+
/** Read a repo's README.md full text (for get_code_example). Null if missing. */
|
|
251
|
+
export function readRepoReadme(repoAbs) {
|
|
252
|
+
const file = path.join(repoAbs, "README.md");
|
|
253
|
+
if (!fs.existsSync(file))
|
|
254
|
+
return null;
|
|
255
|
+
try {
|
|
256
|
+
return fs.readFileSync(file, "utf8");
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Parse the "工程目录 / Project Directory" tree from a README, returning a map of
|
|
264
|
+
* basename -> purpose comment (e.g. "VideoList.ets" -> "视频列表组件").
|
|
265
|
+
* Returns empty map if no tree found.
|
|
266
|
+
*/
|
|
267
|
+
export function parseReadmeTree(readme) {
|
|
268
|
+
const out = new Map();
|
|
269
|
+
if (!readme)
|
|
270
|
+
return out;
|
|
271
|
+
// Find the directory-tree code block: usually after "工程目录"/"Project Directory"/"目录结构".
|
|
272
|
+
const marker = readme.match(/(工程目录|Project Directory|目录结构|目录说明)[^\n]*\n/i);
|
|
273
|
+
let start = marker ? marker.index + marker[0].length : -1;
|
|
274
|
+
let block = "";
|
|
275
|
+
if (start >= 0) {
|
|
276
|
+
// Take the fenced ``` ... ``` block that follows, or indented tree lines.
|
|
277
|
+
const after = readme.slice(start);
|
|
278
|
+
const fence = after.match(/```[\s\S]*?```/);
|
|
279
|
+
if (fence) {
|
|
280
|
+
block = fence[0].replace(/```/g, "");
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// fallback: lines that look like tree (contain ├── or │ or //)
|
|
284
|
+
const lines = after.split(/\r?\n/).filter((l) => /^[│├└\s]*[─-]/.test(l) || l.includes("//"));
|
|
285
|
+
block = lines.join("\n");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (!block)
|
|
289
|
+
return out;
|
|
290
|
+
// Each tree line: optional tree chars, a path/file, optional "// purpose"
|
|
291
|
+
for (const line of block.split(/\r?\n/)) {
|
|
292
|
+
const m = line.match(/([A-Za-z0-9_\-./]+\.(?:ets|ts))\s*\/\/\s*(.+?)\s*$/);
|
|
293
|
+
if (m) {
|
|
294
|
+
const base = m[1].split("/").pop().trim();
|
|
295
|
+
out.set(base, m[2].trim());
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
164
300
|
/** Read a doc's full markdown body. Returns null if missing. */
|
|
165
301
|
export function readDoc(docsDir, docId) {
|
|
166
302
|
const file = path.join(docsDir, `${docId}.md`);
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { getStore, readDoc, resolveCodePath } from "./data.js";
|
|
7
|
+
import { getStore, readDoc, resolveCodePath, readRepoReadme, readReadmeIntro, parseReadmeTree, } from "./data.js";
|
|
8
8
|
import { search } from "./search.js";
|
|
9
9
|
const store = getStore();
|
|
10
10
|
const server = new McpServer({
|
|
@@ -79,11 +79,20 @@ function formatCodeRef(ref) {
|
|
|
79
79
|
const exists = fs.existsSync(abs);
|
|
80
80
|
lines.push(` 本地: ${abs}${exists ? "" : " ⚠️ 路径在磁盘上不存在(检查 BP_CODE_DIR 或自行克隆该仓库)"}`);
|
|
81
81
|
if (exists) {
|
|
82
|
+
// README 简介 + 工程目录树(若有),帮助快速理解仓库用途与文件分工.
|
|
83
|
+
const intro = readReadmeIntro(abs);
|
|
84
|
+
if (intro)
|
|
85
|
+
lines.push(` 简介: ${intro}`);
|
|
86
|
+
const readme = readRepoReadme(abs);
|
|
87
|
+
const tree = readme ? parseReadmeTree(readme) : new Map();
|
|
82
88
|
const entries = listEntryFiles(abs);
|
|
83
89
|
if (entries.length) {
|
|
84
90
|
lines.push(` 入口文件:`);
|
|
85
|
-
for (const e of entries)
|
|
86
|
-
|
|
91
|
+
for (const e of entries) {
|
|
92
|
+
const base = path.basename(e);
|
|
93
|
+
const purpose = tree.get(base);
|
|
94
|
+
lines.push(purpose ? ` - ${e} // ${purpose}` : ` - ${e}`);
|
|
95
|
+
}
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
return lines.join("\n");
|
|
@@ -91,50 +100,102 @@ function formatCodeRef(ref) {
|
|
|
91
100
|
function statusText(s) {
|
|
92
101
|
return s === "cloned" ? "✅ 已克隆" : s === "skipped" ? "⏭️ 核心仓跳过" : "❌ 缺失";
|
|
93
102
|
}
|
|
94
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Find up to 8 entry source files (.ets/.ts) in a repo.
|
|
105
|
+
* Strategy: try a list of well-known source dirs (pages > view/components > ets),
|
|
106
|
+
* each scanned recursively; if none yield files, walk the whole repo (skipping
|
|
107
|
+
* noise dirs) so unusual module layouts (library/, features/, products/) still work.
|
|
108
|
+
*/
|
|
95
109
|
function listEntryFiles(repoAbs) {
|
|
110
|
+
const LIMIT = 8;
|
|
111
|
+
// Ordered: prefer pages/, then view/components, then the ets root of each module.
|
|
112
|
+
// Covers entry/, library/, features/<x>/, products/<phone>/ style layouts.
|
|
96
113
|
const candidates = [
|
|
97
114
|
"entry/src/main/ets/pages",
|
|
115
|
+
"entry/src/main/ets/view",
|
|
116
|
+
"entry/src/main/ets/views",
|
|
117
|
+
"entry/src/main/ets/components",
|
|
98
118
|
"entry/src/main/ets",
|
|
119
|
+
"library/src/main/ets/pages",
|
|
120
|
+
"library/src/main/ets",
|
|
99
121
|
];
|
|
100
122
|
for (const c of candidates) {
|
|
101
|
-
const dir = path.join(repoAbs, c
|
|
102
|
-
if (fs.existsSync(dir)) {
|
|
103
|
-
const files = listDirFiles(dir, [".ets", ".ts"],
|
|
123
|
+
const dir = path.join(repoAbs, c);
|
|
124
|
+
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
|
|
125
|
+
const files = listDirFiles(dir, [".ets", ".ts"], LIMIT);
|
|
104
126
|
if (files.length)
|
|
105
127
|
return files;
|
|
106
128
|
}
|
|
107
129
|
}
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
// Fallback: walk the whole repo, skipping noise. Deep enough for multi-module layouts.
|
|
131
|
+
return walkSources(repoAbs, LIMIT, 8);
|
|
132
|
+
}
|
|
133
|
+
/** True for config/test/build noise files we don't want as "entry" examples. */
|
|
134
|
+
function isNoiseFile(name) {
|
|
135
|
+
const lower = name.toLowerCase();
|
|
136
|
+
if (lower.startsWith("hvigorfile"))
|
|
137
|
+
return true;
|
|
138
|
+
if (lower.endsWith(".test.ts") || lower.endsWith(".test.ets"))
|
|
139
|
+
return true;
|
|
140
|
+
if (lower === "build-profile.ts")
|
|
141
|
+
return true;
|
|
142
|
+
if (lower === "oh-package.json5.ts")
|
|
143
|
+
return true;
|
|
144
|
+
return false;
|
|
114
145
|
}
|
|
146
|
+
/** Rank: pages/ > view/components > Index.ets > other .ets > .ts. Lower = better. */
|
|
147
|
+
function fileRank(fullPath) {
|
|
148
|
+
const norm = fullPath.replace(/\\/g, "/").toLowerCase();
|
|
149
|
+
if (norm.includes("/pages/"))
|
|
150
|
+
return 0;
|
|
151
|
+
if (norm.includes("/view/") || norm.includes("/views/"))
|
|
152
|
+
return 1;
|
|
153
|
+
if (norm.includes("/components/"))
|
|
154
|
+
return 2;
|
|
155
|
+
if (norm.endsWith("/index.ets"))
|
|
156
|
+
return 3;
|
|
157
|
+
if (norm.endsWith(".ets"))
|
|
158
|
+
return 4;
|
|
159
|
+
return 5; // .ts
|
|
160
|
+
}
|
|
161
|
+
/** Recursively collect .ets/.ts source files under dir, skipping noise dirs/files. */
|
|
115
162
|
function listDirFiles(dir, exts, limit) {
|
|
116
|
-
let ents;
|
|
117
|
-
try {
|
|
118
|
-
ents = fs.readdirSync(dir);
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return [];
|
|
122
|
-
}
|
|
123
163
|
const out = [];
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
164
|
+
const visit = (d) => {
|
|
165
|
+
if (out.length >= 64)
|
|
166
|
+
return; // collect a pool then rank+trim
|
|
167
|
+
let ents;
|
|
168
|
+
try {
|
|
169
|
+
ents = fs.readdirSync(d, { withFileTypes: true });
|
|
129
170
|
}
|
|
130
|
-
|
|
131
|
-
|
|
171
|
+
catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const e of ents) {
|
|
175
|
+
if (e.isDirectory()) {
|
|
176
|
+
if (SKIP_DIRS.has(e.name))
|
|
177
|
+
continue;
|
|
178
|
+
visit(path.join(d, e.name));
|
|
179
|
+
}
|
|
180
|
+
else if (e.isFile() && exts.some((x) => e.name.endsWith(x)) && !isNoiseFile(e.name)) {
|
|
181
|
+
out.push(path.join(d, e.name));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
visit(dir);
|
|
186
|
+
out.sort((a, b) => fileRank(a) - fileRank(b) || a.localeCompare(b));
|
|
187
|
+
return out.slice(0, limit);
|
|
132
188
|
}
|
|
133
|
-
const SKIP_DIRS = new Set([
|
|
189
|
+
const SKIP_DIRS = new Set([
|
|
190
|
+
"resources", "build", "oh_modules", "node_modules", ".git", ".cxx",
|
|
191
|
+
"cpp", "libs", "rawfile", "media", "drawable", "element",
|
|
192
|
+
"test", "ohosTest", "testrunner", ".test", ".hvigor", ".idea",
|
|
193
|
+
"commons", "hvigor", "scripts",
|
|
194
|
+
]);
|
|
134
195
|
function walkSources(dir, limit, maxDepth) {
|
|
135
196
|
const out = [];
|
|
136
197
|
const visit = (d, depth) => {
|
|
137
|
-
if (out.length >=
|
|
198
|
+
if (out.length >= 64 || depth > maxDepth)
|
|
138
199
|
return;
|
|
139
200
|
let ents;
|
|
140
201
|
try {
|
|
@@ -144,20 +205,19 @@ function walkSources(dir, limit, maxDepth) {
|
|
|
144
205
|
return;
|
|
145
206
|
}
|
|
146
207
|
for (const e of ents) {
|
|
147
|
-
if (out.length >= limit)
|
|
148
|
-
return;
|
|
149
208
|
if (e.isDirectory()) {
|
|
150
209
|
if (SKIP_DIRS.has(e.name))
|
|
151
210
|
continue;
|
|
152
211
|
visit(path.join(d, e.name), depth + 1);
|
|
153
212
|
}
|
|
154
|
-
else if (e.isFile() && (e.name.endsWith(".ets") || e.name.endsWith(".ts"))) {
|
|
213
|
+
else if (e.isFile() && (e.name.endsWith(".ets") || e.name.endsWith(".ts")) && !isNoiseFile(e.name)) {
|
|
155
214
|
out.push(path.join(d, e.name));
|
|
156
215
|
}
|
|
157
216
|
}
|
|
158
217
|
};
|
|
159
218
|
visit(dir, 0);
|
|
160
|
-
|
|
219
|
+
out.sort((a, b) => fileRank(a) - fileRank(b) || a.localeCompare(b));
|
|
220
|
+
return out.slice(0, limit);
|
|
161
221
|
}
|
|
162
222
|
/* ------------------------------------------------------------------ *
|
|
163
223
|
* Tool 4: list_by_topic
|
package/dist/search.js
CHANGED
|
@@ -58,6 +58,10 @@ export function search(store, query, limit = 8) {
|
|
|
58
58
|
score += scoreAgainst(qTf, tokenize(meta.title), 5);
|
|
59
59
|
score += scoreAgainst(qTf, tokenize(meta.subtitle), 3);
|
|
60
60
|
score += scoreAgainst(qTf, tokenize(meta.topic), 2);
|
|
61
|
+
// All markdown headings — section-level signal, covers full doc (not just first 200 lines).
|
|
62
|
+
score += scoreAgainst(qTf, tokenize(meta.headings), 3);
|
|
63
|
+
// README intro of associated repos — rich signal (APIs used, what it does).
|
|
64
|
+
score += scoreAgainst(qTf, tokenize(meta.readmeDigest), 2);
|
|
61
65
|
// Body scan (first ~200 lines) for recall. Lazy-read per candidate doc.
|
|
62
66
|
const bodyScore = scanBody(store.docsDir, meta.docId, qTf, 200);
|
|
63
67
|
score += bodyScore;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harmonyos-best-practices-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server exposing HarmonyOS best-practices docs
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server exposing HarmonyOS best-practices docs + reference code for retrieval during development.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|