simple-dynamsoft-mcp 2.0.5 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-dynamsoft-mcp",
3
- "version": "2.0.5",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server for Dynamsoft SDKs - Barcode Reader (Mobile/Python/Web) and Dynamic Web TWAIN. Provides documentation, code snippets, and API guidance.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.js CHANGED
@@ -12,6 +12,9 @@ const projectRoot = join(__dirname, "..");
12
12
  const registryUrl = new URL("../data/dynamsoft_sdks.json", import.meta.url);
13
13
  const registry = JSON.parse(readFileSync(registryUrl, "utf8"));
14
14
 
15
+ const dwtDocsUrl = new URL("../data/web-twain-api-docs.json", import.meta.url);
16
+ const dwtDocs = JSON.parse(readFileSync(dwtDocsUrl, "utf8"));
17
+
15
18
  const codeSnippetRoot = join(projectRoot, "code-snippet");
16
19
 
17
20
  // ============================================================================
@@ -213,6 +216,78 @@ function discoverPythonSamples() {
213
216
  return samples;
214
217
  }
215
218
 
219
+ function discoverWebSamples() {
220
+ const categories = {
221
+ "root": [],
222
+ "frameworks": [],
223
+ "scenarios": []
224
+ };
225
+ const webPath = join(codeSnippetRoot, "dynamsoft-barcode-reader", "web");
226
+
227
+ if (!existsSync(webPath)) return categories;
228
+
229
+ // Find HTML files in root
230
+ for (const entry of readdirSync(webPath, { withFileTypes: true })) {
231
+ if (entry.isFile() && entry.name.endsWith(".html")) {
232
+ categories["root"].push(entry.name.replace(".html", ""));
233
+ }
234
+ }
235
+
236
+ // Find samples in subdirectories
237
+ for (const subdir of ["frameworks", "scenarios"]) {
238
+ const subdirPath = join(webPath, subdir);
239
+ if (existsSync(subdirPath)) {
240
+ for (const entry of readdirSync(subdirPath, { withFileTypes: true })) {
241
+ if (entry.isDirectory()) {
242
+ categories[subdir].push(entry.name);
243
+ } else if (entry.isFile() && entry.name.endsWith(".html")) {
244
+ categories[subdir].push(entry.name.replace(".html", ""));
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ // Remove empty categories
251
+ for (const [key, value] of Object.entries(categories)) {
252
+ if (value.length === 0) delete categories[key];
253
+ }
254
+
255
+ return categories;
256
+ }
257
+
258
+ function getWebSamplePath(category, sampleName) {
259
+ const webPath = join(codeSnippetRoot, "dynamsoft-barcode-reader", "web");
260
+
261
+ if (category === "root" || !category) {
262
+ // Try root level
263
+ const htmlPath = join(webPath, `${sampleName}.html`);
264
+ if (existsSync(htmlPath)) return htmlPath;
265
+ } else {
266
+ // Try in subdirectory
267
+ const dirPath = join(webPath, category, sampleName);
268
+ if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
269
+ // Look for index.html or main html file
270
+ const indexPath = join(dirPath, "index.html");
271
+ if (existsSync(indexPath)) return indexPath;
272
+ // Look for any html file
273
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
274
+ if (entry.isFile() && entry.name.endsWith(".html")) {
275
+ return join(dirPath, entry.name);
276
+ }
277
+ }
278
+ }
279
+ // Try as html file directly
280
+ const htmlPath = join(webPath, category, `${sampleName}.html`);
281
+ if (existsSync(htmlPath)) return htmlPath;
282
+ }
283
+
284
+ // Fallback: search all
285
+ const rootPath = join(webPath, `${sampleName}.html`);
286
+ if (existsSync(rootPath)) return rootPath;
287
+
288
+ return null;
289
+ }
290
+
216
291
  function discoverDwtSamples() {
217
292
  const categories = {};
218
293
  const dwtPath = join(codeSnippetRoot, "dynamic-web-twain");
@@ -382,7 +457,9 @@ server.registerTool(
382
457
  lines.push("- `list_sdks` - List all SDKs");
383
458
  lines.push("- `get_sdk_info` - Get detailed SDK info for a platform");
384
459
  lines.push("- `list_samples` - List code samples (mobile)");
460
+ lines.push("- `list_web_samples` - List web barcode reader samples");
385
461
  lines.push("- `get_code_snippet` - Get actual source code");
462
+ lines.push("- `get_web_sample` - Get web barcode reader sample code");
386
463
  lines.push("- `get_quick_start` - Get complete working example");
387
464
  lines.push("- `get_gradle_config` - Get Android build config");
388
465
  lines.push("- `get_license_info` - Get license setup code");
@@ -391,6 +468,8 @@ server.registerTool(
391
468
  lines.push("- `get_python_sample` - Get Python SDK sample code");
392
469
  lines.push("- `get_dwt_sample` - Get Dynamic Web TWAIN sample");
393
470
  lines.push("- `list_dwt_categories` - List DWT sample categories");
471
+ lines.push("- `search_dwt_docs` - Search DWT API documentation");
472
+ lines.push("- `get_dwt_api_doc` - Get specific DWT documentation article");
394
473
 
395
474
  return { content: [{ type: "text", text: lines.join("\n") }] };
396
475
  }
@@ -576,6 +655,99 @@ server.registerTool(
576
655
  }
577
656
  );
578
657
 
658
+ // ============================================================================
659
+ // TOOL: list_web_samples
660
+ // ============================================================================
661
+
662
+ server.registerTool(
663
+ "list_web_samples",
664
+ {
665
+ title: "List Web Barcode Samples",
666
+ description: "List available JavaScript/Web barcode reader code samples",
667
+ inputSchema: {}
668
+ },
669
+ async () => {
670
+ const categories = discoverWebSamples();
671
+ const sdkEntry = registry.sdks["dbr-web"];
672
+
673
+ const lines = [
674
+ "# Web Barcode Reader Samples",
675
+ "",
676
+ `**SDK Version:** ${sdkEntry.version}`,
677
+ `**Install:** \`npm install dynamsoft-barcode-reader-bundle\``,
678
+ `**CDN:** \`${sdkEntry.platforms.web.installation.cdn}\``,
679
+ "",
680
+ "## Available Samples",
681
+ ""
682
+ ];
683
+
684
+ for (const [category, samples] of Object.entries(categories)) {
685
+ const categoryTitle = category === "root" ? "Basic Samples" : category.charAt(0).toUpperCase() + category.slice(1);
686
+ lines.push(`### ${categoryTitle}`);
687
+ lines.push(samples.map(s => `- ${s}`).join("\n"));
688
+ lines.push("");
689
+ }
690
+
691
+ lines.push("Use `get_web_sample` with sample_name to get code.");
692
+
693
+ return { content: [{ type: "text", text: lines.join("\n") }] };
694
+ }
695
+ );
696
+
697
+ // ============================================================================
698
+ // TOOL: get_web_sample
699
+ // ============================================================================
700
+
701
+ server.registerTool(
702
+ "get_web_sample",
703
+ {
704
+ title: "Get Web Barcode Sample",
705
+ description: "Get JavaScript/Web barcode reader sample code",
706
+ inputSchema: {
707
+ sample_name: z.string().describe("Sample name, e.g. hello-world, read-an-image"),
708
+ category: z.string().optional().describe("Category: root, frameworks, scenarios (optional)")
709
+ }
710
+ },
711
+ async ({ sample_name, category }) => {
712
+ const samplePath = getWebSamplePath(category, sample_name);
713
+
714
+ if (!samplePath) {
715
+ const categories = discoverWebSamples();
716
+ const allSamples = Object.entries(categories)
717
+ .map(([cat, samples]) => samples.map(s => `${cat}/${s}`))
718
+ .flat();
719
+ return {
720
+ content: [{
721
+ type: "text",
722
+ text: `Sample "${sample_name}" not found.\n\nAvailable samples:\n${allSamples.map(s => `- ${s}`).join("\n")}\n\nUse \`list_web_samples\` to see all available samples.`
723
+ }]
724
+ };
725
+ }
726
+
727
+ const content = readCodeFile(samplePath);
728
+ if (!content) {
729
+ return { content: [{ type: "text", text: `Could not read "${sample_name}".` }] };
730
+ }
731
+
732
+ const sdkEntry = registry.sdks["dbr-web"];
733
+
734
+ const output = [
735
+ `# Web Barcode Reader: ${sample_name}`,
736
+ "",
737
+ `**SDK Version:** ${sdkEntry.version}`,
738
+ `**Install:** \`npm install dynamsoft-barcode-reader-bundle\``,
739
+ `**CDN:** \`${sdkEntry.platforms.web.installation.cdn}\``,
740
+ `**Trial License:** \`${registry.trial_license}\``,
741
+ "",
742
+ "```html",
743
+ content,
744
+ "```"
745
+ ];
746
+
747
+ return { content: [{ type: "text", text: output.join("\n") }] };
748
+ }
749
+ );
750
+
579
751
  // ============================================================================
580
752
  // TOOL: list_dwt_categories
581
753
  // ============================================================================
@@ -609,6 +781,161 @@ server.registerTool(
609
781
  }
610
782
 
611
783
  lines.push("Use `get_dwt_sample` with category and sample_name to get code.");
784
+ lines.push("");
785
+ lines.push("**API Documentation Tools:**");
786
+ lines.push("- `search_dwt_docs` - Search DWT API documentation by keyword");
787
+ lines.push("- `get_dwt_api_doc` - Get specific DWT documentation article");
788
+
789
+ return { content: [{ type: "text", text: lines.join("\n") }] };
790
+ }
791
+ );
792
+
793
+ // ============================================================================
794
+ // TOOL: search_dwt_docs
795
+ // ============================================================================
796
+
797
+ server.registerTool(
798
+ "search_dwt_docs",
799
+ {
800
+ title: "Search DWT Documentation",
801
+ description: "Search Dynamic Web TWAIN API documentation by keyword. Returns matching articles with titles, URLs, and content excerpts.",
802
+ inputSchema: {
803
+ query: z.string().describe("Search keyword or phrase (e.g., 'LoadImage', 'PDF', 'scanner', 'OCR')"),
804
+ max_results: z.number().optional().describe("Maximum number of results to return (default: 5)")
805
+ }
806
+ },
807
+ async ({ query, max_results = 5 }) => {
808
+ const searchTerms = query.toLowerCase().split(/\s+/);
809
+ const results = [];
810
+
811
+ for (const article of dwtDocs.articles) {
812
+ const titleLower = article.title.toLowerCase();
813
+ const contentLower = article.content.toLowerCase();
814
+ const breadcrumbLower = (article.breadcrumb || "").toLowerCase();
815
+
816
+ // Score based on matches in title (higher weight), breadcrumb, and content
817
+ let score = 0;
818
+ for (const term of searchTerms) {
819
+ if (titleLower.includes(term)) score += 10;
820
+ if (breadcrumbLower.includes(term)) score += 5;
821
+ if (contentLower.includes(term)) score += 1;
822
+ }
823
+
824
+ if (score > 0) {
825
+ // Extract a relevant excerpt (first 300 chars that contain a search term)
826
+ let excerpt = article.content.substring(0, 300);
827
+ for (const term of searchTerms) {
828
+ const idx = contentLower.indexOf(term);
829
+ if (idx > 50) {
830
+ const start = Math.max(0, idx - 50);
831
+ excerpt = "..." + article.content.substring(start, start + 300) + "...";
832
+ break;
833
+ }
834
+ }
835
+
836
+ results.push({
837
+ title: article.title,
838
+ url: article.url,
839
+ breadcrumb: article.breadcrumb,
840
+ score,
841
+ excerpt: excerpt.replace(/\n+/g, " ").trim()
842
+ });
843
+ }
844
+ }
845
+
846
+ // Sort by score descending
847
+ results.sort((a, b) => b.score - a.score);
848
+ const topResults = results.slice(0, max_results);
849
+
850
+ if (topResults.length === 0) {
851
+ return {
852
+ content: [{
853
+ type: "text",
854
+ text: `No documentation found for "${query}".\n\nTry different keywords like:\n- API names: LoadImage, SaveAsPDF, AcquireImage\n- Features: scanner, PDF, OCR, barcode\n- Topics: initialization, upload, viewer`
855
+ }]
856
+ };
857
+ }
858
+
859
+ const lines = [
860
+ `# DWT Documentation Search: "${query}"`,
861
+ "",
862
+ `Found ${results.length} matches. Showing top ${topResults.length}:`,
863
+ ""
864
+ ];
865
+
866
+ for (let i = 0; i < topResults.length; i++) {
867
+ const r = topResults[i];
868
+ lines.push(`## ${i + 1}. ${r.title}`);
869
+ lines.push(`**Category:** ${r.breadcrumb}`);
870
+ lines.push(`**URL:** ${r.url}`);
871
+ lines.push("");
872
+ lines.push(`> ${r.excerpt}`);
873
+ lines.push("");
874
+ }
875
+
876
+ lines.push("Use `get_dwt_api_doc` with the article title to get full documentation.");
877
+
878
+ return { content: [{ type: "text", text: lines.join("\n") }] };
879
+ }
880
+ );
881
+
882
+ // ============================================================================
883
+ // TOOL: get_dwt_api_doc
884
+ // ============================================================================
885
+
886
+ server.registerTool(
887
+ "get_dwt_api_doc",
888
+ {
889
+ title: "Get DWT API Documentation",
890
+ description: "Get full Dynamic Web TWAIN documentation article by title or URL. Use search_dwt_docs first to find relevant articles.",
891
+ inputSchema: {
892
+ title: z.string().describe("Article title or partial title to match (e.g., 'Loading Documents', 'OCR', 'PDF')")
893
+ }
894
+ },
895
+ async ({ title }) => {
896
+ const titleLower = title.toLowerCase();
897
+
898
+ // Try exact match first, then partial match
899
+ let article = dwtDocs.articles.find(a => a.title.toLowerCase() === titleLower);
900
+
901
+ if (!article) {
902
+ // Try partial match on title
903
+ article = dwtDocs.articles.find(a => a.title.toLowerCase().includes(titleLower));
904
+ }
905
+
906
+ if (!article) {
907
+ // Try matching URL
908
+ article = dwtDocs.articles.find(a => a.url.toLowerCase().includes(titleLower));
909
+ }
910
+
911
+ if (!article) {
912
+ // Suggest similar articles
913
+ const suggestions = dwtDocs.articles
914
+ .filter(a => {
915
+ const words = titleLower.split(/\s+/);
916
+ return words.some(w => a.title.toLowerCase().includes(w) || a.breadcrumb.toLowerCase().includes(w));
917
+ })
918
+ .slice(0, 5)
919
+ .map(a => `- ${a.title}`);
920
+
921
+ return {
922
+ content: [{
923
+ type: "text",
924
+ text: `Article "${title}" not found.\n\n${suggestions.length > 0 ? `Similar articles:\n${suggestions.join("\n")}` : "Use search_dwt_docs to find relevant articles."}`
925
+ }]
926
+ };
927
+ }
928
+
929
+ const lines = [
930
+ `# ${article.title}`,
931
+ "",
932
+ `**Category:** ${article.breadcrumb}`,
933
+ `**URL:** ${article.url}`,
934
+ "",
935
+ "---",
936
+ "",
937
+ article.content
938
+ ];
612
939
 
613
940
  return { content: [{ type: "text", text: lines.join("\n") }] };
614
941
  }
@@ -906,6 +1233,49 @@ server.registerTool(
906
1233
  };
907
1234
  }
908
1235
 
1236
+ // Handle Web Barcode Reader SDK
1237
+ if (sdkId === "dbr-web") {
1238
+ const sdkEntry = registry.sdks["dbr-web"];
1239
+ const sampleName = use_case?.includes("image") ? "read-an-image" : "hello-world";
1240
+ const samplePath = getWebSamplePath("root", sampleName);
1241
+
1242
+ if (!samplePath || !existsSync(samplePath)) {
1243
+ return { content: [{ type: "text", text: `Sample not found. Use list_web_samples to see available.` }] };
1244
+ }
1245
+
1246
+ const content = readCodeFile(samplePath);
1247
+
1248
+ return {
1249
+ content: [{
1250
+ type: "text", text: [
1251
+ "# Quick Start: Web Barcode Reader",
1252
+ "",
1253
+ `**SDK Version:** ${sdkEntry.version}`,
1254
+ `**Trial License:** \`${registry.trial_license}\``,
1255
+ "",
1256
+ "## Option 1: CDN",
1257
+ "```html",
1258
+ `<script src="${sdkEntry.platforms.web.installation.cdn}"></script>`,
1259
+ "```",
1260
+ "",
1261
+ "## Option 2: NPM",
1262
+ "```bash",
1263
+ "npm install dynamsoft-barcode-reader-bundle",
1264
+ "```",
1265
+ "",
1266
+ `## Sample: ${sampleName}.html`,
1267
+ "```html",
1268
+ content,
1269
+ "```",
1270
+ "",
1271
+ "## Notes",
1272
+ "- Trial license requires network connection",
1273
+ `- User Guide: ${sdkEntry.platforms.web.docs["user-guide"]}`
1274
+ ].join("\n")
1275
+ }]
1276
+ };
1277
+ }
1278
+
909
1279
  // Handle Mobile SDK (original logic)
910
1280
  const level = normalizeApiLevel(api_level);
911
1281
  const platformKey = platform || "android";
@@ -1406,6 +1776,29 @@ for (const sampleName of pythonSamples) {
1406
1776
  );
1407
1777
  }
1408
1778
 
1779
+ // Register Web barcode reader sample resources
1780
+ const webCategories = discoverWebSamples();
1781
+ for (const [category, samples] of Object.entries(webCategories)) {
1782
+ for (const sampleName of samples) {
1783
+ const resourceUri = `dynamsoft://samples/web/${category}/${sampleName}`;
1784
+ const resourceName = `web-${category}-${sampleName}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1785
+ server.registerResource(
1786
+ resourceName,
1787
+ resourceUri,
1788
+ {
1789
+ title: `Web: ${sampleName}`,
1790
+ description: `Web barcode reader ${category}: ${sampleName}`,
1791
+ mimeType: "text/html"
1792
+ },
1793
+ async (uri) => {
1794
+ const samplePath = getWebSamplePath(category, sampleName);
1795
+ const content = samplePath && existsSync(samplePath) ? readCodeFile(samplePath) : "Sample not found";
1796
+ return { contents: [{ uri: uri.href, text: content, mimeType: "text/html" }] };
1797
+ }
1798
+ );
1799
+ }
1800
+ }
1801
+
1409
1802
  // Register DWT sample resources
1410
1803
  const dwtCategories = discoverDwtSamples();
1411
1804
  for (const [category, samples] of Object.entries(dwtCategories)) {
@@ -1429,6 +1822,35 @@ for (const [category, samples] of Object.entries(dwtCategories)) {
1429
1822
  }
1430
1823
  }
1431
1824
 
1825
+ // Register DWT API documentation resources
1826
+ for (let i = 0; i < dwtDocs.articles.length; i++) {
1827
+ const article = dwtDocs.articles[i];
1828
+ const resourceName = `dwt-doc-${i}`.toLowerCase();
1829
+ const resourceUri = `dynamsoft://docs/dwt/${encodeURIComponent(article.title)}`;
1830
+ server.registerResource(
1831
+ resourceName,
1832
+ resourceUri,
1833
+ {
1834
+ title: `DWT Doc: ${article.title}`,
1835
+ description: `${article.breadcrumb}: ${article.title}`,
1836
+ mimeType: "text/markdown"
1837
+ },
1838
+ async (uri) => {
1839
+ const content = [
1840
+ `# ${article.title}`,
1841
+ "",
1842
+ `**Category:** ${article.breadcrumb}`,
1843
+ `**URL:** ${article.url}`,
1844
+ "",
1845
+ "---",
1846
+ "",
1847
+ article.content
1848
+ ].join("\n");
1849
+ return { contents: [{ uri: uri.href, text: content, mimeType: "text/markdown" }] };
1850
+ }
1851
+ );
1852
+ }
1853
+
1432
1854
  // ============================================================================
1433
1855
  // Start Server
1434
1856
  // ============================================================================
@@ -121,7 +121,7 @@ await test('Server responds to initialize request', async () => {
121
121
  });
122
122
 
123
123
  // Test 2: List tools
124
- await test('tools/list returns all 14 tools', async () => {
124
+ await test('tools/list returns all 18 tools', async () => {
125
125
  const response = await sendRequest({
126
126
  jsonrpc: '2.0',
127
127
  id: 1,
@@ -130,14 +130,15 @@ await test('tools/list returns all 14 tools', async () => {
130
130
 
131
131
  assert(response.result, 'Should have result');
132
132
  assert(response.result.tools, 'Should have tools array');
133
- assert(response.result.tools.length === 14, `Expected 14 tools, got ${response.result.tools.length}`);
133
+ assert(response.result.tools.length === 18, `Expected 18 tools, got ${response.result.tools.length}`);
134
134
 
135
135
  const toolNames = response.result.tools.map(t => t.name);
136
136
  const expectedTools = [
137
137
  'list_sdks', 'get_sdk_info', 'list_samples', 'list_python_samples',
138
- 'list_dwt_categories', 'get_code_snippet', 'get_python_sample',
139
- 'get_dwt_sample', 'get_quick_start', 'get_gradle_config',
140
- 'get_license_info', 'get_api_usage', 'search_samples', 'generate_project'
138
+ 'list_web_samples', 'list_dwt_categories', 'get_code_snippet',
139
+ 'get_web_sample', 'get_python_sample', 'get_dwt_sample', 'get_quick_start',
140
+ 'get_gradle_config', 'get_license_info', 'get_api_usage', 'search_samples',
141
+ 'generate_project', 'search_dwt_docs', 'get_dwt_api_doc'
141
142
  ];
142
143
 
143
144
  for (const expected of expectedTools) {
@@ -347,7 +348,81 @@ await test('generate_project returns project structure', async () => {
347
348
  assert(text.includes('AndroidManifest.xml') || text.includes('build.gradle'), 'Should include project files');
348
349
  });
349
350
 
350
- // Test 14: resources/list returns registered resources
351
+ // Test 14: list_web_samples tool
352
+ await test('list_web_samples returns web barcode samples', async () => {
353
+ const response = await sendRequest({
354
+ jsonrpc: '2.0',
355
+ id: 1,
356
+ method: 'tools/call',
357
+ params: {
358
+ name: 'list_web_samples',
359
+ arguments: {}
360
+ }
361
+ });
362
+
363
+ assert(response.result, 'Should have result');
364
+ assert(response.result.content, 'Should have content');
365
+ const text = response.result.content[0].text;
366
+ assert(text.includes('Web Barcode Reader Samples'), 'Should include web samples header');
367
+ });
368
+
369
+ // Test 15: get_web_sample tool
370
+ await test('get_web_sample returns web barcode sample code', async () => {
371
+ const response = await sendRequest({
372
+ jsonrpc: '2.0',
373
+ id: 1,
374
+ method: 'tools/call',
375
+ params: {
376
+ name: 'get_web_sample',
377
+ arguments: { sample_name: 'hello-world' }
378
+ }
379
+ });
380
+
381
+ assert(response.result, 'Should have result');
382
+ assert(response.result.content, 'Should have content');
383
+ const text = response.result.content[0].text;
384
+ assert(text.includes('Web Barcode Reader') || text.includes('html') || text.includes('not found'), 'Should return sample or indicate not found');
385
+ });
386
+
387
+ // Test 16: search_dwt_docs tool
388
+ await test('search_dwt_docs finds documentation articles', async () => {
389
+ const response = await sendRequest({
390
+ jsonrpc: '2.0',
391
+ id: 1,
392
+ method: 'tools/call',
393
+ params: {
394
+ name: 'search_dwt_docs',
395
+ arguments: { query: 'PDF' }
396
+ }
397
+ });
398
+
399
+ assert(response.result, 'Should have result');
400
+ assert(response.result.content, 'Should have content');
401
+ const text = response.result.content[0].text;
402
+ assert(text.includes('DWT Documentation Search'), 'Should include search header');
403
+ assert(text.includes('PDF') || text.includes('pdf'), 'Should find PDF-related articles');
404
+ });
405
+
406
+ // Test 17: get_dwt_api_doc tool
407
+ await test('get_dwt_api_doc returns documentation article', async () => {
408
+ const response = await sendRequest({
409
+ jsonrpc: '2.0',
410
+ id: 1,
411
+ method: 'tools/call',
412
+ params: {
413
+ name: 'get_dwt_api_doc',
414
+ arguments: { title: 'OCR' }
415
+ }
416
+ });
417
+
418
+ assert(response.result, 'Should have result');
419
+ assert(response.result.content, 'Should have content');
420
+ const text = response.result.content[0].text;
421
+ // Should return either the article or suggestions
422
+ assert(text.includes('OCR') || text.includes('not found'), 'Should handle OCR query');
423
+ });
424
+
425
+ // Test 18: resources/list returns registered resources
351
426
  await test('resources/list returns registered resources', async () => {
352
427
  const response = await sendRequest({
353
428
  jsonrpc: '2.0',
@@ -362,9 +437,10 @@ await test('resources/list returns registered resources', async () => {
362
437
  // Check for expected resource types
363
438
  const uris = response.result.resources.map(r => r.uri);
364
439
  assert(uris.some(u => u.includes('sdk-info')), 'Should have sdk-info resources');
440
+ assert(uris.some(u => u.includes('docs/dwt')), 'Should have DWT doc resources');
365
441
  });
366
442
 
367
- // Test 15: Invalid tool call returns error
443
+ // Test 19: Invalid tool call returns error
368
444
  await test('Invalid tool call returns proper error', async () => {
369
445
  const response = await sendRequest({
370
446
  jsonrpc: '2.0',
@@ -380,7 +456,7 @@ await test('Invalid tool call returns proper error', async () => {
380
456
  'Should return error for invalid tool');
381
457
  });
382
458
 
383
- // Test 16: Tool with invalid arguments returns error
459
+ // Test 20: Tool with invalid arguments returns error
384
460
  await test('Tool with missing required arguments returns error', async () => {
385
461
  const response = await sendRequest({
386
462
  jsonrpc: '2.0',