simple-dynamsoft-mcp 6.3.0 → 7.0.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 (47) hide show
  1. package/.env.example +35 -9
  2. package/README.md +156 -497
  3. package/package.json +13 -7
  4. package/scripts/prebuild-rag-index.mjs +1 -1
  5. package/scripts/run-gemini-tests.mjs +1 -1
  6. package/scripts/sync-submodules.mjs +1 -1
  7. package/scripts/verify-doc-resources.mjs +79 -0
  8. package/src/data/bootstrap.js +475 -0
  9. package/src/data/download-utils.js +99 -0
  10. package/src/data/hydration-mode.js +15 -0
  11. package/src/data/hydration-policy.js +39 -0
  12. package/src/data/repo-map.js +149 -0
  13. package/src/{data-root.js → data/root.js} +1 -1
  14. package/src/{submodule-sync.js → data/submodule-sync.js} +1 -1
  15. package/src/index.js +49 -1499
  16. package/src/observability/logging.js +51 -0
  17. package/src/rag/config.js +96 -0
  18. package/src/rag/index.js +266 -0
  19. package/src/rag/lexical-provider.js +170 -0
  20. package/src/rag/logger.js +46 -0
  21. package/src/rag/profile-config.js +48 -0
  22. package/src/rag/providers.js +585 -0
  23. package/src/rag/search-utils.js +166 -0
  24. package/src/rag/vector-cache.js +323 -0
  25. package/src/server/create-server.js +168 -0
  26. package/src/server/helpers/server-helpers.js +33 -0
  27. package/src/{resource-index → server/resource-index}/paths.js +2 -2
  28. package/src/{resource-index → server/resource-index}/samples.js +9 -1
  29. package/src/{resource-index.js → server/resource-index.js} +158 -93
  30. package/src/server/resources/register-resources.js +56 -0
  31. package/src/server/runtime-config.js +66 -0
  32. package/src/server/tools/register-index-tools.js +130 -0
  33. package/src/server/tools/register-project-tools.js +305 -0
  34. package/src/server/tools/register-quickstart-tools.js +572 -0
  35. package/src/server/tools/register-sample-tools.js +333 -0
  36. package/src/server/tools/register-version-tools.js +136 -0
  37. package/src/server/transports/http.js +84 -0
  38. package/src/server/transports/stdio.js +12 -0
  39. package/src/data-bootstrap.js +0 -255
  40. package/src/rag.js +0 -1203
  41. /package/src/{gemini-retry.js → rag/gemini-retry.js} +0 -0
  42. /package/src/{normalizers.js → server/normalizers.js} +0 -0
  43. /package/src/{resource-index → server/resource-index}/builders.js +0 -0
  44. /package/src/{resource-index → server/resource-index}/config.js +0 -0
  45. /package/src/{resource-index → server/resource-index}/docs-loader.js +0 -0
  46. /package/src/{resource-index → server/resource-index}/uri.js +0 -0
  47. /package/src/{resource-index → server/resource-index}/version-policy.js +0 -0
@@ -0,0 +1,166 @@
1
+ import Fuse from "fuse.js";
2
+
3
+ function createFuseSearch(resourceIndex) {
4
+ return new Fuse(resourceIndex, {
5
+ keys: ["title", "summary", "tags", "uri"],
6
+ threshold: 0.35,
7
+ ignoreLocation: true,
8
+ includeScore: true
9
+ });
10
+ }
11
+
12
+ function attachScore(entry, score, includeScore) {
13
+ if (!includeScore || !Number.isFinite(score)) return entry;
14
+ return { ...entry, score };
15
+ }
16
+
17
+ function normalizeSearchFilters({ product, edition, platform, type }, normalizers) {
18
+ const normalizedProduct = normalizers.normalizeProduct(product);
19
+ const normalizedPlatform = normalizers.normalizePlatform(platform);
20
+ const normalizedEdition = normalizers.normalizeEdition(edition, normalizedPlatform, normalizedProduct);
21
+ return {
22
+ product: normalizedProduct,
23
+ edition: normalizedEdition,
24
+ platform: normalizedPlatform,
25
+ type: type || "any"
26
+ };
27
+ }
28
+
29
+ function entryMatchesScope(entry, filters, matchers) {
30
+ if (filters.product && entry.product !== filters.product) return false;
31
+ if (filters.edition && !matchers.editionMatches(filters.edition, entry.edition)) return false;
32
+ if (filters.platform && !matchers.platformMatches(filters.platform, entry)) return false;
33
+ if (filters.type && filters.type !== "any" && entry.type !== filters.type) return false;
34
+ return true;
35
+ }
36
+
37
+ function normalizeText(text) {
38
+ return String(text || "").replace(/\s+/g, " ").trim();
39
+ }
40
+
41
+ function truncateText(text, maxChars) {
42
+ if (!maxChars || maxChars <= 0) return text;
43
+ if (text.length <= maxChars) return text;
44
+ return text.slice(0, Math.max(0, maxChars));
45
+ }
46
+
47
+ function chunkText(text, chunkSize, chunkOverlap, maxChunks) {
48
+ const cleaned = normalizeText(text);
49
+ if (!cleaned) return [];
50
+ if (!chunkSize || chunkSize <= 0) return [cleaned];
51
+ const overlap = Math.min(Math.max(0, chunkOverlap), Math.max(0, chunkSize - 1));
52
+ const chunks = [];
53
+ let start = 0;
54
+ while (start < cleaned.length) {
55
+ const end = Math.min(start + chunkSize, cleaned.length);
56
+ const chunk = cleaned.slice(start, end).trim();
57
+ if (chunk) chunks.push(chunk);
58
+ if (end >= cleaned.length) break;
59
+ start = Math.max(0, end - overlap);
60
+ if (maxChunks && chunks.length >= maxChunks) break;
61
+ }
62
+ return chunks;
63
+ }
64
+
65
+ function buildEntryBaseText(entry) {
66
+ const parts = [entry.title, entry.summary];
67
+ if (Array.isArray(entry.tags) && entry.tags.length > 0) {
68
+ parts.push(entry.tags.join(", "));
69
+ }
70
+ return normalizeText(parts.filter(Boolean).join("\n"));
71
+ }
72
+
73
+ function buildEmbeddingItems(resourceIndex, ragConfig) {
74
+ const items = [];
75
+ for (const entry of resourceIndex) {
76
+ const baseText = buildEntryBaseText(entry);
77
+ if (!baseText) continue;
78
+ if (entry.type === "doc" && entry.embedText) {
79
+ const chunks = chunkText(entry.embedText, ragConfig.chunkSize, ragConfig.chunkOverlap, ragConfig.maxChunksPerDoc);
80
+ if (chunks.length === 0) {
81
+ items.push({
82
+ id: entry.id,
83
+ uri: entry.uri,
84
+ text: truncateText(baseText, ragConfig.maxTextChars)
85
+ });
86
+ continue;
87
+ }
88
+ chunks.forEach((chunk, index) => {
89
+ const combined = [baseText, chunk].filter(Boolean).join("\n\n");
90
+ items.push({
91
+ id: `${entry.id}#${index}`,
92
+ uri: entry.uri,
93
+ text: truncateText(combined, ragConfig.maxTextChars)
94
+ });
95
+ });
96
+ continue;
97
+ }
98
+ items.push({
99
+ id: entry.id,
100
+ uri: entry.uri,
101
+ text: truncateText(baseText, ragConfig.maxTextChars)
102
+ });
103
+ }
104
+ return items;
105
+ }
106
+
107
+ function buildIndexSignature({ pkgVersion, signatureData, ragConfig }) {
108
+ return JSON.stringify({
109
+ packageVersion: pkgVersion,
110
+ resourceCount: signatureData.resourceCount,
111
+ dcvCoreDocCount: signatureData.dcvCoreDocCount,
112
+ dcvWebDocCount: signatureData.dcvWebDocCount,
113
+ dcvMobileDocCount: signatureData.dcvMobileDocCount,
114
+ dcvServerDocCount: signatureData.dcvServerDocCount,
115
+ dbrWebDocCount: signatureData.dbrWebDocCount,
116
+ dbrMobileDocCount: signatureData.dbrMobileDocCount,
117
+ dbrServerDocCount: signatureData.dbrServerDocCount,
118
+ dwtDocCount: signatureData.dwtDocCount,
119
+ ddvDocCount: signatureData.ddvDocCount,
120
+ versions: signatureData.versions,
121
+ dataSources: signatureData.dataSources,
122
+ chunkSize: ragConfig.chunkSize,
123
+ chunkOverlap: ragConfig.chunkOverlap,
124
+ maxChunksPerDoc: ragConfig.maxChunksPerDoc,
125
+ maxTextChars: ragConfig.maxTextChars
126
+ });
127
+ }
128
+
129
+ function normalizeVector(vector) {
130
+ let sum = 0;
131
+ for (const value of vector) {
132
+ sum += value * value;
133
+ }
134
+ const norm = Math.sqrt(sum);
135
+ if (!norm) return vector.map(() => 0);
136
+ return vector.map((value) => value / norm);
137
+ }
138
+
139
+ function dotProduct(a, b) {
140
+ const len = Math.min(a.length, b.length);
141
+ let sum = 0;
142
+ for (let i = 0; i < len; i++) {
143
+ sum += a[i] * b[i];
144
+ }
145
+ return sum;
146
+ }
147
+
148
+ function isRateLimitError(error, isRateLimitGeminiStatus) {
149
+ if (error?.rateLimited) return true;
150
+ const status = Number(error?.status);
151
+ return isRateLimitGeminiStatus(status);
152
+ }
153
+
154
+ export {
155
+ createFuseSearch,
156
+ attachScore,
157
+ normalizeSearchFilters,
158
+ entryMatchesScope,
159
+ normalizeText,
160
+ truncateText,
161
+ buildEmbeddingItems,
162
+ buildIndexSignature,
163
+ normalizeVector,
164
+ dotProduct,
165
+ isRateLimitError
166
+ };
@@ -0,0 +1,323 @@
1
+ import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, copyFileSync, rmSync, statSync } from "node:fs";
2
+ import { join, basename, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { tmpdir } from "node:os";
5
+ import * as tar from "tar";
6
+
7
+ function ensureDirectory(path) {
8
+ if (!existsSync(path)) {
9
+ mkdirSync(path, { recursive: true });
10
+ }
11
+ }
12
+
13
+ function makeCacheFileName(provider, model, cacheKey) {
14
+ const safeModel = String(model || "default").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 32);
15
+ return `rag-${provider}-${safeModel}-${cacheKey.slice(0, 12)}.json`;
16
+ }
17
+
18
+ function makeCheckpointFileName(provider, model, cacheKey) {
19
+ const safeModel = String(model || "default").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 32);
20
+ return `rag-${provider}-${safeModel}-${cacheKey.slice(0, 12)}.checkpoint.json`;
21
+ }
22
+
23
+ function loadVectorIndexCache(
24
+ cacheFile,
25
+ { cacheKey, signature, provider, model, requireSignature = false } = {}
26
+ ) {
27
+ if (!existsSync(cacheFile)) {
28
+ return { hit: false, reason: "missing", payload: null };
29
+ }
30
+ try {
31
+ const parsed = JSON.parse(readFileSync(cacheFile, "utf8"));
32
+ if (!parsed || (cacheKey && parsed.cacheKey !== cacheKey)) {
33
+ return { hit: false, reason: "cache_key_mismatch", payload: null };
34
+ }
35
+ if (!Array.isArray(parsed.items) || !Array.isArray(parsed.vectors)) {
36
+ return { hit: false, reason: "invalid_payload", payload: null };
37
+ }
38
+ const meta = parsed.meta || {};
39
+ if (provider && meta.provider && meta.provider !== provider) {
40
+ return { hit: false, reason: "provider_mismatch", payload: null };
41
+ }
42
+ if (model && meta.model && meta.model !== model) {
43
+ return { hit: false, reason: "model_mismatch", payload: null };
44
+ }
45
+ if (signature) {
46
+ if (!meta.signature) {
47
+ if (requireSignature) {
48
+ return { hit: false, reason: "missing_signature", payload: null };
49
+ }
50
+ } else if (meta.signature !== signature) {
51
+ return { hit: false, reason: "signature_mismatch", payload: null };
52
+ }
53
+ }
54
+ return { hit: true, reason: "ok", payload: parsed };
55
+ } catch {
56
+ return { hit: false, reason: "parse_error", payload: null };
57
+ }
58
+ }
59
+
60
+ function saveVectorIndexCache(cacheDir, cacheFile, payload) {
61
+ ensureDirectory(cacheDir);
62
+ writeFileSync(cacheFile, JSON.stringify(payload));
63
+ }
64
+
65
+ function loadVectorIndexCheckpoint(checkpointFile, expectedKey, expectedItems) {
66
+ if (!existsSync(checkpointFile)) {
67
+ return { hit: false, reason: "missing", payload: null };
68
+ }
69
+ try {
70
+ const parsed = JSON.parse(readFileSync(checkpointFile, "utf8"));
71
+ if (!parsed || parsed.cacheKey !== expectedKey) {
72
+ return { hit: false, reason: "cache_key_mismatch", payload: null };
73
+ }
74
+ if (!Array.isArray(parsed.items) || !Array.isArray(parsed.vectors)) {
75
+ return { hit: false, reason: "invalid_payload", payload: null };
76
+ }
77
+ if (parsed.items.length !== expectedItems.length) {
78
+ return { hit: false, reason: "items_length_mismatch", payload: null };
79
+ }
80
+ for (let i = 0; i < expectedItems.length; i += 1) {
81
+ if (parsed.items[i]?.id !== expectedItems[i]?.id || parsed.items[i]?.uri !== expectedItems[i]?.uri) {
82
+ return { hit: false, reason: "items_mismatch", payload: null };
83
+ }
84
+ }
85
+ if (parsed.vectors.length > expectedItems.length) {
86
+ return { hit: false, reason: "vectors_overflow", payload: null };
87
+ }
88
+ return { hit: true, reason: "ok", payload: parsed };
89
+ } catch {
90
+ return { hit: false, reason: "parse_error", payload: null };
91
+ }
92
+ }
93
+
94
+ function saveVectorIndexCheckpoint(cacheDir, checkpointFile, payload) {
95
+ ensureDirectory(cacheDir);
96
+ writeFileSync(checkpointFile, JSON.stringify(payload));
97
+ }
98
+
99
+ function clearVectorIndexCheckpoint(checkpointFile) {
100
+ if (existsSync(checkpointFile)) {
101
+ rmSync(checkpointFile, { force: true });
102
+ }
103
+ }
104
+
105
+ function listFilesRecursive(rootDir) {
106
+ const files = [];
107
+ const stack = [rootDir];
108
+ while (stack.length > 0) {
109
+ const current = stack.pop();
110
+ const entries = readdirSync(current, { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ const fullPath = join(current, entry.name);
113
+ if (entry.isDirectory()) {
114
+ stack.push(fullPath);
115
+ } else if (entry.isFile()) {
116
+ files.push(fullPath);
117
+ }
118
+ }
119
+ }
120
+ return files;
121
+ }
122
+
123
+ function readSignaturePackageVersion(signatureRaw) {
124
+ if (!signatureRaw) return "";
125
+ try {
126
+ const parsed = JSON.parse(signatureRaw);
127
+ return String(parsed?.packageVersion || "");
128
+ } catch {
129
+ return "";
130
+ }
131
+ }
132
+
133
+ function listDownloadedCacheCandidatesByProvider(extractRoot, expectedCacheFileName, cacheKey, provider) {
134
+ const allFiles = listFilesRecursive(extractRoot).filter((path) => path.toLowerCase().endsWith(".json")).sort();
135
+ const expectedPath = allFiles.find((path) => basename(path) === expectedCacheFileName);
136
+
137
+ const cachePrefix = cacheKey.slice(0, 12);
138
+ const prefixPath = allFiles.find((path) => {
139
+ const name = basename(path);
140
+ return name.startsWith(`rag-${provider}-`) && name.endsWith(`-${cachePrefix}.json`);
141
+ });
142
+
143
+ const providerFiles = allFiles.filter((path) => basename(path).startsWith(`rag-${provider}-`));
144
+ const unique = [];
145
+ for (const path of [expectedPath, prefixPath, ...providerFiles]) {
146
+ if (!path) continue;
147
+ if (!unique.includes(path)) unique.push(path);
148
+ }
149
+ return unique;
150
+ }
151
+
152
+ function resolvePrebuiltIndexUrlCandidates(provider, ragConfig, legacyPrebuiltIndexUrl) {
153
+ const override = String(ragConfig.prebuiltIndexUrl || "").trim();
154
+ if (override) return [override];
155
+
156
+ const candidates = [];
157
+ if (provider === "local") {
158
+ candidates.push(String(ragConfig.prebuiltIndexUrlLocal || "").trim());
159
+ } else if (provider === "gemini") {
160
+ candidates.push(String(ragConfig.prebuiltIndexUrlGemini || "").trim());
161
+ }
162
+ candidates.push(legacyPrebuiltIndexUrl);
163
+
164
+ const deduped = [];
165
+ for (const candidate of candidates) {
166
+ if (!candidate) continue;
167
+ if (!deduped.includes(candidate)) deduped.push(candidate);
168
+ }
169
+ return deduped;
170
+ }
171
+
172
+ async function downloadPrebuiltArchive(url, outputPath, timeoutMs) {
173
+ const source = String(url || "").trim();
174
+ if (!source) {
175
+ throw new Error("prebuilt URL is empty");
176
+ }
177
+
178
+ if (source.startsWith("file://")) {
179
+ copyFileSync(fileURLToPath(source), outputPath);
180
+ return { sourceType: "file", size: statSync(outputPath).size };
181
+ }
182
+
183
+ if (!/^https?:\/\//i.test(source)) {
184
+ copyFileSync(resolve(source), outputPath);
185
+ return { sourceType: "file", size: statSync(outputPath).size };
186
+ }
187
+
188
+ const controller = new AbortController();
189
+ const timer = setTimeout(() => controller.abort(), Math.max(1000, timeoutMs));
190
+ try {
191
+ const response = await fetch(source, { signal: controller.signal });
192
+ if (!response.ok) {
193
+ throw new Error(`HTTP ${response.status}`);
194
+ }
195
+ const arrayBuffer = await response.arrayBuffer();
196
+ writeFileSync(outputPath, Buffer.from(arrayBuffer));
197
+ return { sourceType: "http", size: arrayBuffer.byteLength };
198
+ } finally {
199
+ clearTimeout(timer);
200
+ }
201
+ }
202
+
203
+ function createVectorCacheHelpers({ ragConfig, pkgVersion, legacyPrebuiltIndexUrl, logRag }) {
204
+ const prebuiltDownloadAttempts = new Map();
205
+
206
+ async function maybeDownloadPrebuiltVectorIndex({ provider, model, cacheKey, signature, cacheFile }) {
207
+ if (!["local", "gemini"].includes(provider)) {
208
+ return { downloaded: false, reason: "provider_not_supported" };
209
+ }
210
+ if (!ragConfig.prebuiltIndexAutoDownload) {
211
+ return { downloaded: false, reason: "auto_download_disabled" };
212
+ }
213
+
214
+ const sourceUrls = resolvePrebuiltIndexUrlCandidates(provider, ragConfig, legacyPrebuiltIndexUrl);
215
+ if (sourceUrls.length === 0) {
216
+ return { downloaded: false, reason: "url_not_set" };
217
+ }
218
+
219
+ const attemptKey = `${provider}:${cacheKey}:${sourceUrls.join("|")}`;
220
+ if (prebuiltDownloadAttempts.has(attemptKey)) {
221
+ return prebuiltDownloadAttempts.get(attemptKey);
222
+ }
223
+
224
+ const expectedCacheFileName = makeCacheFileName(provider, model, cacheKey);
225
+ const attempt = (async () => {
226
+ let lastReason = "not_attempted";
227
+ for (const sourceUrl of sourceUrls) {
228
+ const tempRoot = join(
229
+ tmpdir(),
230
+ `simple-dynamsoft-mcp-rag-prebuilt-${Date.now()}-${Math.random().toString(16).slice(2)}`
231
+ );
232
+ const archivePath = join(tempRoot, "prebuilt-rag-index.tar.gz");
233
+ const extractRoot = join(tempRoot, "extract");
234
+
235
+ ensureDirectory(extractRoot);
236
+ try {
237
+ logRag(
238
+ `prebuilt index download start provider=${provider} url=${sourceUrl} timeout_ms=${ragConfig.prebuiltIndexTimeoutMs}`
239
+ );
240
+ const downloaded = await downloadPrebuiltArchive(sourceUrl, archivePath, ragConfig.prebuiltIndexTimeoutMs);
241
+ logRag(
242
+ `prebuilt index downloaded provider=${provider} source=${downloaded.sourceType} size=${downloaded.size}B url=${sourceUrl}`
243
+ );
244
+
245
+ await tar.x({
246
+ file: archivePath,
247
+ cwd: extractRoot,
248
+ strict: true
249
+ });
250
+
251
+ const candidateFiles = listDownloadedCacheCandidatesByProvider(
252
+ extractRoot,
253
+ expectedCacheFileName,
254
+ cacheKey,
255
+ provider
256
+ );
257
+ if (candidateFiles.length === 0) {
258
+ throw new Error(`cache_file_not_found expected=${expectedCacheFileName}`);
259
+ }
260
+
261
+ for (const sourceCacheFile of candidateFiles) {
262
+ const candidateCache = loadVectorIndexCache(sourceCacheFile, {
263
+ provider,
264
+ model
265
+ });
266
+ if (!candidateCache.hit) {
267
+ continue;
268
+ }
269
+
270
+ const cachePackageVersion = readSignaturePackageVersion(candidateCache.payload?.meta?.signature);
271
+ if (!cachePackageVersion || cachePackageVersion !== pkgVersion) {
272
+ continue;
273
+ }
274
+
275
+ const migratedPayload = {
276
+ ...candidateCache.payload,
277
+ cacheKey,
278
+ meta: {
279
+ ...(candidateCache.payload.meta || {}),
280
+ provider,
281
+ model,
282
+ signature
283
+ }
284
+ };
285
+ saveVectorIndexCache(ragConfig.cacheDir, cacheFile, migratedPayload);
286
+ logRag(
287
+ `prebuilt index installed provider=${provider} cache_file=${cacheFile} source=${basename(sourceCacheFile)} mode=version_only_compat version=${cachePackageVersion}`
288
+ );
289
+ return { downloaded: true, reason: "installed_version_only_compat" };
290
+ }
291
+
292
+ throw new Error(
293
+ `no_compatible_cache expected=${expectedCacheFileName} found=${candidateFiles.map((path) => basename(path)).join(",")}`
294
+ );
295
+ } catch (error) {
296
+ lastReason = `${sourceUrl} => ${error.message}`;
297
+ logRag(`prebuilt index unavailable provider=${provider} url=${sourceUrl} reason=${error.message}`);
298
+ } finally {
299
+ rmSync(tempRoot, { recursive: true, force: true });
300
+ }
301
+ }
302
+ return { downloaded: false, reason: lastReason };
303
+ })();
304
+
305
+ prebuiltDownloadAttempts.set(attemptKey, attempt);
306
+ return attempt;
307
+ }
308
+
309
+ return {
310
+ makeCacheFileName,
311
+ makeCheckpointFileName,
312
+ loadVectorIndexCache,
313
+ saveVectorIndexCache: (cacheFile, payload) => saveVectorIndexCache(ragConfig.cacheDir, cacheFile, payload),
314
+ loadVectorIndexCheckpoint,
315
+ saveVectorIndexCheckpoint: (checkpointFile, payload) => saveVectorIndexCheckpoint(ragConfig.cacheDir, checkpointFile, payload),
316
+ clearVectorIndexCheckpoint,
317
+ maybeDownloadPrebuiltVectorIndex
318
+ };
319
+ }
320
+
321
+ export {
322
+ createVectorCacheHelpers
323
+ };
@@ -0,0 +1,168 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ensureDataScopesHydrated } from "../data/bootstrap.js";
3
+ import { createScopeHydrator } from "./helpers/server-helpers.js";
4
+ import { registerResourceHandlers } from "./resources/register-resources.js";
5
+ import { registerIndexTools } from "./tools/register-index-tools.js";
6
+ import { registerSampleTools } from "./tools/register-sample-tools.js";
7
+ import { registerVersionTools } from "./tools/register-version-tools.js";
8
+ import { registerQuickstartTools } from "./tools/register-quickstart-tools.js";
9
+ import { registerProjectTools } from "./tools/register-project-tools.js";
10
+
11
+ export function createMcpServerInstance({ pkgVersion, resourceIndexApi, ragApi }) {
12
+ const {
13
+ registry,
14
+ LATEST_VERSIONS,
15
+ LATEST_MAJOR,
16
+ discoverDwtSamples,
17
+ discoverDcvMobileSamples,
18
+ discoverDcvWebSamples,
19
+ findCodeFilesInSample,
20
+ getMobileSamplePath,
21
+ getDbrServerSamplePath,
22
+ getDcvMobileSamplePath,
23
+ getDcvServerSamplePath,
24
+ getDcvWebSamplePath,
25
+ getDwtSamplePath,
26
+ getDdvSamplePath,
27
+ readCodeFile,
28
+ getMainCodeFile,
29
+ ensureLatestMajor,
30
+ parseResourceUri,
31
+ parseSampleUri,
32
+ getSampleIdFromUri,
33
+ getSampleEntries,
34
+ buildIndexData,
35
+ getDisplayEdition,
36
+ getDisplayPlatform,
37
+ formatScopeLabel,
38
+ getPinnedResources,
39
+ readResourceContent,
40
+ refreshResourceIndex,
41
+ normalizePlatform,
42
+ normalizeApiLevel,
43
+ normalizeSampleName,
44
+ normalizeProduct,
45
+ normalizeEdition,
46
+ resourceIndex,
47
+ getWebSamplePath
48
+ } = resourceIndexApi;
49
+
50
+ const {
51
+ searchResources,
52
+ getSampleSuggestions,
53
+ refreshRagIndexes
54
+ } = ragApi;
55
+
56
+ const ensureScopeHydrated = createScopeHydrator({
57
+ ensureDataScopesHydrated,
58
+ refreshResourceIndex,
59
+ refreshRagIndexes
60
+ });
61
+
62
+ const server = new McpServer({
63
+ name: "simple-dynamsoft-mcp",
64
+ version: pkgVersion,
65
+ description: "MCP server for latest major versions of Dynamsoft SDKs: Capture Vision, Barcode Reader, Dynamic Web TWAIN, and Document Viewer. Includes guidance for choosing DBR vs DCV by scenario."
66
+ });
67
+
68
+ registerIndexTools({
69
+ server,
70
+ ensureScopeHydrated,
71
+ ensureLatestMajor,
72
+ normalizeProduct,
73
+ normalizePlatform,
74
+ normalizeEdition,
75
+ buildIndexData,
76
+ getSampleIdFromUri,
77
+ formatScopeLabel,
78
+ searchResources
79
+ });
80
+
81
+ registerSampleTools({
82
+ server,
83
+ ensureScopeHydrated,
84
+ ensureLatestMajor,
85
+ normalizeProduct,
86
+ normalizePlatform,
87
+ normalizeEdition,
88
+ normalizeSampleName,
89
+ parseSampleUri,
90
+ resourceIndex,
91
+ getSampleEntries,
92
+ getSampleIdFromUri,
93
+ getDisplayEdition,
94
+ getDisplayPlatform,
95
+ formatScopeLabel,
96
+ searchResources,
97
+ getSampleSuggestions
98
+ });
99
+
100
+ registerVersionTools({
101
+ server,
102
+ ensureLatestMajor,
103
+ normalizeProduct,
104
+ normalizePlatform,
105
+ normalizeEdition,
106
+ LATEST_MAJOR,
107
+ LATEST_VERSIONS
108
+ });
109
+
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
+ });
133
+
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
+ getDwtSamplePath,
155
+ getDdvSamplePath,
156
+ getSampleSuggestions
157
+ });
158
+
159
+ registerResourceHandlers({
160
+ server,
161
+ getPinnedResources,
162
+ parseResourceUri,
163
+ ensureLatestMajor,
164
+ readResourceContent
165
+ });
166
+
167
+ return server;
168
+ }
@@ -0,0 +1,33 @@
1
+ function formatScoreLabel(entry) {
2
+ if (!Number.isFinite(entry?.score)) return "";
3
+ return ` | score: ${entry.score.toFixed(3)}`;
4
+ }
5
+
6
+ function formatScoreNote(entry) {
7
+ if (!Number.isFinite(entry?.score)) return "";
8
+ return ` score=${entry.score.toFixed(3)}`;
9
+ }
10
+
11
+ function createScopeHydrator({ ensureDataScopesHydrated, refreshResourceIndex, refreshRagIndexes }) {
12
+ if (typeof ensureDataScopesHydrated !== "function") {
13
+ throw new TypeError("ensureDataScopesHydrated must be a function");
14
+ }
15
+
16
+ return async function ensureScopeHydrated(scope) {
17
+ const result = await ensureDataScopesHydrated([scope]);
18
+ if (!Array.isArray(result?.hydrated) || result.hydrated.length === 0) return;
19
+
20
+ if (typeof refreshResourceIndex === "function") {
21
+ refreshResourceIndex();
22
+ }
23
+ if (typeof refreshRagIndexes === "function") {
24
+ refreshRagIndexes();
25
+ }
26
+ };
27
+ }
28
+
29
+ export {
30
+ formatScoreLabel,
31
+ formatScoreNote,
32
+ createScopeHydrator
33
+ };
@@ -2,10 +2,10 @@ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { DOC_DIRS, SAMPLE_DIRS } from "./config.js";
5
- import { getResolvedDataRoot } from "../data-root.js";
5
+ import { getResolvedDataRoot } from "../../data/root.js";
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const projectRoot = join(__dirname, "..", "..");
8
+ const projectRoot = join(__dirname, "..", "..", "..");
9
9
 
10
10
  const dataRoot = getResolvedDataRoot();
11
11
  const metadataRoot = join(dataRoot, "metadata");