simple-dynamsoft-mcp 7.2.4 → 7.2.6

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.
@@ -88,8 +88,8 @@
88
88
  "branch": "master",
89
89
  "owner": "dynamsoft-docs",
90
90
  "repo": "web-twain-docs",
91
- "commit": "a38d73c583b8379c0d80337e92f291f6c1ab10a8",
92
- "archiveUrl": "https://codeload.github.com/dynamsoft-docs/web-twain-docs/zip/a38d73c583b8379c0d80337e92f291f6c1ab10a8"
91
+ "commit": "7b8e127b298a2ef9abb7cd870bb742b44a132503",
92
+ "archiveUrl": "https://codeload.github.com/dynamsoft-docs/web-twain-docs/zip/7b8e127b298a2ef9abb7cd870bb742b44a132503"
93
93
  },
94
94
  {
95
95
  "name": "data/samples/dynamic-web-twain",
@@ -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.6",
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
+ };
@@ -2,12 +2,24 @@ import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import { extname, join } from "node:path";
3
3
  import { DDV_PREFERRED_ENTRY_FILES } from "./config.js";
4
4
 
5
- function mapDocTitlesWithOptionalPlatform(articles, includePlatform = false) {
6
- return articles.map((article) => ({
7
- title: article.title,
8
- category: article.breadcrumb || "",
9
- ...(includePlatform && article.platform ? { platform: article.platform } : {})
10
- }));
5
+ function countSamples(sampleData) {
6
+ if (Array.isArray(sampleData)) {
7
+ return sampleData.length;
8
+ }
9
+
10
+ if (sampleData && typeof sampleData === "object") {
11
+ return Object.values(sampleData).reduce((total, value) => {
12
+ return total + countSamples(value);
13
+ }, 0);
14
+ }
15
+
16
+ return 0;
17
+ }
18
+
19
+ function countDiscoveredSamplesByPlatforms(platforms, discoverSamplesForPlatform) {
20
+ return platforms.reduce((total, platform) => {
21
+ return total + countSamples(discoverSamplesForPlatform(platform));
22
+ }, 0);
11
23
  }
12
24
 
13
25
  function getDcvScenarioTags(sampleName) {
@@ -35,20 +47,20 @@ function buildProductSelectionGuidanceText() {
35
47
  return [
36
48
  "# Product Selection Guidance",
37
49
  "",
38
- "## DBR vs DCV",
50
+ "## Dynamsoft Barcode Reader (DBR) vs Dynamsoft Capture Vision (DCV)",
39
51
  "",
40
- "Dynamsoft Capture Vision (DCV) is a superset architecture that aggregates DBR, DLR, DDN, DCP, and DCE.",
52
+ "Dynamsoft Capture Vision (DCV) is a superset architecture that aggregates Dynamsoft Barcode Reader (DBR), Dynamsoft Label Recognizer (DLR), Dynamsoft Document Normalizer (DDN), Dynamsoft Code Parser (DCP), and Dynamsoft Camera Enhancer (DCE).",
41
53
  "",
42
- "Use DBR when you only need barcode reading and do not need DCV workflows.",
54
+ "Use Dynamsoft Barcode Reader (DBR) when you only need barcode reading and do not need Dynamsoft Capture Vision (DCV) workflows.",
43
55
  "",
44
- "Use DCV when your scenario includes:",
56
+ "Use Dynamsoft Capture Vision (DCV) when your scenario includes:",
45
57
  "- VIN scanning",
46
58
  "- MRZ/passport/ID scanning",
47
59
  "- Driver license parsing",
48
60
  "- Document detection/normalization/auto-capture/cropping",
49
61
  "- Multi-task image processing and parsing workflows",
50
62
  "",
51
- "If a query includes MRZ, VIN, driver license, or document-normalization intents, prefer DCV samples/docs."
63
+ "If a query includes MRZ, VIN, driver license, or document-normalization intents, prefer Dynamsoft Capture Vision (DCV) samples and docs."
52
64
  ].join("\n");
53
65
  }
54
66
 
@@ -146,16 +158,17 @@ function buildIndexData({
146
158
  const dcvWebFrameworks = getDcvWebFrameworkPlatforms();
147
159
  const dcvMobilePlatforms = getDcvMobilePlatforms();
148
160
  const dcvServerPlatforms = getDcvServerPlatforms();
149
- const dbrWebSamples = discoverWebSamples();
161
+ const dbrWebSampleCount = countSamples(discoverWebSamples());
150
162
  const dbrWebFrameworks = getDbrWebFrameworkPlatforms();
151
163
  const dbrMobilePlatforms = getDbrMobilePlatforms();
152
164
  const dbrServerPlatforms = getDbrServerPlatforms();
165
+ const dwtSampleCount = countSamples(discoverDwtSamples());
153
166
  const ddvSamples = discoverDdvSamples();
154
167
  const ddvWebFrameworks = getDdvWebFrameworkPlatforms();
155
168
 
156
169
  return {
157
170
  productSelection: {
158
- dcvSupersetSummary: "DCV aggregates DBR, DLR, DDN, DCP, and DCE into one pipeline.",
171
+ dcvSupersetSummary: "Dynamsoft Capture Vision (DCV) aggregates Dynamsoft Barcode Reader (DBR), Dynamsoft Label Recognizer (DLR), Dynamsoft Document Normalizer (DDN), Dynamsoft Code Parser (DCP), and Dynamsoft Camera Enhancer (DCE) into one pipeline.",
159
172
  useDbrWhen: [
160
173
  "Barcode-only workflows where DCV-specific workflows are not required."
161
174
  ],
@@ -175,32 +188,25 @@ function buildIndexData({
175
188
  version: dcvCoreVersion,
176
189
  platforms: ["core"],
177
190
  docCount: dcvCoreDocs.length,
178
- docTitles: mapDocTitlesWithOptionalPlatform(dcvCoreDocs)
191
+ sampleCount: 0
179
192
  },
180
193
  web: {
181
194
  version: dcvWebVersion,
182
195
  platforms: ["js", ...dcvWebFrameworks],
183
- samples: dcvWebSamples,
184
196
  docCount: dcvWebDocs.length,
185
- docTitles: mapDocTitlesWithOptionalPlatform(dcvWebDocs)
197
+ sampleCount: countSamples(dcvWebSamples)
186
198
  },
187
199
  mobile: {
188
200
  version: dcvMobileVersion,
189
201
  platforms: dcvMobilePlatforms,
190
- samples: Object.fromEntries(
191
- dcvMobilePlatforms.map((platform) => [platform, discoverDcvMobileSamples(platform)])
192
- ),
193
202
  docCount: dcvMobileDocs.length,
194
- docTitles: mapDocTitlesWithOptionalPlatform(dcvMobileDocs, true)
203
+ sampleCount: countDiscoveredSamplesByPlatforms(dcvMobilePlatforms, discoverDcvMobileSamples)
195
204
  },
196
205
  server: {
197
206
  version: dcvServerVersion,
198
207
  platforms: dcvServerPlatforms,
199
- samples: Object.fromEntries(
200
- dcvServerPlatforms.map((platform) => [platform, discoverDcvServerSamples(platform)])
201
- ),
202
208
  docCount: dcvServerDocs.length,
203
- docTitles: mapDocTitlesWithOptionalPlatform(dcvServerDocs, true)
209
+ sampleCount: countDiscoveredSamplesByPlatforms(dcvServerPlatforms, discoverDcvServerSamples)
204
210
  }
205
211
  }
206
212
  },
@@ -210,27 +216,20 @@ function buildIndexData({
210
216
  mobile: {
211
217
  version: dbrMobileVersion,
212
218
  platforms: dbrMobilePlatforms,
213
- samples: Object.fromEntries(
214
- dbrMobilePlatforms.map((platform) => [platform, discoverMobileSamples(platform)])
215
- ),
216
219
  docCount: dbrMobileDocs.length,
217
- docTitles: mapDocTitlesWithOptionalPlatform(dbrMobileDocs, true)
220
+ sampleCount: countDiscoveredSamplesByPlatforms(dbrMobilePlatforms, discoverMobileSamples)
218
221
  },
219
222
  web: {
220
223
  version: dbrWebVersion,
221
224
  platforms: ["js", ...dbrWebFrameworks],
222
- samples: dbrWebSamples,
223
225
  docCount: dbrWebDocs.length,
224
- docTitles: mapDocTitlesWithOptionalPlatform(dbrWebDocs)
226
+ sampleCount: dbrWebSampleCount
225
227
  },
226
228
  server: {
227
229
  version: dbrServerVersion,
228
230
  platforms: dbrServerPlatforms,
229
- samples: Object.fromEntries(
230
- dbrServerPlatforms.map((platform) => [platform, discoverDbrServerSamples(platform)])
231
- ),
232
231
  docCount: dbrServerDocs.length,
233
- docTitles: mapDocTitlesWithOptionalPlatform(dbrServerDocs, true)
232
+ sampleCount: countDiscoveredSamplesByPlatforms(dbrServerPlatforms, discoverDbrServerSamples)
234
233
  }
235
234
  }
236
235
  },
@@ -240,12 +239,8 @@ function buildIndexData({
240
239
  web: {
241
240
  version: dwtVersion,
242
241
  platforms: ["js"],
243
- sampleCategories: discoverDwtSamples(),
244
242
  docCount: dwtDocs.articles.length,
245
- docTitles: dwtDocs.articles.map((article) => ({
246
- title: article.title,
247
- category: article.breadcrumb || ""
248
- }))
243
+ sampleCount: dwtSampleCount
249
244
  }
250
245
  }
251
246
  },
@@ -255,12 +250,8 @@ function buildIndexData({
255
250
  web: {
256
251
  version: ddvVersion,
257
252
  platforms: ["js", ...ddvWebFrameworks],
258
- samples: ddvSamples,
259
253
  docCount: ddvDocs.articles.length,
260
- docTitles: ddvDocs.articles.map((article) => ({
261
- title: article.title,
262
- category: article.breadcrumb || ""
263
- }))
254
+ sampleCount: countSamples(ddvSamples)
264
255
  }
265
256
  }
266
257
  }
@@ -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
  }