@kinginsun/mcp-drugsea 0.2.0 → 0.2.1

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 (2) hide show
  1. package/dist/index.js +210 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
- import { clampLimit, clampOffset, fetchDetail, fetchFacets, listSearch, mcpDbDetail, mcpDbSearch, prefersMcpListApi, yaohaiPost, } from "./api.js";
5
+ import { ApiError, clampLimit, clampOffset, fetchDetail, fetchFacets, listSearch, mcpDbDetail, mcpDbSearch, prefersMcpListApi, yaohaiPost, } from "./api.js";
6
6
  import { ATC_HINT, PRODUCT_CN_COMMON_FIELDS, PRODUCT_CN_DETAIL_PATH, PRODUCT_CN_FACET_FIELDS, PRODUCT_CN_FACET_PREFIX, PRODUCT_CN_VIEW_TYPES, REG_CN_COMMON_FIELDS, REG_CN_DETAIL_PATH, REG_CN_FACET_FIELDS, REG_CN_FACET_PREFIX, REG_CN_VIEW_TYPES, applyProductCnDefaults, applyRegCnDefaults, productCnSearchPath, regCnSearchPath, } from "./fields.js";
7
7
  import { EmptyObjectSchema, ProductCnDetailSchema, ProductCnFacetsSchema, ProductCnSearchSchema, RegCnDetailSchema, RegCnFacetsSchema, RegCnSearchSchema, YaohaiCatalogSchema, YaohaiDetailSchema, YaohaiGlobalSearchSchema, YaohaiSearchSchema, YaohaiSmartSearchSchema, } from "./types.js";
8
8
  const PACKAGE_VERSION = "0.2.0";
@@ -111,6 +111,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
111
111
  {
112
112
  name: "yaohai-smart-search",
113
113
  description: "Natural-language Yaohai search: auto-routes the question to up to 3 databases. Use when the user question is broad or the target DB is unclear. " +
114
+ "Falls back to global search when the router matches nothing or finds no rows. " +
114
115
  ROUTING_HINT,
115
116
  inputSchema: {
116
117
  type: "object",
@@ -259,6 +260,170 @@ function encodeId(id) {
259
260
  function asQuery(query) {
260
261
  return { ...(query ?? {}) };
261
262
  }
263
+ async function globalSearchContent(q, query, limit) {
264
+ const gquery = asQuery(query);
265
+ if (q && (gquery.term === undefined || gquery.term === "")) {
266
+ gquery.term = q;
267
+ }
268
+ const content = await yaohaiPost("/g/mcp/yaohai/global-search", {
269
+ query: gquery,
270
+ limit,
271
+ offset: 0,
272
+ });
273
+ return content;
274
+ }
275
+ function allSmartResultsEmpty(content) {
276
+ if (!content || typeof content !== "object") {
277
+ return true;
278
+ }
279
+ const results = content.results;
280
+ if (!Array.isArray(results) || results.length === 0) {
281
+ return true;
282
+ }
283
+ return results.every((entry) => {
284
+ const result = entry?.result;
285
+ if (!result || typeof result !== "object") {
286
+ return true;
287
+ }
288
+ const total = result.total;
289
+ return typeof total === "number" ? total === 0 : true;
290
+ });
291
+ }
292
+ /**
293
+ * The backend router searches the entire question string in one field per DB,
294
+ * which rarely matches (e.g. item="医保目录 阿司匹林" → 0 rows), and sometimes
295
+ * picks a field the DB does not support (e.g. `item` on jicai). Retry each
296
+ * empty matched DB with the individual tokens of the question across the
297
+ * router's field plus the catalog's search_fields for that DB.
298
+ */
299
+ async function retrySmartResultsWithTokens(content, q, limit) {
300
+ const results = content.results;
301
+ if (!Array.isArray(results)) {
302
+ return null;
303
+ }
304
+ const tokens = q.split(/\s+/).filter((t) => t && t !== q);
305
+ if (tokens.length === 0) {
306
+ return null;
307
+ }
308
+ const catalogFields = new Map();
309
+ async function searchFieldsFor(dbname) {
310
+ if (catalogFields.has(dbname)) {
311
+ return catalogFields.get(dbname);
312
+ }
313
+ let keys = [];
314
+ try {
315
+ const cat = (await yaohaiPost("/g/mcp/yaohai/catalog", { q: dbname }));
316
+ const db = (cat.databases ?? []).find((d) => d.id === dbname);
317
+ keys = (db?.search_fields ?? [])
318
+ .map((f) => f.key)
319
+ .filter((k) => typeof k === "string");
320
+ }
321
+ catch {
322
+ keys = [];
323
+ }
324
+ catalogFields.set(dbname, keys);
325
+ return keys;
326
+ }
327
+ let changed = false;
328
+ const patched = [];
329
+ for (const entry of results) {
330
+ const inner = entry?.result;
331
+ if (!inner || typeof inner !== "object") {
332
+ patched.push(entry);
333
+ continue;
334
+ }
335
+ const r = inner;
336
+ if (r.total !== 0 || typeof r.dbname !== "string") {
337
+ patched.push(entry);
338
+ continue;
339
+ }
340
+ const queryApplied = r.query_applied;
341
+ const appliedKeys = queryApplied ? Object.keys(queryApplied) : [];
342
+ const singleFieldWholeQuestion = appliedKeys.length === 1 && queryApplied[appliedKeys[0]] === q;
343
+ if (!singleFieldWholeQuestion) {
344
+ patched.push(entry);
345
+ continue;
346
+ }
347
+ const routerField = appliedKeys[0];
348
+ const fields = [routerField, ...(await searchFieldsFor(r.dbname))];
349
+ const uniqueFields = [...new Set(fields)].slice(0, 6);
350
+ let replaced = false;
351
+ outer: for (const token of tokens.slice(0, 3)) {
352
+ for (const field of uniqueFields) {
353
+ try {
354
+ const retry = (await yaohaiPost("/g/mcp/yaohai/search", {
355
+ dbname: r.dbname,
356
+ query: { [field]: token },
357
+ limit,
358
+ offset: 0,
359
+ }));
360
+ if (typeof retry.total === "number" && retry.total > 0) {
361
+ patched.push({
362
+ ...entry,
363
+ result: { ...retry, retry: { token, field } },
364
+ });
365
+ changed = true;
366
+ replaced = true;
367
+ break outer;
368
+ }
369
+ }
370
+ catch {
371
+ // ignore per-token retry errors; try next field/token
372
+ }
373
+ }
374
+ }
375
+ if (!replaced) {
376
+ patched.push(entry);
377
+ }
378
+ }
379
+ if (!changed) {
380
+ return null;
381
+ }
382
+ return { ...content, results: patched };
383
+ }
384
+ /**
385
+ * Last resort when the router matched databases but every search came back
386
+ * empty (e.g. the question is just a DB keyword like 集采): browse the matched
387
+ * databases without a query so the caller still sees representative rows.
388
+ */
389
+ async function browseMatchedDatabases(content, limit) {
390
+ const matched = content.matched_databases;
391
+ if (!Array.isArray(matched) || matched.length === 0) {
392
+ return null;
393
+ }
394
+ const results = [];
395
+ for (const db of matched.slice(0, 3)) {
396
+ const id = db?.id;
397
+ if (typeof id !== "string") {
398
+ continue;
399
+ }
400
+ try {
401
+ const browse = (await yaohaiPost("/g/mcp/yaohai/search", {
402
+ dbname: id,
403
+ query: {},
404
+ limit,
405
+ offset: 0,
406
+ }));
407
+ if (typeof browse.total === "number" && browse.total > 0) {
408
+ results.push({
409
+ score: db.score,
410
+ result: { ...browse, browse: true },
411
+ });
412
+ }
413
+ }
414
+ catch {
415
+ // DB not browsable — skip
416
+ }
417
+ }
418
+ if (results.length === 0) {
419
+ return null;
420
+ }
421
+ return {
422
+ ...content,
423
+ results,
424
+ note: "Smart-search found no rows for the full question; showing sample rows from the matched databases instead.",
425
+ };
426
+ }
262
427
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
263
428
  const { name, arguments: args } = request.params;
264
429
  try {
@@ -306,15 +471,53 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
306
471
  }
307
472
  case "yaohai-smart-search": {
308
473
  const validated = YaohaiSmartSearchSchema.parse(args);
309
- const body = {
310
- q: validated.q,
311
- limit: clampLimit(validated.limit, YAOHAI_LIMIT_DEFAULT, YAOHAI_LIMIT_MAX),
312
- };
474
+ const limit = clampLimit(validated.limit, YAOHAI_LIMIT_DEFAULT, YAOHAI_LIMIT_MAX);
475
+ const body = { q: validated.q, limit };
313
476
  if (validated.query) {
314
477
  body.query = validated.query;
315
478
  }
316
- const content = await yaohaiPost("/g/mcp/yaohai/smart-search", body);
317
- return ok(content);
479
+ try {
480
+ const content = await yaohaiPost("/g/mcp/yaohai/smart-search", body);
481
+ if (allSmartResultsEmpty(content)) {
482
+ // Router matched DBs but searched the whole question in one field
483
+ // (or picked an unsupported field). Retry with individual tokens
484
+ // across the router's field + the DB's catalog search_fields.
485
+ const retried = await retrySmartResultsWithTokens(content, validated.q, limit);
486
+ if (retried) {
487
+ return ok(retried);
488
+ }
489
+ // Keyword-only question (e.g. "集采"): show sample rows from the
490
+ // matched databases.
491
+ const browsed = await browseMatchedDatabases(content, limit);
492
+ if (browsed) {
493
+ return ok(browsed);
494
+ }
495
+ // Still nothing — try the global panorama.
496
+ const fallback = await globalSearchContent(validated.q, validated.query, limit);
497
+ return ok({
498
+ question: validated.q,
499
+ matched_databases: content.matched_databases,
500
+ fallback: "global-search",
501
+ note: "Smart-search router matched databases but returned no rows; fell back to global search.",
502
+ ...fallback,
503
+ });
504
+ }
505
+ return ok(content);
506
+ }
507
+ catch (error) {
508
+ if (error instanceof ApiError) {
509
+ // Backend router could not match any database (e.g. plain drug-name
510
+ // question) — fall back to the global panorama search.
511
+ const fallback = await globalSearchContent(validated.q, validated.query, limit);
512
+ return ok({
513
+ question: validated.q,
514
+ fallback: "global-search",
515
+ note: `Smart-search router failed (${error.message}); fell back to global search.`,
516
+ ...fallback,
517
+ });
518
+ }
519
+ throw error;
520
+ }
318
521
  }
319
522
  case "product-cn-fields": {
320
523
  EmptyObjectSchema.parse(args ?? {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinginsun/mcp-drugsea",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "MCP server for DrugSea / Yaohai pharmaceutical databases: cross-db search, China marketed products (product_cn), and CDE registration review (reg_cn).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",