@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 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 **12 tools**.
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
- | tail -1 | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{const m=JSON.parse(d);console.log('tools:',m.result.tools.length)})"
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: 12`.
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 12-tool suite (from source)
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: 12 passed, 0 failed / 12 tool calls ---`.
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 12 tools.
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 12-tool suite, commits + tags, then publishes and pushes.
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
- const PACKAGE_VERSION = "0.3.0";
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.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",