@kinginsun/mcp-drugsea 0.1.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.
- package/README.md +15 -18
- package/dist/api.js +27 -25
- package/dist/index.js +211 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
MCP (Model Context Protocol) stdio server for [DrugSea / Yaohai](https://db.drugsea.cn) pharmaceutical databases.
|
|
4
4
|
|
|
5
|
-
The server forwards tool calls to
|
|
5
|
+
The server forwards tool calls to **`https://db3.drugsea.cn/api`** with personal user token auth (`Authorization: Bearer ysk_…`). It covers:
|
|
6
6
|
|
|
7
7
|
- **yaohai-*** — cross-database catalog / search / detail / global / smart-search (`POST /g/mcp/yaohai/*`)
|
|
8
|
-
- **product-cn-*** — already-marketed China products (
|
|
9
|
-
- **reg-cn-*** — CDE registration / review pipeline (
|
|
8
|
+
- **product-cn-*** — already-marketed China products (search/detail via MCP on db3; facets via GET)
|
|
9
|
+
- **reg-cn-*** — CDE registration / review pipeline (search/detail via MCP on db3; facets via GET)
|
|
10
10
|
|
|
11
11
|
**GitHub:** [github.com/kinginsun/mcp-drugsea](https://github.com/kinginsun/mcp-drugsea)
|
|
12
12
|
|
|
@@ -24,25 +24,22 @@ npx -y @kinginsun/mcp-drugsea
|
|
|
24
24
|
|
|
25
25
|
## Configuration
|
|
26
26
|
|
|
27
|
-
### `
|
|
27
|
+
### `YAOHAI_MCP_TOKEN` (required)
|
|
28
28
|
|
|
29
|
-
The
|
|
29
|
+
Authentication uses **personal user tokens only** (`ysk_` + 32 hex chars). The legacy shared `X-Yaohai-Api-Key` header is **not supported**.
|
|
30
30
|
|
|
31
31
|
| Credential | Format | HTTP header |
|
|
32
32
|
|------------|--------|-------------|
|
|
33
|
-
| Shared MCP key | opaque string (server env `YAOHAI_MCP_API_KEY`) | `X-Yaohai-Api-Key` |
|
|
34
33
|
| Personal user token | `ysk_` + 32 hex chars | `Authorization: Bearer ysk_…` |
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
**Note:** Production MCP with personal `ysk_` tokens is deployed at **`https://db3.drugsea.cn/api/g/mcp/*`**. Legacy `api2.drugsea.cn` accepts only the shared `X-Yaohai-Api-Key`. On db3, direct GET list/facet routes return encrypted payloads; this client auto-routes `product-cn-search` / `reg-cn-search` / detail through MCP POST when the base URL contains `db3.drugsea.cn`.
|
|
39
|
-
|
|
40
|
-
Obtain a key from the DrugSea / Yaohai operator, then:
|
|
35
|
+
Obtain a token from DrugSea / Yaohai (user account settings), then:
|
|
41
36
|
|
|
42
37
|
```bash
|
|
43
|
-
export
|
|
38
|
+
export YAOHAI_MCP_TOKEN=ysk_your_token_here
|
|
44
39
|
```
|
|
45
40
|
|
|
41
|
+
On db3, direct GET list routes may return encrypted payloads; this client auto-routes `product-cn-search` / `reg-cn-search` / detail through MCP POST when the base URL contains `db3.drugsea.cn`.
|
|
42
|
+
|
|
46
43
|
### Optional
|
|
47
44
|
|
|
48
45
|
| Variable | Default | Description |
|
|
@@ -60,7 +57,7 @@ export YAOHAI_MCP_API_KEY=your_api_key_here
|
|
|
60
57
|
"command": "npx",
|
|
61
58
|
"args": ["-y", "@kinginsun/mcp-drugsea"],
|
|
62
59
|
"env": {
|
|
63
|
-
"
|
|
60
|
+
"YAOHAI_MCP_TOKEN": "ysk_your_token_here"
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
63
|
}
|
|
@@ -76,7 +73,7 @@ export YAOHAI_MCP_API_KEY=your_api_key_here
|
|
|
76
73
|
"command": "npx",
|
|
77
74
|
"args": ["-y", "@kinginsun/mcp-drugsea"],
|
|
78
75
|
"env": {
|
|
79
|
-
"
|
|
76
|
+
"YAOHAI_MCP_TOKEN": "ysk_your_token_here"
|
|
80
77
|
}
|
|
81
78
|
}
|
|
82
79
|
}
|
|
@@ -99,7 +96,7 @@ If `total > 20`, summarize in chat (about 5–10 sample rows) instead of pasting
|
|
|
99
96
|
|
|
100
97
|
xlsx export is not implemented in this MCP (v1 returns JSON samples only).
|
|
101
98
|
|
|
102
|
-
`product-cn-*` and `reg-cn-*` list/facet/detail calls use the same
|
|
99
|
+
`product-cn-*` and `reg-cn-*` list/facet/detail calls use the same routes as the website where applicable. Some deployments IP-allowlist direct GET paths. The `yaohai-*` tools use `POST /g/mcp/yaohai/*` with Bearer token auth.
|
|
103
100
|
|
|
104
101
|
## Tools
|
|
105
102
|
|
|
@@ -143,17 +140,17 @@ xlsx export is not implemented in this MCP (v1 returns JSON samples only).
|
|
|
143
140
|
|
|
144
141
|
```bash
|
|
145
142
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
|
|
146
|
-
|
|
|
143
|
+
| YAOHAI_MCP_TOKEN=ysk_your_token_here node dist/index.js
|
|
147
144
|
```
|
|
148
145
|
|
|
149
146
|
```bash
|
|
150
147
|
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"yaohai-catalog","arguments":{"q":"医保"}}}' \
|
|
151
|
-
|
|
|
148
|
+
| YAOHAI_MCP_TOKEN=ysk_your_token_here node dist/index.js
|
|
152
149
|
```
|
|
153
150
|
|
|
154
151
|
```bash
|
|
155
152
|
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"product-cn-search","arguments":{"query":{"drug_name":"阿司匹林"},"limit":3}}}' \
|
|
156
|
-
|
|
|
153
|
+
| YAOHAI_MCP_TOKEN=ysk_your_token_here node dist/index.js
|
|
157
154
|
```
|
|
158
155
|
|
|
159
156
|
## Requirements
|
package/dist/api.js
CHANGED
|
@@ -2,12 +2,35 @@ import http from "node:http";
|
|
|
2
2
|
import https from "node:https";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
4
|
import { normalizeRecord, parseFacetList } from "./normalize.js";
|
|
5
|
-
export class
|
|
6
|
-
constructor() {
|
|
7
|
-
super(
|
|
8
|
-
|
|
5
|
+
export class MissingTokenError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message ??
|
|
8
|
+
"Set YAOHAI_MCP_TOKEN to your personal DrugSea token (ysk_ + 32 hex chars)");
|
|
9
|
+
this.name = "MissingTokenError";
|
|
9
10
|
}
|
|
10
11
|
}
|
|
12
|
+
/** @deprecated Use MissingTokenError */
|
|
13
|
+
export const MissingApiKeyError = MissingTokenError;
|
|
14
|
+
const USER_TOKEN_RE = /^ysk_[0-9a-f]{32}$/i;
|
|
15
|
+
export function getUserToken() {
|
|
16
|
+
const token = process.env.YAOHAI_MCP_TOKEN?.trim() || "";
|
|
17
|
+
if (!token) {
|
|
18
|
+
throw new MissingTokenError();
|
|
19
|
+
}
|
|
20
|
+
if (!USER_TOKEN_RE.test(token)) {
|
|
21
|
+
throw new MissingTokenError("YAOHAI_MCP_TOKEN must be a personal user token: ysk_ + 32 hex chars");
|
|
22
|
+
}
|
|
23
|
+
return token;
|
|
24
|
+
}
|
|
25
|
+
/** @deprecated Use getUserToken */
|
|
26
|
+
export const getApiKey = getUserToken;
|
|
27
|
+
export function getAuthHeaders(extra = {}) {
|
|
28
|
+
const token = getUserToken();
|
|
29
|
+
return {
|
|
30
|
+
...extra,
|
|
31
|
+
Authorization: `Bearer ${token}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
11
34
|
export class ApiError extends Error {
|
|
12
35
|
status;
|
|
13
36
|
raw;
|
|
@@ -18,27 +41,6 @@ export class ApiError extends Error {
|
|
|
18
41
|
this.raw = raw;
|
|
19
42
|
}
|
|
20
43
|
}
|
|
21
|
-
export function getApiKey() {
|
|
22
|
-
const token = process.env.YAOHAI_MCP_TOKEN?.trim() || "";
|
|
23
|
-
const key = process.env.YAOHAI_MCP_API_KEY?.trim() || "";
|
|
24
|
-
const value = token || key;
|
|
25
|
-
if (!value) {
|
|
26
|
-
throw new MissingApiKeyError();
|
|
27
|
-
}
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
/** Shared server key → X-Yaohai-Api-Key; user token ysk_… → Authorization Bearer. */
|
|
31
|
-
export function getAuthHeaders(extra = {}) {
|
|
32
|
-
const credential = getApiKey();
|
|
33
|
-
const headers = { ...extra };
|
|
34
|
-
if (credential.startsWith("ysk_")) {
|
|
35
|
-
headers.Authorization = `Bearer ${credential}`;
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
headers["X-Yaohai-Api-Key"] = credential;
|
|
39
|
-
}
|
|
40
|
-
return headers;
|
|
41
|
-
}
|
|
42
44
|
export function getBaseUrl() {
|
|
43
45
|
return (process.env.YAOHAI_BASE_URL || "https://db3.drugsea.cn/api").replace(/\/$/, "");
|
|
44
46
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
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
|
-
const PACKAGE_VERSION = "0.
|
|
8
|
+
const PACKAGE_VERSION = "0.2.0";
|
|
9
9
|
const YAOHAI_LIMIT_MAX = 50;
|
|
10
10
|
const YAOHAI_LIMIT_DEFAULT = 10;
|
|
11
11
|
const CN_LIMIT_MAX = 100;
|
|
@@ -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
|
|
310
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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.1
|
|
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",
|