@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 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/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.
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
- const PACKAGE_VERSION = "0.7.1";
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: "mcp-drugsea",
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. Not for already-marketed products — use product-cn-search. " +
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: clampLimit(validated.limit, YAOHAI_LIMIT_DEFAULT, YAOHAI_LIMIT_MAX),
326
- offset: clampOffset(validated.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: clampLimit(validated.limit, YAOHAI_LIMIT_DEFAULT, YAOHAI_LIMIT_MAX),
412
- offset: clampOffset(validated.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 limit = clampLimit(validated.limit, CN_LIMIT_DEFAULT, CN_LIMIT_MAX);
429
- const offset = clampOffset(validated.offset);
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 limit = clampLimit(validated.limit, CN_LIMIT_DEFAULT, CN_LIMIT_MAX);
479
- const offset = clampOffset(validated.offset);
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.7.1",
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",