@kinginsun/mcp-drugsea 0.7.1 → 0.9.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 +8 -2
- package/dist/api.js +15 -0
- package/dist/environment.js +24 -0
- package/dist/index.js +30 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,13 +178,19 @@ xlsx export is not implemented in this MCP (v1 returns JSON samples only).
|
|
|
178
178
|
| Tool | Parameters | Notes |
|
|
179
179
|
|------|------------|--------|
|
|
180
180
|
| `yaohai-catalog` | `category?`, `q?` | List databases |
|
|
181
|
-
| `yaohai-search` | `dbname`, `query?`, `limit?`, `offset?` | Default limit 10, max 50 |
|
|
181
|
+
| `yaohai-search` | `dbname`, `query?`, `limit?`, `offset?` | Default limit 10, max 50; ≤ 1000 rows per query condition (offset+limit window cap) |
|
|
182
182
|
| `yaohai-detail` | `dbname`, `id` | Skip DBs with `has_detail: false` |
|
|
183
183
|
| `yaohai-facets` | `dbname?`, `query?`, `fields?` | Facets for `dbs`-route DBs. Omit `fields` → discover facet-capable DBs/fields; pass `fields` → fetch buckets |
|
|
184
184
|
| `yaohai-global-search` | `q?`, `query?`, `limit?`, `offset?` | `q` fills `query.term` |
|
|
185
185
|
|
|
186
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.
|
|
187
187
|
|
|
188
|
+
> **Retrieval cap (anti-scraping, enforced client + server):** one distinct query
|
|
189
|
+
> condition can return at most **1000 rows** (`offset + limit ≤ 1000`). A larger
|
|
190
|
+
> `offset` is rejected; `limit` is auto-shrunk near the edge of the window. To
|
|
191
|
+
> reach deeper slices, narrow the filters (date / province / ATC / enterprise)
|
|
192
|
+
> and query again — each *new* condition gets its own 1000-row window.
|
|
193
|
+
|
|
188
194
|
`yaohai-facets` mirrors the ConditionSearch facet filters of the website for the `dbs`-route databases (医保 `yibao`, 基药 `jiyao`, 集采 `jicai`, sales `drugsales`, …). Two modes:
|
|
189
195
|
|
|
190
196
|
- **Discovery** (no `fields`): omit `dbname` to list all 44 facet-capable databases, or pass `dbname` to list its facet-able fields (with `filter_type`).
|
|
@@ -376,7 +382,7 @@ echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"product-cn
|
|
|
376
382
|
|
|
377
383
|
## Releasing (maintainers)
|
|
378
384
|
|
|
379
|
-
`publish.sh` releases the package to npm (which is what makes `npx -y @kinginsun/mcp-drugsea@latest` work). It syncs `src/
|
|
385
|
+
`publish.sh` releases the package to npm (which is what makes `npx -y @kinginsun/mcp-drugsea@latest` work). It syncs `src/environment.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.
|
|
380
386
|
|
|
381
387
|
```bash
|
|
382
388
|
npm login # once, with rights on the @kinginsun scope
|
package/dist/api.js
CHANGED
|
@@ -2,6 +2,7 @@ import http from "node:http";
|
|
|
2
2
|
import https from "node:https";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
4
|
import { normalizeRecord, parseFacetList, stripMcpFields } from "./normalize.js";
|
|
5
|
+
import { ENVIRONMENT_FINGERPRINT } from "./environment.js";
|
|
5
6
|
export class MissingTokenError extends Error {
|
|
6
7
|
constructor(message) {
|
|
7
8
|
super(message ??
|
|
@@ -28,6 +29,7 @@ export function getAuthHeaders(extra = {}) {
|
|
|
28
29
|
const token = getUserToken();
|
|
29
30
|
return {
|
|
30
31
|
...extra,
|
|
32
|
+
"User-Agent": ENVIRONMENT_FINGERPRINT.userAgent,
|
|
31
33
|
Authorization: `Bearer ${token}`,
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -95,6 +97,19 @@ export function clampOffset(offset) {
|
|
|
95
97
|
}
|
|
96
98
|
return Math.max(0, Math.trunc(offset));
|
|
97
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* 单组查询条件最多可获取的行数窗口(与服务端 yaohai_mcp_max_retrieve() 同步,
|
|
102
|
+
* 默认 1000):offset+limit 超出窗口则收缩 limit;offset 已达窗口直接报错,
|
|
103
|
+
* 提示收窄筛选条件而不是继续翻页。
|
|
104
|
+
*/
|
|
105
|
+
export const MAX_RETRIEVE_PER_QUERY = 1000;
|
|
106
|
+
export function enforceRetrievalWindow(limit, offset, max = MAX_RETRIEVE_PER_QUERY) {
|
|
107
|
+
if (offset >= max) {
|
|
108
|
+
throw new ApiError(`A single query condition can return at most ${max} rows(单个查询条件最多获取 ${max} 条): ` +
|
|
109
|
+
`offset=${offset} 已超出窗口上限。请收窄筛选条件(如日期/省份/ATC/企业名)后分页查询。`);
|
|
110
|
+
}
|
|
111
|
+
return { limit: Math.min(limit, max - offset), offset };
|
|
112
|
+
}
|
|
98
113
|
function httpRequest(method, urlStr, headers, body) {
|
|
99
114
|
return new Promise((resolve, reject) => {
|
|
100
115
|
const url = new URL(urlStr);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
export const PACKAGE_NAME = "mcp-drugsea";
|
|
3
|
+
export const PACKAGE_VERSION = "0.9.0";
|
|
4
|
+
/**
|
|
5
|
+
* Capture non-identifying runtime metadata once when the MCP process starts.
|
|
6
|
+
* Deliberately excludes hostname, username, network addresses, and machine IDs.
|
|
7
|
+
*/
|
|
8
|
+
function collectEnvironmentFingerprint() {
|
|
9
|
+
const platform = process.platform;
|
|
10
|
+
const osRelease = os.release();
|
|
11
|
+
const arch = process.arch;
|
|
12
|
+
const nodeVersion = process.version;
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
packageName: PACKAGE_NAME,
|
|
15
|
+
packageVersion: PACKAGE_VERSION,
|
|
16
|
+
nodeVersion,
|
|
17
|
+
platform,
|
|
18
|
+
osRelease,
|
|
19
|
+
arch,
|
|
20
|
+
userAgent: `${PACKAGE_NAME}/${PACKAGE_VERSION} ` +
|
|
21
|
+
`(Node.js ${nodeVersion}; ${platform} ${osRelease}; ${arch})`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export const ENVIRONMENT_FINGERPRINT = collectEnvironmentFingerprint();
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { clampLimit, clampOffset, fetchDetail, fetchFacets, listSearch, mcpDbDetail, mcpDbSearch, prefersMcpListApi, yaohaiPost, } from "./api.js";
|
|
5
|
+
import { clampLimit, clampOffset, enforceRetrievalWindow, fetchDetail, fetchFacets, listSearch, mcpDbDetail, mcpDbSearch, prefersMcpListApi, yaohaiPost, } from "./api.js";
|
|
6
6
|
import { ATC_HINT, PRODUCT_CN_COMMON_FIELDS, PRODUCT_CN_DETAIL_PATH, PRODUCT_CN_FACET_FIELDS, PRODUCT_CN_FACET_PREFIX, PRODUCT_CN_VIEW_TYPES, REG_CN_COMMON_FIELDS, REG_CN_DETAIL_PATH, REG_CN_FACET_FIELDS, REG_CN_FACET_PREFIX, REG_CN_VIEW_TYPES, applyProductCnDefaults, applyRegCnDefaults, productCnSearchPath, regCnSearchPath, } from "./fields.js";
|
|
7
7
|
import { EmptyObjectSchema, ProductCnDetailSchema, ProductCnFacetsSchema, ProductCnSearchSchema, RegCnDetailSchema, RegCnFacetsSchema, RegCnSearchSchema, YaohaiCatalogSchema, YaohaiDetailSchema, YaohaiFacetsSchema, YaohaiGlobalSearchSchema, YaohaiSearchSchema, } from "./types.js";
|
|
8
8
|
import { DBS_FACET_CATALOG } from "./dbs-facets.js";
|
|
9
9
|
import { checkForUpdate, formatUpdateMessage, } from "./update-check.js";
|
|
10
|
-
|
|
10
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from "./environment.js";
|
|
11
11
|
const YAOHAI_LIMIT_MAX = 50;
|
|
12
12
|
const YAOHAI_LIMIT_DEFAULT = 10;
|
|
13
13
|
const CN_LIMIT_MAX = 100;
|
|
@@ -18,6 +18,7 @@ const QUERY_PROP = {
|
|
|
18
18
|
description: "Search/filter fields. Values may be string, number, or string[] (repeat the key for ConditionSearch multiple). Date: \"YYYY-MM-DD to YYYY-MM-DD\". Range: \"min to max\".",
|
|
19
19
|
};
|
|
20
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.";
|
|
21
|
+
const RETRIEVAL_CAP_HINT = "One distinct query condition can return at most 1000 rows total (offset+limit window cap, anti-scraping): paginate within that window, or narrow the filters (date / province / ATC / enterprise) to reach deeper slices — a too-large offset is rejected.";
|
|
21
22
|
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
|
/**
|
|
23
24
|
* Derived from the generated facet catalog so the tool descriptions can never
|
|
@@ -39,7 +40,7 @@ process.on("unhandledRejection", (reason, promise) => {
|
|
|
39
40
|
process.exit(1);
|
|
40
41
|
});
|
|
41
42
|
const server = new Server({
|
|
42
|
-
name:
|
|
43
|
+
name: PACKAGE_NAME,
|
|
43
44
|
version: PACKAGE_VERSION,
|
|
44
45
|
}, {
|
|
45
46
|
capabilities: {
|
|
@@ -74,6 +75,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
74
75
|
{
|
|
75
76
|
name: "yaohai-search",
|
|
76
77
|
description: "Search a single Yaohai database by dbname (from yaohai-catalog). Default limit 10, max 50. " +
|
|
78
|
+
RETRIEVAL_CAP_HINT +
|
|
79
|
+
" " +
|
|
77
80
|
"Use query fields from the catalog's search_fields. " +
|
|
78
81
|
ROUTING_HINT +
|
|
79
82
|
" " +
|
|
@@ -90,7 +93,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
90
93
|
type: "number",
|
|
91
94
|
description: `Row cap (default ${YAOHAI_LIMIT_DEFAULT}, max ${YAOHAI_LIMIT_MAX})`,
|
|
92
95
|
},
|
|
93
|
-
offset: { type: "number", description: "Pagination offset (default 0)" },
|
|
96
|
+
offset: { type: "number", description: "Pagination offset (default 0); offset+limit is capped at 1000 rows per query condition" },
|
|
94
97
|
},
|
|
95
98
|
required: ["dbname"],
|
|
96
99
|
},
|
|
@@ -134,6 +137,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
134
137
|
{
|
|
135
138
|
name: "yaohai-global-search",
|
|
136
139
|
description: "Global drug panorama search (global_search). Pass q as the search term, or query.term. " +
|
|
140
|
+
RETRIEVAL_CAP_HINT +
|
|
141
|
+
" " +
|
|
137
142
|
PRESENTATION_HINT,
|
|
138
143
|
inputSchema: {
|
|
139
144
|
type: "object",
|
|
@@ -144,7 +149,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
144
149
|
type: "number",
|
|
145
150
|
description: `Row cap (default ${YAOHAI_LIMIT_DEFAULT}, max ${YAOHAI_LIMIT_MAX})`,
|
|
146
151
|
},
|
|
147
|
-
offset: { type: "number", description: "Pagination offset (default 0)" },
|
|
152
|
+
offset: { type: "number", description: "Pagination offset (default 0); offset+limit is capped at 1000 rows per query condition" },
|
|
148
153
|
},
|
|
149
154
|
},
|
|
150
155
|
},
|
|
@@ -159,6 +164,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
159
164
|
"Default search_mode=3 (partial). first_approve_date = first listing date; approve_date = latest re-registration (not first listing). " +
|
|
160
165
|
ATC_HINT +
|
|
161
166
|
" Default limit 20, max 100. " +
|
|
167
|
+
RETRIEVAL_CAP_HINT +
|
|
168
|
+
" " +
|
|
162
169
|
"Not for R&D pipeline — use reg-cn-search. " +
|
|
163
170
|
PRESENTATION_HINT,
|
|
164
171
|
inputSchema: {
|
|
@@ -173,7 +180,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
173
180
|
type: "number",
|
|
174
181
|
description: `Row cap (default ${CN_LIMIT_DEFAULT}, max ${CN_LIMIT_MAX})`,
|
|
175
182
|
},
|
|
176
|
-
offset: { type: "number", description: "Pagination offset (default 0)" },
|
|
183
|
+
offset: { type: "number", description: "Pagination offset (default 0); offset+limit is capped at 1000 rows per query condition" },
|
|
177
184
|
view_type: {
|
|
178
185
|
type: "string",
|
|
179
186
|
enum: [...PRODUCT_CN_VIEW_TYPES],
|
|
@@ -221,7 +228,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
221
228
|
description: "Search China drug registration / CDE review (reg_cn): 在研, 受理号, 申报, 审评进度, not-yet-listed. " +
|
|
222
229
|
"Default rows_excluded=1 (drop 备案), search_mode=1. " +
|
|
223
230
|
ATC_HINT +
|
|
224
|
-
" Default limit 20, max 100.
|
|
231
|
+
" Default limit 20, max 100. " +
|
|
232
|
+
RETRIEVAL_CAP_HINT +
|
|
233
|
+
" " +
|
|
234
|
+
"Not for already-marketed products — use product-cn-search. " +
|
|
225
235
|
PRESENTATION_HINT,
|
|
226
236
|
inputSchema: {
|
|
227
237
|
type: "object",
|
|
@@ -235,7 +245,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
235
245
|
type: "number",
|
|
236
246
|
description: `Row cap (default ${CN_LIMIT_DEFAULT}, max ${CN_LIMIT_MAX})`,
|
|
237
247
|
},
|
|
238
|
-
offset: { type: "number", description: "Pagination offset (default 0)" },
|
|
248
|
+
offset: { type: "number", description: "Pagination offset (default 0); offset+limit is capped at 1000 rows per query condition" },
|
|
239
249
|
view_type: {
|
|
240
250
|
type: "string",
|
|
241
251
|
enum: [...REG_CN_VIEW_TYPES],
|
|
@@ -319,11 +329,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
319
329
|
}
|
|
320
330
|
case "yaohai-search": {
|
|
321
331
|
const validated = YaohaiSearchSchema.parse(args);
|
|
332
|
+
const window = enforceRetrievalWindow(clampLimit(validated.limit, YAOHAI_LIMIT_DEFAULT, YAOHAI_LIMIT_MAX), clampOffset(validated.offset));
|
|
322
333
|
const content = await yaohaiPost("/g/mcp/yaohai/search", {
|
|
323
334
|
dbname: validated.dbname,
|
|
324
335
|
query: asQuery(validated.query),
|
|
325
|
-
limit:
|
|
326
|
-
offset:
|
|
336
|
+
limit: window.limit,
|
|
337
|
+
offset: window.offset,
|
|
327
338
|
});
|
|
328
339
|
return ok(content);
|
|
329
340
|
}
|
|
@@ -406,10 +417,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
406
417
|
if (validated.q && (query.term === undefined || query.term === "")) {
|
|
407
418
|
query.term = validated.q;
|
|
408
419
|
}
|
|
420
|
+
const window = enforceRetrievalWindow(clampLimit(validated.limit, YAOHAI_LIMIT_DEFAULT, YAOHAI_LIMIT_MAX), clampOffset(validated.offset));
|
|
409
421
|
const content = await yaohaiPost("/g/mcp/yaohai/global-search", {
|
|
410
422
|
query,
|
|
411
|
-
limit:
|
|
412
|
-
offset:
|
|
423
|
+
limit: window.limit,
|
|
424
|
+
offset: window.offset,
|
|
413
425
|
});
|
|
414
426
|
return ok(content);
|
|
415
427
|
}
|
|
@@ -425,8 +437,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
425
437
|
const validated = ProductCnSearchSchema.parse(args ?? {});
|
|
426
438
|
const viewType = validated.view_type ?? "eslist";
|
|
427
439
|
const query = applyProductCnDefaults(asQuery(validated.query));
|
|
428
|
-
const
|
|
429
|
-
const
|
|
440
|
+
const window = enforceRetrievalWindow(clampLimit(validated.limit, CN_LIMIT_DEFAULT, CN_LIMIT_MAX), clampOffset(validated.offset));
|
|
441
|
+
const limit = window.limit;
|
|
442
|
+
const offset = window.offset;
|
|
430
443
|
const content = prefersMcpListApi()
|
|
431
444
|
? await mcpDbSearch({
|
|
432
445
|
dbname: "product_cn",
|
|
@@ -475,8 +488,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
475
488
|
const validated = RegCnSearchSchema.parse(args ?? {});
|
|
476
489
|
const viewType = validated.view_type ?? "eslist";
|
|
477
490
|
const query = applyRegCnDefaults(asQuery(validated.query));
|
|
478
|
-
const
|
|
479
|
-
const
|
|
491
|
+
const window = enforceRetrievalWindow(clampLimit(validated.limit, CN_LIMIT_DEFAULT, CN_LIMIT_MAX), clampOffset(validated.offset));
|
|
492
|
+
const limit = window.limit;
|
|
493
|
+
const offset = window.offset;
|
|
480
494
|
const content = prefersMcpListApi()
|
|
481
495
|
? await mcpDbSearch({
|
|
482
496
|
dbname: "reg_cn",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinginsun/mcp-drugsea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|