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.
- package/data/metadata/data-manifest.json +2 -2
- package/package.json +4 -2
- package/scripts/check-version-drift.mjs +80 -0
- package/src/rag/config.js +5 -0
- package/src/rag/index.js +5 -1
- package/src/rag/lexical-provider.js +48 -15
- package/src/rag/providers.js +27 -6
- package/src/rag/search-utils.js +144 -4
- package/src/rag/vector-cache.js +17 -1
- package/src/server/create-server.js +121 -99
- package/src/server/normalizers.js +78 -1
- package/src/server/public-offerings.js +12 -0
- package/src/server/resource-index/builders.js +76 -16
- package/src/server/resource-index/config.js +4 -2
- package/src/server/resource-index/docs-loader.js +22 -2
- package/src/server/resource-index/samples.js +24 -3
- package/src/server/resource-index/version-policy.js +34 -8
- package/src/server/resources/register-resources.js +4 -1
- package/src/server/tools/public-routing.js +8 -0
- package/src/server/tools/register-index-tools.js +64 -10
- package/src/server/tools/register-project-tools.js +160 -18
- package/src/server/tools/register-quickstart-tools.js +357 -40
- package/src/server/tools/register-sample-tools.js +10 -5
- package/src/server/tools/register-version-tools.js +10 -5
|
@@ -65,12 +65,19 @@ function getLegacyLink(product, version, edition, platform) {
|
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function retryWithoutVersionHint(currentMajor) {
|
|
69
|
+
return `Retry without the version parameter to get v${currentMajor} content.`;
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
function ensureLatestMajor({ product, version, query, edition, platform, latestMajor }) {
|
|
69
73
|
const inferredProduct = product || inferProductFromQuery(query);
|
|
70
74
|
if (!inferredProduct) return { ok: true };
|
|
71
75
|
|
|
72
76
|
const normalizedEdition = normalizeEdition(edition, platform, inferredProduct);
|
|
73
|
-
|
|
77
|
+
// Only an explicit version/constraint parameter may trigger the legacy guard.
|
|
78
|
+
// Version-like tokens in free-text queries ("I'm on DBR 9.6 and ...") must NOT
|
|
79
|
+
// block a search for content this server hosts (issue #139).
|
|
80
|
+
const requestedMajor = parseMajorVersion(version);
|
|
74
81
|
|
|
75
82
|
if (inferredProduct === "mrz" && !normalizedEdition && requestedMajor && requestedMajor === latestMajor.dcv) {
|
|
76
83
|
return { ok: true, latestMajor: latestMajor.dcv };
|
|
@@ -87,35 +94,51 @@ function ensureLatestMajor({ product, version, query, edition, platform, latestM
|
|
|
87
94
|
if (inferredProduct === "ddv") {
|
|
88
95
|
return {
|
|
89
96
|
ok: false,
|
|
90
|
-
message:
|
|
97
|
+
message: [
|
|
98
|
+
`This MCP server only serves the latest major version of DDV (v${currentMajor}).`,
|
|
99
|
+
retryWithoutVersionHint(currentMajor)
|
|
100
|
+
].join("\n")
|
|
91
101
|
};
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
if (inferredProduct === "dcv") {
|
|
95
105
|
return {
|
|
96
106
|
ok: false,
|
|
97
|
-
message:
|
|
107
|
+
message: [
|
|
108
|
+
`This MCP server only serves the latest major version of DCV (v${currentMajor}).`,
|
|
109
|
+
retryWithoutVersionHint(currentMajor)
|
|
110
|
+
].join("\n")
|
|
98
111
|
};
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
if (inferredProduct === "mrz" || inferredProduct === "mds") {
|
|
102
115
|
return {
|
|
103
116
|
ok: false,
|
|
104
|
-
message:
|
|
117
|
+
message: [
|
|
118
|
+
`This MCP server only serves the latest major version of ${inferredProduct.toUpperCase()} (v${currentMajor}).`,
|
|
119
|
+
retryWithoutVersionHint(currentMajor)
|
|
120
|
+
].join("\n")
|
|
105
121
|
};
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
if (inferredProduct === "dbr" && requestedMajor < 9) {
|
|
109
125
|
return {
|
|
110
126
|
ok: false,
|
|
111
|
-
message:
|
|
127
|
+
message: [
|
|
128
|
+
`This MCP server only serves the latest major version of DBR (v${currentMajor}). DBR versions prior to v9 are not available.`,
|
|
129
|
+
"Note: in v10+ setLicenseKey was replaced by LicenseManager.initLicense.",
|
|
130
|
+
retryWithoutVersionHint(currentMajor)
|
|
131
|
+
].join("\n")
|
|
112
132
|
};
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
if (inferredProduct === "dwt" && requestedMajor < 16) {
|
|
116
136
|
return {
|
|
117
137
|
ok: false,
|
|
118
|
-
message:
|
|
138
|
+
message: [
|
|
139
|
+
`This MCP server only serves the latest major version of DWT (v${currentMajor}). DWT versions prior to v16 are not available.`,
|
|
140
|
+
retryWithoutVersionHint(currentMajor)
|
|
141
|
+
].join("\n")
|
|
119
142
|
};
|
|
120
143
|
}
|
|
121
144
|
|
|
@@ -126,7 +149,9 @@ function ensureLatestMajor({ product, version, query, edition, platform, latestM
|
|
|
126
149
|
ok: false,
|
|
127
150
|
message: [
|
|
128
151
|
`This MCP server only serves the latest major version of DBR (v${currentMajor}).`,
|
|
129
|
-
link ? `Legacy docs: ${link}` : fallback
|
|
152
|
+
link ? `Legacy docs: ${link}` : fallback,
|
|
153
|
+
"Note: in v10+ setLicenseKey was replaced by LicenseManager.initLicense.",
|
|
154
|
+
retryWithoutVersionHint(currentMajor)
|
|
130
155
|
].join("\n")
|
|
131
156
|
};
|
|
132
157
|
}
|
|
@@ -139,7 +164,8 @@ function ensureLatestMajor({ product, version, query, edition, platform, latestM
|
|
|
139
164
|
ok: false,
|
|
140
165
|
message: [
|
|
141
166
|
`This MCP server only serves the latest major version of DWT (v${currentMajor}).`,
|
|
142
|
-
legacyNote
|
|
167
|
+
legacyNote,
|
|
168
|
+
retryWithoutVersionHint(currentMajor)
|
|
143
169
|
].join("\n")
|
|
144
170
|
};
|
|
145
171
|
}
|
|
@@ -42,7 +42,10 @@ export function registerResourceHandlers({
|
|
|
42
42
|
|
|
43
43
|
const resource = await readResourceContent(uriStr);
|
|
44
44
|
if (!resource) {
|
|
45
|
-
|
|
45
|
+
// Doc URIs embed a positional index + exact version, so they can go stale
|
|
46
|
+
// across data refreshes. Point the agent back to search to re-discover the
|
|
47
|
+
// current URI instead of dead-ending (issue #153).
|
|
48
|
+
throw new Error(`Resource not found: ${uriStr}. The URI may be stale (doc URIs change across data refreshes). Call search with keywords from the title to get the current URI.`);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
return { contents: [resource] };
|
|
@@ -24,6 +24,14 @@ function getPublicMobileSamplesUrl(platform) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function getUnsupportedPublicScopeRedirect(product, edition, platform) {
|
|
27
|
+
if (product === "mrz" && edition === "mobile") {
|
|
28
|
+
return {
|
|
29
|
+
label: "MRZ",
|
|
30
|
+
docsUrl: "https://www.dynamsoft.com/capture-vision/docs/mobile/",
|
|
31
|
+
samplesUrl: getPublicMobileSamplesUrl(platform)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
if (product === "mrz" && edition === "server") {
|
|
28
36
|
return {
|
|
29
37
|
label: "MRZ",
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatScoreLabel, formatScoreNote } from "../helpers/server-helpers.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildUnknownPublicProductResponse,
|
|
5
|
+
isKnownPublicOffering,
|
|
6
|
+
DBR_ONLY_EDITIONS_NOTE,
|
|
7
|
+
PRODUCT_SELECTION_GUIDANCE,
|
|
8
|
+
WEB_ONLY_OMIT_NOTE
|
|
9
|
+
} from "../public-offerings.js";
|
|
4
10
|
import { buildUnsupportedPublicScopeResponse } from "./public-routing.js";
|
|
11
|
+
import { validatePlatform, validateEdition } from "../normalizers.js";
|
|
12
|
+
import { logEvent } from "../../observability/logging.js";
|
|
5
13
|
|
|
6
14
|
export function registerIndexTools({
|
|
7
15
|
server,
|
|
@@ -18,6 +26,26 @@ export function registerIndexTools({
|
|
|
18
26
|
getSampleEntries,
|
|
19
27
|
getSampleSuggestions
|
|
20
28
|
}) {
|
|
29
|
+
// Build a resource_link description that lets an agent choose WITHOUT a
|
|
30
|
+
// follow-up read: type | scope | version | score - summary, then the matched
|
|
31
|
+
// snippet and canonical URL when available (issue #146).
|
|
32
|
+
function buildHitDescription(entry) {
|
|
33
|
+
const versionLabel = entry.version ? `v${entry.version}` : "n/a";
|
|
34
|
+
const scopeLabel = formatScopeLabel(entry);
|
|
35
|
+
const sampleId = entry.type === "sample" ? getSampleIdFromUri(entry.uri) : "";
|
|
36
|
+
const sampleHint = sampleId ? ` | sample_id: ${sampleId}` : "";
|
|
37
|
+
const scoreLabel = formatScoreLabel(entry);
|
|
38
|
+
let desc = `${entry.type.toUpperCase()} | ${scopeLabel} | ${versionLabel}${scoreLabel} - ${entry.summary}${sampleHint}`;
|
|
39
|
+
if (entry.matchedSnippet) {
|
|
40
|
+
const snippet = String(entry.matchedSnippet).replace(/\s+/g, " ").slice(0, 220);
|
|
41
|
+
desc += ` — "${snippet}"`;
|
|
42
|
+
}
|
|
43
|
+
if (entry.type === "doc" && entry.url) {
|
|
44
|
+
desc += ` | ${entry.url}`;
|
|
45
|
+
}
|
|
46
|
+
return desc;
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
function looksLikeSampleIdQuery(value) {
|
|
22
50
|
const normalized = String(value || "").trim();
|
|
23
51
|
if (!normalized) return false;
|
|
@@ -33,7 +61,7 @@ export function registerIndexTools({
|
|
|
33
61
|
"",
|
|
34
62
|
"WHEN TO USE:",
|
|
35
63
|
"- As the first call in any conversation to discover what is available.",
|
|
36
|
-
|
|
64
|
+
`- To determine valid product/edition/platform combinations before calling other tools. Note: ${PRODUCT_SELECTION_GUIDANCE}`,
|
|
37
65
|
"- To get public product-selection guidance (DBR for barcode-only; MRZ for machine-readable-zone workflows; MDS for document scan and normalization workflows).",
|
|
38
66
|
"",
|
|
39
67
|
"WHEN NOT TO USE:",
|
|
@@ -84,8 +112,8 @@ export function registerIndexTools({
|
|
|
84
112
|
"PARAMETERS:",
|
|
85
113
|
"- query (required): Keywords or exact sample ID. Examples: 'barcode scanning from camera', 'MRZ passport reader', 'hello-world'.",
|
|
86
114
|
"- product: dbr, dwt, ddv, mrz, or mds. Use DBR for barcode-only, MRZ for passport/machine-readable-zone workflows, and MDS for document scan or normalization workflows.",
|
|
87
|
-
|
|
88
|
-
|
|
115
|
+
`- edition: core, mobile, web, or server. ${DBR_ONLY_EDITIONS_NOTE}`,
|
|
116
|
+
`- platform: only DBR spans multiple platforms (android, ios, js, python, cpp, java, dotnet, nodejs, react, vue, angular, flutter, react-native, maui, etc.). ${WEB_ONLY_OMIT_NOTE}`,
|
|
89
117
|
"- version: Version constraint (e.g. '10', '11.x'). Only latest major is served by default.",
|
|
90
118
|
"- type: 'doc', 'sample', 'index', 'policy', or 'any' (default). Use 'sample' to restrict to sample results.",
|
|
91
119
|
"- limit: 1-10 (default 5). Max number of results.",
|
|
@@ -97,8 +125,8 @@ export function registerIndexTools({
|
|
|
97
125
|
inputSchema: {
|
|
98
126
|
query: z.string().trim().min(1, "Query is required.").describe("Keywords to search across docs and samples."),
|
|
99
127
|
product: z.string().optional().describe("Product: dbr, dwt, ddv, mrz, mds"),
|
|
100
|
-
edition: z.string().optional().describe(
|
|
101
|
-
platform: z.string().optional().describe(
|
|
128
|
+
edition: z.string().optional().describe(`Edition: core, mobile, web, server/desktop. ${DBR_ONLY_EDITIONS_NOTE}`),
|
|
129
|
+
platform: z.string().optional().describe(`Platform (DBR only spans multiple): android, ios, maui, react-native, flutter, js, python, cpp, java, dotnet, nodejs, angular, blazor, capacitor, electron, es6, native-ts, next, nuxt, pwa, react, requirejs, svelte, vue, webview, spm, core. ${WEB_ONLY_OMIT_NOTE}`),
|
|
102
130
|
version: z.string().optional().describe("Version constraint (major or full version)"),
|
|
103
131
|
type: z.enum(["doc", "sample", "index", "policy", "any"]).optional(),
|
|
104
132
|
limit: z.number().int().min(1).max(10).optional().describe("Max results (default 5)")
|
|
@@ -116,6 +144,17 @@ export function registerIndexTools({
|
|
|
116
144
|
return buildUnknownPublicProductResponse(product);
|
|
117
145
|
}
|
|
118
146
|
|
|
147
|
+
// Validate filter values so a typo names the failing dimension instead of
|
|
148
|
+
// silently filtering every result to zero (issue #152).
|
|
149
|
+
const platformCheck = validatePlatform(platform);
|
|
150
|
+
if (!platformCheck.ok) {
|
|
151
|
+
return { isError: true, content: [{ type: "text", text: platformCheck.message }] };
|
|
152
|
+
}
|
|
153
|
+
const editionCheck = validateEdition(edition);
|
|
154
|
+
if (!editionCheck.ok) {
|
|
155
|
+
return { isError: true, content: [{ type: "text", text: editionCheck.message }] };
|
|
156
|
+
}
|
|
157
|
+
|
|
119
158
|
const normalizedPlatform = normalizePlatform(platform);
|
|
120
159
|
const normalizedEdition = normalizeEdition(edition, normalizedPlatform, normalizedProduct);
|
|
121
160
|
const unsupportedScopeResponse = buildUnsupportedPublicScopeResponse(normalizedProduct, normalizedEdition, normalizedPlatform);
|
|
@@ -174,7 +213,7 @@ export function registerIndexTools({
|
|
|
174
213
|
type: "resource_link",
|
|
175
214
|
uri: entry.uri,
|
|
176
215
|
name: entry.title,
|
|
177
|
-
description:
|
|
216
|
+
description: buildHitDescription(entry),
|
|
178
217
|
mimeType: entry.mimeType,
|
|
179
218
|
annotations: {
|
|
180
219
|
audience: ["assistant"],
|
|
@@ -242,7 +281,7 @@ export function registerIndexTools({
|
|
|
242
281
|
type: "resource_link",
|
|
243
282
|
uri: entry.uri,
|
|
244
283
|
name: entry.title,
|
|
245
|
-
description:
|
|
284
|
+
description: buildHitDescription(entry),
|
|
246
285
|
mimeType: entry.mimeType,
|
|
247
286
|
annotations: {
|
|
248
287
|
audience: ["assistant"],
|
|
@@ -266,6 +305,15 @@ export function registerIndexTools({
|
|
|
266
305
|
}
|
|
267
306
|
}
|
|
268
307
|
|
|
308
|
+
// Zero-result queries are the highest-value eval/synonym-gap signal — log
|
|
309
|
+
// them so they can be harvested from production logs (issue #156).
|
|
310
|
+
logEvent("search", "zero_result_query", {
|
|
311
|
+
query,
|
|
312
|
+
product: normalizedProduct || "",
|
|
313
|
+
edition: normalizedEdition || "",
|
|
314
|
+
platform: normalizedPlatform || "",
|
|
315
|
+
type: type || "any"
|
|
316
|
+
});
|
|
269
317
|
return {
|
|
270
318
|
content: [{
|
|
271
319
|
type: "text",
|
|
@@ -274,10 +322,16 @@ export function registerIndexTools({
|
|
|
274
322
|
};
|
|
275
323
|
}
|
|
276
324
|
|
|
325
|
+
// Capture Vision / DCV is the umbrella framework, not a selectable public
|
|
326
|
+
// product — steer the agent to the right offering (issue #150).
|
|
327
|
+
const dcvUmbrella = /\b(capture[\s-]?vision|dcv)\b/i.test(query) && !normalizedProduct;
|
|
328
|
+
const leadText = dcvUmbrella
|
|
329
|
+
? `"Capture Vision" (DCV) is Dynamsoft's umbrella framework — pick the public product for your task: DBR (barcodes), MRZ (passports/IDs), MDS (document capture), DWT (scanner hardware), DDV (document viewer). Results below span the closest matches.\nFound ${topResults.length} result(s) for "${query}".`
|
|
330
|
+
: `Found ${topResults.length} result(s) for "${query}". Read the links you need with resources/read.`;
|
|
277
331
|
const content = [
|
|
278
332
|
{
|
|
279
333
|
type: "text",
|
|
280
|
-
text:
|
|
334
|
+
text: leadText
|
|
281
335
|
}
|
|
282
336
|
];
|
|
283
337
|
|
|
@@ -291,7 +345,7 @@ export function registerIndexTools({
|
|
|
291
345
|
type: "resource_link",
|
|
292
346
|
uri: entry.uri,
|
|
293
347
|
name: entry.title,
|
|
294
|
-
description:
|
|
348
|
+
description: buildHitDescription(entry),
|
|
295
349
|
mimeType: entry.mimeType,
|
|
296
350
|
annotations: {
|
|
297
351
|
audience: ["assistant"],
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, join, relative } from "node:path";
|
|
2
|
+
import { basename, dirname, extname, join, relative } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
buildUnknownPublicProductResponse,
|
|
6
|
+
isKnownPublicOffering,
|
|
7
|
+
API_LEVEL_NOTE,
|
|
8
|
+
DBR_ONLY_EDITIONS_NOTE,
|
|
9
|
+
WEB_ONLY_OMIT_NOTE
|
|
10
|
+
} from "../public-offerings.js";
|
|
5
11
|
import { buildUnsupportedPublicScopeResponse } from "./public-routing.js";
|
|
6
12
|
|
|
7
13
|
export function registerProjectTools({
|
|
@@ -82,12 +88,12 @@ export function registerProjectTools({
|
|
|
82
88
|
"",
|
|
83
89
|
"PARAMETERS:",
|
|
84
90
|
"- product (required): dbr, dwt, ddv, mrz, or mds.",
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
`- edition: mobile, web, or server. ${DBR_ONLY_EDITIONS_NOTE}`,
|
|
92
|
+
`- platform: only DBR spans multiple platforms (android, ios, js, python, cpp, java, dotnet, nodejs, react, vue, angular, flutter, react-native, maui, etc.). ${WEB_ONLY_OMIT_NOTE}`,
|
|
87
93
|
"- version: Version constraint. Latest major is used by default.",
|
|
88
94
|
"- sample_id: Sample identifier as returned by list_samples (e.g. 'hello-world', 'ScanSingleBarcode'). Requires product/edition.",
|
|
89
95
|
"- resource_uri: A sample:// URI as returned by search (e.g. 'sample://dbr/mobile/android/10/high-level/ScanSingleBarcode' or 'sample://mrz/server/python/3/mrz_scanner'). Preferred over sample_id when available.",
|
|
90
|
-
|
|
96
|
+
`- api_level: ${API_LEVEL_NOTE}`,
|
|
91
97
|
"",
|
|
92
98
|
"RETURNS: A text block containing all project files inline, each under a heading with its relative path and wrapped in a fenced code block. Files larger than 50KB are excluded. No zip file is created.",
|
|
93
99
|
"",
|
|
@@ -96,13 +102,15 @@ export function registerProjectTools({
|
|
|
96
102
|
"RELATED TOOLS: list_samples (discover sample IDs), search (find samples by keyword), get_quickstart (quick single-file snippet)."
|
|
97
103
|
].join("\n"),
|
|
98
104
|
inputSchema: {
|
|
99
|
-
product: z.string().
|
|
100
|
-
edition: z.string().optional().describe(
|
|
101
|
-
platform: z.string().optional().describe(
|
|
105
|
+
product: z.string().optional().describe("Product: dbr, dwt, ddv, mrz, mds. Optional when resource_uri is provided (the URI already encodes it)."),
|
|
106
|
+
edition: z.string().optional().describe(`Edition: mobile, web, server/desktop. ${DBR_ONLY_EDITIONS_NOTE}`),
|
|
107
|
+
platform: z.string().optional().describe(`Platform (DBR only spans multiple): android, ios, maui, react-native, flutter, js, python, cpp, java, dotnet, nodejs, angular, blazor, capacitor, electron, es6, native-ts, next, nuxt, pwa, react, requirejs, svelte, vue, webview. ${WEB_ONLY_OMIT_NOTE}`),
|
|
102
108
|
version: z.string().optional().describe("Version constraint"),
|
|
103
109
|
sample_id: z.string().optional().describe("Sample identifier (name or path)"),
|
|
104
110
|
resource_uri: z.string().optional().describe("Resource URI returned by search"),
|
|
105
|
-
api_level: z.string().optional().describe(
|
|
111
|
+
api_level: z.string().optional().describe(`API level: ${API_LEVEL_NOTE}`),
|
|
112
|
+
files: z.array(z.string()).optional().describe("Optional subset: exact relative paths or *.ext patterns to return (default: all files)"),
|
|
113
|
+
manifest_only: z.boolean().optional().describe("If true, return only the file list (paths + sizes), not file contents")
|
|
106
114
|
},
|
|
107
115
|
annotations: {
|
|
108
116
|
readOnlyHint: true,
|
|
@@ -111,7 +119,7 @@ export function registerProjectTools({
|
|
|
111
119
|
openWorldHint: false
|
|
112
120
|
}
|
|
113
121
|
},
|
|
114
|
-
async ({ product, edition, platform, version, sample_id, resource_uri, api_level }) => {
|
|
122
|
+
async ({ product, edition, platform, version, sample_id, resource_uri, api_level, files: fileFilter, manifest_only }) => {
|
|
115
123
|
let sampleInfo = null;
|
|
116
124
|
if (resource_uri) {
|
|
117
125
|
const parsed = parseResourceUri(resource_uri);
|
|
@@ -145,6 +153,13 @@ export function registerProjectTools({
|
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
if (!sampleInfo && !product) {
|
|
157
|
+
return {
|
|
158
|
+
isError: true,
|
|
159
|
+
content: [{ type: "text", text: "Provide a resource_uri (from search) or a product + sample_id. Use search or list_samples to discover one." }]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
148
163
|
const normalizedProduct = normalizeProduct(sampleInfo?.product || product);
|
|
149
164
|
if ((sampleInfo?.product || product) && !isKnownPublicOffering(normalizedProduct)) {
|
|
150
165
|
return buildUnknownPublicProductResponse(sampleInfo?.product || product);
|
|
@@ -152,8 +167,14 @@ export function registerProjectTools({
|
|
|
152
167
|
|
|
153
168
|
const normalizedPlatform = normalizePlatform(sampleInfo?.platform || platform);
|
|
154
169
|
const normalizedEdition = normalizeEdition(sampleInfo?.edition || edition, normalizedPlatform, normalizedProduct);
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
// A concrete sample:// URI names a hydrated sample we can serve inline, so
|
|
171
|
+
// it must bypass the scope-level redirect (which is for scope-only asks).
|
|
172
|
+
// Without this, search emits mrz/mds mobile/server URIs that this tool then
|
|
173
|
+
// refuses, and its own serving branches below are unreachable (issue #131).
|
|
174
|
+
if (!sampleInfo) {
|
|
175
|
+
const unsupportedScopeResponse = buildUnsupportedPublicScopeResponse(normalizedProduct, normalizedEdition, normalizedPlatform);
|
|
176
|
+
if (unsupportedScopeResponse) return unsupportedScopeResponse;
|
|
177
|
+
}
|
|
157
178
|
|
|
158
179
|
await ensureScopeHydrated({
|
|
159
180
|
product: normalizedProduct,
|
|
@@ -265,7 +286,20 @@ export function registerProjectTools({
|
|
|
265
286
|
} else if (normalizedProduct === "dcv" && normalizedEdition === "server") {
|
|
266
287
|
samplePath = getDcvServerSamplePath(normalizedPlatform || "python", sampleName);
|
|
267
288
|
} else if (normalizedProduct === "dbr" && normalizedEdition === "web") {
|
|
268
|
-
|
|
289
|
+
// Accept both bare ("hello-world") and category-qualified
|
|
290
|
+
// ("scenarios/read-a-drivers-license", "frameworks/react") sample ids.
|
|
291
|
+
if (sampleName.includes("/")) {
|
|
292
|
+
const slash = sampleName.indexOf("/");
|
|
293
|
+
const cat = sampleName.slice(0, slash);
|
|
294
|
+
const name = sampleName.slice(slash + 1);
|
|
295
|
+
samplePath = getWebSamplePath(cat, name)
|
|
296
|
+
|| getWebSamplePath(undefined, name)
|
|
297
|
+
|| getWebSamplePath(undefined, sampleName);
|
|
298
|
+
} else {
|
|
299
|
+
samplePath = getWebSamplePath("basics", sampleName)
|
|
300
|
+
|| getWebSamplePath("scenarios", sampleName)
|
|
301
|
+
|| getWebSamplePath(undefined, sampleName);
|
|
302
|
+
}
|
|
269
303
|
} else if (normalizedProduct === "dbr" && normalizedEdition === "server") {
|
|
270
304
|
samplePath = getDbrServerSamplePath(normalizedPlatform || "python", sampleName);
|
|
271
305
|
} else if (normalizedProduct === "dwt") {
|
|
@@ -338,7 +372,11 @@ export function registerProjectTools({
|
|
|
338
372
|
const textExtensions = [
|
|
339
373
|
".java", ".kt", ".swift", ".m", ".h", ".xml", ".gradle", ".properties",
|
|
340
374
|
".pro", ".json", ".plist", ".storyboard", ".xib", ".gitignore", ".md",
|
|
341
|
-
".js", ".jsx", ".ts", ".tsx", ".vue", ".cjs", ".html", ".css", ".py"
|
|
375
|
+
".js", ".jsx", ".ts", ".tsx", ".vue", ".cjs", ".mjs", ".html", ".css", ".py",
|
|
376
|
+
// C/C++, .NET, build/project files so server samples aren't returned empty.
|
|
377
|
+
".cpp", ".cc", ".cxx", ".c", ".hpp", ".hh", ".cs", ".csproj", ".vcxproj",
|
|
378
|
+
".sln", ".txt", ".cmake", ".config", ".yaml", ".yml", ".gradlew", ".dart",
|
|
379
|
+
".rb", ".sh", ".bat", ".xaml", ".razor"
|
|
342
380
|
];
|
|
343
381
|
|
|
344
382
|
const files = [];
|
|
@@ -386,16 +424,120 @@ export function registerProjectTools({
|
|
|
386
424
|
addFile(samplePath);
|
|
387
425
|
}
|
|
388
426
|
|
|
389
|
-
|
|
427
|
+
// Reference closure: single-file web samples (e.g. an index.html that
|
|
428
|
+
// initSettingsFromFile('./read_dl.json')) are delivered without their
|
|
429
|
+
// siblings by the walk above. Pull in files the payload references so the
|
|
430
|
+
// project is runnable as delivered (issue #137).
|
|
431
|
+
const includedPaths = new Set(files.map((f) => f.path));
|
|
432
|
+
const refPattern = /(?:src|href)\s*=\s*["']([^"']+)["']|from\s+["']([^"']+)["']|initSettingsFromFile\(\s*["']([^"']+)["']|["'](\.\.?\/[^"']+\.(?:json|css|js|xml))["']/g;
|
|
433
|
+
const pending = [...files];
|
|
434
|
+
while (pending.length) {
|
|
435
|
+
const file = pending.pop();
|
|
436
|
+
const fileDir = dirname(join(rootDir, file.path));
|
|
437
|
+
let match;
|
|
438
|
+
refPattern.lastIndex = 0;
|
|
439
|
+
while ((match = refPattern.exec(file.content)) !== null) {
|
|
440
|
+
const ref = match[1] || match[2] || match[3] || match[4];
|
|
441
|
+
if (!ref) continue;
|
|
442
|
+
// Skip anything that isn't a project-relative path: absolute URLs,
|
|
443
|
+
// data URIs, node_modules, and — importantly — filesystem-absolute
|
|
444
|
+
// paths ("/etc/passwd"), which must never be read (Copilot #463).
|
|
445
|
+
if (/^(https?:)?\/\//.test(ref) || ref.startsWith("data:") || ref.startsWith("/") || ref.includes("node_modules")) continue;
|
|
446
|
+
const cleanRef = ref.split(/[?#]/)[0];
|
|
447
|
+
const resolved = join(fileDir, cleanRef);
|
|
448
|
+
const relPath = relative(rootDir, resolved);
|
|
449
|
+
// Only follow references that stay INSIDE the sample tree. Out-of-tree
|
|
450
|
+
// refs (../../CustomTemplates/x.json) can't be represented in the flat
|
|
451
|
+
// payload and a basename rewrite wouldn't satisfy the original ref, so
|
|
452
|
+
// we don't chase them (avoids host-file traversal too).
|
|
453
|
+
if (relPath.startsWith("..") || includedPaths.has(relPath)) continue;
|
|
454
|
+
// Text assets only — never inline binary content.
|
|
455
|
+
const ext = extname(resolved).toLowerCase();
|
|
456
|
+
if (!textExtensions.includes(ext)) continue;
|
|
457
|
+
if (!existsSync(resolved)) continue;
|
|
458
|
+
try {
|
|
459
|
+
if (!statSync(resolved).isFile()) continue;
|
|
460
|
+
} catch { continue; }
|
|
461
|
+
try {
|
|
462
|
+
const content = readFileSync(resolved, "utf-8").replace(/\r\n/g, "\n");
|
|
463
|
+
const added = { path: relPath, content, ext: (ext.replace(".", "") || "text") };
|
|
464
|
+
files.push(added);
|
|
465
|
+
pending.push(added);
|
|
466
|
+
includedPaths.add(relPath);
|
|
467
|
+
} catch { /* skip unreadable */ }
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const MAX_FILE_BYTES = 50000;
|
|
472
|
+
let selected = files;
|
|
473
|
+
if (Array.isArray(fileFilter) && fileFilter.length) {
|
|
474
|
+
const matchers = fileFilter.map((pat) => {
|
|
475
|
+
if (pat.startsWith("*.")) {
|
|
476
|
+
const ext = pat.slice(1);
|
|
477
|
+
return (p) => p.endsWith(ext);
|
|
478
|
+
}
|
|
479
|
+
return (p) => p === pat || basename(p) === pat;
|
|
480
|
+
});
|
|
481
|
+
selected = files.filter((f) => matchers.some((m) => m(f.path)));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Entry files first, then everything else (stable) (issue #154).
|
|
485
|
+
const ENTRY_ORDER = ["index.html", "main.tsx", "main.ts", "main.js", "App.tsx", "App.jsx", "App.vue", "MainActivity.kt", "MainActivity.java", "ViewController.swift", "pubspec.yaml", "package.json", "pom.xml"];
|
|
486
|
+
const entryRank = (p) => {
|
|
487
|
+
const base = basename(p);
|
|
488
|
+
const idx = ENTRY_ORDER.indexOf(base);
|
|
489
|
+
return idx === -1 ? ENTRY_ORDER.length : idx;
|
|
490
|
+
};
|
|
491
|
+
selected = [...selected].sort((a, b) => entryRank(a.path) - entryRank(b.path));
|
|
492
|
+
|
|
493
|
+
const excluded = selected.filter((f) => f.content.length >= MAX_FILE_BYTES);
|
|
494
|
+
const validFiles = selected.filter((f) => f.content.length < MAX_FILE_BYTES);
|
|
495
|
+
|
|
496
|
+
// Empty payload → actionable miss, never a silent empty success (issue #138).
|
|
497
|
+
if (files.length === 0 || (validFiles.length === 0 && !manifest_only)) {
|
|
498
|
+
const suggestions = await getSampleSuggestions({
|
|
499
|
+
query: sampleQuery,
|
|
500
|
+
product: normalizedProduct,
|
|
501
|
+
edition: normalizedEdition,
|
|
502
|
+
platform: normalizedPlatform,
|
|
503
|
+
limit: 5
|
|
504
|
+
});
|
|
505
|
+
const content = [{
|
|
506
|
+
type: "text",
|
|
507
|
+
text: [
|
|
508
|
+
`No readable files found for "${sampleLabel}".`,
|
|
509
|
+
suggestions.length ? "Related samples:" : "Try list_samples or search to find a valid sample."
|
|
510
|
+
].join("\n")
|
|
511
|
+
}];
|
|
512
|
+
for (const entry of suggestions) {
|
|
513
|
+
const sampleId = entry.type === "sample" ? getSampleIdFromUri(entry.uri) : "";
|
|
514
|
+
content.push({
|
|
515
|
+
type: "resource_link",
|
|
516
|
+
uri: entry.uri,
|
|
517
|
+
name: entry.title,
|
|
518
|
+
description: `${entry.type.toUpperCase()} | ${formatScopeLabel(entry)} | ${entry.version ? "v" + entry.version : "n/a"} - ${entry.summary}${sampleId ? " | sample_id: " + sampleId : ""}`,
|
|
519
|
+
mimeType: entry.mimeType,
|
|
520
|
+
annotations: { audience: ["assistant"], priority: 0.6 }
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return { isError: true, content };
|
|
524
|
+
}
|
|
390
525
|
|
|
526
|
+
const totalKb = (selected.reduce((sum, f) => sum + f.content.length, 0) / 1024).toFixed(1);
|
|
391
527
|
const output = [
|
|
392
528
|
`# Sample Files: ${sampleLabel}`,
|
|
393
529
|
"",
|
|
394
|
-
|
|
395
|
-
|
|
530
|
+
`## Files (${validFiles.length}${excluded.length ? ` of ${selected.length}` : ""}, ${totalKb} KB total)`,
|
|
531
|
+
...validFiles.map((f) => `- ${f.path} (${(f.content.length / 1024).toFixed(1)} KB)`),
|
|
532
|
+
excluded.length ? `Excluded (>50KB): ${excluded.map((f) => f.path).join(", ")}` : "",
|
|
396
533
|
""
|
|
397
|
-
];
|
|
534
|
+
].filter((line) => line !== "");
|
|
535
|
+
|
|
536
|
+
if (manifest_only) {
|
|
537
|
+
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
538
|
+
}
|
|
398
539
|
|
|
540
|
+
output.push("", "Note: Files are returned inline and no downloadable zip is created.", "");
|
|
399
541
|
for (const file of validFiles) {
|
|
400
542
|
output.push(`## ${file.path}`);
|
|
401
543
|
output.push("```" + (file.ext || "text"));
|