@kinginsun/mcp-drugsea 0.3.0 → 0.5.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.
- package/README.md +92 -20
- package/dist/dbs-facets.js +461 -0
- package/dist/index.js +167 -2
- package/dist/types.js +18 -0
- package/dist/update-check.js +235 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ MCP (Model Context Protocol) stdio server for [DrugSea / Yaohai](https://db.drug
|
|
|
4
4
|
|
|
5
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
|
-
- **yaohai-*** — cross-database catalog / search / detail / global (`POST /g/mcp/yaohai
|
|
7
|
+
- **yaohai-*** — cross-database catalog / search / detail / facets / global (`POST /g/mcp/yaohai/*`; facets via GET)
|
|
8
8
|
- **product-cn-*** — already-marketed China products (search/detail via MCP on db3; facets via GET)
|
|
9
9
|
- **reg-cn-*** — CDE registration / review pipeline (search/detail via MCP on db3; facets via GET)
|
|
10
10
|
|
|
@@ -16,12 +16,69 @@ The server forwards tool calls to **`https://db3.drugsea.cn/api`** with personal
|
|
|
16
16
|
npm install -g @kinginsun/mcp-drugsea
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Or run without installing:
|
|
19
|
+
Or run without installing (`@latest` makes npx re-resolve the newest published version each launch instead of pinning to its cache):
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npx -y @kinginsun/mcp-drugsea
|
|
22
|
+
npx -y @kinginsun/mcp-drugsea@latest
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Updating
|
|
26
|
+
|
|
27
|
+
How updates reach you depends on how you installed the server.
|
|
28
|
+
|
|
29
|
+
### 1. `npx` with `@latest` — automatic (recommended)
|
|
30
|
+
|
|
31
|
+
The MCP configs in this README use:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
"args": ["-y", "@kinginsun/mcp-drugsea@latest"]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`@latest` makes npx resolve the newest published version from the registry on every launch (registry metadata is cached locally for up to ~5 minutes), so you pick up new releases just by reloading the MCP server. No action needed.
|
|
38
|
+
|
|
39
|
+
If your config uses the **bare package name** (`"-y", "@kinginsun/mcp-drugsea"`), npx keys its cache by the exact package argument and may keep serving a stale version. Add `@latest` and reload.
|
|
40
|
+
|
|
41
|
+
### 2. Global install — manual
|
|
42
|
+
|
|
43
|
+
`npm install -g` pins the version you installed; nothing updates it automatically.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm update -g @kinginsun/mcp-drugsea
|
|
47
|
+
npm ls -g @kinginsun/mcp-drugsea # confirm the new version
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3. Stuck on an old version? Clear the npx cache
|
|
51
|
+
|
|
52
|
+
`npm cache clean --force` does **not** touch the npx cache — that is the usual reason an update appears not to take effect. npm 11 has dedicated subcommands:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm cache npx ls # list cached npx entries
|
|
56
|
+
npm cache npx rm <key> # remove the stale entry for this package
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Older npm versions: remove the cache directory manually (`rm -rf ~/.npm/_npx`; on Windows `%LOCALAPPDATA%\npm-cache\_npx`).
|
|
60
|
+
|
|
61
|
+
### In-server update notice
|
|
62
|
+
|
|
63
|
+
Every launch performs a **non-blocking** version check against the registry and, when a newer version is published, reports it on two channels:
|
|
64
|
+
|
|
65
|
+
- **stderr** — shown in your MCP client's server log panel;
|
|
66
|
+
- **an MCP `notifications/message` log notification** (`level: warning`).
|
|
67
|
+
|
|
68
|
+
The notice includes the exact commands above. It never blocks startup, never writes to stdout (which carries JSON-RPC), and fails silently when the registry is unreachable — results are cached for 24h so repeated launches do not re-query.
|
|
69
|
+
|
|
70
|
+
Disable it with `YAOHAI_MCP_UPDATE_CHECK=0`. Behind a proxy or using an npm mirror, set `YAOHAI_NPM_REGISTRY` to your registry origin.
|
|
71
|
+
|
|
72
|
+
### Pinning a version
|
|
73
|
+
|
|
74
|
+
Prefer stability over freshness? Pin an exact version or a range instead of `@latest`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
"args": ["-y", "@kinginsun/mcp-drugsea@0.4.0"]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A range such as `@^0.4.0` auto-updates within `0.4.x` only, which avoids picking up breaking changes from a future minor release.
|
|
81
|
+
|
|
25
82
|
## Configuration
|
|
26
83
|
|
|
27
84
|
### `YAOHAI_MCP_TOKEN` (required)
|
|
@@ -61,6 +118,8 @@ On db3, direct GET list routes may return encrypted payloads; this client auto-r
|
|
|
61
118
|
| `YAOHAI_BASE_URL` | `https://db3.drugsea.cn/api` | API origin (no trailing slash). MCP: `/g/mcp/yaohai/*` |
|
|
62
119
|
| `YAOHAI_VERIFY_SSL` | `true` | Set `false` / `0` / `off` to skip TLS certificate verification (needed on some prod hosts) |
|
|
63
120
|
| `YAOHAI_USE_MCP_LIST` | auto on db3 | Force product/reg search via MCP POST instead of GET |
|
|
121
|
+
| `YAOHAI_MCP_UPDATE_CHECK` | enabled | Set `0` / `false` / `off` to disable the startup version check (see [Updating](#updating)) |
|
|
122
|
+
| `YAOHAI_NPM_REGISTRY` | `https://registry.npmjs.org` | Registry used by the version check. Point at a mirror if npmjs.org is unreachable from your network |
|
|
64
123
|
|
|
65
124
|
### Cursor MCP (`~/.cursor/mcp.json`)
|
|
66
125
|
|
|
@@ -69,7 +128,7 @@ On db3, direct GET list routes may return encrypted payloads; this client auto-r
|
|
|
69
128
|
"mcpServers": {
|
|
70
129
|
"drugsea": {
|
|
71
130
|
"command": "npx",
|
|
72
|
-
"args": ["-y", "@kinginsun/mcp-drugsea"],
|
|
131
|
+
"args": ["-y", "@kinginsun/mcp-drugsea@latest"],
|
|
73
132
|
"env": {
|
|
74
133
|
"YAOHAI_MCP_TOKEN": "ysk_your_token_here"
|
|
75
134
|
}
|
|
@@ -85,7 +144,7 @@ On db3, direct GET list routes may return encrypted payloads; this client auto-r
|
|
|
85
144
|
"mcpServers": {
|
|
86
145
|
"drugsea": {
|
|
87
146
|
"command": "npx",
|
|
88
|
-
"args": ["-y", "@kinginsun/mcp-drugsea"],
|
|
147
|
+
"args": ["-y", "@kinginsun/mcp-drugsea@latest"],
|
|
89
148
|
"env": {
|
|
90
149
|
"YAOHAI_MCP_TOKEN": "ysk_your_token_here"
|
|
91
150
|
}
|
|
@@ -100,7 +159,7 @@ On db3, direct GET list routes may return encrypted payloads; this client auto-r
|
|
|
100
159
|
|-------------|--------|
|
|
101
160
|
| Already listed in China (国药准字, 批准文号, 上市, 医保/集采状态) | `product-cn-fields` → `product-cn-search` / `product-cn-facets` → `product-cn-detail` |
|
|
102
161
|
| R&D / CDE (在研, 受理号, 审评, 尚未上市) | `reg-cn-fields` → `reg-cn-search` / `reg-cn-facets` → `reg-cn-detail` |
|
|
103
|
-
| Other DBs (医保 `yibao`, 基药 `jiyao`, 集采 `jicai`, trials, DMF, …) | `yaohai-catalog` → `yaohai-search` → `yaohai-detail` |
|
|
162
|
+
| Other DBs (医保 `yibao`, 基药 `jiyao`, 集采 `jicai`, trials, DMF, …) | `yaohai-catalog` → `yaohai-search` / `yaohai-facets` → `yaohai-detail` |
|
|
104
163
|
| Global panorama | `yaohai-global-search` |
|
|
105
164
|
| Unclear which DB | `yaohai-catalog` (list DBs by keyword/category) → `yaohai-search`, or `yaohai-global-search` |
|
|
106
165
|
|
|
@@ -121,10 +180,18 @@ xlsx export is not implemented in this MCP (v1 returns JSON samples only).
|
|
|
121
180
|
| `yaohai-catalog` | `category?`, `q?` | List databases |
|
|
122
181
|
| `yaohai-search` | `dbname`, `query?`, `limit?`, `offset?` | Default limit 10, max 50 |
|
|
123
182
|
| `yaohai-detail` | `dbname`, `id` | Skip DBs with `has_detail: false` |
|
|
183
|
+
| `yaohai-facets` | `dbname?`, `query?`, `fields?` | Facets for `dbs`-route DBs. Omit `fields` → discover facet-capable DBs/fields; pass `fields` → fetch buckets |
|
|
124
184
|
| `yaohai-global-search` | `q?`, `query?`, `limit?`, `offset?` | `q` fills `query.term` |
|
|
125
185
|
|
|
126
186
|
When the target database is unclear, use `yaohai-catalog` (filter by `category` / `q`) to pick a `dbname`, then `yaohai-search`; or use `yaohai-global-search` for a cross-database panorama query.
|
|
127
187
|
|
|
188
|
+
`yaohai-facets` mirrors the ConditionSearch facet filters of the website for the `dbs`-route databases (医保 `yibao`, 基药 `jiyao`, 集采 `jicai`, sales `drugsales`, …). Two modes:
|
|
189
|
+
|
|
190
|
+
- **Discovery** (no `fields`): omit `dbname` to list all 44 facet-capable databases, or pass `dbname` to list its facet-able fields (with `filter_type`).
|
|
191
|
+
- **Fetch** (`dbname` + `fields`): returns aggregation buckets (`value`/`count`) for the named terms fields, optionally narrowed by `query`. Only `terms`-type fields are exposed (44 DBs, 129 fields); date/range/tree filters are not faceted here.
|
|
192
|
+
|
|
193
|
+
For the two dedicated ES routes use `product-cn-facets` / `reg-cn-facets` instead — `yaohai-facets` returns an actionable hint if you pass `product_cn` / `reg_cn`.
|
|
194
|
+
|
|
128
195
|
### product_cn (marketed)
|
|
129
196
|
|
|
130
197
|
| Tool | Parameters | Notes |
|
|
@@ -170,7 +237,7 @@ Add the server to your client config so it auto-starts. Example for Cursor (`~/.
|
|
|
170
237
|
"mcpServers": {
|
|
171
238
|
"drugsea": {
|
|
172
239
|
"command": "npx",
|
|
173
|
-
"args": ["-y", "@kinginsun/mcp-drugsea"],
|
|
240
|
+
"args": ["-y", "@kinginsun/mcp-drugsea@latest"],
|
|
174
241
|
"env": {
|
|
175
242
|
"YAOHAI_MCP_TOKEN": "ysk_your_token_here"
|
|
176
243
|
}
|
|
@@ -179,7 +246,7 @@ Add the server to your client config so it auto-starts. Example for Cursor (`~/.
|
|
|
179
246
|
}
|
|
180
247
|
```
|
|
181
248
|
|
|
182
|
-
Then reload MCP servers in the client (Cursor: Settings → MCP → refresh). The client should list **
|
|
249
|
+
Then reload MCP servers in the client (Cursor: Settings → MCP → refresh). The client should list **13 tools**.
|
|
183
250
|
|
|
184
251
|
### Step 1 — Install (optional, for local/CLI use)
|
|
185
252
|
|
|
@@ -187,7 +254,7 @@ Either run via npx on demand (no install needed), or install globally / from sou
|
|
|
187
254
|
|
|
188
255
|
```bash
|
|
189
256
|
# Option A: run without installing (what the MCP configs above do)
|
|
190
|
-
npx -y @kinginsun/mcp-drugsea
|
|
257
|
+
npx -y @kinginsun/mcp-drugsea@latest
|
|
191
258
|
|
|
192
259
|
# Option B: global install
|
|
193
260
|
npm install -g @kinginsun/mcp-drugsea
|
|
@@ -221,17 +288,19 @@ printf '%s\n' \
|
|
|
221
288
|
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"1.0"}}}' \
|
|
222
289
|
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
|
223
290
|
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
|
|
224
|
-
| YAOHAI_MCP_TOKEN=$YAOHAI_MCP_TOKEN npx -y @kinginsun/mcp-drugsea \
|
|
225
|
-
|
|
|
291
|
+
| YAOHAI_MCP_TOKEN=$YAOHAI_MCP_TOKEN npx -y @kinginsun/mcp-drugsea@latest \
|
|
292
|
+
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{for(const l of d.split('\n')){if(!l.trim())continue;let m;try{m=JSON.parse(l)}catch{continue}if(m.id===2)console.log('tools:',m.result.tools.length)}})"
|
|
226
293
|
```
|
|
227
294
|
|
|
228
|
-
Expected: `tools:
|
|
295
|
+
Expected: `tools: 13`.
|
|
296
|
+
|
|
297
|
+
The filter selects the response with `"id":2` rather than piping through `tail -1`, because the server may also emit a `notifications/message` (version-update notice) on stdout. Both are valid JSON-RPC, but only one is the answer you asked for.
|
|
229
298
|
|
|
230
299
|
One-liner variant without the handshake (also works with this server):
|
|
231
300
|
|
|
232
301
|
```bash
|
|
233
302
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
|
|
234
|
-
| YAOHAI_MCP_TOKEN=ysk_your_token_here npx -y @kinginsun/mcp-drugsea
|
|
303
|
+
| YAOHAI_MCP_TOKEN=ysk_your_token_here npx -y @kinginsun/mcp-drugsea@latest
|
|
235
304
|
```
|
|
236
305
|
|
|
237
306
|
### Step 4 — Test real tool calls
|
|
@@ -239,20 +308,20 @@ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
|
|
|
239
308
|
```bash
|
|
240
309
|
# Catalog lookup (no external DB data needed)
|
|
241
310
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"yaohai-catalog","arguments":{"q":"医保"}}}' \
|
|
242
|
-
| YAOHAI_MCP_TOKEN=ysk_your_token_here npx -y @kinginsun/mcp-drugsea
|
|
311
|
+
| YAOHAI_MCP_TOKEN=ysk_your_token_here npx -y @kinginsun/mcp-drugsea@latest
|
|
243
312
|
|
|
244
313
|
# China marketed products search
|
|
245
314
|
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"product-cn-search","arguments":{"query":{"drug_name":"阿司匹林"},"limit":3}}}' \
|
|
246
|
-
| YAOHAI_MCP_TOKEN=ysk_your_token_here npx -y @kinginsun/mcp-drugsea
|
|
315
|
+
| YAOHAI_MCP_TOKEN=ysk_your_token_here npx -y @kinginsun/mcp-drugsea@latest
|
|
247
316
|
|
|
248
317
|
# Global panorama search
|
|
249
318
|
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"yaohai-global-search","arguments":{"q":"阿司匹林","limit":3}}}' \
|
|
250
|
-
| YAOHAI_MCP_TOKEN=ysk_your_token_here npx -y @kinginsun/mcp-drugsea
|
|
319
|
+
| YAOHAI_MCP_TOKEN=ysk_your_token_here npx -y @kinginsun/mcp-drugsea@latest
|
|
251
320
|
```
|
|
252
321
|
|
|
253
322
|
Expected: each response has `"isError":false` and non-empty `content`.
|
|
254
323
|
|
|
255
|
-
### Step 5 — Full
|
|
324
|
+
### Step 5 — Full 13-tool suite (from source)
|
|
256
325
|
|
|
257
326
|
```bash
|
|
258
327
|
git clone https://github.com/kinginsun/mcp-drugsea.git
|
|
@@ -261,13 +330,13 @@ npm install && npm run build
|
|
|
261
330
|
YAOHAI_MCP_TOKEN=ysk_your_token_here node scripts/test-all-tools.mjs
|
|
262
331
|
```
|
|
263
332
|
|
|
264
|
-
Expected final line: `--- Summary:
|
|
333
|
+
Expected final line: `--- Summary: 14 passed, 0 failed / 14 tool calls ---`.
|
|
265
334
|
|
|
266
335
|
### Step 6 — Verify inside the MCP client
|
|
267
336
|
|
|
268
337
|
After reloading MCP servers in the client, ask the agent:
|
|
269
338
|
|
|
270
|
-
1. "List the drugsea tools" → should see
|
|
339
|
+
1. "List the drugsea tools" → should see 13 tools.
|
|
271
340
|
2. "Search 阿司匹林 in product-cn" → should return rows with `total > 0`.
|
|
272
341
|
3. "Global search: PD-1" → should return panorama results without error.
|
|
273
342
|
|
|
@@ -282,6 +351,9 @@ After reloading MCP servers in the client, ask the agent:
|
|
|
282
351
|
| `mcp-drugsea: command not found` when running npx | You are inside the package source dir — run from another directory or use `node dist/index.js` |
|
|
283
352
|
| Empty/encrypted payload from product/reg GET | Use default db3 base URL (auto MCP POST routing) or set `YAOHAI_USE_MCP_LIST=true` |
|
|
284
353
|
| TLS errors on some hosts | Set `YAOHAI_VERIFY_SSL=false` |
|
|
354
|
+
| Client keeps running an old version | npx cache is keyed by the exact package arg — use `@latest` in `args`, or clear it with `npm cache npx ls` / `npm cache npx rm <key>`. See [Updating](#updating). |
|
|
355
|
+
| Global install never updates | `npm install -g` pins the version — run `npm update -g @kinginsun/mcp-drugsea` |
|
|
356
|
+
| No update notice appears | Expected when you are already current, when the registry is unreachable (check fails silently), or when `YAOHAI_MCP_UPDATE_CHECK=0`. Clients also need the server's `logging` capability, so check the MCP server log panel for the stderr line as well. |
|
|
285
357
|
|
|
286
358
|
## Manual stdio test
|
|
287
359
|
|
|
@@ -302,7 +374,7 @@ echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"product-cn
|
|
|
302
374
|
|
|
303
375
|
## Releasing (maintainers)
|
|
304
376
|
|
|
305
|
-
`publish.sh` releases the package to npm (which is what makes `npx -y @kinginsun/mcp-drugsea` work). It syncs `src/index.ts`'s `PACKAGE_VERSION` with `package.json`, builds clean, audits the tarball for leaked tokens, runs the
|
|
377
|
+
`publish.sh` releases the package to npm (which is what makes `npx -y @kinginsun/mcp-drugsea@latest` work). It syncs `src/index.ts`'s `PACKAGE_VERSION` with `package.json`, builds clean, audits the tarball for leaked tokens, runs the 13-tool suite plus the facet and update-check suites, commits + tags, then publishes and pushes.
|
|
306
378
|
|
|
307
379
|
```bash
|
|
308
380
|
npm login # once, with rights on the @kinginsun scope
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facet (条件筛选) catalog for dbs-route databases — GENERATED FILE.
|
|
3
|
+
*
|
|
4
|
+
* Source of truth: drugsea frontend
|
|
5
|
+
* frontend/src/routes/more/DBS/components/commonSearch/ConditionSearchPanel.js
|
|
6
|
+
* which hardcodes the per-db condition filters the web UI renders.
|
|
7
|
+
*
|
|
8
|
+
* Regenerate (do not hand-edit):
|
|
9
|
+
* node scripts/extract-dbs-facets.mjs --emit-ts
|
|
10
|
+
*
|
|
11
|
+
* Scope: `terms` fields only — the ones that return aggregated bucket lists
|
|
12
|
+
* (`content.list[]` with `ct` counts). `date`/`range`/`tree` fields are UI
|
|
13
|
+
* pickers and are excluded, which also drops the two 器械备案 dbs entirely.
|
|
14
|
+
*
|
|
15
|
+
* Note on prefixes: taken verbatim from the frontend URL. They currently equal
|
|
16
|
+
* the catalog `api_path` for all 44 dbs, but the frontend is
|
|
17
|
+
* authoritative — `reg_cn` already demonstrates that a facet prefix can diverge
|
|
18
|
+
* from api_path.
|
|
19
|
+
*/
|
|
20
|
+
export const DBS_FACET_CATALOG = {
|
|
21
|
+
cde_yfb_registration: {
|
|
22
|
+
title: "原料药、药用辅料和药包材登记信息公示",
|
|
23
|
+
category: "注册情报",
|
|
24
|
+
prefix: "/c/cde_yfb_registration/eslist",
|
|
25
|
+
fields: {
|
|
26
|
+
reg_num_year: { title: "批准年份", filter_type: "multiple" },
|
|
27
|
+
local_or_import: { title: "产品来源", filter_type: "multiple" },
|
|
28
|
+
yfb_type: { title: "原辅包类型", filter_type: "multiple" },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
cmchk_pcm: {
|
|
32
|
+
title: "香港注册中成药",
|
|
33
|
+
category: "上市情报",
|
|
34
|
+
prefix: "/cmchk_pcm/eslist",
|
|
35
|
+
fields: {
|
|
36
|
+
formula_type: { title: "注册类型", filter_type: "multiple" },
|
|
37
|
+
package_label_cn: { title: "药材组成", filter_type: "multiple" },
|
|
38
|
+
pmpw_flag: { title: "PMPW标记", filter_type: "multiple" },
|
|
39
|
+
dosage_form_cn: { title: "剂型", filter_type: "multiple" },
|
|
40
|
+
is_export: { title: "是否出口", filter_type: "multiple" },
|
|
41
|
+
is_coexist_cp: { title: "是否药典收录", filter_type: "multiple" },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
cn_orange_book: {
|
|
45
|
+
title: "中国上市化学药品目录集",
|
|
46
|
+
category: "药政参考",
|
|
47
|
+
prefix: "/c/cn_orange_book/eslist",
|
|
48
|
+
fields: {
|
|
49
|
+
dosage_form: { title: "剂型", filter_type: "multiple" },
|
|
50
|
+
administration_route: { title: "给药途径", filter_type: "multiple" },
|
|
51
|
+
is_reference_drug: { title: "参比制剂", filter_type: "multiple" },
|
|
52
|
+
is_standard_drug: { title: "标准制剂", filter_type: "multiple" },
|
|
53
|
+
category: { title: "收录类别", filter_type: "multiple" },
|
|
54
|
+
marketing_status: { title: "上市销售状态", filter_type: "multiple" },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
cn_reference_drugs: {
|
|
58
|
+
title: "仿制药参比制剂目录",
|
|
59
|
+
category: "药政参考",
|
|
60
|
+
prefix: "/c/cn_reference_drugs/eslist",
|
|
61
|
+
fields: {
|
|
62
|
+
dosage_form: { title: "药品剂型", filter_type: "multiple" },
|
|
63
|
+
batch_no: { title: "公布批次", filter_type: "multiple" },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
cn_reference_drugs_publicity: {
|
|
67
|
+
title: "仿制药参比制剂目录(征求意见稿)",
|
|
68
|
+
category: "药政参考",
|
|
69
|
+
prefix: "/c/cn_reference_drugs_publicity/eslist",
|
|
70
|
+
fields: {
|
|
71
|
+
publicity_type: { title: "公示类型", filter_type: "multiple" },
|
|
72
|
+
dosage_form: { title: "药品剂型", filter_type: "multiple" },
|
|
73
|
+
batch_no: { title: "公布批次", filter_type: "multiple" },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
dpd: {
|
|
77
|
+
title: "加拿大上市药品",
|
|
78
|
+
category: "上市情报",
|
|
79
|
+
prefix: "/dpd/eslist",
|
|
80
|
+
fields: {
|
|
81
|
+
dosage_form: { title: "剂型", filter_type: "multiple" },
|
|
82
|
+
current_status: { title: "最新状态", filter_type: "multiple" },
|
|
83
|
+
route_of_administration: { title: "给药途径", filter_type: "multiple" },
|
|
84
|
+
class: { title: "适用对象", filter_type: "multiple" },
|
|
85
|
+
schedule: { title: "药品类别", filter_type: "multiple" },
|
|
86
|
+
has_sms: { title: "是否有说明书", filter_type: "multiple" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
drug_law: {
|
|
90
|
+
title: "药品法规知识库",
|
|
91
|
+
category: "药闻速递",
|
|
92
|
+
prefix: "/h/drug_law/eslist",
|
|
93
|
+
fields: {
|
|
94
|
+
source: { title: "法规来源", filter_type: "multiple" },
|
|
95
|
+
main_category: { title: "一级分类", filter_type: "multiple" },
|
|
96
|
+
category: { title: "公告栏目", filter_type: "multiple" },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
drugsales: {
|
|
100
|
+
title: "全终端药品销售",
|
|
101
|
+
category: "市场情报",
|
|
102
|
+
prefix: "/drugsales/eslist",
|
|
103
|
+
defaultQuery: { "groupid": "205" },
|
|
104
|
+
fields: {
|
|
105
|
+
region: { title: "区域", filter_type: "multiple" },
|
|
106
|
+
country: { title: "国家", filter_type: "multiple" },
|
|
107
|
+
dosage_form: { title: "剂型", filter_type: "multiple" },
|
|
108
|
+
year: { title: "年份", filter_type: "multiple" },
|
|
109
|
+
sales_channel: { title: "销售渠道", filter_type: "multiple" },
|
|
110
|
+
drug_type: { title: "药品类型", filter_type: "multiple" },
|
|
111
|
+
administration_route: { title: "给药途径", filter_type: "multiple" },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
fda_dmf: {
|
|
115
|
+
title: "美国DMF数据库",
|
|
116
|
+
category: "上市情报",
|
|
117
|
+
prefix: "/fda_dmf/eslist",
|
|
118
|
+
fields: {
|
|
119
|
+
status: { title: "DMF状态", filter_type: "multiple" },
|
|
120
|
+
type: { title: "DMF类型", filter_type: "multiple" },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
fda_ndc: {
|
|
124
|
+
title: "美国药品NDC数据库",
|
|
125
|
+
category: "上市情报",
|
|
126
|
+
prefix: "/fda_ndc/eslist",
|
|
127
|
+
fields: {
|
|
128
|
+
has_sms: { title: "是否有说明书", filter_type: "multiple" },
|
|
129
|
+
MARKETINGCATEGORYNAME: { title: "市场分类", filter_type: "multiple" },
|
|
130
|
+
PRODUCTTYPENAME: { title: "产品类型", filter_type: "multiple" },
|
|
131
|
+
DOSAGEFORMNAME: { title: "药品剂型", filter_type: "multiple" },
|
|
132
|
+
ROUTENAME: { title: "给药途径", filter_type: "multiple" },
|
|
133
|
+
NDC_EXCLUDE_FLAG: { title: "NDC排除标记", filter_type: "multiple" },
|
|
134
|
+
PHARM_CLASSES: { title: "药理分类", filter_type: "multiple" },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
gov_drugs: {
|
|
138
|
+
title: "政府用药目录",
|
|
139
|
+
category: "药政参考",
|
|
140
|
+
prefix: "/c/gov_drugs/eslist",
|
|
141
|
+
fields: {
|
|
142
|
+
province: { title: "省份", filter_type: "multiple" },
|
|
143
|
+
drug_type: { title: "药品类型", filter_type: "multiple" },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
herb_formulas: {
|
|
147
|
+
title: "中药方剂数据库",
|
|
148
|
+
category: "行业参考",
|
|
149
|
+
prefix: "/herb_formulas/eslist",
|
|
150
|
+
fields: {
|
|
151
|
+
herbs: { title: "组成药材", filter_type: "multiple" },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
herbs: {
|
|
155
|
+
title: "中药材数据库",
|
|
156
|
+
category: "行业参考",
|
|
157
|
+
prefix: "/herbs/eslist",
|
|
158
|
+
fields: {
|
|
159
|
+
efficacy_class: { title: "功效分类", filter_type: "multiple" },
|
|
160
|
+
family_classification: { title: "科属分类", filter_type: "multiple" },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
hk_doh: {
|
|
164
|
+
title: "香港上市药品",
|
|
165
|
+
category: "上市情报",
|
|
166
|
+
prefix: "/hk_doh/eslist",
|
|
167
|
+
fields: {
|
|
168
|
+
legal_classification: { title: "法律分类", filter_type: "multiple" },
|
|
169
|
+
sale_requirement: { title: "销售要求", filter_type: "multiple" },
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
hma: {
|
|
173
|
+
title: "欧盟HMA上市药品",
|
|
174
|
+
category: "上市情报",
|
|
175
|
+
prefix: "/hma/eslist",
|
|
176
|
+
fields: {
|
|
177
|
+
Ph_form: { title: "药品剂型", filter_type: "multiple" },
|
|
178
|
+
RMS: { title: "参考成员国", filter_type: "multiple" },
|
|
179
|
+
product_outcome: { title: "市场状态", filter_type: "multiple" },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
isaf_drugs: {
|
|
183
|
+
title: "澳门上市药品",
|
|
184
|
+
category: "上市情报",
|
|
185
|
+
prefix: "/isaf_drugs/eslist",
|
|
186
|
+
fields: {
|
|
187
|
+
dosage_form: { title: "剂型", filter_type: "multiple" },
|
|
188
|
+
administration_route: { title: "给药途径", filter_type: "multiple" },
|
|
189
|
+
ingredients: { title: "活性成分", filter_type: "multiple" },
|
|
190
|
+
type: { title: "法定类别", filter_type: "multiple" },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
isaf_tcm: {
|
|
194
|
+
title: "澳门中成药与天然药物",
|
|
195
|
+
category: "上市情报",
|
|
196
|
+
prefix: "/isaf_tcm/eslist",
|
|
197
|
+
fields: {
|
|
198
|
+
dosage_form: { title: "剂型", filter_type: "multiple" },
|
|
199
|
+
administration_route: { title: "给药途径", filter_type: "multiple" },
|
|
200
|
+
formula: { title: "配方", filter_type: "multiple" },
|
|
201
|
+
type: { title: "法定类别", filter_type: "multiple" },
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
japan_dmf: {
|
|
205
|
+
title: "日本DMF数据库",
|
|
206
|
+
category: "上市情报",
|
|
207
|
+
prefix: "/japan_dmf/eslist",
|
|
208
|
+
fields: {
|
|
209
|
+
registration_type_cn: { title: "注册类型", filter_type: "multiple" },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
jicai: {
|
|
213
|
+
title: "国家与地方集采数据库",
|
|
214
|
+
category: "市场情报",
|
|
215
|
+
prefix: "/jicai/eslist",
|
|
216
|
+
fields: {
|
|
217
|
+
jc_type: { title: "集采类型", filter_type: "multiple" },
|
|
218
|
+
jc_project: { title: "集采项目", filter_type: "multiple" },
|
|
219
|
+
region: { title: "中选区域", filter_type: "multiple" },
|
|
220
|
+
execution_status: { title: "执行状态", filter_type: "multiple" },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
jicai_mulu: {
|
|
224
|
+
title: "jicai_mulu",
|
|
225
|
+
category: "其他",
|
|
226
|
+
prefix: "/jicai_mulu/eslist",
|
|
227
|
+
fields: {
|
|
228
|
+
jc_type: { title: "集采类型", filter_type: "multiple" },
|
|
229
|
+
jc_project: { title: "集采项目", filter_type: "multiple" },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
jiyao: {
|
|
233
|
+
title: "基药目录",
|
|
234
|
+
category: "市场准入",
|
|
235
|
+
prefix: "/jiyao/eslist",
|
|
236
|
+
fields: {
|
|
237
|
+
province: { title: "地区", filter_type: "multiple" },
|
|
238
|
+
dosage_form: { title: "剂型", filter_type: "multiple" },
|
|
239
|
+
drug_type: { title: "药品类型", filter_type: "multiple" },
|
|
240
|
+
std_catalog_version: { title: "基药版本", filter_type: "multiple" },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
medical_device: {
|
|
244
|
+
title: "国产器械(注册)",
|
|
245
|
+
category: "NMPA基础库",
|
|
246
|
+
prefix: "/medical_device/eslist",
|
|
247
|
+
fields: {
|
|
248
|
+
std_product_type: { title: "管理类别", filter_type: "multiple" },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
medical_device_jinkou: {
|
|
252
|
+
title: "进口器械(注册)",
|
|
253
|
+
category: "NMPA基础库",
|
|
254
|
+
prefix: "/medical_device_jinkou/eslist",
|
|
255
|
+
fields: {
|
|
256
|
+
std_product_type: { title: "管理类别", filter_type: "multiple" },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
nhsa_code: {
|
|
260
|
+
title: "医保药品分类与代码",
|
|
261
|
+
category: "市场准入",
|
|
262
|
+
prefix: "/c/nhsa_code/eslist",
|
|
263
|
+
fields: {
|
|
264
|
+
insurance_type: { title: "医保类型", filter_type: "multiple" },
|
|
265
|
+
registered_dosage_form: { title: "注册剂型", filter_type: "multiple" },
|
|
266
|
+
min_preparation_unit: { title: "最小制剂单位", filter_type: "multiple" },
|
|
267
|
+
min_package_unit: { title: "最小包装单位", filter_type: "multiple" },
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
nhsa_herbs: {
|
|
271
|
+
title: "中药饮片信息",
|
|
272
|
+
category: "市场准入",
|
|
273
|
+
prefix: "/c/nhsa_herbs/eslist",
|
|
274
|
+
fields: {
|
|
275
|
+
efficacy_classification: { title: "功效分类", filter_type: "multiple" },
|
|
276
|
+
region_name: { title: "地区", filter_type: "multiple" },
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
nhsa_hospital_prepration: {
|
|
280
|
+
title: "医疗机构制剂信息",
|
|
281
|
+
category: "市场准入",
|
|
282
|
+
prefix: "/c/nhsa_hospital_prepration/eslist",
|
|
283
|
+
fields: {
|
|
284
|
+
preparation_type: { title: "制剂类别", filter_type: "multiple" },
|
|
285
|
+
region: { title: "地区", filter_type: "multiple" },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
nmpa_buchongbeian: {
|
|
289
|
+
title: "境内生产药品备案信息公示",
|
|
290
|
+
category: "NMPA基础库",
|
|
291
|
+
prefix: "/nmpa_buchongbeian/eslist",
|
|
292
|
+
fields: {
|
|
293
|
+
dosage_form: { title: "剂型", filter_type: "multiple" },
|
|
294
|
+
drug_type: { title: "药品类型", filter_type: "multiple" },
|
|
295
|
+
record_office: { title: "备案机关", filter_type: "multiple" },
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
nmpa_gmp: {
|
|
299
|
+
title: "GMP认证",
|
|
300
|
+
category: "NMPA基础库",
|
|
301
|
+
prefix: "/nmpa_gmp/eslist",
|
|
302
|
+
fields: {
|
|
303
|
+
std_province: { title: "省市", filter_type: "multiple" },
|
|
304
|
+
std_gmp_status: { title: "证书状态", filter_type: "multiple" },
|
|
305
|
+
in_sfda: { title: "是否有效", filter_type: "multiple" },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
nmpa_guochan: {
|
|
309
|
+
title: "国产药品",
|
|
310
|
+
category: "NMPA基础库",
|
|
311
|
+
prefix: "/f/nmpa_guochan/eslist",
|
|
312
|
+
fields: {
|
|
313
|
+
dosage_form: { title: "药品剂型", filter_type: "multiple" },
|
|
314
|
+
drug_type: { title: "产品类别", filter_type: "multiple" },
|
|
315
|
+
in_sfda: { title: "是否有效", filter_type: "multiple" },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
nmpa_jinkou: {
|
|
319
|
+
title: "进口药品",
|
|
320
|
+
category: "NMPA基础库",
|
|
321
|
+
prefix: "/f/nmpa_jinkou/eslist",
|
|
322
|
+
fields: {
|
|
323
|
+
dosage_form: { title: "药品剂型", filter_type: "multiple" },
|
|
324
|
+
drug_type: { title: "产品类别", filter_type: "multiple" },
|
|
325
|
+
in_sfda: { title: "是否有效", filter_type: "multiple" },
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
nmpa_reg_patent: {
|
|
329
|
+
title: "药品注册相关专利信息",
|
|
330
|
+
category: "NMPA基础库",
|
|
331
|
+
prefix: "/f/nmpa_reg_patent/eslist",
|
|
332
|
+
fields: {
|
|
333
|
+
patent_type: { title: "专利类型", filter_type: "multiple" },
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
nmpa_tcm_granules: {
|
|
337
|
+
title: "中药配方颗粒备案信息",
|
|
338
|
+
category: "NMPA基础库",
|
|
339
|
+
prefix: "/f/nmpa_tcm_granules/eslist",
|
|
340
|
+
fields: {
|
|
341
|
+
filing_status: { title: "备案状态", filter_type: "multiple" },
|
|
342
|
+
record_province: { title: "备案省局", filter_type: "multiple" },
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
nmpa_tcm_protection: {
|
|
346
|
+
title: "中药保护品种",
|
|
347
|
+
category: "NMPA基础库",
|
|
348
|
+
prefix: "/nmpa_tcm_protection/eslist",
|
|
349
|
+
fields: {
|
|
350
|
+
dosage_form: { title: "剂型", filter_type: "multiple" },
|
|
351
|
+
protect_period: { title: "保护期限", filter_type: "multiple" },
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
nmpa_tsspxx_gc: {
|
|
355
|
+
title: "国产保健食品注册",
|
|
356
|
+
category: "NMPA基础库",
|
|
357
|
+
prefix: "/f/nmpa_tsspxx_gc/eslist",
|
|
358
|
+
fields: {
|
|
359
|
+
in_sfda: { title: "是否有效", filter_type: "multiple" },
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
nmpa_tsspxx_jk: {
|
|
363
|
+
title: "进口保健食品注册",
|
|
364
|
+
category: "NMPA基础库",
|
|
365
|
+
prefix: "/f/nmpa_tsspxx_jk/eslist",
|
|
366
|
+
fields: {
|
|
367
|
+
in_sfda: { title: "是否有效", filter_type: "multiple" },
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
se_notice: {
|
|
371
|
+
title: "医药上市公司公告",
|
|
372
|
+
category: "药闻速递",
|
|
373
|
+
prefix: "/h/se_notice/eslist",
|
|
374
|
+
fields: {
|
|
375
|
+
se: { title: "公告来源", filter_type: "multiple" },
|
|
376
|
+
is_transferred_to_references: { title: "文献标记", filter_type: "multiple" },
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
targets: {
|
|
380
|
+
title: "药物靶点数据库",
|
|
381
|
+
category: "行业参考",
|
|
382
|
+
prefix: "/targets/eslist",
|
|
383
|
+
fields: {
|
|
384
|
+
target_type: { title: "靶点类型", filter_type: "multiple" },
|
|
385
|
+
kind: { title: "种类", filter_type: "multiple" },
|
|
386
|
+
organism: { title: "生物体", filter_type: "multiple" },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
tw_fda: {
|
|
390
|
+
title: "台湾上市药品",
|
|
391
|
+
category: "上市情报",
|
|
392
|
+
prefix: "/tw_fda/eslist",
|
|
393
|
+
fields: {
|
|
394
|
+
mixture: { title: "单复方", filter_type: "multiple" },
|
|
395
|
+
drug_clasify_code: { title: "药品分类", filter_type: "multiple" },
|
|
396
|
+
lblLicknd: { title: "许可证种类", filter_type: "multiple" },
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
uk_emc: {
|
|
400
|
+
title: "英国上市药品",
|
|
401
|
+
category: "上市情报",
|
|
402
|
+
prefix: "/uk_emc/eslist",
|
|
403
|
+
fields: {
|
|
404
|
+
drug_type: { title: "药品类型", filter_type: "multiple" },
|
|
405
|
+
legal_category: { title: "法律类别", filter_type: "multiple" },
|
|
406
|
+
ATC_code: { title: "治疗领域", filter_type: "multiple" },
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
yibao: {
|
|
410
|
+
title: "医保目录",
|
|
411
|
+
category: "市场准入",
|
|
412
|
+
prefix: "/yibao/eslist",
|
|
413
|
+
fields: {
|
|
414
|
+
province: { title: "医保地区", filter_type: "multiple" },
|
|
415
|
+
drug_type: { title: "药品类型", filter_type: "multiple" },
|
|
416
|
+
insurance_level: { title: "医保类型", filter_type: "multiple" },
|
|
417
|
+
std_catalog_version: { title: "医保版本", filter_type: "multiple" },
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
yzpj_products: {
|
|
421
|
+
title: "一致性评价产品",
|
|
422
|
+
category: "注册情报",
|
|
423
|
+
prefix: "/b/yzpj_products/list",
|
|
424
|
+
fields: {
|
|
425
|
+
latest_status: { title: "最高进展", filter_type: "multiple" },
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
zb_news: {
|
|
429
|
+
title: "全国招标动态",
|
|
430
|
+
category: "药闻速递",
|
|
431
|
+
prefix: "/c/zb_news/eslist",
|
|
432
|
+
fields: {
|
|
433
|
+
city: { title: "省份", filter_type: "multiple" },
|
|
434
|
+
website: { title: "网站", filter_type: "multiple" },
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
zldj: {
|
|
438
|
+
title: "药品专利信息公示",
|
|
439
|
+
category: "行业参考",
|
|
440
|
+
prefix: "/c/zldj/eslist",
|
|
441
|
+
fields: {
|
|
442
|
+
registration_status: { title: "登记状态", filter_type: "multiple" },
|
|
443
|
+
form_type: { title: "登记表类型", filter_type: "multiple" },
|
|
444
|
+
is_public: { title: "专利信息公开", filter_type: "multiple" },
|
|
445
|
+
drug_type: { title: "药品类型", filter_type: "multiple" },
|
|
446
|
+
dosage_form: { title: "药品剂型", filter_type: "multiple" },
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
zlsm: {
|
|
450
|
+
title: "药品专利声明",
|
|
451
|
+
category: "行业参考",
|
|
452
|
+
prefix: "/c/zlsm/eslist",
|
|
453
|
+
fields: {
|
|
454
|
+
drug_type: { title: "药品类型", filter_type: "multiple" },
|
|
455
|
+
dosage_form: { title: "药品剂型", filter_type: "multiple" },
|
|
456
|
+
register_type: { title: "注册分类", filter_type: "multiple" },
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
/** Databases that expose facets, sorted. Handy for validation and tool docs. */
|
|
461
|
+
export const DBS_FACET_DBNAME_LIST = Object.keys(DBS_FACET_CATALOG).sort();
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { 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
|
-
import { EmptyObjectSchema, ProductCnDetailSchema, ProductCnFacetsSchema, ProductCnSearchSchema, RegCnDetailSchema, RegCnFacetsSchema, RegCnSearchSchema, YaohaiCatalogSchema, YaohaiDetailSchema, YaohaiGlobalSearchSchema, YaohaiSearchSchema, } from "./types.js";
|
|
8
|
-
|
|
7
|
+
import { EmptyObjectSchema, ProductCnDetailSchema, ProductCnFacetsSchema, ProductCnSearchSchema, RegCnDetailSchema, RegCnFacetsSchema, RegCnSearchSchema, YaohaiCatalogSchema, YaohaiDetailSchema, YaohaiFacetsSchema, YaohaiGlobalSearchSchema, YaohaiSearchSchema, } from "./types.js";
|
|
8
|
+
import { DBS_FACET_CATALOG } from "./dbs-facets.js";
|
|
9
|
+
import { checkForUpdate, formatUpdateMessage, } from "./update-check.js";
|
|
10
|
+
const PACKAGE_VERSION = "0.5.0";
|
|
9
11
|
const YAOHAI_LIMIT_MAX = 50;
|
|
10
12
|
const YAOHAI_LIMIT_DEFAULT = 10;
|
|
11
13
|
const CN_LIMIT_MAX = 100;
|
|
@@ -17,6 +19,17 @@ const QUERY_PROP = {
|
|
|
17
19
|
};
|
|
18
20
|
const PRESENTATION_HINT = "If total > 20, summarize in chat (about 5–10 sample rows) instead of dumping the full table. Include frontend source links when present.";
|
|
19
21
|
const ROUTING_HINT = "Already-marketed China products (国药准字, 批准文号, 上市, 医保/集采) → product-cn-* tools. R&D / CDE pipeline (在研, 受理号, 审评, 尚未上市) → reg-cn-* tools. Other DBs (医保 yibao, 基药 jiyao, 集采 jicai, trials, global) → yaohai-*. Do not use yaohai-search with dbname product_cn or reg_cn when the dedicated tools apply.";
|
|
22
|
+
/**
|
|
23
|
+
* Derived from the generated facet catalog so the tool descriptions can never
|
|
24
|
+
* drift from the data they describe.
|
|
25
|
+
*/
|
|
26
|
+
const DBS_FACET_DBS = Object.keys(DBS_FACET_CATALOG).sort();
|
|
27
|
+
const DBS_FACET_DB_COUNT = DBS_FACET_DBS.length;
|
|
28
|
+
const DBS_FACET_FIELD_COUNT = DBS_FACET_DBS.reduce((n, db) => n + Object.keys(DBS_FACET_CATALOG[db].fields).length, 0);
|
|
29
|
+
/** A few well-known dbs, used to hint coverage without listing all 44. */
|
|
30
|
+
const DBS_FACET_EXAMPLES = ["yibao", "jiyao", "jicai", "dpd", "fda_ndc", "uk_emc", "nmpa_gmp"]
|
|
31
|
+
.filter((db) => db in DBS_FACET_CATALOG)
|
|
32
|
+
.join(", ");
|
|
20
33
|
process.on("uncaughtException", (error) => {
|
|
21
34
|
console.error("Uncaught Exception:", error);
|
|
22
35
|
process.exit(1);
|
|
@@ -32,6 +45,9 @@ const server = new Server({
|
|
|
32
45
|
capabilities: {
|
|
33
46
|
resources: {},
|
|
34
47
|
tools: {},
|
|
48
|
+
// Declared so clients accept the notifications/message we emit when a
|
|
49
|
+
// newer version of this package is published.
|
|
50
|
+
logging: {},
|
|
35
51
|
},
|
|
36
52
|
});
|
|
37
53
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -91,6 +107,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
91
107
|
required: ["dbname", "id"],
|
|
92
108
|
},
|
|
93
109
|
},
|
|
110
|
+
{
|
|
111
|
+
name: "yaohai-facets",
|
|
112
|
+
description: "Facet distributions (条件筛选) for a dbs database — aggregated value+count buckets for filterable fields. " +
|
|
113
|
+
`Works for ${DBS_FACET_DB_COUNT} dbs reached via yaohai-search (${DBS_FACET_EXAMPLES}, …). ` +
|
|
114
|
+
`DISCOVERY MODE: omit \`fields\` to list available facet fields (add \`dbname\` for one db, omit it for all ${DBS_FACET_DB_COUNT}). ` +
|
|
115
|
+
"FETCH MODE: pass `fields` (required with `dbname`) to get buckets for those fields. One HTTP request fires per field, so request only the 2–4 you need. " +
|
|
116
|
+
"Pass the same `query` filters you used in yaohai-search to facet within that result set. " +
|
|
117
|
+
"Not available for product_cn / reg_cn — use product-cn-facets / reg-cn-facets instead.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
dbname: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Database id from yaohai-catalog, e.g. yibao, jiyao, dpd, uk_emc. Required when `fields` is given. Omit to list all facet-capable databases.",
|
|
124
|
+
},
|
|
125
|
+
query: QUERY_PROP,
|
|
126
|
+
fields: {
|
|
127
|
+
type: "array",
|
|
128
|
+
items: { type: "string" },
|
|
129
|
+
description: "Facet field keys (see discovery mode). Omit to get the catalog instead of buckets. Example for yibao: [\"province\", \"drug_type\"].",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
94
134
|
{
|
|
95
135
|
name: "yaohai-global-search",
|
|
96
136
|
description: "Global drug panorama search (global_search). Pass q as the search term, or query.term. " +
|
|
@@ -242,6 +282,27 @@ function encodeId(id) {
|
|
|
242
282
|
function asQuery(query) {
|
|
243
283
|
return { ...(query ?? {}) };
|
|
244
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Structured "not facet-capable" response.
|
|
287
|
+
*
|
|
288
|
+
* Returned as a normal (non-error) payload so an agent can self-correct in one
|
|
289
|
+
* step instead of having to parse an exception message. Covers the common cases:
|
|
290
|
+
* a dbname that is not in the catalog at all, and one that exists but is not a
|
|
291
|
+
* dbs-route database (product_cn / reg_cn have dedicated facet tools; the other
|
|
292
|
+
* custom/aggs routes expose no condition filters).
|
|
293
|
+
*/
|
|
294
|
+
function facetDbUnknown(dbname) {
|
|
295
|
+
const near = DBS_FACET_DBS.filter((db) => db.includes(dbname) || dbname.includes(db)).slice(0, 5);
|
|
296
|
+
return {
|
|
297
|
+
dbname,
|
|
298
|
+
supported: false,
|
|
299
|
+
error: `No facet fields known for dbname "${dbname}".`,
|
|
300
|
+
hint: "Only dbs-route databases support yaohai-facets. " +
|
|
301
|
+
"For product_cn use product-cn-facets; for reg_cn use reg-cn-facets. " +
|
|
302
|
+
`Call yaohai-facets with no arguments to list all ${DBS_FACET_DB_COUNT} facet-capable databases.`,
|
|
303
|
+
...(near.length > 0 ? { did_you_mean: near } : {}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
245
306
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
246
307
|
const { name, arguments: args } = request.params;
|
|
247
308
|
try {
|
|
@@ -274,6 +335,71 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
274
335
|
});
|
|
275
336
|
return ok(content);
|
|
276
337
|
}
|
|
338
|
+
case "yaohai-facets": {
|
|
339
|
+
const validated = YaohaiFacetsSchema.parse(args ?? {});
|
|
340
|
+
// Discovery mode: no `fields` -> describe what can be facetted.
|
|
341
|
+
// Without this, agents would have to guess field names and would hit
|
|
342
|
+
// "Unknown facet fields" from fetchFacets.
|
|
343
|
+
if (!validated.fields) {
|
|
344
|
+
if (validated.dbname) {
|
|
345
|
+
const entry = DBS_FACET_CATALOG[validated.dbname];
|
|
346
|
+
if (!entry) {
|
|
347
|
+
return ok(facetDbUnknown(validated.dbname));
|
|
348
|
+
}
|
|
349
|
+
return ok({
|
|
350
|
+
dbname: validated.dbname,
|
|
351
|
+
title: entry.title,
|
|
352
|
+
category: entry.category,
|
|
353
|
+
facet_prefix: entry.prefix,
|
|
354
|
+
facet_count: Object.keys(entry.fields).length,
|
|
355
|
+
facets: entry.fields,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return ok({
|
|
359
|
+
facet_capable_databases: DBS_FACET_DB_COUNT,
|
|
360
|
+
total_facet_fields: DBS_FACET_FIELD_COUNT,
|
|
361
|
+
note: "Call yaohai-facets with a dbname to list its fields, or pass dbname + fields to fetch buckets. " +
|
|
362
|
+
"product_cn and reg_cn use product-cn-facets / reg-cn-facets instead.",
|
|
363
|
+
databases: DBS_FACET_DBS.map((db) => ({
|
|
364
|
+
dbname: db,
|
|
365
|
+
title: DBS_FACET_CATALOG[db].title,
|
|
366
|
+
category: DBS_FACET_CATALOG[db].category,
|
|
367
|
+
facet_count: Object.keys(DBS_FACET_CATALOG[db].fields).length,
|
|
368
|
+
fields: Object.keys(DBS_FACET_CATALOG[db].fields),
|
|
369
|
+
})),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
// Fetch mode: both dbname and fields are required.
|
|
373
|
+
if (!validated.dbname) {
|
|
374
|
+
throw new Error("dbname is required when fetching facets. Omit `fields` to list facet-capable databases.");
|
|
375
|
+
}
|
|
376
|
+
const entry = DBS_FACET_CATALOG[validated.dbname];
|
|
377
|
+
if (!entry) {
|
|
378
|
+
return ok(facetDbUnknown(validated.dbname));
|
|
379
|
+
}
|
|
380
|
+
// Report unknown fields up front with the valid list, rather than letting
|
|
381
|
+
// fetchFacets throw on the first one and lose the rest.
|
|
382
|
+
const known = Object.keys(entry.fields);
|
|
383
|
+
const unknown = validated.fields.filter((f) => !known.includes(f));
|
|
384
|
+
if (unknown.length > 0) {
|
|
385
|
+
throw new Error(`Unknown facet field(s) for ${validated.dbname}: ${unknown.join(", ")}. ` +
|
|
386
|
+
`Valid fields: ${known.join(", ")}.`);
|
|
387
|
+
}
|
|
388
|
+
// Per-db required params (e.g. drugsales needs groupid=205) go first so
|
|
389
|
+
// caller-supplied values can still override them.
|
|
390
|
+
const query = { ...(entry.defaultQuery ?? {}), ...asQuery(validated.query) };
|
|
391
|
+
const content = await fetchFacets({
|
|
392
|
+
prefix: entry.prefix,
|
|
393
|
+
query,
|
|
394
|
+
fields: validated.fields,
|
|
395
|
+
catalog: entry.fields,
|
|
396
|
+
});
|
|
397
|
+
return ok({
|
|
398
|
+
dbname: validated.dbname,
|
|
399
|
+
title: entry.title,
|
|
400
|
+
...content,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
277
403
|
case "yaohai-global-search": {
|
|
278
404
|
const validated = YaohaiGlobalSearchSchema.parse(args ?? {});
|
|
279
405
|
const query = asQuery(validated.query);
|
|
@@ -403,8 +529,47 @@ function ok(content) {
|
|
|
403
529
|
isError: false,
|
|
404
530
|
};
|
|
405
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* Tell the user a newer version is published, on both channels we have:
|
|
534
|
+
* - stderr, which MCP clients surface in their server log panel and which is
|
|
535
|
+
* safe for a stdio server (stdout carries JSON-RPC and must stay clean);
|
|
536
|
+
* - a `notifications/message` log notification, which reaches clients that
|
|
537
|
+
* render MCP log messages to the user.
|
|
538
|
+
*
|
|
539
|
+
* Never throws: diagnostics must not take the server down.
|
|
540
|
+
*/
|
|
541
|
+
async function announceUpdateIfAvailable() {
|
|
542
|
+
try {
|
|
543
|
+
const result = await checkForUpdate(PACKAGE_VERSION);
|
|
544
|
+
if (!result.updateAvailable)
|
|
545
|
+
return;
|
|
546
|
+
const message = formatUpdateMessage(result);
|
|
547
|
+
if (!message)
|
|
548
|
+
return;
|
|
549
|
+
console.error(`[mcp-drugsea] ${message.split("\n").join("\n[mcp-drugsea] ")}`);
|
|
550
|
+
try {
|
|
551
|
+
await server.sendLoggingMessage({
|
|
552
|
+
level: "warning",
|
|
553
|
+
logger: "update-check",
|
|
554
|
+
data: message,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// Client may not have declared logging support; stderr line still landed.
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// Swallow everything: an update check is never worth surfacing.
|
|
563
|
+
}
|
|
564
|
+
}
|
|
406
565
|
async function main() {
|
|
407
566
|
const transport = new StdioServerTransport();
|
|
567
|
+
// Fire only after the initialize handshake completes, so the notification is
|
|
568
|
+
// never emitted before the client is ready to receive it. Deliberately not
|
|
569
|
+
// awaited: startup latency must not depend on registry reachability.
|
|
570
|
+
server.oninitialized = () => {
|
|
571
|
+
void announceUpdateIfAvailable();
|
|
572
|
+
};
|
|
408
573
|
await server.connect(transport);
|
|
409
574
|
}
|
|
410
575
|
main().catch((error) => {
|
package/dist/types.js
CHANGED
|
@@ -20,6 +20,24 @@ export const YaohaiDetailSchema = z.object({
|
|
|
20
20
|
dbname: z.string().min(1),
|
|
21
21
|
id: z.string().min(1),
|
|
22
22
|
});
|
|
23
|
+
/**
|
|
24
|
+
* Facet request for a dbs-route database.
|
|
25
|
+
*
|
|
26
|
+
* Dual-mode, so one tool covers both discovery and fetching:
|
|
27
|
+
* - `fields` omitted -> return the facet catalog (available fields). Add
|
|
28
|
+
* `dbname` to narrow to one database, omit it to list all 44.
|
|
29
|
+
* - `fields` given -> fetch those facet distributions for `dbname`.
|
|
30
|
+
*
|
|
31
|
+
* Discovery mode exists because agents otherwise have to guess field names and
|
|
32
|
+
* would hit "Unknown facet fields". Fetching requires `fields` to be explicit:
|
|
33
|
+
* one HTTP request fires per field, so an implicit "all of them" would be a slow
|
|
34
|
+
* 129-request fan-out.
|
|
35
|
+
*/
|
|
36
|
+
export const YaohaiFacetsSchema = z.object({
|
|
37
|
+
dbname: z.string().optional(),
|
|
38
|
+
query: QueryObjectSchema.optional(),
|
|
39
|
+
fields: z.array(z.string().min(1)).min(1).optional(),
|
|
40
|
+
});
|
|
23
41
|
export const YaohaiGlobalSearchSchema = z.object({
|
|
24
42
|
q: z.string().optional(),
|
|
25
43
|
query: QueryObjectSchema.optional(),
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking npm update check.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: MCP clients usually launch the server with
|
|
5
|
+
* `npx -y @kinginsun/mcp-drugsea`. A bare package name resolves against the
|
|
6
|
+
* local npx cache and can pin users to a stale version indefinitely, and
|
|
7
|
+
* `npm install -g` users never auto-update at all. Since we cannot retroactively
|
|
8
|
+
* patch code that is already installed on a user's machine, every release ships
|
|
9
|
+
* this check so subsequent versions can tell the user how to upgrade.
|
|
10
|
+
*
|
|
11
|
+
* Hard constraints (do not weaken):
|
|
12
|
+
* - MUST NOT write to stdout: this is a stdio JSON-RPC server, stdout is the
|
|
13
|
+
* protocol channel. Diagnostics go to stderr / notifications only.
|
|
14
|
+
* - MUST NOT throw or reject: a version check can never break server startup.
|
|
15
|
+
* - MUST NOT block startup: the caller fires this off without awaiting connect.
|
|
16
|
+
* - MUST be cheap: one small request per process, TTL-cached across processes.
|
|
17
|
+
*/
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
export const PACKAGE_NAME = "@kinginsun/mcp-drugsea";
|
|
22
|
+
/** Registry doc for a single version; ~2 KB, far cheaper than the full packument. */
|
|
23
|
+
function registryLatestUrl() {
|
|
24
|
+
const base = (process.env.YAOHAI_NPM_REGISTRY || "https://registry.npmjs.org")
|
|
25
|
+
.trim()
|
|
26
|
+
.replace(/\/+$/, "");
|
|
27
|
+
// Scoped names must be percent-encoded in the path.
|
|
28
|
+
return `${base}/${PACKAGE_NAME.replace("/", "%2F")}/latest`;
|
|
29
|
+
}
|
|
30
|
+
/** How long a cached "latest version" answer stays fresh. */
|
|
31
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
/** Fail fast: a slow registry must never be felt by the user. */
|
|
33
|
+
const FETCH_TIMEOUT_MS = 1500;
|
|
34
|
+
/** Truthiness for env flags: "0", "", "false", "no", "off" all mean disabled. */
|
|
35
|
+
function flagDisabled(raw) {
|
|
36
|
+
if (raw === undefined)
|
|
37
|
+
return false;
|
|
38
|
+
const value = raw.trim().toLowerCase();
|
|
39
|
+
return ["0", "", "false", "no", "off"].includes(value);
|
|
40
|
+
}
|
|
41
|
+
export function updateCheckEnabled() {
|
|
42
|
+
return !flagDisabled(process.env.YAOHAI_MCP_UPDATE_CHECK);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Minimal semver precedence compare, avoiding a runtime dependency.
|
|
46
|
+
* Returns >0 if a is newer than b, <0 if older, 0 if equal.
|
|
47
|
+
* Handles `MAJOR.MINOR.PATCH` with an optional `-prerelease`; a prerelease of
|
|
48
|
+
* the same numeric triple sorts below the release (per semver §11).
|
|
49
|
+
*/
|
|
50
|
+
export function compareVersions(a, b) {
|
|
51
|
+
const parse = (input) => {
|
|
52
|
+
const [core, ...rest] = input.trim().replace(/^v/i, "").split("-");
|
|
53
|
+
const parts = core.split(".").map((n) => {
|
|
54
|
+
const parsed = Number.parseInt(n, 10);
|
|
55
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
56
|
+
});
|
|
57
|
+
while (parts.length < 3)
|
|
58
|
+
parts.push(0);
|
|
59
|
+
return { parts: parts.slice(0, 3), prerelease: rest.join("-") };
|
|
60
|
+
};
|
|
61
|
+
const left = parse(a);
|
|
62
|
+
const right = parse(b);
|
|
63
|
+
for (let i = 0; i < 3; i++) {
|
|
64
|
+
if (left.parts[i] !== right.parts[i])
|
|
65
|
+
return left.parts[i] - right.parts[i];
|
|
66
|
+
}
|
|
67
|
+
// Numeric triples tie: presence of a prerelease decides.
|
|
68
|
+
if (left.prerelease && !right.prerelease)
|
|
69
|
+
return -1;
|
|
70
|
+
if (!left.prerelease && right.prerelease)
|
|
71
|
+
return 1;
|
|
72
|
+
if (left.prerelease && right.prerelease) {
|
|
73
|
+
return left.prerelease < right.prerelease
|
|
74
|
+
? -1
|
|
75
|
+
: left.prerelease > right.prerelease
|
|
76
|
+
? 1
|
|
77
|
+
: 0;
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
function cacheFile() {
|
|
82
|
+
try {
|
|
83
|
+
const base = process.env.XDG_CACHE_HOME?.trim() ||
|
|
84
|
+
(os.homedir() ? path.join(os.homedir(), ".cache") : os.tmpdir());
|
|
85
|
+
return path.join(base, "mcp-drugsea", "update-check.json");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function readCache() {
|
|
92
|
+
const file = cacheFile();
|
|
93
|
+
if (!file)
|
|
94
|
+
return undefined;
|
|
95
|
+
try {
|
|
96
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
if (typeof parsed.latest === "string" &&
|
|
99
|
+
parsed.latest.length > 0 &&
|
|
100
|
+
typeof parsed.checkedAt === "number") {
|
|
101
|
+
return { latest: parsed.latest, checkedAt: parsed.checkedAt };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Missing, unreadable, or corrupt cache is always fine.
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
function writeCache(latest) {
|
|
110
|
+
const file = cacheFile();
|
|
111
|
+
if (!file)
|
|
112
|
+
return;
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
115
|
+
fs.writeFileSync(file, `${JSON.stringify({ latest, checkedAt: Date.now() })}\n`, "utf8");
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Read-only home, sandboxed FS, full disk: skip caching silently.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function fetchLatest() {
|
|
122
|
+
// Node >= 18 guarantees global fetch (see package.json engines).
|
|
123
|
+
if (typeof fetch !== "function")
|
|
124
|
+
return undefined;
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetch(registryLatestUrl(), {
|
|
127
|
+
headers: { accept: "application/json" },
|
|
128
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
129
|
+
redirect: "follow",
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok)
|
|
132
|
+
return undefined;
|
|
133
|
+
const body = (await response.json());
|
|
134
|
+
return typeof body.version === "string" && body.version.length > 0
|
|
135
|
+
? body.version
|
|
136
|
+
: undefined;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Offline, DNS failure, dead proxy, TLS interception, timeout: all expected.
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Resolve the newest published version, preferring the network and falling back
|
|
145
|
+
* to a still-fresh cache entry so offline users get a useful answer too.
|
|
146
|
+
*/
|
|
147
|
+
/**
|
|
148
|
+
* Resolve the newest published version.
|
|
149
|
+
*
|
|
150
|
+
* Cache-first: a fresh cache entry means zero network traffic and zero added
|
|
151
|
+
* startup latency for every launch inside the TTL window. Only when the cache is
|
|
152
|
+
* missing/expired do we consult the registry, and a failed lookup degrades to the
|
|
153
|
+
* stale cache entry so offline users still get a useful answer.
|
|
154
|
+
*/
|
|
155
|
+
async function resolveLatest() {
|
|
156
|
+
const cached = readCache();
|
|
157
|
+
if (cached && Date.now() - cached.checkedAt <= CACHE_TTL_MS) {
|
|
158
|
+
return { latest: cached.latest, detail: "fresh cache; registry not contacted" };
|
|
159
|
+
}
|
|
160
|
+
const live = await fetchLatest();
|
|
161
|
+
if (live) {
|
|
162
|
+
writeCache(live);
|
|
163
|
+
return { latest: live };
|
|
164
|
+
}
|
|
165
|
+
if (cached) {
|
|
166
|
+
return {
|
|
167
|
+
latest: cached.latest,
|
|
168
|
+
detail: "registry unreachable; used stale cached version",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return { detail: "registry unreachable and no cache" };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check for a newer published version. Resolves with a result object and never
|
|
175
|
+
* rejects; callers may safely ignore the promise.
|
|
176
|
+
*/
|
|
177
|
+
export async function checkForUpdate(currentVersion) {
|
|
178
|
+
try {
|
|
179
|
+
if (!updateCheckEnabled()) {
|
|
180
|
+
return { current: currentVersion, updateAvailable: false, status: "disabled" };
|
|
181
|
+
}
|
|
182
|
+
if (!/^\d+\.\d+\.\d+/.test(currentVersion.trim())) {
|
|
183
|
+
// Dev builds like "0.0.0-dev" or a git hash: nothing sane to compare.
|
|
184
|
+
return {
|
|
185
|
+
current: currentVersion,
|
|
186
|
+
updateAvailable: false,
|
|
187
|
+
status: "error",
|
|
188
|
+
detail: "non-semver build version",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const resolved = await resolveLatest();
|
|
192
|
+
if (!resolved.latest) {
|
|
193
|
+
return {
|
|
194
|
+
current: currentVersion,
|
|
195
|
+
updateAvailable: false,
|
|
196
|
+
status: "error",
|
|
197
|
+
detail: resolved.detail,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const updateAvailable = compareVersions(resolved.latest, currentVersion) > 0;
|
|
201
|
+
return {
|
|
202
|
+
current: currentVersion,
|
|
203
|
+
latest: resolved.latest,
|
|
204
|
+
updateAvailable,
|
|
205
|
+
status: updateAvailable ? "update-available" : "up-to-date",
|
|
206
|
+
detail: resolved.detail,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
// Belt and braces: this function is contractually non-throwing.
|
|
211
|
+
return {
|
|
212
|
+
current: currentVersion,
|
|
213
|
+
updateAvailable: false,
|
|
214
|
+
status: "error",
|
|
215
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Human-readable upgrade instructions, matching the README "Updating" section.
|
|
221
|
+
* Used for both the stderr line and the MCP logging notification.
|
|
222
|
+
*/
|
|
223
|
+
export function formatUpdateMessage(result) {
|
|
224
|
+
if (!result.updateAvailable || !result.latest)
|
|
225
|
+
return "";
|
|
226
|
+
const lines = [
|
|
227
|
+
`${PACKAGE_NAME} ${result.current} is installed; ${result.latest} is available.`,
|
|
228
|
+
"To update:",
|
|
229
|
+
' npx users - change args to ["-y", "@kinginsun/mcp-drugsea@latest"], then reload the MCP server',
|
|
230
|
+
" global - npm update -g " + PACKAGE_NAME,
|
|
231
|
+
" stale npx - npm cache npx ls && npm cache npx rm <key>",
|
|
232
|
+
"Set YAOHAI_MCP_UPDATE_CHECK=0 to silence this message.",
|
|
233
|
+
];
|
|
234
|
+
return lines.join("\n");
|
|
235
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinginsun/mcp-drugsea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|