simple-dynamsoft-mcp 2.2.1 → 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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/http/wrapper.js +480 -0
  3. package/package.json +53 -45
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
 
@@ -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)
@@ -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": "2.2.1",
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
- "code-snippet",
21
- "README.md"
22
- ],
23
- "scripts": {
24
- "start": "node src/index.js",
25
- "test": "node test/server.test.js"
26
- },
27
- "keywords": [
28
- "mcp",
29
- "model-context-protocol",
30
- "dynamsoft",
31
- "barcode-reader",
32
- "barcode-scanner",
33
- "document-scanner",
34
- "web-twain",
35
- "android",
36
- "ios",
37
- "python",
38
- "javascript",
39
- "mobile"
40
- ],
41
- "dependencies": {
42
- "@modelcontextprotocol/sdk": "^1.25.2",
43
- "zod": "~3.24.0"
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
+ }