simple-dynamsoft-mcp 7.0.0 → 7.1.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/README.md CHANGED
@@ -223,13 +223,23 @@ Location:
223
223
 
224
224
  The server exposes this minimal tool surface:
225
225
 
226
- - `get_index`
227
- - `search`
228
- - `list_samples`
229
- - `resolve_sample`
230
- - `resolve_version`
231
- - `get_quickstart`
232
- - `generate_project`
226
+ - `get_index` -- compact product/version/sample index with selection guidance
227
+ - `search` -- semantic search across docs and samples (also accepts exact sample IDs)
228
+ - `list_samples` -- browse available samples for a product/edition/platform
229
+ - `resolve_version` -- resolve current version for a product/edition/platform
230
+ - `get_quickstart` -- opinionated quickstart: picks a sample by scenario, returns code + install instructions
231
+ - `get_sample_files` -- get full project files for a known sample (discovered via list_samples or search)
232
+
233
+ ## Companion: Dynamsoft SDK Skills
234
+
235
+ For AI agents that support skills (Claude Code, OpenCode, Codex), install [dynamsoft-sdk-skills](https://github.com/user/dynamsoft-sdk-skills) for guided integration workflows:
236
+
237
+ npx dynamsoft-sdk-skills install --all
238
+
239
+ - **Skills** provide integration patterns, gotchas, and decision trees (loaded into agent context)
240
+ - **MCP Server** provides runtime tools: version resolution, doc search, sample browsing, and retrieval of full sample project files
241
+
242
+ Both work independently, but together the skills guide agents to use MCP tools at the right moments.
233
243
 
234
244
  ## Quick Troubleshooting
235
245
 
@@ -8,8 +8,8 @@
8
8
  "branch": "main",
9
9
  "owner": "dynamsoft-docs",
10
10
  "repo": "barcode-reader-docs-js",
11
- "commit": "10c2fc25201f89eb5a6a412bce5fe2fb7e4b3f5f",
12
- "archiveUrl": "https://codeload.github.com/dynamsoft-docs/barcode-reader-docs-js/zip/10c2fc25201f89eb5a6a412bce5fe2fb7e4b3f5f"
11
+ "commit": "acab784e29ff035b355dcd6d6ebe284fcb34eef3",
12
+ "archiveUrl": "https://codeload.github.com/dynamsoft-docs/barcode-reader-docs-js/zip/acab784e29ff035b355dcd6d6ebe284fcb34eef3"
13
13
  },
14
14
  {
15
15
  "name": "data/documentation/barcode-reader-docs-mobile",
@@ -88,8 +88,8 @@
88
88
  "branch": "master",
89
89
  "owner": "dynamsoft-docs",
90
90
  "repo": "web-twain-docs",
91
- "commit": "5da9b65024e8eeecab1dced934638c57cafe0b79",
92
- "archiveUrl": "https://codeload.github.com/dynamsoft-docs/web-twain-docs/zip/5da9b65024e8eeecab1dced934638c57cafe0b79"
91
+ "commit": "779f3794064a224c3a26740470250389ca9aa0ce",
92
+ "archiveUrl": "https://codeload.github.com/dynamsoft-docs/web-twain-docs/zip/779f3794064a224c3a26740470250389ca9aa0ce"
93
93
  },
94
94
  {
95
95
  "name": "data/samples/dynamic-web-twain",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-dynamsoft-mcp",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "MCP server for Dynamsoft SDKs - Capture Vision, Barcode Reader (Mobile/Python/Web), Dynamic Web TWAIN, and Document Viewer. Provides documentation, code snippets, and API guidance.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -75,7 +75,10 @@ registerIndexTools({
75
75
  buildIndexData,
76
76
  getSampleIdFromUri,
77
77
  formatScopeLabel,
78
- searchResources
78
+ searchResources,
79
+ normalizeSampleName,
80
+ getSampleEntries,
81
+ getSampleSuggestions
79
82
  });
80
83
 
81
84
  registerSampleTools({
@@ -85,16 +88,10 @@ registerSampleTools({
85
88
  normalizeProduct,
86
89
  normalizePlatform,
87
90
  normalizeEdition,
88
- normalizeSampleName,
89
- parseSampleUri,
90
- resourceIndex,
91
91
  getSampleEntries,
92
92
  getSampleIdFromUri,
93
93
  getDisplayEdition,
94
- getDisplayPlatform,
95
- formatScopeLabel,
96
- searchResources,
97
- getSampleSuggestions
94
+ getDisplayPlatform
98
95
  });
99
96
 
100
97
  registerVersionTools({
@@ -11,7 +11,10 @@ export function registerIndexTools({
11
11
  buildIndexData,
12
12
  getSampleIdFromUri,
13
13
  formatScopeLabel,
14
- searchResources
14
+ searchResources,
15
+ normalizeSampleName,
16
+ getSampleEntries,
17
+ getSampleSuggestions
15
18
  }) {
16
19
  server.registerTool(
17
20
  "get_index",
@@ -29,7 +32,7 @@ export function registerIndexTools({
29
32
  "search",
30
33
  {
31
34
  title: "Search",
32
- description: "Semantic (RAG) search across docs and samples with fuzzy fallback; returns resource links for lazy loading. Prefer DCV for MRZ/VIN/document-normalization/driver-license scenarios; DBR for barcode-only.",
35
+ description: "Search across docs and samples with semantic (RAG) search and fuzzy fallback. Accepts keywords or exact sample IDs. Returns resource links for lazy loading. Prefer DCV for MRZ/VIN/document-normalization/driver-license scenarios; DBR for barcode-only.",
33
36
  inputSchema: {
34
37
  query: z.string().describe("Keywords to search across docs and samples."),
35
38
  product: z.string().optional().describe("Product: dcv, dbr, dwt, ddv"),
@@ -68,16 +71,126 @@ export function registerIndexTools({
68
71
  }
69
72
 
70
73
  const maxResults = Math.min(limit || 5, 10);
74
+
75
+ // Exact sample-ID fast path (when searching for samples)
76
+ const effectiveType = type || "any";
77
+ if (effectiveType === "sample" || effectiveType === "any") {
78
+ const normalizedQuery = normalizeSampleName(query);
79
+ const scopedSamples = getSampleEntries({
80
+ product: normalizedProduct,
81
+ edition: normalizedEdition,
82
+ platform: normalizedPlatform
83
+ });
84
+ const exactMatches = scopedSamples.filter((entry) => {
85
+ const entryId = getSampleIdFromUri(entry.uri);
86
+ return entryId && entryId.toLowerCase() === normalizedQuery.toLowerCase();
87
+ });
88
+ if (exactMatches.length > 0) {
89
+ const selected = exactMatches.slice(0, maxResults);
90
+ const content = [
91
+ {
92
+ type: "text",
93
+ text: `Found ${selected.length} exact match(es) for "${query}". Read the links you need with resources/read.`
94
+ }
95
+ ];
96
+
97
+ for (const entry of selected) {
98
+ const versionLabel = entry.version ? `v${entry.version}` : "n/a";
99
+ const scopeLabel = formatScopeLabel(entry);
100
+ const sampleId = getSampleIdFromUri(entry.uri);
101
+ const sampleHint = sampleId ? ` | sample_id: ${sampleId}` : "";
102
+ const scoreLabel = formatScoreLabel(entry);
103
+ content.push({
104
+ type: "resource_link",
105
+ uri: entry.uri,
106
+ name: entry.title,
107
+ description: `${entry.type.toUpperCase()} | ${scopeLabel} | ${versionLabel}${scoreLabel} - ${entry.summary}${sampleHint}`,
108
+ mimeType: entry.mimeType,
109
+ annotations: {
110
+ audience: ["assistant"],
111
+ priority: 0.8
112
+ }
113
+ });
114
+ }
115
+
116
+ const plainLines = selected.map((entry, index) => {
117
+ const sampleId = getSampleIdFromUri(entry.uri);
118
+ const action = "get_sample_files resource_uri";
119
+ const sampleNote = sampleId ? ` sample_id=${sampleId}` : "";
120
+ const scoreNote = formatScoreNote(entry);
121
+ return `- ${index + 1}. ${entry.uri}${sampleNote}${scoreNote} (${action})`;
122
+ });
123
+ content.push({
124
+ type: "text",
125
+ text: ["Plain URIs (copy/paste):", ...plainLines].join("\n")
126
+ });
127
+
128
+ return { content };
129
+ }
130
+ }
131
+
71
132
  const topResults = await searchResources({
72
133
  query,
73
134
  product: normalizedProduct,
74
135
  edition: normalizedEdition,
75
136
  platform: normalizedPlatform,
76
- type: type || "any",
137
+ type: effectiveType,
77
138
  limit: maxResults
78
139
  });
79
140
 
80
141
  if (topResults.length === 0) {
142
+ // Only try sample suggestions when searching samples or any type
143
+ if (effectiveType === "sample" || effectiveType === "any") {
144
+ const suggestions = await getSampleSuggestions({
145
+ query,
146
+ product: normalizedProduct,
147
+ edition: normalizedEdition,
148
+ platform: normalizedPlatform,
149
+ limit: maxResults
150
+ });
151
+
152
+ if (suggestions.length > 0) {
153
+ const content = [
154
+ {
155
+ type: "text",
156
+ text: `No exact results for "${query}". Related samples:`
157
+ }
158
+ ];
159
+
160
+ for (const entry of suggestions) {
161
+ const versionLabel = entry.version ? `v${entry.version}` : "n/a";
162
+ const scopeLabel = formatScopeLabel(entry);
163
+ const sampleId = entry.type === "sample" ? getSampleIdFromUri(entry.uri) : "";
164
+ const sampleHint = sampleId ? ` | sample_id: ${sampleId}` : "";
165
+ const scoreLabel = formatScoreLabel(entry);
166
+ content.push({
167
+ type: "resource_link",
168
+ uri: entry.uri,
169
+ name: entry.title,
170
+ description: `${entry.type.toUpperCase()} | ${scopeLabel} | ${versionLabel}${scoreLabel} - ${entry.summary}${sampleHint}`,
171
+ mimeType: entry.mimeType,
172
+ annotations: {
173
+ audience: ["assistant"],
174
+ priority: 0.6
175
+ }
176
+ });
177
+ }
178
+
179
+ const plainLines = suggestions.map((entry, index) => {
180
+ const sampleId = entry.type === "sample" ? getSampleIdFromUri(entry.uri) : "";
181
+ const sampleNote = sampleId ? ` sample_id=${sampleId}` : "";
182
+ const scoreNote = formatScoreNote(entry);
183
+ return `- ${index + 1}. ${entry.uri}${sampleNote}${scoreNote}`;
184
+ });
185
+ content.push({
186
+ type: "text",
187
+ text: ["Plain URIs (copy/paste):", ...plainLines].join("\n")
188
+ });
189
+
190
+ return { content };
191
+ }
192
+ }
193
+
81
194
  return {
82
195
  content: [{
83
196
  type: "text",
@@ -114,7 +227,7 @@ export function registerIndexTools({
114
227
 
115
228
  const plainLines = topResults.map((entry, index) => {
116
229
  const sampleId = entry.type === "sample" ? getSampleIdFromUri(entry.uri) : "";
117
- const action = entry.type === "sample" ? "generate_project resource_uri" : "resources/read uri";
230
+ const action = entry.type === "sample" ? "get_sample_files resource_uri" : "resources/read uri";
118
231
  const sampleNote = sampleId ? ` sample_id=${sampleId}` : "";
119
232
  const scoreNote = formatScoreNote(entry);
120
233
  return `- ${index + 1}. ${entry.uri}${sampleNote}${scoreNote} (${action})`;
@@ -27,10 +27,10 @@ export function registerProjectTools({
27
27
  getSampleSuggestions
28
28
  }) {
29
29
  server.registerTool(
30
- "generate_project",
30
+ "get_sample_files",
31
31
  {
32
- title: "Generate Project",
33
- description: "Generate a project structure from a sample and return files inline (no zip/download).",
32
+ title: "Get Sample Files",
33
+ description: "Get project files for a known sample (by sample_id or resource_uri) and return them inline. Use list_samples or search to discover sample IDs first.",
34
34
  inputSchema: {
35
35
  product: z.string().describe("Product: dcv, dbr, dwt, or ddv"),
36
36
  edition: z.string().optional().describe("Edition: mobile, web, server/desktop"),
@@ -284,10 +284,10 @@ export function registerProjectTools({
284
284
  const validFiles = files.filter((f) => f.content.length < 50000);
285
285
 
286
286
  const output = [
287
- `# Project Generation: ${sampleLabel}`,
287
+ `# Sample Files: ${sampleLabel}`,
288
288
  "",
289
- "This output contains the file structure for the project.",
290
- "Note: This tool returns files inline and does not create a downloadable zip.",
289
+ "Below are the retrieved sample project files.",
290
+ "Note: Files are returned inline and no downloadable zip is created.",
291
291
  ""
292
292
  ];
293
293
 
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
- import { formatScoreLabel, formatScoreNote } from "../helpers/server-helpers.js";
3
2
 
4
3
  export function registerSampleTools({
5
4
  server,
@@ -8,16 +7,10 @@ export function registerSampleTools({
8
7
  normalizeProduct,
9
8
  normalizePlatform,
10
9
  normalizeEdition,
11
- normalizeSampleName,
12
- parseSampleUri,
13
- resourceIndex,
14
10
  getSampleEntries,
15
11
  getSampleIdFromUri,
16
12
  getDisplayEdition,
17
- getDisplayPlatform,
18
- formatScopeLabel,
19
- searchResources,
20
- getSampleSuggestions
13
+ getDisplayPlatform
21
14
  }) {
22
15
  server.registerTool(
23
16
  "list_samples",
@@ -100,234 +93,4 @@ export function registerSampleTools({
100
93
  };
101
94
  }
102
95
  );
103
-
104
- server.registerTool(
105
- "resolve_sample",
106
- {
107
- title: "Resolve Sample",
108
- description: "Resolve a sample_id (or sample URI) to matching sample URIs.",
109
- inputSchema: {
110
- sample_id: z.string().describe("Sample identifier or sample:// URI"),
111
- product: z.string().optional().describe("Product: dcv, dbr, dwt, ddv"),
112
- edition: z.string().optional().describe("Edition: core, mobile, web, server/desktop"),
113
- 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"),
114
- limit: z.number().int().min(1).max(10).optional().describe("Max results (default 5)")
115
- }
116
- },
117
- async ({ sample_id, product, edition, platform, limit }) => {
118
- if (!sample_id || !sample_id.trim()) {
119
- return { isError: true, content: [{ type: "text", text: "sample_id is required." }] };
120
- }
121
-
122
- const normalizedProduct = normalizeProduct(product);
123
- const normalizedPlatform = normalizePlatform(platform);
124
- const normalizedEdition = normalizeEdition(edition, normalizedPlatform, normalizedProduct);
125
-
126
- await ensureScopeHydrated({
127
- product: normalizedProduct,
128
- edition: normalizedEdition,
129
- platform: normalizedPlatform,
130
- type: "sample"
131
- });
132
-
133
- const policy = ensureLatestMajor({
134
- product: normalizedProduct,
135
- version: undefined,
136
- query: sample_id,
137
- edition: normalizedEdition,
138
- platform: normalizedPlatform
139
- });
140
-
141
- if (!policy.ok) {
142
- return { isError: true, content: [{ type: "text", text: policy.message }] };
143
- }
144
-
145
- if (sample_id.includes("://")) {
146
- const parsed = parseSampleUri(sample_id);
147
- if (!parsed) {
148
- return {
149
- isError: true,
150
- content: [{
151
- type: "text",
152
- text: "sample_id looks like a URI but is not a valid sample:// URI. For doc:// URIs, use resources/read."
153
- }]
154
- };
155
- }
156
- const entry = resourceIndex.find((item) => item.uri === sample_id && item.type === "sample");
157
- if (!entry) {
158
- return {
159
- isError: true,
160
- content: [{
161
- type: "text",
162
- text: `Sample URI not found in index: ${sample_id}. Use list_samples or search.`
163
- }]
164
- };
165
- }
166
-
167
- const payload = [{
168
- sample_id: getSampleIdFromUri(entry.uri),
169
- uri: entry.uri,
170
- product: entry.product,
171
- edition: getDisplayEdition(entry.edition),
172
- platform: getDisplayPlatform(entry.platform),
173
- version: entry.version,
174
- title: entry.title,
175
- summary: entry.summary
176
- }];
177
-
178
- const output = {
179
- query: sample_id,
180
- returned: payload.length,
181
- samples: payload
182
- };
183
-
184
- return {
185
- content: [{
186
- type: "text",
187
- text: [
188
- `Found ${payload.length} match(es) for "${sample_id}".`,
189
- "Plain URIs (copy/paste):",
190
- `- 1. ${entry.uri} (sample_id: ${payload[0].sample_id})`,
191
- "",
192
- "JSON:",
193
- JSON.stringify(output, null, 2)
194
- ].join("\n")
195
- }, {
196
- type: "resource_link",
197
- uri: entry.uri,
198
- name: entry.title,
199
- description: `SAMPLE | ${formatScopeLabel(entry)} | v${entry.version}${formatScoreLabel(entry)} | sample_id: ${payload[0].sample_id}`,
200
- mimeType: entry.mimeType,
201
- annotations: {
202
- audience: ["assistant"],
203
- priority: 0.8
204
- }
205
- }]
206
- };
207
- }
208
-
209
- const sampleQuery = normalizeSampleName(sample_id);
210
- const maxResults = Math.min(limit || 5, 10);
211
-
212
- const scopedSamples = getSampleEntries({
213
- product: normalizedProduct,
214
- edition: normalizedEdition,
215
- platform: normalizedPlatform
216
- });
217
-
218
- let matches = scopedSamples.filter((entry) => {
219
- const entryId = getSampleIdFromUri(entry.uri);
220
- return entryId && entryId.toLowerCase() === sampleQuery.toLowerCase();
221
- });
222
-
223
- if (matches.length === 0) {
224
- matches = await searchResources({
225
- query: sample_id,
226
- product: normalizedProduct,
227
- edition: normalizedEdition,
228
- platform: normalizedPlatform,
229
- type: "sample",
230
- limit: maxResults
231
- });
232
- }
233
-
234
- const selected = matches.slice(0, maxResults);
235
- if (selected.length === 0) {
236
- const suggestions = await getSampleSuggestions({
237
- query: sample_id,
238
- product: normalizedProduct,
239
- edition: normalizedEdition,
240
- platform: normalizedPlatform,
241
- limit: maxResults
242
- });
243
-
244
- const content = [{
245
- type: "text",
246
- text: suggestions.length
247
- ? `No exact sample match for "${sample_id}". Related samples:`
248
- : `No samples found for "${sample_id}". Try list_samples or search.`
249
- }];
250
-
251
- for (const entry of suggestions) {
252
- const sampleId = getSampleIdFromUri(entry.uri);
253
- content.push({
254
- type: "resource_link",
255
- uri: entry.uri,
256
- name: entry.title,
257
- description: `${entry.type.toUpperCase()} | ${formatScopeLabel(entry)} | v${entry.version}${formatScoreLabel(entry)} | sample_id: ${sampleId || "n/a"}`,
258
- mimeType: entry.mimeType,
259
- annotations: {
260
- audience: ["assistant"],
261
- priority: 0.6
262
- }
263
- });
264
- }
265
-
266
- if (suggestions.length) {
267
- const plainLines = suggestions.map((entry, index) => {
268
- const sampleId = getSampleIdFromUri(entry.uri);
269
- const sampleNote = sampleId ? ` (sample_id: ${sampleId})` : "";
270
- const scoreNote = formatScoreNote(entry);
271
- return `- ${index + 1}. ${entry.uri}${sampleNote}${scoreNote}`;
272
- });
273
- content.push({
274
- type: "text",
275
- text: ["Plain URIs (copy/paste):", ...plainLines].join("\n")
276
- });
277
- }
278
-
279
- return { isError: true, content };
280
- }
281
-
282
- const payload = selected.map((entry) => ({
283
- sample_id: getSampleIdFromUri(entry.uri),
284
- uri: entry.uri,
285
- product: entry.product,
286
- edition: getDisplayEdition(entry.edition),
287
- platform: getDisplayPlatform(entry.platform),
288
- version: entry.version,
289
- title: entry.title,
290
- summary: entry.summary
291
- }));
292
-
293
- const lines = [
294
- `Found ${selected.length} match(es) for "${sample_id}".`,
295
- "Plain URIs (copy/paste):",
296
- ...selected.map((entry, index) => {
297
- const sampleId = getSampleIdFromUri(entry.uri);
298
- const sampleNote = sampleId ? ` (sample_id: ${sampleId})` : "";
299
- const scoreNote = formatScoreNote(entry);
300
- return `- ${index + 1}. ${entry.uri}${sampleNote}${scoreNote}`;
301
- })
302
- ];
303
-
304
- const output = {
305
- query: sample_id,
306
- returned: payload.length,
307
- samples: payload
308
- };
309
-
310
- const content = [{
311
- type: "text",
312
- text: `${lines.join("\n")}\n\nJSON:\n${JSON.stringify(output, null, 2)}`
313
- }];
314
-
315
- for (const entry of selected) {
316
- const sampleId = getSampleIdFromUri(entry.uri);
317
- content.push({
318
- type: "resource_link",
319
- uri: entry.uri,
320
- name: entry.title,
321
- description: `${entry.type.toUpperCase()} | ${formatScopeLabel(entry)} | v${entry.version}${formatScoreLabel(entry)} | sample_id: ${sampleId || "n/a"}`,
322
- mimeType: entry.mimeType,
323
- annotations: {
324
- audience: ["assistant"],
325
- priority: 0.8
326
- }
327
- });
328
- }
329
-
330
- return { content };
331
- }
332
- );
333
96
  }