sciverse-mcp-server 0.15.1 → 1.0.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/dist/generated/tools.d.ts +6 -1
- package/dist/generated/tools.js +208 -12
- package/dist/server.js +5 -4
- package/dist/tools.js +91 -9
- package/package.json +2 -2
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
export declare const TOOLS_VERSION = "0.
|
|
1
|
+
export declare const TOOLS_VERSION = "1.0.0";
|
|
2
2
|
export interface ToolDef {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
5
5
|
inputSchema: Record<string, unknown>;
|
|
6
6
|
}
|
|
7
|
+
export declare const LEGACY_TOOLS: ToolDef[];
|
|
8
|
+
export declare const LEGACY_ENDPOINTS: Record<string, {
|
|
9
|
+
method: "GET" | "POST";
|
|
10
|
+
path: string;
|
|
11
|
+
}>;
|
|
7
12
|
export declare const TOOLS: ToolDef[];
|
|
8
13
|
export declare const ENDPOINTS: Record<string, {
|
|
9
14
|
method: "GET" | "POST";
|
package/dist/generated/tools.js
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
// Auto-generated by scripts/gen-tools.mjs. Do not edit.
|
|
2
2
|
// Source of truth: openapi.yaml
|
|
3
|
-
export const TOOLS_VERSION = "0.
|
|
4
|
-
export const
|
|
3
|
+
export const TOOLS_VERSION = "1.0.0";
|
|
4
|
+
export const LEGACY_TOOLS = [
|
|
5
5
|
{
|
|
6
6
|
"name": "search_papers",
|
|
7
|
-
"description": "
|
|
7
|
+
"description": "Deprecated compatibility tool. Prefer search_metadata; legacy paper convenience parameters apply only to papers.",
|
|
8
8
|
"inputSchema": {
|
|
9
9
|
"type": "object",
|
|
10
10
|
"properties": {
|
|
11
|
+
"fields": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"maxItems": 128,
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "string"
|
|
16
|
+
},
|
|
17
|
+
"description": "返回字段,须为当前collection已授权且可投影字段;省略返回默认字段集与授权交集。",
|
|
18
|
+
"x-en-description": "Project authorized, projectable fields from this collection; omit for authorized defaults."
|
|
19
|
+
},
|
|
11
20
|
"collection": {
|
|
12
21
|
"type": "string",
|
|
13
22
|
"enum": [
|
|
14
23
|
"papers",
|
|
15
24
|
"authors",
|
|
16
|
-
"sources"
|
|
25
|
+
"sources",
|
|
26
|
+
"patents"
|
|
17
27
|
],
|
|
18
28
|
"default": "papers",
|
|
19
29
|
"description": "检索的实体集合。papers(默认,论文)/ authors(作者)/ sources(来源期刊)。 各 collection 字段集不同,用 list_catalog(collection=<name>)学习对应 schema。 注意:本工具的便捷字段(authors/journals/year_from/subjects 等)只对 papers 有意义; 查 authors/sources 时改用 filters_advanced + 该 collection 的字段名(如 authors 的 summary_stats.h_index / orcid,sources 的 issn / is_oa)。authors 用 orcid、 sources 用 issn 与论文检索结果关联。",
|
|
@@ -175,10 +185,186 @@ export const TOOLS = [
|
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
}
|
|
188
|
+
}
|
|
189
|
+
];
|
|
190
|
+
export const LEGACY_ENDPOINTS = {
|
|
191
|
+
"search_papers": {
|
|
192
|
+
"method": "POST",
|
|
193
|
+
"path": "/meta-search"
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
export const TOOLS = [
|
|
197
|
+
{
|
|
198
|
+
"name": "search_metadata",
|
|
199
|
+
"description": "Search metadata in one collection: papers, patents, authors or sources. Use list_catalog to discover authorized fields before choosing filters, sort and fields. Supports keywords, cursor pagination and facets; no implicit field sorting. Returns results, total_count and next_cursor with collection-specific fields. Use read_content for paper/patent text and semantic_search for paper semantic retrieval. Author/source IDs cannot be used to read content.",
|
|
200
|
+
"inputSchema": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"additionalProperties": false,
|
|
203
|
+
"properties": {
|
|
204
|
+
"fields": {
|
|
205
|
+
"type": "array",
|
|
206
|
+
"maxItems": 128,
|
|
207
|
+
"items": {
|
|
208
|
+
"type": "string"
|
|
209
|
+
},
|
|
210
|
+
"description": "返回字段,须为当前collection已授权且可投影字段;省略返回默认字段集与授权交集。",
|
|
211
|
+
"x-en-description": "Project authorized, projectable fields from this collection; omit for authorized defaults."
|
|
212
|
+
},
|
|
213
|
+
"collection": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"enum": [
|
|
216
|
+
"papers",
|
|
217
|
+
"authors",
|
|
218
|
+
"sources",
|
|
219
|
+
"patents"
|
|
220
|
+
],
|
|
221
|
+
"default": "papers",
|
|
222
|
+
"description": "实体集合;用 list_catalog 查看对应字段权限和能力。"
|
|
223
|
+
},
|
|
224
|
+
"query": {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"description": "BM25 关键词,匹配当前实体已授权的可检索字段;专利包含 invention_title、abstract、claims.claim_text。留空则纯靠结构化过滤。\n普通关键词是宽松匹配(任一词命中、按相关性排序)。\n\n也支持布尔检索式:全大写 AND / OR / NOT、括号分组、引号短语;优先级\nNOT > AND > OR,相邻词隐式 AND。例如\n (histopathology OR pathology) AND (\"deep learning\" OR \"machine learning\") AND (prognosis OR survival)\n布尔式里每个检索词都是硬条件、不做放宽——0 命中就是 0。\n- query 只放检索词:「检索式1(预后预测)」这类标签/说明也会变成必须命中的词。\n- 小写 and/or 是普通词;要检索字面量 OR(如比值比)加引号 \"OR\"。\n- 引号需与运算符同时出现才生效:只写 \"spread through air spaces\" 不带运算符时\n 按普通关键词处理;写成 \"spread through air spaces\" AND lung 才是短语精确匹配。\n- 最多 64 个检索词、括号嵌套 10 层,超限返回 400;复杂检索请拆成多次调用。\n",
|
|
227
|
+
"x-en-description": "BM25 keywords over authorized searchable fields in this collection; patents use invention_title, abstract and claims.claim_text. Leave empty for\npure structured filtering. Plain keywords match loosely (any word, ranked by\nrelevance).\n\nBoolean syntax is also supported: UPPERCASE AND / OR / NOT, ( ) grouping and\n\"quoted phrases\"; precedence NOT > AND > OR, adjacent words are implicitly AND.\nExample:\n (histopathology OR pathology) AND (\"deep learning\" OR \"machine learning\") AND (prognosis OR survival)\nIn boolean mode every term is a hard requirement and nothing is relaxed —\n0 hits means 0.\n- Put only search terms in query: labels or instructions such as\n \"检索式1(预后预测)\" become required terms too.\n- Lowercase and/or are ordinary words; quote \"OR\" to search the literal\n (e.g. odds ratio).\n- Quotes only take effect alongside an operator: a query that is just\n \"spread through air spaces\" is treated as plain keywords, whereas\n \"spread through air spaces\" AND lung is an exact phrase match.\n- Max 64 terms and 10 nesting levels (400 otherwise); split complex searches\n into several calls.\n",
|
|
228
|
+
"maxLength": 2048
|
|
229
|
+
},
|
|
230
|
+
"page": {
|
|
231
|
+
"type": "integer",
|
|
232
|
+
"default": 1,
|
|
233
|
+
"minimum": 1
|
|
234
|
+
},
|
|
235
|
+
"page_size": {
|
|
236
|
+
"type": "integer",
|
|
237
|
+
"default": 25,
|
|
238
|
+
"minimum": 1,
|
|
239
|
+
"maximum": 50,
|
|
240
|
+
"description": "每页条数。省略时为服务端默认 25(SDK / MCP 不注入默认值);工具层上限 50。"
|
|
241
|
+
},
|
|
242
|
+
"freshness_boost": {
|
|
243
|
+
"type": "string",
|
|
244
|
+
"enum": [
|
|
245
|
+
"NONE",
|
|
246
|
+
"MILD",
|
|
247
|
+
"STRONG"
|
|
248
|
+
],
|
|
249
|
+
"default": "NONE",
|
|
250
|
+
"description": "模糊搜索新鲜度加权:结果偏向新文献(仅 query 非空时生效;传排序\n(sort_by_year 非 none / sort_advanced)时被忽略,硬排优先)。\nMILD: 近 10 年加权,适合日常查文献;STRONG: 近 3 年加权,适合跟踪\n研究方向 / 追最新进展。与 impact_boost / language_affinity 可叠加\n(均为乘法因子)。boost 生效时为浅翻页:不产 next_cursor、不支持\ncursor 深翻页。\n"
|
|
251
|
+
},
|
|
252
|
+
"impact_boost": {
|
|
253
|
+
"type": "string",
|
|
254
|
+
"enum": [
|
|
255
|
+
"NONE",
|
|
256
|
+
"MILD",
|
|
257
|
+
"STRONG"
|
|
258
|
+
],
|
|
259
|
+
"default": "NONE",
|
|
260
|
+
"description": "模糊搜索影响力加权:高被引文献在保留相关性的前提下上浮(仅 query\n非空时生效;传排序时被忽略)。MILD: 轻度上浮,相关性仍主导;\nSTRONG: 明显偏向高被引。引用因子有界、零被引中性(不会归零)。\n与 freshness_boost / language_affinity 可叠加;boost 生效时为浅翻页。\n"
|
|
261
|
+
},
|
|
262
|
+
"language_affinity": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"enum": [
|
|
265
|
+
"NONE",
|
|
266
|
+
"MILD",
|
|
267
|
+
"STRONG"
|
|
268
|
+
],
|
|
269
|
+
"default": "NONE",
|
|
270
|
+
"description": "模糊搜索语言亲和加权:非 query 语言的结果降序、但不排除(仅 query\n非空时生效;传排序时被忽略)。目标语言由服务端从 query 文本判定\n(假名→ja / 谚文→ko / 汉字→zh / 拉丁→en,其他书写系统不生效);\n语言未知的文献保持中性不降权。MILD: 非目标语言 ×0.5,跨语言强相关\n结果仍可上浮;STRONG: ×0.2,几乎只看目标语言。与 freshness_boost /\nimpact_boost 可叠加;boost 生效时为浅翻页。要硬排除某语言请改用\nfilters_advanced 的 language 字段(如 {\"field\":\"language\",\"value\":\"en\"},\n软硬两层语义不同:本参数只调序,filter 直接排除)。\n"
|
|
271
|
+
},
|
|
272
|
+
"filters": {
|
|
273
|
+
"type": "array",
|
|
274
|
+
"description": "按当前实体 catalog 的字段能力填写。",
|
|
275
|
+
"items": {
|
|
276
|
+
"type": "object",
|
|
277
|
+
"required": [
|
|
278
|
+
"field",
|
|
279
|
+
"operator",
|
|
280
|
+
"value"
|
|
281
|
+
],
|
|
282
|
+
"properties": {
|
|
283
|
+
"field": {
|
|
284
|
+
"type": "string"
|
|
285
|
+
},
|
|
286
|
+
"operator": {
|
|
287
|
+
"type": "string",
|
|
288
|
+
"description": "过滤操作符。MATCH(分词模糊)适用于 author、keywords(输入 \"Hinton\" 命中 \"Geoffrey Hinton\"); MATCH_PHRASE(短语模糊)适用于 publication_venue_name_unified,整词连续匹配(\"Nature\" 命中 \"Nature Communications\";非前缀匹配,\"Nature Comm\" 不会命中); doi 用 EQ,服务端归一化(去 doi.org 前缀+转小写)后精确匹配。MATCH/MATCH_PHRASE 仅对配了 text 子字段的字段有效。",
|
|
289
|
+
"enum": [
|
|
290
|
+
"FILTER_OP_EQ",
|
|
291
|
+
"FILTER_OP_NE",
|
|
292
|
+
"FILTER_OP_GT",
|
|
293
|
+
"FILTER_OP_GTE",
|
|
294
|
+
"FILTER_OP_LT",
|
|
295
|
+
"FILTER_OP_LTE",
|
|
296
|
+
"FILTER_OP_IN",
|
|
297
|
+
"FILTER_OP_NIN",
|
|
298
|
+
"FILTER_OP_CONTAINS",
|
|
299
|
+
"FILTER_OP_MATCH",
|
|
300
|
+
"FILTER_OP_MATCH_PHRASE"
|
|
301
|
+
],
|
|
302
|
+
"default": "FILTER_OP_EQ"
|
|
303
|
+
},
|
|
304
|
+
"value": {}
|
|
305
|
+
},
|
|
306
|
+
"additionalProperties": false
|
|
307
|
+
},
|
|
308
|
+
"maxItems": 128
|
|
309
|
+
},
|
|
310
|
+
"sort": {
|
|
311
|
+
"type": "array",
|
|
312
|
+
"description": "按当前实体 catalog 的字段能力填写。",
|
|
313
|
+
"items": {
|
|
314
|
+
"type": "object",
|
|
315
|
+
"required": [
|
|
316
|
+
"field",
|
|
317
|
+
"order"
|
|
318
|
+
],
|
|
319
|
+
"properties": {
|
|
320
|
+
"field": {
|
|
321
|
+
"type": "string"
|
|
322
|
+
},
|
|
323
|
+
"order": {
|
|
324
|
+
"type": "string",
|
|
325
|
+
"enum": [
|
|
326
|
+
"SORT_ORDER_DESC",
|
|
327
|
+
"SORT_ORDER_ASC"
|
|
328
|
+
],
|
|
329
|
+
"default": "SORT_ORDER_DESC"
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
"additionalProperties": false
|
|
333
|
+
},
|
|
334
|
+
"maxItems": 32
|
|
335
|
+
},
|
|
336
|
+
"cursor": {
|
|
337
|
+
"type": "string",
|
|
338
|
+
"description": "使用上页 next_cursor 原样续查,保持查询条件不变;不与 page>1 共用。"
|
|
339
|
+
},
|
|
340
|
+
"facets": {
|
|
341
|
+
"type": "array",
|
|
342
|
+
"maxItems": 5,
|
|
343
|
+
"items": {
|
|
344
|
+
"type": "object",
|
|
345
|
+
"additionalProperties": false,
|
|
346
|
+
"required": [
|
|
347
|
+
"field"
|
|
348
|
+
],
|
|
349
|
+
"properties": {
|
|
350
|
+
"field": {
|
|
351
|
+
"type": "string"
|
|
352
|
+
},
|
|
353
|
+
"size": {
|
|
354
|
+
"type": "integer",
|
|
355
|
+
"minimum": 1,
|
|
356
|
+
"maximum": 50,
|
|
357
|
+
"default": 10
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
178
364
|
},
|
|
179
365
|
{
|
|
180
366
|
"name": "semantic_search",
|
|
181
|
-
"description": "Natural-language semantic search returning relevant paper chunks for\nRAG-style answering.\nUse when: \"How does Transformer attention work?\", \"What are recent\nmethods for protein structure prediction?\".\nNot for: precise field filtering (use
|
|
367
|
+
"description": "Natural-language semantic search returning relevant paper chunks for\nRAG-style answering.\nUse when: \"How does Transformer attention work?\", \"What are recent\nmethods for protein structure prediction?\".\nNot for: precise field filtering (use search_metadata) or fetching full\noriginal text (use read_content).\nReturns: list of chunks; each entry has chunk_id, doc_id, abstract,\nchunk, score, title, offset.\nTypical chain: semantic_search → pick chunk → read_content(doc_id,\noffset).",
|
|
182
368
|
"inputSchema": {
|
|
183
369
|
"type": "object",
|
|
184
370
|
"required": [
|
|
@@ -265,7 +451,7 @@ export const TOOLS = [
|
|
|
265
451
|
},
|
|
266
452
|
{
|
|
267
453
|
"name": "list_catalog",
|
|
268
|
-
"description": "Returns the schema catalog for
|
|
454
|
+
"description": "Returns the schema catalog for search_metadata: every field name, type,\nwhether it's filterable / sortable, default-return status, human\ndescription, and applicable FilterOperators.\nUse when: \"Which field do I filter by DOI?\", \"What values can\naccess_oa_status take?\", \"What's the right enum for metadata_type?\".\nNot for: actually searching papers (use search_metadata / semantic_search).\nTypical pattern: call once when first encountering Sciverse or facing\nan ambiguous field need, then construct precise search_metadata filters\nfrom the returned schema.\nPass include_sample_values=true to also fetch top-20 values for\nenum-like fields (OpenSearch terms aggregation, 24h cached).",
|
|
269
455
|
"inputSchema": {
|
|
270
456
|
"type": "object",
|
|
271
457
|
"properties": {
|
|
@@ -274,7 +460,8 @@ export const TOOLS = [
|
|
|
274
460
|
"enum": [
|
|
275
461
|
"papers",
|
|
276
462
|
"authors",
|
|
277
|
-
"sources"
|
|
463
|
+
"sources",
|
|
464
|
+
"patents"
|
|
278
465
|
],
|
|
279
466
|
"default": "papers",
|
|
280
467
|
"description": "字段 catalog 所属实体集合。papers(默认)/ authors / sources,各 collection 字段不同。"
|
|
@@ -295,7 +482,7 @@ export const TOOLS = [
|
|
|
295
482
|
},
|
|
296
483
|
{
|
|
297
484
|
"name": "list_paper_relations",
|
|
298
|
-
"description": "Paginate the full relation list of a paper. citations/references/related_works\nare unbounded arrays (up to 340k entries for a single paper) and are NOT\nprojectable in
|
|
485
|
+
"description": "Paginate the full relation list of a paper. citations/references/related_works\nare unbounded arrays (up to 340k entries for a single paper) and are NOT\nprojectable in search_metadata, so this endpoint is the only way to read them.\nUse when: \"What does paper X cite?\" (relation=REFERENCES), \"Which papers cite\npaper X?\" (relation=CITATIONS), \"Works related to paper X\" (relation=RELATED_WORKS).\nNote: CITATIONS (incoming: who cites me) and REFERENCES (outgoing: who I cite)\nare opposite directions.\nTypical chain: get unique_id from search_metadata / semantic_search, then paginate\nhere by relation.\nTwo limits (CITATIONS only; REFERENCES/RELATED_WORKS max out at 11833/20 in practice):\nmore than 10000 relations returns 429; page*page_size above 10000 returns 400.\nIn both cases switch to search_metadata with filters on\nreferences_unique_id — it supports deep paging and arbitrary sorting.\ntotal_count counts in-corpus matches only, so it can differ from the paper's own\ncitation_count by about 1%.",
|
|
299
486
|
"inputSchema": {
|
|
300
487
|
"type": "object",
|
|
301
488
|
"required": [
|
|
@@ -305,7 +492,7 @@ export const TOOLS = [
|
|
|
305
492
|
"properties": {
|
|
306
493
|
"unique_id": {
|
|
307
494
|
"type": "string",
|
|
308
|
-
"description": "目标论文 unique_id(如 paper:10.1038/xxx),来自
|
|
495
|
+
"description": "目标论文 unique_id(如 paper:10.1038/xxx),来自 search_metadata / semantic_search;勿传 doc_id。"
|
|
309
496
|
},
|
|
310
497
|
"relation": {
|
|
311
498
|
"type": "string",
|
|
@@ -332,13 +519,22 @@ export const TOOLS = [
|
|
|
332
519
|
},
|
|
333
520
|
{
|
|
334
521
|
"name": "read_content",
|
|
335
|
-
"description": "Read a range of a paper
|
|
522
|
+
"description": "Read a range of a paper or patent’s original text addressed in Unicode code\npoints (offset/limit count characters like Python len(), not bytes).\nTypically used with a doc_id/offset returned by semantic_search to\nexpand context (read more text before or after a chunk).\nReturns: text fragment, bytes_returned (UTF-8 byte length of text, for\nreference only), next_offset (code-point offset of the next fragment —\npage with it, never with bytes_returned), more (boolean).\nServer behaviour: limit above 524288 is silently clamped; omitting\noffset returns the whole document ignoring limit — the SDKs / MCP\nserver send offset=0 and limit=4096 by default, so pass offset\nexplicitly when calling the HTTP API directly.",
|
|
336
523
|
"inputSchema": {
|
|
337
524
|
"type": "object",
|
|
338
525
|
"properties": {
|
|
526
|
+
"collection": {
|
|
527
|
+
"type": "string",
|
|
528
|
+
"enum": [
|
|
529
|
+
"papers",
|
|
530
|
+
"patents"
|
|
531
|
+
],
|
|
532
|
+
"default": "papers",
|
|
533
|
+
"description": "正文实体;专利必须传 patents,doc_id 来自元数据查询,不能用公开号。"
|
|
534
|
+
},
|
|
339
535
|
"doc_id": {
|
|
340
536
|
"type": "string",
|
|
341
|
-
"description": "文献 ID(来自
|
|
537
|
+
"description": "文献 ID(来自 search_metadata / semantic_search)。"
|
|
342
538
|
},
|
|
343
539
|
"offset": {
|
|
344
540
|
"type": "integer",
|
|
@@ -379,7 +575,7 @@ export const TOOLS = [
|
|
|
379
575
|
}
|
|
380
576
|
];
|
|
381
577
|
export const ENDPOINTS = {
|
|
382
|
-
"
|
|
578
|
+
"search_metadata": {
|
|
383
579
|
"method": "POST",
|
|
384
580
|
"path": "/meta-search"
|
|
385
581
|
},
|
package/dist/server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// TODO: 后续可加 SSE / Streamable HTTP 形态(目前只支持 stdio,覆盖主流 coding agent)。
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { TOOLS, TOOLS_VERSION } from "./generated/tools.js";
|
|
5
|
+
import { TOOLS, LEGACY_TOOLS, TOOLS_VERSION } from "./generated/tools.js";
|
|
6
6
|
import { executeTool } from "./tools.js";
|
|
7
7
|
export function createServer(config) {
|
|
8
8
|
const server = new Server({
|
|
@@ -13,9 +13,10 @@ export function createServer(config) {
|
|
|
13
13
|
tools: {},
|
|
14
14
|
},
|
|
15
15
|
});
|
|
16
|
-
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
17
|
-
tools: TOOLS
|
|
18
|
-
|
|
16
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
17
|
+
const tools = process.env.SCIVERSE_ENABLE_LEGACY_TOOLS === "true" ? [...TOOLS, ...LEGACY_TOOLS] : TOOLS;
|
|
18
|
+
return { tools };
|
|
19
|
+
});
|
|
19
20
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
20
21
|
const { name, arguments: args } = request.params;
|
|
21
22
|
const result = await executeTool(config, name, (args ?? {}));
|
package/dist/tools.js
CHANGED
|
@@ -3,10 +3,76 @@
|
|
|
3
3
|
// `toBackendPayload` 是从 packages/typescript/src/client.ts 的 `toBackendPayload`
|
|
4
4
|
// 复制过来的(约 50 行)。MCP 包刻意不依赖 TS SDK,避免 monorepo 内部环依赖。
|
|
5
5
|
// 后续如有第 4 处复制,再用 codegen 收敛。
|
|
6
|
-
import { ENDPOINTS } from "./generated/tools.js";
|
|
6
|
+
import { ENDPOINTS, LEGACY_ENDPOINTS } from "./generated/tools.js";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
const PASSTHROUGH = ["query", "page", "page_size", "fields", "collection"];
|
|
8
|
+
const PASSTHROUGH = ["query", "page", "page_size", "fields", "collection", "freshness_boost", "impact_boost", "language_affinity"];
|
|
9
|
+
const COLLECTIONS = ["papers", "authors", "sources", "patents"];
|
|
10
|
+
const CANONICAL_SEARCH_KEYS = [
|
|
11
|
+
"query", "collection", "filters", "sort", "fields", "page", "page_size",
|
|
12
|
+
"cursor", "facets", "freshness_boost", "impact_boost", "language_affinity",
|
|
13
|
+
];
|
|
14
|
+
const PAPER_CONVENIENCE_KEYS = [
|
|
15
|
+
"title_contains", "abstract_contains", "authors", "year_from", "year_to",
|
|
16
|
+
"journals", "subjects",
|
|
17
|
+
];
|
|
18
|
+
function validateCollection(collection, allowed = COLLECTIONS) {
|
|
19
|
+
if (collection !== undefined && (typeof collection !== "string" || !allowed.includes(collection))) {
|
|
20
|
+
throw new Error(`collection must be one of ${allowed.join(" / ")}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function rejectUnknown(args, allowed) {
|
|
24
|
+
for (const key of Object.keys(args)) {
|
|
25
|
+
if (!allowed.includes(key))
|
|
26
|
+
throw new Error(`unknown parameter: ${key}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function toMetadataPayload(args) {
|
|
30
|
+
rejectUnknown(args, CANONICAL_SEARCH_KEYS);
|
|
31
|
+
// query/collection 的 null 与省略等价,其余参数不做静默过滤。
|
|
32
|
+
args = { ...args, collection: args.collection ?? "papers" };
|
|
33
|
+
if (args.query === null)
|
|
34
|
+
delete args.query;
|
|
35
|
+
validateCollection(args.collection);
|
|
36
|
+
for (const [key, allowed] of [
|
|
37
|
+
["filters", ["field", "operator", "value"]],
|
|
38
|
+
["sort", ["field", "order"]],
|
|
39
|
+
["facets", ["field", "size"]],
|
|
40
|
+
]) {
|
|
41
|
+
if (args[key] === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
if (!Array.isArray(args[key]))
|
|
44
|
+
throw new Error(`${key} must be an array`);
|
|
45
|
+
for (const item of args[key]) {
|
|
46
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
47
|
+
throw new Error(`${key} entries must be objects`);
|
|
48
|
+
rejectUnknown(item, allowed);
|
|
49
|
+
if (key === "facets") {
|
|
50
|
+
const { size } = item;
|
|
51
|
+
if (size !== undefined && (typeof size !== "number" || !Number.isInteger(size) || size < 1 || size > 50)) {
|
|
52
|
+
throw new Error("facets size must be an integer from 1 to 50");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { ...args, collection: args.collection ?? "papers" };
|
|
58
|
+
}
|
|
9
59
|
function toBackendPayload(args) {
|
|
60
|
+
const collection = args.collection ?? "papers";
|
|
61
|
+
validateCollection(collection);
|
|
62
|
+
let sortByYear = args.sort_by_year ?? "auto";
|
|
63
|
+
if (typeof sortByYear !== "string" || !["auto", "none", "asc", "desc"].includes(sortByYear)) {
|
|
64
|
+
throw new Error("sort_by_year must be one of auto / none / asc / desc");
|
|
65
|
+
}
|
|
66
|
+
if (collection !== "papers") {
|
|
67
|
+
if (sortByYear === "asc" || sortByYear === "desc") {
|
|
68
|
+
throw new Error("sort_by_year asc/desc is only supported for collection papers");
|
|
69
|
+
}
|
|
70
|
+
for (const key of PAPER_CONVENIENCE_KEYS) {
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(args, key)) {
|
|
72
|
+
throw new Error(`${key} is only supported for collection papers`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
10
76
|
const out = {};
|
|
11
77
|
const filters = [];
|
|
12
78
|
const sort = [];
|
|
@@ -41,9 +107,8 @@ function toBackendPayload(args) {
|
|
|
41
107
|
}
|
|
42
108
|
// sort_by_year 默认 auto:有 query(或 sort_advanced)时不加年份排序——保 BM25
|
|
43
109
|
// 相关性且软加权可用;纯结构化筛选时按年份降序(后端默认序实质乱序)。
|
|
44
|
-
let sortByYear = args.sort_by_year ?? "auto";
|
|
45
110
|
if (sortByYear === "auto") {
|
|
46
|
-
sortByYear = args.query || (Array.isArray(args.sort_advanced) && args.sort_advanced.length > 0)
|
|
111
|
+
sortByYear = collection !== "papers" || args.query || (Array.isArray(args.sort_advanced) && args.sort_advanced.length > 0)
|
|
47
112
|
? "none" : "desc";
|
|
48
113
|
}
|
|
49
114
|
if (sortByYear !== "none") {
|
|
@@ -167,7 +232,9 @@ async function callBinary(config, path, query) {
|
|
|
167
232
|
};
|
|
168
233
|
}
|
|
169
234
|
export async function executeTool(config, name, args) {
|
|
170
|
-
const
|
|
235
|
+
const endpoints = process.env.SCIVERSE_ENABLE_LEGACY_TOOLS === "true"
|
|
236
|
+
? { ...ENDPOINTS, ...LEGACY_ENDPOINTS } : ENDPOINTS;
|
|
237
|
+
const endpoint = Object.prototype.hasOwnProperty.call(endpoints, name) ? endpoints[name] : undefined;
|
|
171
238
|
if (!endpoint) {
|
|
172
239
|
return {
|
|
173
240
|
isError: true,
|
|
@@ -175,8 +242,17 @@ export async function executeTool(config, name, args) {
|
|
|
175
242
|
};
|
|
176
243
|
}
|
|
177
244
|
switch (name) {
|
|
178
|
-
case "
|
|
179
|
-
|
|
245
|
+
case "search_metadata":
|
|
246
|
+
case "search_papers": {
|
|
247
|
+
let body;
|
|
248
|
+
try {
|
|
249
|
+
body = name === "search_metadata" ? toMetadataPayload(args) : toBackendPayload(args);
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: e.message }) }] };
|
|
253
|
+
}
|
|
254
|
+
return call(config, "POST", endpoint.path, { body });
|
|
255
|
+
}
|
|
180
256
|
case "semantic_search": {
|
|
181
257
|
let body;
|
|
182
258
|
try {
|
|
@@ -202,7 +278,13 @@ export async function executeTool(config, name, args) {
|
|
|
202
278
|
return call(config, "GET", endpoint.path, { query });
|
|
203
279
|
}
|
|
204
280
|
case "read_content": {
|
|
205
|
-
const { doc_id, offset, limit } = args;
|
|
281
|
+
const { doc_id, collection, offset, limit } = args;
|
|
282
|
+
try {
|
|
283
|
+
validateCollection(collection, ["papers", "patents"]);
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: e.message }) }] };
|
|
287
|
+
}
|
|
206
288
|
if (!doc_id) {
|
|
207
289
|
return {
|
|
208
290
|
isError: true,
|
|
@@ -211,7 +293,7 @@ export async function executeTool(config, name, args) {
|
|
|
211
293
|
}
|
|
212
294
|
// 后端未传 offset 时返回整篇全文并忽略 limit;这里始终显式传两者,避免把整篇论文灌进模型上下文。
|
|
213
295
|
return call(config, "GET", endpoint.path, {
|
|
214
|
-
query: { doc_id, offset: offset ?? 0, limit: limit ?? 4096 },
|
|
296
|
+
query: { doc_id, offset: offset ?? 0, limit: limit ?? 4096, collection },
|
|
215
297
|
});
|
|
216
298
|
}
|
|
217
299
|
case "get_resource": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sciverse-mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sciverse MCP server —
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sciverse MCP server — metadata search across papers, patents, authors and sources, paper semantic retrieval and document content reading.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/opendatalab/Sciverse-Agent-Tools/tree/main/packages/mcp",
|
|
7
7
|
"repository": {
|