simple-dynamsoft-mcp 7.3.54 → 7.4.1

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.
@@ -61,110 +61,132 @@ export function createMcpServerInstance({ pkgVersion, resourceIndexApi, ragApi }
61
61
  refreshRagIndexes
62
62
  });
63
63
 
64
- const server = new McpServer({
65
- name: "simple-dynamsoft-mcp",
66
- version: pkgVersion,
67
- description: "MCP server for the public Dynamsoft offerings: Barcode Reader, Dynamic Web TWAIN, Document Viewer, MRZ, and MDS. Includes guidance for choosing the right public product by workflow."
68
- });
64
+ const server = new McpServer({
65
+ name: "simple-dynamsoft-mcp",
66
+ version: pkgVersion,
67
+ description: "MCP server for the public Dynamsoft offerings: Barcode Reader, Dynamic Web TWAIN, Document Viewer, MRZ, and MDS. Includes guidance for choosing the right public product by workflow."
68
+ }, {
69
+ instructions: [
70
+ "This server answers questions about five public Dynamsoft offerings: DBR, DWT, DDV, MRZ, MDS.",
71
+ "",
72
+ "How to use it:",
73
+ "- Call get_index first to see valid product/edition/platform combinations before asking the user anything.",
74
+ "- Only ask the user to disambiguate dimensions that actually vary for the chosen product. Do NOT ask about dimensions that don't apply.",
75
+ "",
76
+ "Platform/edition rules (important):",
77
+ "- DBR (Dynamsoft Barcode Reader) is the ONLY product with multiple editions: mobile, web, and server/desktop. Ask which one when it is unclear.",
78
+ "- DWT (Dynamic Web TWAIN), DDV (Document Viewer), MRZ (MRZ Scanner), and MDS (Mobile Document Scanner) are web/JavaScript-only in this MCP. NEVER ask the user which platform or language for these — there is no .NET/Java/C++/mobile option here. Go straight to the web quickstart or samples.",
79
+ "",
80
+ "api_level rule:",
81
+ "- api_level (high-level / low-level) applies ONLY to DBR mobile. Never ask about it, or pass it, for web, server, or any product other than DBR mobile.",
82
+ "",
83
+ "For scopes this MCP does not index (e.g. MRZ/MDS on mobile or server), tools return official documentation and sample links — pass those on rather than inventing code."
84
+ ].join("\n")
85
+ });
69
86
 
70
- registerIndexTools({
71
- server,
72
- ensureScopeHydrated,
73
- ensureLatestMajor,
74
- normalizeProduct,
75
- normalizePlatform,
76
- normalizeEdition,
77
- buildIndexData,
78
- getSampleIdFromUri,
79
- formatScopeLabel,
80
- searchResources,
81
- normalizeSampleName,
82
- getSampleEntries,
83
- getSampleSuggestions
84
- });
87
+ registerIndexTools({
88
+ server,
89
+ ensureScopeHydrated,
90
+ ensureLatestMajor,
91
+ normalizeProduct,
92
+ normalizePlatform,
93
+ normalizeEdition,
94
+ buildIndexData,
95
+ getSampleIdFromUri,
96
+ formatScopeLabel,
97
+ searchResources,
98
+ normalizeSampleName,
99
+ getSampleEntries,
100
+ getSampleSuggestions
101
+ });
85
102
 
86
- registerSampleTools({
87
- server,
88
- registry,
89
- ensureScopeHydrated,
90
- ensureLatestMajor,
91
- normalizeProduct,
92
- normalizePlatform,
93
- normalizeEdition,
94
- getSampleEntries,
95
- getSampleIdFromUri,
96
- getDisplayEdition,
97
- getDisplayPlatform
98
- });
103
+ registerSampleTools({
104
+ server,
105
+ registry,
106
+ ensureScopeHydrated,
107
+ ensureLatestMajor,
108
+ normalizeProduct,
109
+ normalizePlatform,
110
+ normalizeEdition,
111
+ getSampleEntries,
112
+ getSampleIdFromUri,
113
+ getDisplayEdition,
114
+ getDisplayPlatform
115
+ });
99
116
 
100
- registerVersionTools({
101
- server,
102
- ensureLatestMajor,
103
- normalizeProduct,
104
- normalizePlatform,
105
- normalizeEdition,
106
- LATEST_MAJOR,
107
- LATEST_VERSIONS
108
- });
117
+ registerVersionTools({
118
+ server,
119
+ ensureLatestMajor,
120
+ normalizeProduct,
121
+ normalizePlatform,
122
+ normalizeEdition,
123
+ LATEST_MAJOR,
124
+ LATEST_VERSIONS
125
+ });
109
126
 
110
- registerQuickstartTools({
111
- server,
112
- registry,
113
- ensureScopeHydrated,
114
- ensureLatestMajor,
115
- normalizeProduct,
116
- normalizePlatform,
117
- normalizeEdition,
118
- normalizeApiLevel,
119
- discoverDcvMobileSamples,
120
- discoverDcvWebSamples,
121
- findCodeFilesInSample,
122
- getMobileSamplePath,
123
- getDbrServerSamplePath,
124
- getDcvMobileSamplePath,
125
- getDcvServerSamplePath,
126
- getDcvWebSamplePath,
127
- getDwtSamplePath,
128
- getDdvSamplePath,
129
- readCodeFile,
130
- getMainCodeFile,
131
- getWebSamplePath
132
- });
127
+ registerQuickstartTools({
128
+ server,
129
+ registry,
130
+ ensureScopeHydrated,
131
+ ensureLatestMajor,
132
+ normalizeProduct,
133
+ normalizePlatform,
134
+ normalizeEdition,
135
+ normalizeApiLevel,
136
+ discoverDcvMobileSamples,
137
+ discoverDcvWebSamples,
138
+ findCodeFilesInSample,
139
+ getMobileSamplePath,
140
+ getDbrServerSamplePath,
141
+ getDcvMobileSamplePath,
142
+ getDcvServerSamplePath,
143
+ getDcvWebSamplePath,
144
+ getDwtSamplePath,
145
+ getDdvSamplePath,
146
+ readCodeFile,
147
+ getMainCodeFile,
148
+ getWebSamplePath,
149
+ getMrzWebSamplePath,
150
+ getMdsWebSamplePath,
151
+ getSampleSuggestions,
152
+ getSampleIdFromUri,
153
+ formatScopeLabel
154
+ });
133
155
 
134
- registerProjectTools({
135
- server,
136
- ensureScopeHydrated,
137
- ensureLatestMajor,
138
- normalizeProduct,
139
- normalizePlatform,
140
- normalizeEdition,
141
- normalizeApiLevel,
142
- normalizeSampleName,
143
- parseResourceUri,
144
- parseSampleUri,
145
- formatScopeLabel,
146
- getSampleIdFromUri,
147
- discoverDwtSamples,
148
- getMobileSamplePath,
149
- getWebSamplePath,
150
- getDbrServerSamplePath,
151
- getDcvMobileSamplePath,
152
- getDcvServerSamplePath,
153
- getDcvWebSamplePath,
154
- getMrzWebSamplePath,
155
- getMdsWebSamplePath,
156
- getDwtSamplePath,
157
- getDdvSamplePath,
158
- getSampleSuggestions
159
- });
156
+ registerProjectTools({
157
+ server,
158
+ ensureScopeHydrated,
159
+ ensureLatestMajor,
160
+ normalizeProduct,
161
+ normalizePlatform,
162
+ normalizeEdition,
163
+ normalizeApiLevel,
164
+ normalizeSampleName,
165
+ parseResourceUri,
166
+ parseSampleUri,
167
+ formatScopeLabel,
168
+ getSampleIdFromUri,
169
+ discoverDwtSamples,
170
+ getMobileSamplePath,
171
+ getWebSamplePath,
172
+ getDbrServerSamplePath,
173
+ getDcvMobileSamplePath,
174
+ getDcvServerSamplePath,
175
+ getDcvWebSamplePath,
176
+ getMrzWebSamplePath,
177
+ getMdsWebSamplePath,
178
+ getDwtSamplePath,
179
+ getDdvSamplePath,
180
+ getSampleSuggestions
181
+ });
160
182
 
161
- registerResourceHandlers({
162
- server,
163
- getPinnedResources,
164
- parseResourceUri,
165
- ensureLatestMajor,
166
- readResourceContent
167
- });
183
+ registerResourceHandlers({
184
+ server,
185
+ getPinnedResources,
186
+ parseResourceUri,
187
+ ensureLatestMajor,
188
+ readResourceContent
189
+ });
168
190
 
169
- return server;
191
+ return server;
170
192
  }
@@ -306,6 +306,79 @@ function isWebPlatform(platform) {
306
306
  return platform === "web" || isWebFrameworkPlatform(platform);
307
307
  }
308
308
 
309
+ // Canonical platform values a caller may pass. Derived from the alias table so
310
+ // this stays in sync automatically as platforms are added (issue #152).
311
+ // "core" is a valid (DCV-internal) platform advertised in tool descriptions but
312
+ // isn't an alias value, so add it explicitly to keep the validator consistent
313
+ // with what the schema tells agents to pass.
314
+ const KNOWN_PLATFORMS = new Set([...Object.values(platformAliases), "core"]);
315
+ const KNOWN_EDITIONS = new Set(["core", "mobile", "web", "server"]);
316
+
317
+ function nearestKnownKey(value, candidates) {
318
+ const v = String(value || "").toLowerCase();
319
+ if (!v) return "";
320
+ // Prefix / substring match first (cheap, catches "reactnative" -> "react-native").
321
+ let best = "";
322
+ let bestScore = Infinity;
323
+ for (const candidate of candidates) {
324
+ const c = candidate.toLowerCase();
325
+ if (c === v) return candidate;
326
+ const stripped = v.replace(/[-_.\s]/g, "");
327
+ const cStripped = c.replace(/[-_.\s]/g, "");
328
+ if (cStripped === stripped || cStripped.startsWith(stripped) || stripped.startsWith(cStripped)) {
329
+ return candidate;
330
+ }
331
+ // Fallback: length-difference heuristic as a light distance proxy.
332
+ if (cStripped.includes(stripped) || stripped.includes(cStripped)) {
333
+ const score = Math.abs(cStripped.length - stripped.length);
334
+ if (score < bestScore) {
335
+ bestScore = score;
336
+ best = candidate;
337
+ }
338
+ }
339
+ }
340
+ return best;
341
+ }
342
+
343
+ function validatePlatform(value) {
344
+ if (value === undefined || value === null || value === "") {
345
+ return { ok: true, normalized: "" };
346
+ }
347
+ const normalized = normalizePlatform(value);
348
+ if (KNOWN_PLATFORMS.has(normalized)) {
349
+ return { ok: true, normalized };
350
+ }
351
+ const valid = [...KNOWN_PLATFORMS].sort();
352
+ const suggestion = nearestKnownKey(value, [...Object.keys(platformAliases), ...KNOWN_PLATFORMS]);
353
+ const suggestionText = suggestion ? ` Did you mean "${normalizePlatform(suggestion) || suggestion}"?` : "";
354
+ return {
355
+ ok: false,
356
+ normalized,
357
+ message: `Unknown platform "${value}". Valid: ${valid.join(", ")}.${suggestionText}`
358
+ };
359
+ }
360
+
361
+ function validateEdition(value) {
362
+ if (value === undefined || value === null || value === "") {
363
+ return { ok: true, normalized: "" };
364
+ }
365
+ // Normalize first so documented aliases the tool schema advertises
366
+ // ("server/desktop", "desktop") and platform-implied editions ("python")
367
+ // are accepted rather than rejected as unknown.
368
+ const normalized = normalizeEdition(value);
369
+ if (KNOWN_EDITIONS.has(normalized)) {
370
+ return { ok: true, normalized };
371
+ }
372
+ const valid = [...KNOWN_EDITIONS];
373
+ const suggestion = nearestKnownKey(value, valid);
374
+ const suggestionText = suggestion ? ` Did you mean "${suggestion}"?` : "";
375
+ return {
376
+ ok: false,
377
+ normalized,
378
+ message: `Unknown edition "${value}". Valid: ${valid.join(", ")} (aliases like server/desktop, desktop are accepted).${suggestionText}`
379
+ };
380
+ }
381
+
309
382
  function inferProductFromQuery(query) {
310
383
  if (!query) return "";
311
384
  const normalized = query.toLowerCase();
@@ -347,5 +420,9 @@ export {
347
420
  isServerPlatform,
348
421
  isWebFrameworkPlatform,
349
422
  isWebPlatform,
350
- inferProductFromQuery
423
+ inferProductFromQuery,
424
+ KNOWN_PLATFORMS,
425
+ KNOWN_EDITIONS,
426
+ validatePlatform,
427
+ validateEdition
351
428
  };
@@ -35,6 +35,14 @@ const HYDRATION_PRODUCT_MAP = {
35
35
 
36
36
  const PUBLIC_OFFERINGS_TEXT = "dbr, dwt, ddv, mrz, or mds";
37
37
 
38
+ // Shared lineup-policy wording for tool descriptions and input-schema notes.
39
+ // Single source: when the lineup changes (e.g. a product gains an edition),
40
+ // edit here and the long-form server `instructions` in create-server.js.
41
+ const WEB_ONLY_OMIT_NOTE = "DWT/DDV/MRZ/MDS are web/JS-only (omit for them).";
42
+ const DBR_ONLY_EDITIONS_NOTE = `Only DBR has multiple editions; ${WEB_ONLY_OMIT_NOTE}`;
43
+ const API_LEVEL_NOTE = "high-level or low-level. DBR mobile ONLY; ignored for web/server/other products.";
44
+ const PRODUCT_SELECTION_GUIDANCE = "DBR is the only product with multiple editions (mobile/web/server). DWT, DDV, MRZ, and MDS are web/JavaScript-only here, so do not ask the user which platform or language for them. api_level (high-level/low-level) applies only to DBR mobile.";
45
+
38
46
  function normalizePublicOffering(product) {
39
47
  if (!product) return "";
40
48
  const normalized = product.trim().toLowerCase();
@@ -64,6 +72,10 @@ function buildUnknownPublicProductResponse(requestedProduct) {
64
72
  export {
65
73
  PUBLIC_OFFERING_PRODUCTS,
66
74
  PUBLIC_OFFERINGS_TEXT,
75
+ WEB_ONLY_OMIT_NOTE,
76
+ DBR_ONLY_EDITIONS_NOTE,
77
+ API_LEVEL_NOTE,
78
+ PRODUCT_SELECTION_GUIDANCE,
67
79
  normalizePublicOffering,
68
80
  getHydrationProduct,
69
81
  isKnownPublicOffering,
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import { extname, join } from "node:path";
3
3
  import { DDV_PREFERRED_ENTRY_FILES } from "./config.js";
4
- import { PUBLIC_OFFERING_PRODUCTS } from "../public-offerings.js";
4
+ import { PRODUCT_SELECTION_GUIDANCE, PUBLIC_OFFERING_PRODUCTS } from "../public-offerings.js";
5
5
 
6
6
  const MRZ_MATCHER = /(?:\bmrz\b|machine[-\s]?readable[-\s]?zone|passport)/i;
7
7
  const MDS_MATCHER = /(?:document[-\s]scan|document scanner|document scanning|document normalizer|document normalization|normaliz|auto[-\s]?capture|crop|cropping|deskew)/i;
@@ -36,18 +36,45 @@ function countDiscoveredSamplesByPlatforms(platforms, discoverSamplesForPlatform
36
36
  function getDcvScenarioTags(sampleName) {
37
37
  const normalized = String(sampleName || "").toLowerCase();
38
38
  const tags = [];
39
- if (normalized.includes("mrz")) {
39
+ if (normalized.includes("mrz") || normalized.includes("passport")) {
40
40
  tags.push("mrz", "passport", "id-card", "machine-readable-zone");
41
41
  }
42
- if (normalized.includes("driver") || normalized.includes("license")) {
42
+ if (normalized.includes("driver") || normalized.includes("license") || normalized.includes("licence") || normalized.includes("dl")) {
43
43
  tags.push("driver-license", "id-card", "dl", "aamva");
44
44
  }
45
- if (normalized.includes("document")) {
45
+ if (normalized.includes("document") || normalized.includes("normaliz") || normalized.includes("deskew") || normalized.includes("scan-a-document")) {
46
46
  tags.push("document-scan", "document-normalization", "auto-capture", "cropping", "deskew");
47
47
  }
48
48
  if (normalized.includes("gs1")) {
49
49
  tags.push("gs1", "application-identifiers", "ai");
50
50
  }
51
+ if (normalized.includes("qr")) {
52
+ tags.push("qr", "qrcode", "qr-code");
53
+ }
54
+ if (normalized.includes("pdf417") || normalized.includes("pdf-417")) {
55
+ tags.push("pdf417");
56
+ }
57
+ if (normalized.includes("datamatrix") || normalized.includes("data-matrix")) {
58
+ tags.push("datamatrix", "data-matrix");
59
+ }
60
+ if (normalized.includes("vin")) {
61
+ tags.push("vin", "vehicle-identification");
62
+ }
63
+ if (normalized.includes("pdf")) {
64
+ tags.push("pdf", "multi-page");
65
+ }
66
+ if (normalized.includes("image") || normalized.includes("file")) {
67
+ tags.push("image", "still-image", "file", "upload");
68
+ }
69
+ if (normalized.includes("camera") || normalized.includes("video") || normalized.includes("webcam") || normalized.includes("scan-a-single") || normalized.includes("scan-single")) {
70
+ tags.push("camera", "webcam", "video", "live");
71
+ }
72
+ if (normalized.includes("hello") || normalized.includes("getting-started") || normalized.includes("quickstart")) {
73
+ tags.push("hello-world", "getting-started", "quickstart");
74
+ }
75
+ if (normalized.includes("multiple") || normalized.includes("batch")) {
76
+ tags.push("multiple", "batch");
77
+ }
51
78
  return Array.from(new Set(tags));
52
79
  }
53
80
 
@@ -194,6 +221,16 @@ function addMarkdownDocResources({
194
221
  const platform = article.platform || defaultPlatform;
195
222
  const tags = [...baseTags, platform];
196
223
  if (article.breadcrumb) tags.push(...article.breadcrumb.toLowerCase().split(/\s*>\s*/));
224
+ // Fold frontmatter keywords into tags so domain-vocabulary search hits (#142).
225
+ if (Array.isArray(article.keywords)) {
226
+ for (const kw of article.keywords) {
227
+ const norm = String(kw).toLowerCase().trim();
228
+ if (norm && !tags.includes(norm)) tags.push(norm);
229
+ }
230
+ }
231
+ // Prefer the frontmatter description as the summary (distinct per doc) over
232
+ // the breadcrumb (identical across sibling docs) (#142).
233
+ const baseSummary = article.description || article.breadcrumb || defaultSummary;
197
234
 
198
235
  addPublicResourceToIndex(addResourceToIndex, {
199
236
  id: `${idPrefix}-${i}`,
@@ -204,12 +241,13 @@ function addMarkdownDocResources({
204
241
  platform,
205
242
  version,
206
243
  majorVersion,
244
+ url: article.url,
207
245
  title: product === "mrz" || product === "mds"
208
246
  ? rewritePublicTitle(article.title, product)
209
247
  : article.title,
210
248
  summary: product === "mrz" || product === "mds"
211
- ? rewritePublicSummary(article.breadcrumb || defaultSummary, product)
212
- : (article.breadcrumb || defaultSummary),
249
+ ? rewritePublicSummary(baseSummary, product)
250
+ : baseSummary,
213
251
  embedText: article.content,
214
252
  mimeType: "text/markdown",
215
253
  tags,
@@ -356,35 +394,47 @@ function buildIndexData({
356
394
 
357
395
  return {
358
396
  productSelection: {
397
+ guidance: PRODUCT_SELECTION_GUIDANCE,
359
398
  publicOfferings: [...PUBLIC_OFFERING_PRODUCTS],
360
399
  offerings: {
361
400
  dwt: {
362
401
  name: "Dynamic Web TWAIN",
363
402
  abbreviation: "DWT",
364
- whenToUse: ["Browser-based document acquisition and scanner control."]
403
+ webOnly: true,
404
+ platforms: ["js"],
405
+ whenToUse: ["Browser-based document acquisition and scanner control. Web/JavaScript only."]
365
406
  },
366
407
  ddv: {
367
408
  name: "Dynamsoft Document Viewer",
368
409
  abbreviation: "DDV",
369
- whenToUse: ["Standalone viewing plus extension paths for mobile, annotation, multi-page handling, and PDF output."]
410
+ webOnly: true,
411
+ platforms: ["js"],
412
+ whenToUse: ["Standalone viewing plus annotation, multi-page handling, and PDF output. Web/JavaScript only."]
370
413
  },
371
414
  dbr: {
372
415
  name: "Dynamsoft Barcode Reader",
373
416
  abbreviation: "DBR",
417
+ webOnly: false,
418
+ editions: ["mobile", "web", "server"],
374
419
  whenToUse: [
375
- "Barcode workflows across server/desktop, web, and mobile.",
376
- "Use the foundational API by default on web; BarcodeScanner RTU is a minimal-simplicity option; mobile supports both foundational API and BarcodeScanner RTU."
420
+ "Barcode workflows across server/desktop, web, and mobile. The only product with multiple editions.",
421
+ "Web uses a foundational-first flow (BarcodeScanner RTU also available); this is not selected via api_level.",
422
+ "api_level (high-level = BarcodeScanner RTU, low-level = foundational) applies only to DBR mobile."
377
423
  ]
378
424
  },
379
425
  mrz: {
380
426
  name: "MRZ Scanner",
381
427
  abbreviation: "MRZ",
382
- whenToUse: ["Passport and machine-readable-zone workflows on public web/mobile solution or RTU paths."]
428
+ webOnly: true,
429
+ platforms: ["js"],
430
+ whenToUse: ["Passport and machine-readable-zone workflows. Served here as web/JavaScript only; mobile/server scopes return official reference links."]
383
431
  },
384
432
  mds: {
385
433
  name: "Mobile Document Scanner",
386
434
  abbreviation: "MDS",
387
- whenToUse: ["Document scan and normalization workflows on the public web-only solution or RTU path."]
435
+ webOnly: true,
436
+ platforms: ["js"],
437
+ whenToUse: ["Document scan and normalization workflows. Served here as web/JavaScript only; mobile/server scopes return official reference links."]
388
438
  }
389
439
  }
390
440
  },
@@ -422,7 +472,9 @@ function buildIndexData({
422
472
 
423
473
  return {
424
474
  productSelection: {
425
- dcvSupersetSummary: "Dynamsoft Capture Vision (DCV) aggregates Dynamsoft Barcode Reader (DBR), Dynamsoft Label Recognizer (DLR), Dynamsoft Document Normalizer (DDN), Dynamsoft Code Parser (DCP), and Dynamsoft Camera Enhancer (DCE) into one pipeline.",
475
+ guidance: PRODUCT_SELECTION_GUIDANCE,
476
+ publicOfferings: [...PUBLIC_OFFERING_PRODUCTS],
477
+ dcvSupersetSummary: "Dynamsoft Capture Vision (DCV) aggregates Dynamsoft Barcode Reader (DBR), Dynamsoft Label Recognizer (DLR), Dynamsoft Document Normalizer (DDN), Dynamsoft Code Parser (DCP), and Dynamsoft Camera Enhancer (DCE) into one pipeline. DCV is internal only and is not a selectable public product.",
426
478
  useDbrWhen: [
427
479
  "Barcode-only workflows where DCV-specific workflows are not required."
428
480
  ],
@@ -985,12 +1037,20 @@ function buildResourceIndex({
985
1037
  version: dbrWebVersion,
986
1038
  majorVersion: LATEST_MAJOR.dbr,
987
1039
  title: `Web sample: ${sampleName} (${category})`,
988
- summary: `DBR web sample ${category}/${sampleName}.`,
1040
+ summary: `DBR web ${category} sample: ${sampleName.replace(/[-_]/g, " ")}.`,
989
1041
  mimeType: "text/html",
990
- tags: ["sample", "dbr", "web", category, sampleName],
1042
+ tags: ["sample", "dbr", "web", category, sampleName, ...getDcvScenarioTags(sampleName)],
1043
+ embedText: `DBR web ${category} sample ${sampleName.replace(/[-_]/g, " ")} ${getDcvScenarioTags(sampleName).join(" ")}`,
991
1044
  loadContent: async () => {
992
1045
  const samplePath = getWebSamplePath(category, sampleName);
993
- const content = samplePath && existsSync(samplePath) ? readCodeFile(samplePath) : "Sample not found";
1046
+ let content = "Sample not found";
1047
+ if (samplePath && existsSync(samplePath)) {
1048
+ // Full-project samples (frameworks/*) resolve to a directory; point
1049
+ // to get_sample_files rather than reading a directory as a file.
1050
+ content = statSync(samplePath).isDirectory()
1051
+ ? `This is a multi-file ${category} project. Use get_sample_files with sample_id ${category}/${sampleName} to retrieve all files.`
1052
+ : readCodeFile(samplePath);
1053
+ }
994
1054
  return { text: content, mimeType: "text/html" };
995
1055
  }
996
1056
  });
@@ -63,12 +63,14 @@ const DOCS_CONFIG = {
63
63
  mrzWeb: {
64
64
  urlBase: "https://www.dynamsoft.com/mrz-scanner/docs/web/",
65
65
  excludeDirs: [".git", ".github", ".vscode", ".vs", "_data", "_includes", "_layouts", "assets"],
66
- excludeFiles: ["README.md", "search.md", "error.md"]
66
+ // Current MRZ docs are v3.x; exclude the legacy v1.x/v2.x guides and any private drafts (#144).
67
+ excludeFiles: ["README.md", "search.md", "error.md", "*-v1.1.md", "*-v2.0.md", "*-v2.1.md", "*private*"]
67
68
  },
68
69
  mdsWeb: {
69
70
  urlBase: "https://www.dynamsoft.com/mobile-document-scanner/docs/web/",
70
71
  excludeDirs: [".git", ".github", ".vscode", ".vs", "_data", "_includes", "_layouts", "assets"],
71
- excludeFiles: ["README.md", "search.md", "error.md"]
72
+ // Exclude internal "private" draft docs (bodies are HTML comments) (#144).
73
+ excludeFiles: ["README.md", "search.md", "error.md", "*private*"]
72
74
  },
73
75
  dcvMobile: {
74
76
  urlBase: "https://www.dynamsoft.com/capture-vision/docs/mobile/",
@@ -59,9 +59,20 @@ function buildBreadcrumbFromPath(relativePath) {
59
59
  function collectMarkdownFiles(rootDir, options = {}) {
60
60
  const files = [];
61
61
  const excludeDirs = new Set(options.excludeDirs || []);
62
- const excludeFiles = new Set(options.excludeFiles || []);
62
+ const rawExcludeFiles = options.excludeFiles || [];
63
+ // Exact names go in a Set; entries containing '*' become glob regexes so
64
+ // configs can exclude stale/internal docs like '*-v1.1.md' or '*private*' (#144).
65
+ const excludeFiles = new Set(rawExcludeFiles.filter((f) => !f.includes("*")));
66
+ const excludePatterns = rawExcludeFiles
67
+ .filter((f) => f.includes("*"))
68
+ .map((f) => new RegExp("^" + f.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$", "i"));
63
69
  const includeDirNames = new Set(options.includeDirNames || []);
64
70
 
71
+ function isExcludedFile(name) {
72
+ if (excludeFiles.has(name)) return true;
73
+ return excludePatterns.some((re) => re.test(name));
74
+ }
75
+
65
76
  function walk(dir) {
66
77
  if (!existsSync(dir)) return;
67
78
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -72,7 +83,7 @@ function collectMarkdownFiles(rootDir, options = {}) {
72
83
  continue;
73
84
  }
74
85
  if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
75
- if (excludeFiles.has(entry.name)) continue;
86
+ if (isExcludedFile(entry.name)) continue;
76
87
  files.push(join(dir, entry.name));
77
88
  }
78
89
  }
@@ -100,11 +111,20 @@ function loadMarkdownDocs({ rootDir, urlBase, includeDirNames = [], excludeDirs
100
111
  const title = parsed.meta.title || getHeadingTitle(parsed.body) || formatSegmentLabel(relativePath.replace(/\.md$/i, "").split("/").pop());
101
112
  if (!title) continue;
102
113
  const breadcrumb = parsed.meta.breadcrumbText || buildBreadcrumbFromPath(relativePath) || title;
114
+ // Frontmatter description/keywords are the docs' own human summaries and
115
+ // search vocabulary — previously parsed and discarded (issue #142).
116
+ const description = typeof parsed.meta.description === "string" ? parsed.meta.description.trim() : "";
117
+ const keywordsRaw = parsed.meta.keywords;
118
+ const keywords = Array.isArray(keywordsRaw)
119
+ ? keywordsRaw.map((k) => String(k).trim()).filter(Boolean)
120
+ : (typeof keywordsRaw === "string" ? keywordsRaw.split(",").map((k) => k.trim()).filter(Boolean) : []);
103
121
  articles.push({
104
122
  title,
105
123
  url: markdownPathToUrl(urlBase, relativePath),
106
124
  content: parsed.body.trim(),
107
125
  breadcrumb,
126
+ description,
127
+ keywords,
108
128
  path: relativePath
109
129
  });
110
130
  }
@@ -265,17 +265,19 @@ function getDcvWebFrameworkPlatforms() {
265
265
  }
266
266
 
267
267
  function discoverWebSamples() {
268
- const categories = { root: [], frameworks: [], scenarios: [] };
268
+ const categories = { root: [], basics: [], frameworks: [], scenarios: [] };
269
269
  const webPath = getDbrWebSamplesRoot();
270
270
  if (!webPath || !existsSync(webPath)) return categories;
271
271
 
272
272
  for (const entry of readdirSync(webPath, { withFileTypes: true })) {
273
- if (entry.isFile() && entry.name.endsWith(".html")) {
273
+ // README.html / index.html at the repo root are navigation shells, not
274
+ // samples — indexing them wastes result slots and a fetch (issue #140).
275
+ if (entry.isFile() && entry.name.endsWith(".html") && entry.name !== "index.html" && entry.name.toLowerCase() !== "readme.html") {
274
276
  categories.root.push(entry.name.replace(".html", ""));
275
277
  }
276
278
  }
277
279
 
278
- for (const subdir of ["frameworks", "scenarios"]) {
280
+ for (const subdir of ["basics", "frameworks", "scenarios"]) {
279
281
  const subdirPath = join(webPath, subdir);
280
282
  if (!existsSync(subdirPath)) continue;
281
283
  for (const entry of readdirSync(subdirPath, { withFileTypes: true })) {
@@ -363,6 +365,10 @@ function getWebSamplePath(category, sampleName) {
363
365
  for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
364
366
  if (entry.isFile() && entry.name.endsWith(".html")) return join(dirPath, entry.name);
365
367
  }
368
+ // Full-project sample (e.g. frameworks/react, a Vite app) with no
369
+ // top-level html: return the directory so get_sample_files can walk it,
370
+ // making the URI search emits actually fetchable (issue #130).
371
+ return dirPath;
366
372
  }
367
373
  const htmlPath = join(webPath, category, `${sampleName}.html`);
368
374
  if (existsSync(htmlPath)) return htmlPath;
@@ -415,6 +421,16 @@ function discoverDwtSamples() {
415
421
  const categories = {};
416
422
  if (!existsSync(SAMPLE_ROOTS.dwt)) return categories;
417
423
 
424
+ // Root-level *.html samples (e.g. edit-image.html, localstorage.html) were
425
+ // previously skipped because only directories were scanned (issue #140).
426
+ const rootSamples = [];
427
+ for (const entry of readdirSync(SAMPLE_ROOTS.dwt, { withFileTypes: true })) {
428
+ if (entry.isFile() && entry.name.endsWith(".html") && entry.name !== "index.html" && entry.name.toLowerCase() !== "readme.html") {
429
+ rootSamples.push(entry.name.replace(".html", ""));
430
+ }
431
+ }
432
+ if (rootSamples.length > 0) categories.root = rootSamples;
433
+
418
434
  for (const entry of readdirSync(SAMPLE_ROOTS.dwt, { withFileTypes: true })) {
419
435
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
420
436
  const categoryPath = join(SAMPLE_ROOTS.dwt, entry.name);
@@ -594,6 +610,11 @@ function getDbrServerSamplePath(platform, sampleName) {
594
610
 
595
611
  function getDwtSamplePath(category, sampleName) {
596
612
  const fileName = sampleName.endsWith(".html") ? sampleName : `${sampleName}.html`;
613
+ // "root" maps to a top-level *.html directly under the DWT samples root.
614
+ if (category === "root") {
615
+ const rootFile = getExistingPath(join(SAMPLE_ROOTS.dwt, fileName));
616
+ return rootFile || null;
617
+ }
597
618
  const categoryPath = getExistingPath(join(SAMPLE_ROOTS.dwt, category));
598
619
  if (!categoryPath) return null;
599
620