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.
@@ -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
- const requestedMajor = parseMajorVersion(version) ?? detectMajorFromQuery(query);
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: `This MCP server only serves the latest major version of DDV (v${currentMajor}).`
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: `This MCP server only serves the latest major version of DCV (v${currentMajor}).`
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: `This MCP server only serves the latest major version of ${inferredProduct.toUpperCase()} (v${currentMajor}).`
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: `This MCP server only serves the latest major version of DBR (v${currentMajor}). DBR versions prior to v9 are not available.`
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: `This MCP server only serves the latest major version of DWT (v${currentMajor}). DWT versions prior to v16 are not available.`
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
- throw new Error(`Resource not found: ${uriStr}`);
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 { buildUnknownPublicProductResponse, isKnownPublicOffering } from "../public-offerings.js";
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
- "- To determine valid product/edition/platform combinations before calling other tools.",
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
- "- edition: core, mobile, web, or server.",
88
- "- platform: android, ios, js, python, cpp, java, dotnet, nodejs, react, vue, angular, flutter, react-native, maui, etc.",
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("Edition: core, mobile, web, server/desktop"),
101
- platform: z.string().optional().describe("Platform: 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"),
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: `${entry.type.toUpperCase()} | ${scopeLabel} | ${versionLabel}${scoreLabel} - ${entry.summary}${sampleHint}`,
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: `${entry.type.toUpperCase()} | ${scopeLabel} | ${versionLabel}${scoreLabel} - ${entry.summary}${sampleHint}`,
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: `Found ${topResults.length} result(s) for "${query}". Read the links you need with resources/read.`
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: `${entry.type.toUpperCase()} | ${scopeLabel} | ${versionLabel}${scoreLabel} - ${entry.summary}${sampleHint}`,
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 { buildUnknownPublicProductResponse, isKnownPublicOffering } from "../public-offerings.js";
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
- "- edition: mobile, web, or server.",
86
- "- platform: android, ios, js, python, cpp, java, dotnet, nodejs, react, vue, angular, flutter, react-native, maui, etc.",
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
- "- api_level: 'high-level' or 'low-level' (DBR mobile only).",
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().trim().min(1, "Product is required.").describe("Product: dbr, dwt, ddv, mrz, mds"),
100
- edition: z.string().optional().describe("Edition: mobile, web, server/desktop"),
101
- platform: z.string().optional().describe("Platform: 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"),
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("API level: high-level or low-level (mobile only)")
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
- const unsupportedScopeResponse = buildUnsupportedPublicScopeResponse(normalizedProduct, normalizedEdition, normalizedPlatform);
156
- if (unsupportedScopeResponse) return unsupportedScopeResponse;
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
- samplePath = getWebSamplePath(undefined, sampleName);
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
- const validFiles = files.filter((f) => f.content.length < 50000);
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
- "Below are the retrieved sample project files.",
395
- "Note: Files are returned inline and no downloadable zip is created.",
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"));