simple-dynamsoft-mcp 7.2.4 → 7.2.5

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.
@@ -108,8 +108,8 @@
108
108
  "branch": "main",
109
109
  "owner": "Dynamsoft",
110
110
  "repo": "barcode-reader-javascript-samples",
111
- "commit": "2f0a723e88e3271170672e61c214a3d1d7a31328",
112
- "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-javascript-samples/zip/2f0a723e88e3271170672e61c214a3d1d7a31328"
111
+ "commit": "deb6320704e324073cd0cb5d6edb8064ce130726",
112
+ "archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-javascript-samples/zip/deb6320704e324073cd0cb5d6edb8064ce130726"
113
113
  },
114
114
  {
115
115
  "name": "data/samples/dynamsoft-barcode-reader-c-cpp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-dynamsoft-mcp",
3
- "version": "7.2.4",
3
+ "version": "7.2.5",
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": {
@@ -24,7 +24,7 @@
24
24
  "scripts": {
25
25
  "start": "node src/index.js",
26
26
  "test": "npm run test:lite",
27
- "test:unit": "node --test test/unit/gemini-retry.test.js test/unit/profile-config.test.js test/unit/lexical-provider.test.js test/unit/hydration-mode.test.js test/unit/hydration-policy.test.js test/unit/repo-map.test.js test/unit/download-utils.test.js test/unit/logging.test.js test/unit/rag-signature-manifest.test.js test/unit/create-server.test.js test/unit/server-helpers.test.js",
27
+ "test:unit": "node --test test/unit/gemini-retry.test.js test/unit/profile-config.test.js test/unit/lexical-provider.test.js test/unit/hydration-mode.test.js test/unit/hydration-policy.test.js test/unit/repo-map.test.js test/unit/download-utils.test.js test/unit/logging.test.js test/unit/rag-signature-manifest.test.js test/unit/startup-timing.test.js test/unit/http-startup-readiness.test.js test/unit/resource-index-startup.test.js test/unit/create-server.test.js test/unit/server-helpers.test.js",
28
28
  "test:lite": "npm run test:stdio && npm run test:http && npm run test:package",
29
29
  "test:lexical": "node --test test/integration/stdio.test.js test/integration/http.test.js",
30
30
  "test:gemini": "node scripts/run-gemini-tests.mjs",
package/src/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { readFileSync } from "node:fs";
3
3
  import { ensureDataReady } from "./data/bootstrap.js";
4
4
  import { maybeSyncSubmodulesOnStart } from "./data/submodule-sync.js";
5
+ import { createStartupTimingTracker } from "./observability/startup-timing.js";
5
6
  import { createMcpServerInstance } from "./server/create-server.js";
6
7
  import { MCP_HTTP_PATH, resolveRuntimeConfig } from "./server/runtime-config.js";
7
8
  import { startStdioServer } from "./server/transports/stdio.js";
@@ -10,48 +11,132 @@ import { logEvent } from "./observability/logging.js";
10
11
 
11
12
  const pkgUrl = new URL("../package.json", import.meta.url);
12
13
  const pkg = JSON.parse(readFileSync(pkgUrl, "utf8"));
14
+ const startupTiming = createStartupTimingTracker();
13
15
 
14
- await maybeSyncSubmodulesOnStart();
15
- const dataStatus = await ensureDataReady();
16
- logEvent("data", "startup_mode", {
17
- mode: dataStatus.mode,
18
- path: dataStatus.dataRoot,
19
- cache_source: dataStatus.mode.includes("downloaded") ? (dataStatus.downloaded ? "fresh-download" : "cache") : "n/a"
16
+ let runtimeConfig;
17
+ try {
18
+ runtimeConfig = resolveRuntimeConfig();
19
+ } catch (error) {
20
+ const message = error instanceof Error ? error.message : String(error);
21
+ logEvent("transport", "config_error", { message }, { level: "error" });
22
+ process.exit(1);
23
+ }
24
+
25
+ startupTiming.mark("runtime_config_resolved", {
26
+ transport: runtimeConfig.transport
20
27
  });
21
28
 
22
- const resourceIndexApi = await import("./server/resource-index.js");
23
- const ragApi = await import("./rag/index.js");
29
+ let resourceIndexApi = null;
30
+ let ragApi = null;
24
31
 
25
- logEvent("profile", "resolved", {
26
- profile: ragApi.ragConfig.profile,
27
- provider: ragApi.ragConfig.provider,
28
- provider_source: ragApi.ragConfig.providerSource,
29
- fallback: ragApi.ragConfig.fallback,
30
- fallback_source: ragApi.ragConfig.fallbackSource
31
- });
32
+ const startupState = {
33
+ ready: false,
34
+ stage: "initializing"
35
+ };
32
36
 
33
- const createServer = () => createMcpServerInstance({
34
- pkgVersion: pkg.version,
35
- resourceIndexApi,
36
- ragApi
37
- });
37
+ function startupCacheSource(dataStatus) {
38
+ if (!dataStatus.mode.includes("downloaded")) return "n/a";
39
+ return dataStatus.downloaded ? "fresh-download" : "cache";
40
+ }
41
+
42
+ function createServer() {
43
+ if (!resourceIndexApi || !ragApi) {
44
+ throw new Error("MCP server is still initializing");
45
+ }
46
+
47
+ return createMcpServerInstance({
48
+ pkgVersion: pkg.version,
49
+ resourceIndexApi,
50
+ ragApi
51
+ });
52
+ }
53
+
54
+ function logResolvedProfile() {
55
+ logEvent("profile", "resolved", {
56
+ profile: ragApi.ragConfig.profile,
57
+ provider: ragApi.ragConfig.provider,
58
+ provider_source: ragApi.ragConfig.providerSource,
59
+ fallback: ragApi.ragConfig.fallback,
60
+ fallback_source: ragApi.ragConfig.fallbackSource
61
+ });
62
+ }
38
63
 
39
64
  async function maybePrewarm() {
40
- if (!ragApi.ragConfig.prewarm) return;
65
+ if (!ragApi?.ragConfig?.prewarm) return;
66
+
67
+ startupTiming.mark("prewarm_triggered", {
68
+ provider: ragApi.ragConfig.provider,
69
+ block: ragApi.ragConfig.prewarmBlock ? "true" : "false"
70
+ });
71
+
41
72
  if (ragApi.ragConfig.prewarmBlock) {
42
73
  await ragApi.prewarmRagIndex();
43
- } else {
44
- void ragApi.prewarmRagIndex();
74
+ startupTiming.mark("prewarm_done", {
75
+ provider: ragApi.ragConfig.provider
76
+ });
77
+ return;
45
78
  }
79
+
80
+ void ragApi.prewarmRagIndex().then(
81
+ () => {
82
+ startupTiming.mark("prewarm_done", {
83
+ provider: ragApi.ragConfig.provider
84
+ });
85
+ },
86
+ () => {
87
+ // prewarmRagIndex already logs failure details.
88
+ }
89
+ );
46
90
  }
47
91
 
48
- let runtimeConfig;
49
- try {
50
- runtimeConfig = resolveRuntimeConfig();
51
- } catch (error) {
92
+ async function initializeRuntime() {
93
+ startupState.stage = "submodule_sync";
94
+ await maybeSyncSubmodulesOnStart();
95
+ startupTiming.mark("submodule_sync_done");
96
+
97
+ startupState.stage = "data_resolve";
98
+ const dataStatus = await ensureDataReady();
99
+ logEvent("data", "startup_mode", {
100
+ mode: dataStatus.mode,
101
+ path: dataStatus.dataRoot,
102
+ cache_source: startupCacheSource(dataStatus)
103
+ });
104
+ startupTiming.mark("data_ready", {
105
+ mode: dataStatus.mode,
106
+ cache_source: startupCacheSource(dataStatus)
107
+ });
108
+
109
+ startupState.stage = "resource_index_import";
110
+ resourceIndexApi = await import("./server/resource-index.js");
111
+ startupTiming.mark("resource_index_module_loaded");
112
+
113
+ startupState.stage = "resource_index_build";
114
+ const indexReady = resourceIndexApi.ensureResourceIndexReady();
115
+ startupTiming.mark("resource_index_ready", {
116
+ resource_count: indexReady.resourceCount
117
+ });
118
+
119
+ startupState.stage = "rag_import";
120
+ ragApi = await import("./rag/index.js");
121
+ startupTiming.mark("rag_module_loaded", {
122
+ provider: ragApi.ragConfig.provider,
123
+ fallback: ragApi.ragConfig.fallback
124
+ });
125
+
126
+ logResolvedProfile();
127
+
128
+ startupState.ready = true;
129
+ startupState.stage = "mcp_ready";
130
+ startupTiming.mark("mcp_ready");
131
+
132
+ return dataStatus;
133
+ }
134
+
135
+ function logStartupFailure(error) {
52
136
  const message = error instanceof Error ? error.message : String(error);
53
- logEvent("transport", "config_error", { message }, { level: "error" });
54
- process.exit(1);
137
+ startupState.stage = "startup_failed";
138
+ startupState.ready = false;
139
+ logEvent("startup", "failed", { message }, { level: "error" });
55
140
  }
56
141
 
57
142
  if (runtimeConfig.transport === "http") {
@@ -59,10 +144,38 @@ if (runtimeConfig.transport === "http") {
59
144
  host: runtimeConfig.host,
60
145
  port: runtimeConfig.port,
61
146
  mcpPath: MCP_HTTP_PATH,
62
- createServer
147
+ createServer,
148
+ isReady: () => startupState.ready,
149
+ getReadinessState: () => ({ stage: startupState.stage })
63
150
  });
151
+ startupTiming.mark("http_listener_ready", {
152
+ host: runtimeConfig.host,
153
+ port: runtimeConfig.port
154
+ });
155
+
156
+ try {
157
+ const dataStatus = await initializeRuntime();
158
+ await maybePrewarm();
159
+ startupTiming.complete({
160
+ transport: "http",
161
+ data_mode: dataStatus.mode
162
+ });
163
+ } catch (error) {
164
+ logStartupFailure(error);
165
+ process.exit(1);
166
+ }
64
167
  } else {
65
- await startStdioServer({ createServer });
168
+ try {
169
+ const dataStatus = await initializeRuntime();
170
+ await startStdioServer({ createServer });
171
+ startupTiming.mark("stdio_listener_ready");
172
+ await maybePrewarm();
173
+ startupTiming.complete({
174
+ transport: "stdio",
175
+ data_mode: dataStatus.mode
176
+ });
177
+ } catch (error) {
178
+ logStartupFailure(error);
179
+ process.exit(1);
180
+ }
66
181
  }
67
-
68
- await maybePrewarm();
@@ -0,0 +1,41 @@
1
+ import { logEvent } from "./logging.js";
2
+
3
+ function createStartupTimingTracker({ now = Date.now, emit } = {}) {
4
+ const emitEvent =
5
+ typeof emit === "function"
6
+ ? emit
7
+ : (component, event, payload) => {
8
+ logEvent(component, event, payload);
9
+ };
10
+
11
+ const startedAt = now();
12
+ let previousAt = startedAt;
13
+
14
+ function mark(stage, fields = {}) {
15
+ const currentAt = now();
16
+ emitEvent("startup", "stage", {
17
+ stage,
18
+ elapsed_ms: currentAt - previousAt,
19
+ since_boot_ms: currentAt - startedAt,
20
+ ...fields
21
+ });
22
+ previousAt = currentAt;
23
+ }
24
+
25
+ function complete(fields = {}) {
26
+ const currentAt = now();
27
+ emitEvent("startup", "complete", {
28
+ total_ms: currentAt - startedAt,
29
+ ...fields
30
+ });
31
+ }
32
+
33
+ return {
34
+ mark,
35
+ complete
36
+ };
37
+ }
38
+
39
+ export {
40
+ createStartupTimingTracker
41
+ };
@@ -280,6 +280,7 @@ function buildVersionPolicyText() {
280
280
 
281
281
  const resourceIndex = [];
282
282
  const resourceIndexByUri = new Map();
283
+ let resourceIndexReady = false;
283
284
 
284
285
  function addResourceToIndex(entry) {
285
286
  resourceIndex.push(entry);
@@ -368,10 +369,16 @@ function refreshResourceIndex() {
368
369
  for (const entry of resourceIndex) {
369
370
  resourceIndexByUri.set(entry.uri, entry);
370
371
  }
372
+ resourceIndexReady = true;
371
373
  return { resourceCount: resourceIndex.length };
372
374
  }
373
375
 
374
- refreshResourceIndex();
376
+ function ensureResourceIndexReady() {
377
+ if (!resourceIndexReady) {
378
+ return refreshResourceIndex();
379
+ }
380
+ return { resourceCount: resourceIndex.length };
381
+ }
375
382
 
376
383
  function editionMatches(normalizedEdition, entryEdition) {
377
384
  if (!normalizedEdition) return true;
@@ -397,6 +404,8 @@ function platformMatches(normalizedPlatform, entry) {
397
404
  }
398
405
 
399
406
  function getSampleEntries({ product, edition, platform }) {
407
+ ensureResourceIndexReady();
408
+
400
409
  const normalizedProduct = normalizeProduct(product);
401
410
  const normalizedPlatform = normalizePlatform(platform);
402
411
  const normalizedEdition = normalizeEdition(edition, normalizedPlatform, normalizedProduct);
@@ -425,6 +434,7 @@ function formatScopeLabel(entry) {
425
434
  }
426
435
 
427
436
  function getPinnedResources() {
437
+ ensureResourceIndexReady();
428
438
  return resourceIndex.filter((entry) => entry.pinned);
429
439
  }
430
440
 
@@ -464,6 +474,8 @@ function buildResourceLookupCandidates(uri) {
464
474
  }
465
475
 
466
476
  async function readResourceContent(uri) {
477
+ ensureResourceIndexReady();
478
+
467
479
  let resource = null;
468
480
  for (const candidate of buildResourceLookupCandidates(uri)) {
469
481
  resource = resourceIndexByUri.get(candidate);
@@ -481,6 +493,8 @@ async function readResourceContent(uri) {
481
493
  }
482
494
 
483
495
  function getRagSignatureData() {
496
+ ensureResourceIndexReady();
497
+
484
498
  return {
485
499
  resourceCount: resourceIndex.length,
486
500
  dcvCoreDocCount: dcvCoreDocs.length,
@@ -585,6 +599,7 @@ export {
585
599
  buildIndexData,
586
600
  buildResourceIndex,
587
601
  refreshResourceIndex,
602
+ ensureResourceIndexReady,
588
603
  editionMatches,
589
604
  platformMatches,
590
605
  getDisplayEdition,
@@ -2,7 +2,15 @@ import { createServer as createHttpServer } from "node:http";
2
2
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
3
  import { logEvent } from "../../observability/logging.js";
4
4
 
5
- async function startHttpServer({ host, port, mcpPath, createServer }) {
5
+ async function startHttpServer({
6
+ host,
7
+ port,
8
+ mcpPath,
9
+ createServer,
10
+ isReady = () => true,
11
+ getReadinessState = () => ({}),
12
+ registerSignalHandlers = true
13
+ }) {
6
14
  logEvent("transport", "server_start", { mode: "http", host, port, path: mcpPath });
7
15
 
8
16
  const httpServer = createHttpServer(async (req, res) => {
@@ -13,20 +21,50 @@ async function startHttpServer({ host, port, mcpPath, createServer }) {
13
21
  return;
14
22
  }
15
23
 
16
- const server = createServer();
17
- const transport = new StreamableHTTPServerTransport({
18
- sessionIdGenerator: undefined
19
- });
24
+ if (!isReady()) {
25
+ const readiness = getReadinessState();
26
+ logEvent("transport", "request_rejected_not_ready", {
27
+ mode: "http",
28
+ path: requestUrl.pathname,
29
+ stage: readiness?.stage || "initializing"
30
+ }, { level: "debug" });
31
+
32
+ res.writeHead(503, {
33
+ "content-type": "application/json; charset=utf-8",
34
+ "retry-after": "2"
35
+ });
36
+ res.end(
37
+ JSON.stringify({
38
+ jsonrpc: "2.0",
39
+ error: {
40
+ code: -32000,
41
+ message: "Server is warming up. Please retry shortly.",
42
+ data: {
43
+ stage: readiness?.stage || "initializing"
44
+ }
45
+ },
46
+ id: null
47
+ })
48
+ );
49
+ return;
50
+ }
51
+
52
+ let server = null;
53
+ let transport = null;
20
54
 
21
55
  let closed = false;
22
56
  const closeResources = async () => {
23
57
  if (closed) return;
24
58
  closed = true;
25
59
  try {
26
- await transport.close();
60
+ if (transport) {
61
+ await transport.close();
62
+ }
27
63
  } catch {}
28
64
  try {
29
- await server.close();
65
+ if (server) {
66
+ await server.close();
67
+ }
30
68
  } catch {}
31
69
  };
32
70
 
@@ -35,6 +73,10 @@ async function startHttpServer({ host, port, mcpPath, createServer }) {
35
73
  });
36
74
 
37
75
  try {
76
+ server = createServer();
77
+ transport = new StreamableHTTPServerTransport({
78
+ sessionIdGenerator: undefined
79
+ });
38
80
  await server.connect(transport);
39
81
  await transport.handleRequest(req, res);
40
82
  } catch (error) {
@@ -71,12 +113,14 @@ async function startHttpServer({ host, port, mcpPath, createServer }) {
71
113
  process.exit(0);
72
114
  };
73
115
 
74
- process.on("SIGINT", () => {
75
- void shutdown("SIGINT");
76
- });
77
- process.on("SIGTERM", () => {
78
- void shutdown("SIGTERM");
79
- });
116
+ if (registerSignalHandlers) {
117
+ process.on("SIGINT", () => {
118
+ void shutdown("SIGINT");
119
+ });
120
+ process.on("SIGTERM", () => {
121
+ void shutdown("SIGTERM");
122
+ });
123
+ }
80
124
 
81
125
  return { httpServer };
82
126
  }