simple-dynamsoft-mcp 2.2.0 ā 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +13 -2
- package/http/wrapper.js +480 -0
- package/package.json +53 -45
- package/test/server.test.js +0 -496
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xiao Ling
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ https://github.com/user-attachments/assets/cc1c5f4b-1461-4462-897a-75abc20d62a6
|
|
|
17
17
|
- **Trial License Included**: Ready-to-use trial license for quick testing
|
|
18
18
|
- **Multiple SDKs**: Barcode Reader (Mobile/Python/Web) + Dynamic Web TWAIN
|
|
19
19
|
- **Multiple API Levels**: High-level (simple) and low-level (advanced) options
|
|
20
|
+
- **HTTP MCP Wrapper for Copilot Studio**: `npm start:http` runs `http/wrapper.js`, exposing MCP over HTTP at `/mcp` (POST JSON-RPC, optional GET SSE). Discovery (tools/resources) is returned inline on `initialize` and `notifications/initialized`, plus SSE push when enabled.
|
|
20
21
|
|
|
21
22
|
## Available Tools
|
|
22
23
|
|
|
@@ -87,8 +88,8 @@ Configuration:
|
|
|
87
88
|
|
|
88
89
|
Global Location:
|
|
89
90
|
|
|
90
|
-
- **macOS**:
|
|
91
|
-
- **Windows**: `%
|
|
91
|
+
- **macOS**: `~/Library/Application Support/Code/User/mcp.json`
|
|
92
|
+
- **Windows**: `%APPDATA%\Code\User\mcp.json`
|
|
92
93
|
|
|
93
94
|
```json
|
|
94
95
|
{
|
|
@@ -163,6 +164,16 @@ If you prefer running from source:
|
|
|
163
164
|
}
|
|
164
165
|
```
|
|
165
166
|
|
|
167
|
+
### Copilot Studio / HTTP Wrapper
|
|
168
|
+
|
|
169
|
+
- Install deps: `npm install`
|
|
170
|
+
- Run the HTTP wrapper: `npm start:http` (listens on `http://localhost:3333`)
|
|
171
|
+
- POST `/mcp` for JSON-RPC (initialize returns capabilities, instructions, and discovery inline)
|
|
172
|
+
- GET `/mcp` for SSE (optional; discovery also pushed here if enabled)
|
|
173
|
+
- GET `/health` for status
|
|
174
|
+
|
|
175
|
+
The wrapper proxies to the MCP stdio child (`./src/index.js`) and embeds `mcp-session-id` plus an instructions block in the initialize response for compatibility with Copilot Studio.
|
|
176
|
+
|
|
166
177
|
## Supported SDKs
|
|
167
178
|
|
|
168
179
|
### Dynamsoft Barcode Reader Mobile (v11.2.5000)
|
package/http/wrapper.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP HTTP Wrapper for Copilot Studio
|
|
4
|
+
*
|
|
5
|
+
* - POST /mcp: JSON-RPC proxy to the MCP stdio child
|
|
6
|
+
* - GET /mcp: SSE stream (kept for compatibility; Copilot may or may not use it)
|
|
7
|
+
* - Proxies MCP requests, forwards notifications to SSE, and returns discovery inline
|
|
8
|
+
* - Handles notifications/initialized with a discovery response
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import express from "express";
|
|
12
|
+
import bodyParser from "body-parser";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import process from "node:process";
|
|
16
|
+
import crypto from "node:crypto";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
|
|
19
|
+
// -----------------------------
|
|
20
|
+
// Config
|
|
21
|
+
// -----------------------------
|
|
22
|
+
const PORT = process.env.PORT ? Number(process.env.PORT) : 3333;
|
|
23
|
+
const MCP_COMMAND = process.env.MCP_COMMAND || process.execPath; // node
|
|
24
|
+
const MCP_ARGS = process.env.MCP_ARGS
|
|
25
|
+
? JSON.parse(process.env.MCP_ARGS)
|
|
26
|
+
: ["./src/index.js"]; // relative to repo root
|
|
27
|
+
const WORKDIR = process.env.WORKDIR || process.cwd();
|
|
28
|
+
|
|
29
|
+
const ENABLE_SSE_PUSH_DISCOVERY = process.env.SSE_PUSH_DISCOVERY !== "0"; // default ON
|
|
30
|
+
const SSE_KEEPALIVE_MS = process.env.SSE_KEEPALIVE_MS
|
|
31
|
+
? Number(process.env.SSE_KEEPALIVE_MS)
|
|
32
|
+
: 15000;
|
|
33
|
+
const SESSION_ID = crypto.randomUUID();
|
|
34
|
+
|
|
35
|
+
// Log levels: error < warn < info < debug
|
|
36
|
+
const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
37
|
+
const LOG_LEVEL = LEVELS[(process.env.LOG_LEVEL || "info").toLowerCase()] ?? LEVELS.info;
|
|
38
|
+
|
|
39
|
+
// -----------------------------
|
|
40
|
+
// Utils
|
|
41
|
+
// -----------------------------
|
|
42
|
+
function nowIso() {
|
|
43
|
+
return new Date().toISOString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function safeJson(obj) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(obj, null, 2);
|
|
49
|
+
} catch {
|
|
50
|
+
return String(obj);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeId() {
|
|
55
|
+
return crypto.randomUUID();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function shouldLog(level) {
|
|
59
|
+
return (LEVELS[level] ?? LEVELS.info) <= LOG_LEVEL;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function logAt(level, ...args) {
|
|
63
|
+
if (shouldLog(level)) {
|
|
64
|
+
console.log(...args);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function err(...args) {
|
|
69
|
+
console.error(...args);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const MCP_INSTRUCTIONS = `# Dynamsoft MCP Server
|
|
73
|
+
|
|
74
|
+
Use these tools to answer questions about Dynamsoft SDKs:
|
|
75
|
+
- list_sdks: show available SDKs and platforms
|
|
76
|
+
- get_sdk_info: versions, install/licensing, docs for a platform
|
|
77
|
+
- list_samples / list_python_samples / list_dwt_categories: browse samples
|
|
78
|
+
- get_code_snippet / get_python_sample / get_dwt_sample: fetch code
|
|
79
|
+
- get_quick_start: full quick start for a target
|
|
80
|
+
- get_gradle_config, get_license_info, get_api_usage, search_samples
|
|
81
|
+
|
|
82
|
+
Workflow:
|
|
83
|
+
1) Call tools/list to discover names/schemas (or use discovery from initialize).
|
|
84
|
+
2) Invoke the relevant tool with arguments.
|
|
85
|
+
3) Use resources/list + resources/read if you need pre-registered sample/code resources.`;
|
|
86
|
+
|
|
87
|
+
// -----------------------------
|
|
88
|
+
// Response helpers (SSE-style if requested)
|
|
89
|
+
// -----------------------------
|
|
90
|
+
function wantsSse(req) {
|
|
91
|
+
return (req.headers.accept || "").includes("text/event-stream");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sendResponse(res, payload, useSse = false) {
|
|
95
|
+
const sessionHeader = Buffer.from(
|
|
96
|
+
JSON.stringify({ sessionId: SESSION_ID })
|
|
97
|
+
).toString("base64");
|
|
98
|
+
|
|
99
|
+
res.setHeader("mcp-session-id", sessionHeader);
|
|
100
|
+
if (useSse) {
|
|
101
|
+
res.status(200);
|
|
102
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
103
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
104
|
+
res.setHeader("Connection", "keep-alive");
|
|
105
|
+
res.flushHeaders?.();
|
|
106
|
+
res.write(`event: message\n`);
|
|
107
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
108
|
+
res.end();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
res.status(200).json(payload);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// -----------------------------
|
|
115
|
+
// Spawn MCP child
|
|
116
|
+
// -----------------------------
|
|
117
|
+
logAt("info", "");
|
|
118
|
+
logAt("info", `[MCP WRAPPER] MCP child starting...`);
|
|
119
|
+
logAt("info", `[MCP WRAPPER] -> ${MCP_COMMAND} ${MCP_ARGS.join(" ")}`);
|
|
120
|
+
logAt("info", `[MCP WRAPPER] Working dir: ${WORKDIR}`);
|
|
121
|
+
logAt("info", "");
|
|
122
|
+
|
|
123
|
+
const child = spawn(MCP_COMMAND, MCP_ARGS, {
|
|
124
|
+
cwd: WORKDIR,
|
|
125
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
126
|
+
env: process.env,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
logAt("info", `[MCP WRAPPER] MCP child started pid=${child.pid}`);
|
|
130
|
+
|
|
131
|
+
child.on("exit", (code, signal) => {
|
|
132
|
+
err(`[MCP WRAPPER] MCP child exited code=${code} signal=${signal}`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
child.stderr.on("data", (buf) => {
|
|
136
|
+
err(`[MCP STDERR] ${buf.toString("utf8")}`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// -----------------------------
|
|
140
|
+
// MCP stdio JSON-RPC transport
|
|
141
|
+
// -----------------------------
|
|
142
|
+
let stdoutBuffer = "";
|
|
143
|
+
const pending = new Map(); // id -> {resolve,reject,ts,timeout}
|
|
144
|
+
|
|
145
|
+
child.stdout.on("data", (buf) => {
|
|
146
|
+
stdoutBuffer += buf.toString("utf8");
|
|
147
|
+
|
|
148
|
+
// MCP stdio messages are newline-delimited JSON (NDJSON)
|
|
149
|
+
while (true) {
|
|
150
|
+
const idx = stdoutBuffer.indexOf("\n");
|
|
151
|
+
if (idx < 0) break;
|
|
152
|
+
|
|
153
|
+
const line = stdoutBuffer.slice(0, idx).trim();
|
|
154
|
+
stdoutBuffer = stdoutBuffer.slice(idx + 1);
|
|
155
|
+
|
|
156
|
+
if (!line) continue;
|
|
157
|
+
|
|
158
|
+
let msg;
|
|
159
|
+
try {
|
|
160
|
+
msg = JSON.parse(line);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
err(`[MCP WRAPPER] Failed to parse MCP stdout line: ${line}`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Response?
|
|
167
|
+
if (msg && msg.id && pending.has(String(msg.id))) {
|
|
168
|
+
const p = pending.get(String(msg.id));
|
|
169
|
+
pending.delete(String(msg.id));
|
|
170
|
+
clearTimeout(p.timeout);
|
|
171
|
+
p.resolve(msg);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Notification / server event -> forward to SSE clients if any
|
|
176
|
+
broadcastSse({
|
|
177
|
+
event: "mcp",
|
|
178
|
+
data: msg,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
function sendToChild(req, { timeoutMs = 30000 } = {}) {
|
|
184
|
+
const id = req.id != null ? String(req.id) : null;
|
|
185
|
+
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const payload = safeJson(req).replace(/\n/g, "");
|
|
188
|
+
// write NDJSON line
|
|
189
|
+
child.stdin.write(payload + "\n", "utf8");
|
|
190
|
+
|
|
191
|
+
// Notifications have no id; resolve immediately.
|
|
192
|
+
if (!id) {
|
|
193
|
+
resolve({ ok: true, notification: true });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const t = setTimeout(() => {
|
|
198
|
+
pending.delete(id);
|
|
199
|
+
reject(new Error(`MCP child timeout waiting id=${id}`));
|
|
200
|
+
}, timeoutMs);
|
|
201
|
+
|
|
202
|
+
pending.set(id, { resolve, reject, ts: Date.now(), timeout: t });
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// -----------------------------
|
|
207
|
+
// SSE clients
|
|
208
|
+
// -----------------------------
|
|
209
|
+
const sseClients = new Set(); // res
|
|
210
|
+
let keepaliveTimer = null;
|
|
211
|
+
|
|
212
|
+
function broadcastSse({ event = "message", data }) {
|
|
213
|
+
const msg = typeof data === "string" ? data : JSON.stringify(data);
|
|
214
|
+
for (const res of sseClients) {
|
|
215
|
+
try {
|
|
216
|
+
res.write(`event: ${event}\n`);
|
|
217
|
+
res.write(`data: ${msg}\n\n`);
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function startKeepalive() {
|
|
223
|
+
if (keepaliveTimer) return;
|
|
224
|
+
keepaliveTimer = setInterval(() => {
|
|
225
|
+
broadcastSse({
|
|
226
|
+
event: "ping",
|
|
227
|
+
data: { t: Date.now() },
|
|
228
|
+
});
|
|
229
|
+
}, SSE_KEEPALIVE_MS);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function stopKeepaliveIfNoClients() {
|
|
233
|
+
if (sseClients.size === 0 && keepaliveTimer) {
|
|
234
|
+
clearInterval(keepaliveTimer);
|
|
235
|
+
keepaliveTimer = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// -----------------------------
|
|
240
|
+
// Express app
|
|
241
|
+
// -----------------------------
|
|
242
|
+
const app = express();
|
|
243
|
+
|
|
244
|
+
// Accept JSON bodies
|
|
245
|
+
app.use(bodyParser.json({ limit: "2mb" }));
|
|
246
|
+
|
|
247
|
+
// Basic HTTP logger
|
|
248
|
+
app.use((req, res, next) => {
|
|
249
|
+
logAt("info", `[HTTP ${nowIso()}] ${req.method} ${req.path}`);
|
|
250
|
+
next();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Health endpoint
|
|
254
|
+
app.get("/health", (req, res) => {
|
|
255
|
+
res.json({
|
|
256
|
+
ok: true,
|
|
257
|
+
time: nowIso(),
|
|
258
|
+
childPid: child.pid,
|
|
259
|
+
sseClients: sseClients.size,
|
|
260
|
+
host: os.hostname(),
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// SSE endpoint (kept for compatibility; Copilot may or may not use it)
|
|
265
|
+
app.get("/mcp", (req, res) => {
|
|
266
|
+
logAt("info", `[SSE] Client connected: ${req.headers["user-agent"] || "unknown"}`);
|
|
267
|
+
|
|
268
|
+
res.status(200);
|
|
269
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
270
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
271
|
+
res.setHeader("Connection", "keep-alive");
|
|
272
|
+
res.setHeader("X-Accel-Buffering", "no"); // helpful for proxies/nginx
|
|
273
|
+
res.flushHeaders?.();
|
|
274
|
+
|
|
275
|
+
// Send initial event
|
|
276
|
+
res.write(`event: open\n`);
|
|
277
|
+
res.write(`data: ${JSON.stringify({ ok: true, time: nowIso() })}\n\n`);
|
|
278
|
+
|
|
279
|
+
sseClients.add(res);
|
|
280
|
+
startKeepalive();
|
|
281
|
+
|
|
282
|
+
req.on("close", () => {
|
|
283
|
+
sseClients.delete(res);
|
|
284
|
+
stopKeepaliveIfNoClients();
|
|
285
|
+
logAt("info", `[SSE] Client disconnected. active=${sseClients.size}`);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// MCP JSON-RPC endpoint
|
|
290
|
+
app.post("/mcp", async (req, res) => {
|
|
291
|
+
const useSse = wantsSse(req);
|
|
292
|
+
// Log headers (and body if enabled) for debugging
|
|
293
|
+
logAt("info", `[/mcp] headers: ${safeJson(req.headers)}`);
|
|
294
|
+
logAt("debug", `[/mcp] raw body: ${safeJson(req.body)}`);
|
|
295
|
+
|
|
296
|
+
const body = req.body;
|
|
297
|
+
|
|
298
|
+
// Handle notifications/initialized (Copilot sends this and expects no error)
|
|
299
|
+
if (
|
|
300
|
+
body &&
|
|
301
|
+
body.jsonrpc === "2.0" &&
|
|
302
|
+
body.method === "notifications/initialized"
|
|
303
|
+
) {
|
|
304
|
+
logAt("info", `[/mcp] notification received: notifications/initialized`);
|
|
305
|
+
const useSse = wantsSse(req);
|
|
306
|
+
try {
|
|
307
|
+
const discovery = await fetchDiscovery();
|
|
308
|
+
const notifPayload = {
|
|
309
|
+
jsonrpc: "2.0",
|
|
310
|
+
method: "discovery",
|
|
311
|
+
params: discovery,
|
|
312
|
+
};
|
|
313
|
+
sendResponse(res, notifPayload, useSse);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
err(`[DISCOVERY] notification fetch failed: ${e?.message || e}`);
|
|
316
|
+
return res.status(204).end();
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// JSON-RPC requests
|
|
322
|
+
try {
|
|
323
|
+
// initialize
|
|
324
|
+
if (body?.method === "initialize") {
|
|
325
|
+
// Forward initialize to child
|
|
326
|
+
const childResp = await sendToChild(body);
|
|
327
|
+
|
|
328
|
+
// Patch capability set (prompts sometimes required)
|
|
329
|
+
if (childResp?.result) {
|
|
330
|
+
childResp.result.capabilities = childResp.result.capabilities || {};
|
|
331
|
+
childResp.result.capabilities.tools = { listChanged: true };
|
|
332
|
+
childResp.result.capabilities.resources = { listChanged: true };
|
|
333
|
+
childResp.result.capabilities.prompts = { listChanged: true }; // Copilot quirk
|
|
334
|
+
childResp.result.capabilities.logging = {};
|
|
335
|
+
childResp.result.instructions = MCP_INSTRUCTIONS;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Fetch tools/resources immediately for clients that do not use SSE
|
|
339
|
+
try {
|
|
340
|
+
const discovery = await fetchDiscovery();
|
|
341
|
+
childResp.result.discovery = discovery;
|
|
342
|
+
} catch (e) {
|
|
343
|
+
err(`[DISCOVERY] fetch failed: ${e?.message || e}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
sendResponse(res, childResp, useSse);
|
|
347
|
+
|
|
348
|
+
// ---- Proactive discovery push over SSE (Copilot workaround)
|
|
349
|
+
// (This runs only if SSE is enabled; safe to keep even if unused)
|
|
350
|
+
if (ENABLE_SSE_PUSH_DISCOVERY) {
|
|
351
|
+
setTimeout(async () => {
|
|
352
|
+
try {
|
|
353
|
+
await pushDiscoveryToSse();
|
|
354
|
+
} catch (e) {
|
|
355
|
+
err(`[DISCOVERY] push failed: ${e?.message || e}`);
|
|
356
|
+
}
|
|
357
|
+
}, 250);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Forward other methods to child
|
|
364
|
+
const childResp = await sendToChild(body);
|
|
365
|
+
|
|
366
|
+
// If childResp is notification ack
|
|
367
|
+
if (childResp?.notification) {
|
|
368
|
+
return res.status(204).end();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
logAt("debug", `[/mcp] MCP response: ${safeJson(childResp)}`);
|
|
372
|
+
return sendResponse(res, childResp, useSse);
|
|
373
|
+
} catch (e) {
|
|
374
|
+
err(`[/mcp] Error proxying request: ${e?.stack || e}`);
|
|
375
|
+
|
|
376
|
+
// JSON-RPC error object
|
|
377
|
+
const id = body?.id ?? null;
|
|
378
|
+
return sendResponse(res, {
|
|
379
|
+
jsonrpc: "2.0",
|
|
380
|
+
id,
|
|
381
|
+
error: {
|
|
382
|
+
code: -32000,
|
|
383
|
+
message: String(e?.message || e),
|
|
384
|
+
},
|
|
385
|
+
}, useSse);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// -----------------------------
|
|
390
|
+
// Proactive discovery push
|
|
391
|
+
// -----------------------------
|
|
392
|
+
async function pushDiscoveryToSse() {
|
|
393
|
+
if (sseClients.size === 0) {
|
|
394
|
+
logAt("debug", `[DISCOVERY] No SSE clients; skipping push.`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
logAt("debug", `[DISCOVERY] Querying tools/list + resources/list from MCP child...`);
|
|
399
|
+
|
|
400
|
+
// tools/list
|
|
401
|
+
const toolsReq = {
|
|
402
|
+
jsonrpc: "2.0",
|
|
403
|
+
id: makeId(),
|
|
404
|
+
method: "tools/list",
|
|
405
|
+
params: {},
|
|
406
|
+
};
|
|
407
|
+
const toolsResp = await sendToChild(toolsReq, { timeoutMs: 30000 });
|
|
408
|
+
|
|
409
|
+
// resources/list
|
|
410
|
+
const resReq = {
|
|
411
|
+
jsonrpc: "2.0",
|
|
412
|
+
id: makeId(),
|
|
413
|
+
method: "resources/list",
|
|
414
|
+
params: {},
|
|
415
|
+
};
|
|
416
|
+
const resResp = await sendToChild(resReq, { timeoutMs: 30000 });
|
|
417
|
+
|
|
418
|
+
logAt("debug", `[DISCOVERY] tools/list result keys: ${Object.keys(toolsResp || {})}`);
|
|
419
|
+
logAt("debug", `[DISCOVERY] resources/list result keys: ${Object.keys(resResp || {})}`);
|
|
420
|
+
|
|
421
|
+
// Push as SSE events (even if client didn't ask!)
|
|
422
|
+
// This is a compatibility hack for Copilot Studio UI.
|
|
423
|
+
broadcastSse({ event: "mcp", data: toolsResp });
|
|
424
|
+
broadcastSse({ event: "mcp", data: resResp });
|
|
425
|
+
|
|
426
|
+
// Also emit āserver readyā event
|
|
427
|
+
broadcastSse({
|
|
428
|
+
event: "ready",
|
|
429
|
+
data: {
|
|
430
|
+
time: nowIso(),
|
|
431
|
+
toolsCount: toolsResp?.result?.tools?.length ?? null,
|
|
432
|
+
resourcesCount: resResp?.result?.resources?.length ?? null,
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
logAt(
|
|
437
|
+
"info",
|
|
438
|
+
`[DISCOVERY] pushed tools/resources over SSE. tools=${toolsResp?.result?.tools?.length ?? "?"} resources=${resResp?.result?.resources?.length ?? "?"}`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Fallback: fetch discovery for clients that don't use SSE
|
|
443
|
+
async function fetchDiscovery() {
|
|
444
|
+
logAt("debug", `[DISCOVERY] Fetching tools/resources for non-SSE client...`);
|
|
445
|
+
|
|
446
|
+
const toolsReq = {
|
|
447
|
+
jsonrpc: "2.0",
|
|
448
|
+
id: makeId(),
|
|
449
|
+
method: "tools/list",
|
|
450
|
+
params: {},
|
|
451
|
+
};
|
|
452
|
+
const resReq = {
|
|
453
|
+
jsonrpc: "2.0",
|
|
454
|
+
id: makeId(),
|
|
455
|
+
method: "resources/list",
|
|
456
|
+
params: {},
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const [toolsResp, resResp] = await Promise.all([
|
|
460
|
+
sendToChild(toolsReq, { timeoutMs: 30000 }),
|
|
461
|
+
sendToChild(resReq, { timeoutMs: 30000 }),
|
|
462
|
+
]);
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
tools: toolsResp?.result?.tools ?? [],
|
|
466
|
+
resources: resResp?.result?.resources ?? [],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// -----------------------------
|
|
471
|
+
// Start server
|
|
472
|
+
// -----------------------------
|
|
473
|
+
app.listen(PORT, "0.0.0.0", () => {
|
|
474
|
+
logAt("info", "");
|
|
475
|
+
logAt("info", `[MCP WRAPPER] Listening on http://localhost:${PORT}`);
|
|
476
|
+
logAt("info", `[MCP WRAPPER] GET /mcp = SSE stream (Copilot Studio expects this)`);
|
|
477
|
+
logAt("info", `[MCP WRAPPER] POST /mcp = JSON-RPC endpoint`);
|
|
478
|
+
logAt("info", `[MCP WRAPPER] /health = status`);
|
|
479
|
+
logAt("info", "");
|
|
480
|
+
});
|
package/package.json
CHANGED
|
@@ -1,45 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "simple-dynamsoft-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP server for Dynamsoft SDKs - Barcode Reader (Mobile/Python/Web) and Dynamic Web TWAIN. Provides documentation, code snippets, and API guidance.",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "simple-dynamsoft-mcp",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "MCP server for Dynamsoft SDKs - Barcode Reader (Mobile/Python/Web) and Dynamic Web TWAIN. Provides documentation, code snippets, and API guidance.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/yushulx/simple-dynamsoft-mcp.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"simple-dynamsoft-mcp": "./src/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"data",
|
|
20
|
+
"http",
|
|
21
|
+
"code-snippet",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"start": "node src/index.js",
|
|
26
|
+
"start:http": "node http/wrapper.js",
|
|
27
|
+
"test": "npm run test:stdio && npm run test:http",
|
|
28
|
+
"test:stdio": "node test/server.test.js",
|
|
29
|
+
"test:http": "node test/http-wrapper.test.js"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"dynamsoft",
|
|
35
|
+
"barcode-reader",
|
|
36
|
+
"barcode-scanner",
|
|
37
|
+
"document-scanner",
|
|
38
|
+
"web-twain",
|
|
39
|
+
"android",
|
|
40
|
+
"ios",
|
|
41
|
+
"python",
|
|
42
|
+
"javascript",
|
|
43
|
+
"mobile"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
47
|
+
"express": "^4.21.2",
|
|
48
|
+
"zod": "~3.24.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"supertest": "^7.1.3"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/test/server.test.js
DELETED
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Automated tests for Dynamsoft MCP Server
|
|
5
|
-
* Run with: node test/server.test.js
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { spawn } from 'child_process';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
import { dirname, join } from 'path';
|
|
11
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = dirname(__filename);
|
|
14
|
-
const serverPath = join(__dirname, '..', 'src', 'index.js');
|
|
15
|
-
|
|
16
|
-
// Test counters
|
|
17
|
-
let passed = 0;
|
|
18
|
-
let failed = 0;
|
|
19
|
-
const results = [];
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Send a JSON-RPC request to the server and get the response
|
|
23
|
-
*/
|
|
24
|
-
async function sendRequest(request) {
|
|
25
|
-
return new Promise((resolve, reject) => {
|
|
26
|
-
const proc = spawn('node', [serverPath], {
|
|
27
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
let stdout = '';
|
|
31
|
-
let stderr = '';
|
|
32
|
-
|
|
33
|
-
proc.stdout.on('data', (data) => {
|
|
34
|
-
stdout += data.toString();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
proc.stderr.on('data', (data) => {
|
|
38
|
-
stderr += data.toString();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
proc.on('close', (code) => {
|
|
42
|
-
try {
|
|
43
|
-
// Parse only the JSON-RPC response (last complete JSON object)
|
|
44
|
-
const lines = stdout.trim().split('\n');
|
|
45
|
-
const jsonLine = lines.find(line => {
|
|
46
|
-
try {
|
|
47
|
-
const parsed = JSON.parse(line);
|
|
48
|
-
return parsed.jsonrpc === '2.0';
|
|
49
|
-
} catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (jsonLine) {
|
|
55
|
-
resolve(JSON.parse(jsonLine));
|
|
56
|
-
} else {
|
|
57
|
-
reject(new Error(`No valid JSON-RPC response. stdout: ${stdout}, stderr: ${stderr}`));
|
|
58
|
-
}
|
|
59
|
-
} catch (e) {
|
|
60
|
-
reject(new Error(`Failed to parse response: ${e.message}. stdout: ${stdout}`));
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
proc.on('error', reject);
|
|
65
|
-
|
|
66
|
-
// Send the request and close stdin
|
|
67
|
-
proc.stdin.write(JSON.stringify(request) + '\n');
|
|
68
|
-
proc.stdin.end();
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Run a test case
|
|
74
|
-
*/
|
|
75
|
-
async function test(name, fn) {
|
|
76
|
-
try {
|
|
77
|
-
await fn();
|
|
78
|
-
passed++;
|
|
79
|
-
results.push({ name, status: 'ā
PASSED' });
|
|
80
|
-
console.log(`ā
${name}`);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
failed++;
|
|
83
|
-
results.push({ name, status: 'ā FAILED', error: error.message });
|
|
84
|
-
console.log(`ā ${name}`);
|
|
85
|
-
console.log(` Error: ${error.message}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Assert helper
|
|
91
|
-
*/
|
|
92
|
-
function assert(condition, message) {
|
|
93
|
-
if (!condition) {
|
|
94
|
-
throw new Error(message || 'Assertion failed');
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ============================================
|
|
99
|
-
// Test Cases
|
|
100
|
-
// ============================================
|
|
101
|
-
|
|
102
|
-
console.log('\nš§Ŗ Dynamsoft MCP Server Test Suite\n');
|
|
103
|
-
console.log('='.repeat(50));
|
|
104
|
-
|
|
105
|
-
// Test 1: Server initialization
|
|
106
|
-
await test('Server responds to initialize request', async () => {
|
|
107
|
-
const response = await sendRequest({
|
|
108
|
-
jsonrpc: '2.0',
|
|
109
|
-
id: 1,
|
|
110
|
-
method: 'initialize',
|
|
111
|
-
params: {
|
|
112
|
-
protocolVersion: '2024-11-05',
|
|
113
|
-
capabilities: {},
|
|
114
|
-
clientInfo: { name: 'test-client', version: '1.0.0' }
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
assert(response.result, 'Should have result');
|
|
119
|
-
assert(response.result.serverInfo, 'Should have serverInfo');
|
|
120
|
-
assert(response.result.serverInfo.name === 'simple-dynamsoft-mcp', 'Server name should match');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Test 2: List tools
|
|
124
|
-
await test('tools/list returns all 18 tools', async () => {
|
|
125
|
-
const response = await sendRequest({
|
|
126
|
-
jsonrpc: '2.0',
|
|
127
|
-
id: 1,
|
|
128
|
-
method: 'tools/list'
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
assert(response.result, 'Should have result');
|
|
132
|
-
assert(response.result.tools, 'Should have tools array');
|
|
133
|
-
assert(response.result.tools.length === 18, `Expected 18 tools, got ${response.result.tools.length}`);
|
|
134
|
-
|
|
135
|
-
const toolNames = response.result.tools.map(t => t.name);
|
|
136
|
-
const expectedTools = [
|
|
137
|
-
'list_sdks', 'get_sdk_info', 'list_samples', 'list_python_samples',
|
|
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'
|
|
142
|
-
];
|
|
143
|
-
|
|
144
|
-
for (const expected of expectedTools) {
|
|
145
|
-
assert(toolNames.includes(expected), `Missing tool: ${expected}`);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Test 3: list_sdks tool
|
|
150
|
-
await test('list_sdks returns SDK information', async () => {
|
|
151
|
-
const response = await sendRequest({
|
|
152
|
-
jsonrpc: '2.0',
|
|
153
|
-
id: 1,
|
|
154
|
-
method: 'tools/call',
|
|
155
|
-
params: {
|
|
156
|
-
name: 'list_sdks',
|
|
157
|
-
arguments: {}
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
assert(response.result, 'Should have result');
|
|
162
|
-
assert(response.result.content, 'Should have content');
|
|
163
|
-
assert(response.result.content.length > 0, 'Should have content items');
|
|
164
|
-
|
|
165
|
-
const text = response.result.content[0].text;
|
|
166
|
-
assert(text.includes('dbr-mobile'), 'Should include dbr-mobile');
|
|
167
|
-
assert(text.includes('dbr-python'), 'Should include dbr-python');
|
|
168
|
-
assert(text.includes('dbr-web'), 'Should include dbr-web');
|
|
169
|
-
assert(text.includes('dwt'), 'Should include dwt');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Test 4: get_sdk_info tool
|
|
173
|
-
await test('get_sdk_info returns detailed SDK info', async () => {
|
|
174
|
-
const response = await sendRequest({
|
|
175
|
-
jsonrpc: '2.0',
|
|
176
|
-
id: 1,
|
|
177
|
-
method: 'tools/call',
|
|
178
|
-
params: {
|
|
179
|
-
name: 'get_sdk_info',
|
|
180
|
-
arguments: { sdk_id: 'dbr-mobile' }
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
assert(response.result, 'Should have result');
|
|
185
|
-
assert(response.result.content, 'Should have content');
|
|
186
|
-
|
|
187
|
-
const text = response.result.content[0].text;
|
|
188
|
-
assert(text.includes('Android') || text.includes('android'), 'Should include Android');
|
|
189
|
-
assert(text.includes('11.2.5000'), 'Should include version');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// Test 5: get_license_info tool (requires platform parameter)
|
|
193
|
-
await test('get_license_info returns trial license', async () => {
|
|
194
|
-
const response = await sendRequest({
|
|
195
|
-
jsonrpc: '2.0',
|
|
196
|
-
id: 1,
|
|
197
|
-
method: 'tools/call',
|
|
198
|
-
params: {
|
|
199
|
-
name: 'get_license_info',
|
|
200
|
-
arguments: { platform: 'android' }
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
assert(response.result, 'Should have result');
|
|
205
|
-
assert(!response.result.isError, 'Should not be an error');
|
|
206
|
-
|
|
207
|
-
const text = response.result.content[0].text;
|
|
208
|
-
assert(text.includes('DLS2') || text.includes('License'), 'Should include license info');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Test 6: list_samples tool
|
|
212
|
-
await test('list_samples returns mobile samples', async () => {
|
|
213
|
-
const response = await sendRequest({
|
|
214
|
-
jsonrpc: '2.0',
|
|
215
|
-
id: 1,
|
|
216
|
-
method: 'tools/call',
|
|
217
|
-
params: {
|
|
218
|
-
name: 'list_samples',
|
|
219
|
-
arguments: { platform: 'android' }
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
assert(response.result, 'Should have result');
|
|
224
|
-
|
|
225
|
-
const text = response.result.content[0].text;
|
|
226
|
-
assert(text.includes('android'), 'Should include android');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// Test 7: list_python_samples tool
|
|
230
|
-
await test('list_python_samples returns Python samples', async () => {
|
|
231
|
-
const response = await sendRequest({
|
|
232
|
-
jsonrpc: '2.0',
|
|
233
|
-
id: 1,
|
|
234
|
-
method: 'tools/call',
|
|
235
|
-
params: {
|
|
236
|
-
name: 'list_python_samples',
|
|
237
|
-
arguments: {}
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
assert(response.result, 'Should have result');
|
|
242
|
-
// Should return samples or indicate no local samples
|
|
243
|
-
assert(response.result.content, 'Should have content');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// Test 8: list_dwt_categories tool
|
|
247
|
-
await test('list_dwt_categories returns DWT categories', async () => {
|
|
248
|
-
const response = await sendRequest({
|
|
249
|
-
jsonrpc: '2.0',
|
|
250
|
-
id: 1,
|
|
251
|
-
method: 'tools/call',
|
|
252
|
-
params: {
|
|
253
|
-
name: 'list_dwt_categories',
|
|
254
|
-
arguments: {}
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
assert(response.result, 'Should have result');
|
|
259
|
-
// Should return categories or indicate they exist
|
|
260
|
-
assert(response.result.content, 'Should have content');
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Test 9: get_quick_start tool
|
|
264
|
-
await test('get_quick_start returns quick start guide', async () => {
|
|
265
|
-
const response = await sendRequest({
|
|
266
|
-
jsonrpc: '2.0',
|
|
267
|
-
id: 1,
|
|
268
|
-
method: 'tools/call',
|
|
269
|
-
params: {
|
|
270
|
-
name: 'get_quick_start',
|
|
271
|
-
arguments: { sdk_id: 'dbr-mobile', platform: 'android' }
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
assert(response.result, 'Should have result');
|
|
276
|
-
|
|
277
|
-
const text = response.result.content[0].text;
|
|
278
|
-
assert(text.includes('Quick Start') || text.includes('Android'), 'Should include quick start info');
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// Test 10: get_gradle_config tool
|
|
282
|
-
await test('get_gradle_config returns Gradle configuration', async () => {
|
|
283
|
-
const response = await sendRequest({
|
|
284
|
-
jsonrpc: '2.0',
|
|
285
|
-
id: 1,
|
|
286
|
-
method: 'tools/call',
|
|
287
|
-
params: {
|
|
288
|
-
name: 'get_gradle_config',
|
|
289
|
-
arguments: {}
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
assert(response.result, 'Should have result');
|
|
294
|
-
|
|
295
|
-
const text = response.result.content[0].text;
|
|
296
|
-
assert(text.includes('gradle') || text.includes('Gradle') || text.includes('implementation'),
|
|
297
|
-
'Should include Gradle config');
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// Test 11: get_api_usage tool
|
|
301
|
-
await test('get_api_usage returns API usage info', async () => {
|
|
302
|
-
const response = await sendRequest({
|
|
303
|
-
jsonrpc: '2.0',
|
|
304
|
-
id: 1,
|
|
305
|
-
method: 'tools/call',
|
|
306
|
-
params: {
|
|
307
|
-
name: 'get_api_usage',
|
|
308
|
-
arguments: { sdk_id: 'dbr-mobile', api_name: 'decode' }
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
assert(response.result, 'Should have result');
|
|
313
|
-
assert(response.result.content, 'Should have content');
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// Test 12: search_samples tool
|
|
317
|
-
await test('search_samples finds samples by keyword', async () => {
|
|
318
|
-
const response = await sendRequest({
|
|
319
|
-
jsonrpc: '2.0',
|
|
320
|
-
id: 1,
|
|
321
|
-
method: 'tools/call',
|
|
322
|
-
params: {
|
|
323
|
-
name: 'search_samples',
|
|
324
|
-
arguments: { query: 'barcode' }
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
assert(response.result, 'Should have result');
|
|
329
|
-
assert(response.result.content, 'Should have content');
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// Test 13: generate_project tool
|
|
333
|
-
await test('generate_project returns project structure', async () => {
|
|
334
|
-
const response = await sendRequest({
|
|
335
|
-
jsonrpc: '2.0',
|
|
336
|
-
id: 1,
|
|
337
|
-
method: 'tools/call',
|
|
338
|
-
params: {
|
|
339
|
-
name: 'generate_project',
|
|
340
|
-
arguments: { platform: 'android', sample_name: 'ScanSingleBarcode' }
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
assert(response.result, 'Should have result');
|
|
345
|
-
assert(response.result.content, 'Should have content');
|
|
346
|
-
const text = response.result.content[0].text;
|
|
347
|
-
assert(text.includes('# Project Generation:'), 'Should include project generation header');
|
|
348
|
-
assert(text.includes('AndroidManifest.xml') || text.includes('build.gradle'), 'Should include project files');
|
|
349
|
-
});
|
|
350
|
-
|
|
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
|
|
426
|
-
await test('resources/list returns registered resources', async () => {
|
|
427
|
-
const response = await sendRequest({
|
|
428
|
-
jsonrpc: '2.0',
|
|
429
|
-
id: 1,
|
|
430
|
-
method: 'resources/list'
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
assert(response.result, 'Should have result');
|
|
434
|
-
assert(response.result.resources, 'Should have resources array');
|
|
435
|
-
assert(response.result.resources.length > 0, 'Should have at least one resource');
|
|
436
|
-
|
|
437
|
-
// Check for expected resource types
|
|
438
|
-
const uris = response.result.resources.map(r => r.uri);
|
|
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');
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// Test 19: Invalid tool call returns error
|
|
444
|
-
await test('Invalid tool call returns proper error', async () => {
|
|
445
|
-
const response = await sendRequest({
|
|
446
|
-
jsonrpc: '2.0',
|
|
447
|
-
id: 1,
|
|
448
|
-
method: 'tools/call',
|
|
449
|
-
params: {
|
|
450
|
-
name: 'nonexistent_tool',
|
|
451
|
-
arguments: {}
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
assert(response.error || (response.result && response.result.isError),
|
|
456
|
-
'Should return error for invalid tool');
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
// Test 20: Tool with invalid arguments returns error
|
|
460
|
-
await test('Tool with missing required arguments returns error', async () => {
|
|
461
|
-
const response = await sendRequest({
|
|
462
|
-
jsonrpc: '2.0',
|
|
463
|
-
id: 1,
|
|
464
|
-
method: 'tools/call',
|
|
465
|
-
params: {
|
|
466
|
-
name: 'get_license_info',
|
|
467
|
-
arguments: {} // Missing required platform
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
assert(response.result && response.result.isError,
|
|
472
|
-
'Should return error for missing required argument');
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
// ============================================
|
|
476
|
-
// Test Summary
|
|
477
|
-
// ============================================
|
|
478
|
-
|
|
479
|
-
console.log('\n' + '='.repeat(50));
|
|
480
|
-
console.log('\nš Test Summary\n');
|
|
481
|
-
console.log(` Total: ${passed + failed}`);
|
|
482
|
-
console.log(` Passed: ${passed} ā
`);
|
|
483
|
-
console.log(` Failed: ${failed} ā`);
|
|
484
|
-
console.log(` Rate: ${((passed / (passed + failed)) * 100).toFixed(1)}%`);
|
|
485
|
-
|
|
486
|
-
if (failed > 0) {
|
|
487
|
-
console.log('\nā Failed Tests:');
|
|
488
|
-
results.filter(r => r.status.includes('FAILED')).forEach(r => {
|
|
489
|
-
console.log(` - ${r.name}: ${r.error}`);
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
console.log('\n' + '='.repeat(50));
|
|
494
|
-
|
|
495
|
-
// Exit with appropriate code
|
|
496
|
-
process.exit(failed > 0 ? 1 : 0);
|