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.
- package/.env.example +35 -9
- package/README.md +156 -497
- package/package.json +13 -7
- package/scripts/prebuild-rag-index.mjs +1 -1
- package/scripts/run-gemini-tests.mjs +1 -1
- package/scripts/sync-submodules.mjs +1 -1
- package/scripts/verify-doc-resources.mjs +79 -0
- package/src/data/bootstrap.js +475 -0
- package/src/data/download-utils.js +99 -0
- package/src/data/hydration-mode.js +15 -0
- package/src/data/hydration-policy.js +39 -0
- package/src/data/repo-map.js +149 -0
- package/src/{data-root.js → data/root.js} +1 -1
- package/src/{submodule-sync.js → data/submodule-sync.js} +1 -1
- package/src/index.js +49 -1499
- package/src/observability/logging.js +51 -0
- package/src/rag/config.js +96 -0
- package/src/rag/index.js +266 -0
- package/src/rag/lexical-provider.js +170 -0
- package/src/rag/logger.js +46 -0
- package/src/rag/profile-config.js +48 -0
- package/src/rag/providers.js +585 -0
- package/src/rag/search-utils.js +166 -0
- package/src/rag/vector-cache.js +323 -0
- package/src/server/create-server.js +168 -0
- package/src/server/helpers/server-helpers.js +33 -0
- package/src/{resource-index → server/resource-index}/paths.js +2 -2
- package/src/{resource-index → server/resource-index}/samples.js +9 -1
- package/src/{resource-index.js → server/resource-index.js} +158 -93
- package/src/server/resources/register-resources.js +56 -0
- package/src/server/runtime-config.js +66 -0
- package/src/server/tools/register-index-tools.js +130 -0
- package/src/server/tools/register-project-tools.js +305 -0
- package/src/server/tools/register-quickstart-tools.js +572 -0
- package/src/server/tools/register-sample-tools.js +333 -0
- package/src/server/tools/register-version-tools.js +136 -0
- package/src/server/transports/http.js +84 -0
- package/src/server/transports/stdio.js +12 -0
- package/src/data-bootstrap.js +0 -255
- package/src/rag.js +0 -1203
- /package/src/{gemini-retry.js → rag/gemini-retry.js} +0 -0
- /package/src/{normalizers.js → server/normalizers.js} +0 -0
- /package/src/{resource-index → server/resource-index}/builders.js +0 -0
- /package/src/{resource-index → server/resource-index}/config.js +0 -0
- /package/src/{resource-index → server/resource-index}/docs-loader.js +0 -0
- /package/src/{resource-index → server/resource-index}/uri.js +0 -0
- /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 "
|
|
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");
|