@cyanheads/pubmed-mcp-server 2.6.12 → 2.7.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 +33 -12
- package/dist/config/server-config.d.ts +5 -0
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +45 -0
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.js +19 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts +71 -23
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js +671 -141
- package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts +87 -0
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js +217 -0
- package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js.map +1 -0
- package/dist/services/error-contracts.d.ts +18 -1
- package/dist/services/error-contracts.d.ts.map +1 -1
- package/dist/services/error-contracts.js +21 -4
- package/dist/services/error-contracts.js.map +1 -1
- package/dist/services/europe-pmc/api-client.d.ts +47 -0
- package/dist/services/europe-pmc/api-client.d.ts.map +1 -0
- package/dist/services/europe-pmc/api-client.js +123 -0
- package/dist/services/europe-pmc/api-client.js.map +1 -0
- package/dist/services/europe-pmc/europe-pmc-service.d.ts +71 -0
- package/dist/services/europe-pmc/europe-pmc-service.d.ts.map +1 -0
- package/dist/services/europe-pmc/europe-pmc-service.js +243 -0
- package/dist/services/europe-pmc/europe-pmc-service.js.map +1 -0
- package/dist/services/europe-pmc/request-queue.d.ts +33 -0
- package/dist/services/europe-pmc/request-queue.d.ts.map +1 -0
- package/dist/services/europe-pmc/request-queue.js +107 -0
- package/dist/services/europe-pmc/request-queue.js.map +1 -0
- package/dist/services/europe-pmc/types.d.ts +123 -0
- package/dist/services/europe-pmc/types.d.ts.map +1 -0
- package/dist/services/europe-pmc/types.js +16 -0
- package/dist/services/europe-pmc/types.js.map +1 -0
- package/dist/services/ncbi/response-handler.js +1 -1
- package/dist/services/unpaywall/unpaywall-service.d.ts.map +1 -1
- package/dist/services/unpaywall/unpaywall-service.js +14 -13
- package/dist/services/unpaywall/unpaywall-service.js.map +1 -1
- package/package.json +5 -4
- package/server.json +4 -4
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Low-level HTTP client for Europe PMC's REST API. Builds URLs,
|
|
3
|
+
* injects the optional contact email, and exposes single-attempt search and
|
|
4
|
+
* fullTextXML calls. Retry logic lives in `EuropePmcService`.
|
|
5
|
+
* @module src/services/europe-pmc/api-client
|
|
6
|
+
*/
|
|
7
|
+
import { McpError, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
8
|
+
import { fetchWithTimeout, httpErrorFromResponse, logger, requestContextService, } from '@cyanheads/mcp-ts-core/utils';
|
|
9
|
+
import { recoveryFor } from '../../services/error-contracts.js';
|
|
10
|
+
import { EUROPEPMC_API_BASE } from './types.js';
|
|
11
|
+
const USER_AGENT = 'pubmed-mcp-server (+https://github.com/cyanheads/pubmed-mcp-server)';
|
|
12
|
+
/** Low-level HTTP client for Europe PMC. Single-attempt — retries upstream. */
|
|
13
|
+
export class EuropePmcApiClient {
|
|
14
|
+
config;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Execute a search. Returns the raw JSON response body as a string so
|
|
20
|
+
* `EuropePmcService` can parse and surface SerializationError consistently
|
|
21
|
+
* when the body is malformed.
|
|
22
|
+
*/
|
|
23
|
+
async search(params) {
|
|
24
|
+
const url = this.buildSearchUrl(params);
|
|
25
|
+
const ctx = requestContextService.createRequestContext({
|
|
26
|
+
operation: 'EuropePmcSearch',
|
|
27
|
+
query: params.query,
|
|
28
|
+
});
|
|
29
|
+
let response;
|
|
30
|
+
try {
|
|
31
|
+
response = await fetchWithTimeout(url, this.config.timeoutMs, ctx, {
|
|
32
|
+
headers: { Accept: 'application/json', 'User-Agent': USER_AGENT },
|
|
33
|
+
...(params.signal && { signal: params.signal }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof McpError)
|
|
38
|
+
throw error;
|
|
39
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
40
|
+
throw serviceUnavailable(`Europe PMC search request failed: ${msg}`, { reason: 'europepmc_unreachable', ...recoveryFor('europepmc_unreachable') }, { cause: error });
|
|
41
|
+
}
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw await httpErrorFromResponse(response, {
|
|
44
|
+
service: 'Europe PMC',
|
|
45
|
+
data: { url, reason: 'europepmc_unreachable', ...recoveryFor('europepmc_unreachable') },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return response.text();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Fetch the JATS full-text XML for an EPMC record by its internal id.
|
|
52
|
+
* Returns `{ kind: 'not-available' }` for 404 — EPMC has the record but
|
|
53
|
+
* doesn't publish a full-text XML for it (very common for preprints).
|
|
54
|
+
*/
|
|
55
|
+
async fullTextXml(epmcId, signal) {
|
|
56
|
+
const url = `${EUROPEPMC_API_BASE}/${encodeURIComponent(epmcId)}/fullTextXML`;
|
|
57
|
+
const ctx = requestContextService.createRequestContext({
|
|
58
|
+
operation: 'EuropePmcFullTextXml',
|
|
59
|
+
epmcId,
|
|
60
|
+
});
|
|
61
|
+
let response;
|
|
62
|
+
try {
|
|
63
|
+
response = await fetchWithTimeout(url, this.config.timeoutMs, ctx, {
|
|
64
|
+
headers: {
|
|
65
|
+
Accept: 'application/xml, text/xml, */*;q=0.5',
|
|
66
|
+
'User-Agent': USER_AGENT,
|
|
67
|
+
},
|
|
68
|
+
...(signal && { signal }),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof McpError)
|
|
73
|
+
throw error;
|
|
74
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
75
|
+
throw serviceUnavailable(`Europe PMC fullTextXML request failed: ${msg}`, { reason: 'europepmc_unreachable', epmcId, ...recoveryFor('europepmc_unreachable') }, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
if (response.status === 404) {
|
|
78
|
+
return { kind: 'not-available', reason: 'EPMC has no fullTextXML for this record' };
|
|
79
|
+
}
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw await httpErrorFromResponse(response, {
|
|
82
|
+
service: 'Europe PMC fullTextXML',
|
|
83
|
+
data: { epmcId, reason: 'europepmc_unreachable', ...recoveryFor('europepmc_unreachable') },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const xml = await response.text();
|
|
87
|
+
if (!xml.trim()) {
|
|
88
|
+
logger.debug('Europe PMC returned an empty fullTextXML body.', requestContextService.createRequestContext({
|
|
89
|
+
operation: 'EuropePmcFullTextXmlEmpty',
|
|
90
|
+
epmcId,
|
|
91
|
+
}));
|
|
92
|
+
return { kind: 'not-available', reason: 'EPMC returned an empty fullTextXML body' };
|
|
93
|
+
}
|
|
94
|
+
return { kind: 'found', xml };
|
|
95
|
+
}
|
|
96
|
+
buildSearchUrl(params) {
|
|
97
|
+
const finalParams = {
|
|
98
|
+
query: this.buildQueryString(params),
|
|
99
|
+
format: 'json',
|
|
100
|
+
resultType: params.resultType ?? 'core',
|
|
101
|
+
pageSize: String(params.pageSize ?? 25),
|
|
102
|
+
cursorMark: params.cursorMark ?? '*',
|
|
103
|
+
};
|
|
104
|
+
if (params.sort)
|
|
105
|
+
finalParams.sort = params.sort;
|
|
106
|
+
if (this.config.email)
|
|
107
|
+
finalParams.email = this.config.email;
|
|
108
|
+
return `${EUROPEPMC_API_BASE}/search?${new URLSearchParams(finalParams).toString()}`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Combine the caller's query with an optional source filter. EPMC's query
|
|
112
|
+
* syntax supports `SRC:"X"` field tokens — we OR-join the requested sources
|
|
113
|
+
* into a parenthesized clause and AND it with the user's query.
|
|
114
|
+
*/
|
|
115
|
+
buildQueryString(params) {
|
|
116
|
+
const base = params.query.trim();
|
|
117
|
+
if (!params.sources || params.sources.length === 0)
|
|
118
|
+
return base;
|
|
119
|
+
const sourceClause = params.sources.map((s) => `SRC:"${s}"`).join(' OR ');
|
|
120
|
+
return `(${base}) AND (${sourceClause})`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=api-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../../../src/services/europe-pmc/api-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Europe PMC service. Wraps the EPMC REST API with rate-limiting,
|
|
3
|
+
* retries, and JATS XML parsing. Two methods: `search()` for keyword discovery
|
|
4
|
+
* across the EPMC corpus (MED/PMC/PPR/PAT/AGR) and `fullTextXml()` for fetching
|
|
5
|
+
* a record's full-text JATS. The XML parser matches NCBI's ordered config so
|
|
6
|
+
* `parsePmcArticle` consumes the result without modification.
|
|
7
|
+
*
|
|
8
|
+
* Optional service: only constructed when `EUROPEPMC_ENABLED=true` (the
|
|
9
|
+
* default). `getEuropePmcService()` returns `undefined` when disabled so
|
|
10
|
+
* callers can skip the chain step gracefully.
|
|
11
|
+
*
|
|
12
|
+
* @module src/services/europe-pmc/europe-pmc-service
|
|
13
|
+
*/
|
|
14
|
+
import type { JatsNode } from '../../services/ncbi/parsing/pmc-xml-helpers.js';
|
|
15
|
+
import { EuropePmcApiClient } from './api-client.js';
|
|
16
|
+
import { EuropePmcRequestQueue } from './request-queue.js';
|
|
17
|
+
import type { EuropePmcFullTextResult, EuropePmcSearchParams, EuropePmcSearchResult, EuropePmcSource } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Facade over the Europe PMC REST API. Two methods:
|
|
20
|
+
* - `search()` — keyword search across MED/PMC/PPR/PAT/AGR.
|
|
21
|
+
* - `fullTextXml()` — JATS full text for an EPMC record.
|
|
22
|
+
*
|
|
23
|
+
* Both honor `ctx.signal` for cancellation and retry transient failures with
|
|
24
|
+
* capped exponential backoff plus jitter.
|
|
25
|
+
*/
|
|
26
|
+
export declare class EuropePmcService {
|
|
27
|
+
private readonly client;
|
|
28
|
+
private readonly queue;
|
|
29
|
+
private readonly maxRetries;
|
|
30
|
+
private readonly orderedXmlParser;
|
|
31
|
+
constructor(client: EuropePmcApiClient, queue: EuropePmcRequestQueue, maxRetries: number);
|
|
32
|
+
/**
|
|
33
|
+
* Search Europe PMC. Cursor-based pagination — pass `cursorMark: '*'` (or
|
|
34
|
+
* omit) for the first page; pass the returned `nextCursorMark` for the next.
|
|
35
|
+
*/
|
|
36
|
+
search(params: EuropePmcSearchParams): Promise<EuropePmcSearchResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch the JATS full text for an EPMC record. Returns:
|
|
39
|
+
* - `{ kind: 'found', xml, epmcId, source }` — JATS XML string usable
|
|
40
|
+
* directly by tool callers that hold their own parser, or via
|
|
41
|
+
* `parseFullTextXml()` for the parsed tree.
|
|
42
|
+
* - `{ kind: 'not-available', reason }` — EPMC has the record but
|
|
43
|
+
* publishes no fullTextXML (404 or empty body).
|
|
44
|
+
*/
|
|
45
|
+
fullTextXml(epmcId: string, source: EuropePmcSource, signal?: AbortSignal): Promise<EuropePmcFullTextResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Parse a JATS XML string into the ordered node tree consumed by
|
|
48
|
+
* `parsePmcArticle`. Returns the `<article>` JatsNode, or `undefined` when
|
|
49
|
+
* the body doesn't contain an article element (malformed / empty).
|
|
50
|
+
*
|
|
51
|
+
* Throws `SerializationError` only for fundamentally invalid XML; an
|
|
52
|
+
* article-free but well-formed body returns `undefined` so callers can
|
|
53
|
+
* surface a `no-epmc-fulltext` outcome without a hard failure.
|
|
54
|
+
*/
|
|
55
|
+
parseFullTextXml(xml: string): JatsNode | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Retry wrapper for transient errors. Mirrors NCBI's `withRetry` minus the
|
|
58
|
+
* service-level deadline — EPMC requests are cheaper individually and the
|
|
59
|
+
* caller (typically `ctx.signal`) bounds the total chain.
|
|
60
|
+
*/
|
|
61
|
+
private withRetry;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Initialize the Europe PMC service when enabled. Safe to call regardless of
|
|
65
|
+
* config — `EUROPEPMC_ENABLED=false` leaves the service unset so callers see
|
|
66
|
+
* `undefined` and skip the chain step.
|
|
67
|
+
*/
|
|
68
|
+
export declare function initEuropePmcService(): void;
|
|
69
|
+
/** Returns the initialized service, or `undefined` when EPMC is disabled. */
|
|
70
|
+
export declare function getEuropePmcService(): EuropePmcService | undefined;
|
|
71
|
+
//# sourceMappingURL=europe-pmc-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"europe-pmc-service.d.ts","sourceRoot":"","sources":["../../../src/services/europe-pmc/europe-pmc-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,OAAO,KAAK,EAAE,QAAQ,EAAgB,MAAM,4CAA4C,CAAC;AAEzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EACV,uBAAuB,EAEvB,qBAAqB,EAErB,qBAAqB,EACrB,eAAe,EAChB,MAAM,YAAY,CAAC;AA2BpB;;;;;;;GAOG;AACH,qBAAa,gBAAgB;IAIzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAL7B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAY;gBAG1B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,qBAAqB,EAC5B,UAAU,EAAE,MAAM;IAmBrC;;;OAGG;IACG,MAAM,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IA0C3E;;;;;;;OAOG;IACG,WAAW,CACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC;IAkBnC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAgCnD;;;;OAIG;YACW,SAAS;CAsDxB;AAMD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAuB3C;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,SAAS,CAElE"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Europe PMC service. Wraps the EPMC REST API with rate-limiting,
|
|
3
|
+
* retries, and JATS XML parsing. Two methods: `search()` for keyword discovery
|
|
4
|
+
* across the EPMC corpus (MED/PMC/PPR/PAT/AGR) and `fullTextXml()` for fetching
|
|
5
|
+
* a record's full-text JATS. The XML parser matches NCBI's ordered config so
|
|
6
|
+
* `parsePmcArticle` consumes the result without modification.
|
|
7
|
+
*
|
|
8
|
+
* Optional service: only constructed when `EUROPEPMC_ENABLED=true` (the
|
|
9
|
+
* default). `getEuropePmcService()` returns `undefined` when disabled so
|
|
10
|
+
* callers can skip the chain step gracefully.
|
|
11
|
+
*
|
|
12
|
+
* @module src/services/europe-pmc/europe-pmc-service
|
|
13
|
+
*/
|
|
14
|
+
import { internalError, JsonRpcErrorCode, McpError, serializationError, } from '@cyanheads/mcp-ts-core/errors';
|
|
15
|
+
import { logger, requestContextService } from '@cyanheads/mcp-ts-core/utils';
|
|
16
|
+
import { XMLParser, XMLValidator } from 'fast-xml-parser';
|
|
17
|
+
import { getServerConfig } from '../../config/server-config.js';
|
|
18
|
+
import { recoveryFor } from '../../services/error-contracts.js';
|
|
19
|
+
import { ensureArray } from '../../services/ncbi/parsing/xml-helpers.js';
|
|
20
|
+
import { EuropePmcApiClient } from './api-client.js';
|
|
21
|
+
import { EuropePmcRequestQueue } from './request-queue.js';
|
|
22
|
+
/** Retryable transient codes — same set NCBI uses. */
|
|
23
|
+
const RETRYABLE_CODES = new Set([
|
|
24
|
+
JsonRpcErrorCode.ServiceUnavailable,
|
|
25
|
+
JsonRpcErrorCode.Timeout,
|
|
26
|
+
JsonRpcErrorCode.RateLimited,
|
|
27
|
+
]);
|
|
28
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
29
|
+
function abortableSleep(ms, signal) {
|
|
30
|
+
if (!signal)
|
|
31
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
32
|
+
if (signal.aborted)
|
|
33
|
+
return Promise.reject(signal.reason);
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const onAbort = () => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
reject(signal.reason);
|
|
38
|
+
};
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
signal.removeEventListener('abort', onAbort);
|
|
41
|
+
resolve();
|
|
42
|
+
}, ms);
|
|
43
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Facade over the Europe PMC REST API. Two methods:
|
|
48
|
+
* - `search()` — keyword search across MED/PMC/PPR/PAT/AGR.
|
|
49
|
+
* - `fullTextXml()` — JATS full text for an EPMC record.
|
|
50
|
+
*
|
|
51
|
+
* Both honor `ctx.signal` for cancellation and retry transient failures with
|
|
52
|
+
* capped exponential backoff plus jitter.
|
|
53
|
+
*/
|
|
54
|
+
export class EuropePmcService {
|
|
55
|
+
client;
|
|
56
|
+
queue;
|
|
57
|
+
maxRetries;
|
|
58
|
+
orderedXmlParser;
|
|
59
|
+
constructor(client, queue, maxRetries) {
|
|
60
|
+
this.client = client;
|
|
61
|
+
this.queue = queue;
|
|
62
|
+
this.maxRetries = maxRetries;
|
|
63
|
+
/**
|
|
64
|
+
* EPMC's fullTextXML is JATS Z39.96 — same DTD PMC uses — so the parser
|
|
65
|
+
* config mirrors `NcbiResponseHandler.orderedXmlParser`. `preserveOrder`
|
|
66
|
+
* keeps inline mixed content readable; `trimValues: false` retains spaces
|
|
67
|
+
* between text and inline children.
|
|
68
|
+
*/
|
|
69
|
+
this.orderedXmlParser = new XMLParser({
|
|
70
|
+
preserveOrder: true,
|
|
71
|
+
ignoreAttributes: false,
|
|
72
|
+
attributeNamePrefix: '@_',
|
|
73
|
+
parseTagValue: true,
|
|
74
|
+
trimValues: false,
|
|
75
|
+
processEntities: true,
|
|
76
|
+
htmlEntities: true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Search Europe PMC. Cursor-based pagination — pass `cursorMark: '*'` (or
|
|
81
|
+
* omit) for the first page; pass the returned `nextCursorMark` for the next.
|
|
82
|
+
*/
|
|
83
|
+
async search(params) {
|
|
84
|
+
const text = await this.queue.enqueue(() => this.withRetry(() => this.client.search(params), 'search', params.signal), 'search', params.signal);
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw serializationError('Failed to parse Europe PMC search JSON response.', {
|
|
91
|
+
reason: 'europepmc_invalid_response',
|
|
92
|
+
responseSnippet: text.substring(0, 200),
|
|
93
|
+
...recoveryFor('europepmc_invalid_response'),
|
|
94
|
+
}, { cause: error });
|
|
95
|
+
}
|
|
96
|
+
const hits = ensureArray(parsed.resultList?.result);
|
|
97
|
+
const echoed = parsed.request?.queryString ?? params.query;
|
|
98
|
+
// EPMC echoes back the input cursor mark on the final page, so absence of
|
|
99
|
+
// an explicit "next" or equality with the request's cursor marks the end.
|
|
100
|
+
const cursorMark = parsed.request?.cursorMark ?? params.cursorMark ?? '*';
|
|
101
|
+
const nextCursor = parsed.nextCursorMark && parsed.nextCursorMark !== cursorMark
|
|
102
|
+
? parsed.nextCursorMark
|
|
103
|
+
: undefined;
|
|
104
|
+
return {
|
|
105
|
+
hits,
|
|
106
|
+
hitCount: parsed.hitCount ?? hits.length,
|
|
107
|
+
...(nextCursor && { nextCursorMark: nextCursor }),
|
|
108
|
+
cursorMark,
|
|
109
|
+
query: echoed,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Fetch the JATS full text for an EPMC record. Returns:
|
|
114
|
+
* - `{ kind: 'found', xml, epmcId, source }` — JATS XML string usable
|
|
115
|
+
* directly by tool callers that hold their own parser, or via
|
|
116
|
+
* `parseFullTextXml()` for the parsed tree.
|
|
117
|
+
* - `{ kind: 'not-available', reason }` — EPMC has the record but
|
|
118
|
+
* publishes no fullTextXML (404 or empty body).
|
|
119
|
+
*/
|
|
120
|
+
async fullTextXml(epmcId, source, signal) {
|
|
121
|
+
const outcome = await this.queue.enqueue(() => this.withRetry(() => this.client.fullTextXml(epmcId, signal), `fullTextXml(${epmcId})`, signal), `fullTextXml(${epmcId})`, signal);
|
|
122
|
+
if (outcome.kind === 'not-available') {
|
|
123
|
+
return { kind: 'not-available', reason: outcome.reason };
|
|
124
|
+
}
|
|
125
|
+
return { kind: 'found', xml: outcome.xml, epmcId, source };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Parse a JATS XML string into the ordered node tree consumed by
|
|
129
|
+
* `parsePmcArticle`. Returns the `<article>` JatsNode, or `undefined` when
|
|
130
|
+
* the body doesn't contain an article element (malformed / empty).
|
|
131
|
+
*
|
|
132
|
+
* Throws `SerializationError` only for fundamentally invalid XML; an
|
|
133
|
+
* article-free but well-formed body returns `undefined` so callers can
|
|
134
|
+
* surface a `no-epmc-fulltext` outcome without a hard failure.
|
|
135
|
+
*/
|
|
136
|
+
parseFullTextXml(xml) {
|
|
137
|
+
const validationResult = XMLValidator.validate(xml.replace(/<!DOCTYPE[^>]*>/gi, ''));
|
|
138
|
+
if (validationResult !== true) {
|
|
139
|
+
throw serializationError('Received invalid XML from Europe PMC.', {
|
|
140
|
+
reason: 'europepmc_invalid_response',
|
|
141
|
+
responseSnippet: xml.substring(0, 200),
|
|
142
|
+
...recoveryFor('europepmc_invalid_response'),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = this.orderedXmlParser.parse(xml);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const parserError = error instanceof Error ? error.message : String(error);
|
|
151
|
+
throw serializationError(`Failed to parse Europe PMC fullTextXML response: ${parserError}`, {
|
|
152
|
+
reason: 'europepmc_invalid_response',
|
|
153
|
+
parserError,
|
|
154
|
+
responseSnippet: xml.substring(0, 200),
|
|
155
|
+
...recoveryFor('europepmc_invalid_response'),
|
|
156
|
+
}, { cause: error });
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(parsed))
|
|
159
|
+
return;
|
|
160
|
+
const nodes = parsed;
|
|
161
|
+
return nodes.find((n) => 'article' in n);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Retry wrapper for transient errors. Mirrors NCBI's `withRetry` minus the
|
|
165
|
+
* service-level deadline — EPMC requests are cheaper individually and the
|
|
166
|
+
* caller (typically `ctx.signal`) bounds the total chain.
|
|
167
|
+
*/
|
|
168
|
+
async withRetry(execute, label, signal) {
|
|
169
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
170
|
+
if (signal?.aborted)
|
|
171
|
+
throw signal.reason;
|
|
172
|
+
try {
|
|
173
|
+
return await execute();
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (signal?.aborted)
|
|
177
|
+
throw signal.reason;
|
|
178
|
+
if (!(error instanceof McpError))
|
|
179
|
+
throw error;
|
|
180
|
+
if (!RETRYABLE_CODES.has(error.code))
|
|
181
|
+
throw error;
|
|
182
|
+
if (attempt < this.maxRetries) {
|
|
183
|
+
const baseDelay = Math.min(1000 * 2 ** attempt, MAX_BACKOFF_MS);
|
|
184
|
+
const jitter = baseDelay * (0.75 + 0.5 * Math.random());
|
|
185
|
+
const retryDelay = Math.round(jitter);
|
|
186
|
+
logger.warning(`Europe PMC ${label} failed. Retrying (${attempt + 1}/${this.maxRetries}) in ${retryDelay}ms.`, requestContextService.createRequestContext({
|
|
187
|
+
operation: 'EuropePmcRetry',
|
|
188
|
+
label,
|
|
189
|
+
attempt: attempt + 1,
|
|
190
|
+
retryDelay,
|
|
191
|
+
}));
|
|
192
|
+
await abortableSleep(retryDelay, signal);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const attempts = this.maxRetries + 1;
|
|
196
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
197
|
+
throw new McpError(error.code, `${msg} (failed after ${attempts} attempts)`, {
|
|
198
|
+
reason: 'europepmc_unreachable',
|
|
199
|
+
label,
|
|
200
|
+
attempts,
|
|
201
|
+
...recoveryFor('europepmc_unreachable'),
|
|
202
|
+
}, { cause: error });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
throw internalError('Europe PMC request failed after all retries.', {
|
|
206
|
+
reason: 'europepmc_unreachable',
|
|
207
|
+
label,
|
|
208
|
+
...recoveryFor('europepmc_unreachable'),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// ─── Init / Accessor ────────────────────────────────────────────────────────
|
|
213
|
+
let _service;
|
|
214
|
+
/**
|
|
215
|
+
* Initialize the Europe PMC service when enabled. Safe to call regardless of
|
|
216
|
+
* config — `EUROPEPMC_ENABLED=false` leaves the service unset so callers see
|
|
217
|
+
* `undefined` and skip the chain step.
|
|
218
|
+
*/
|
|
219
|
+
export function initEuropePmcService() {
|
|
220
|
+
const config = getServerConfig();
|
|
221
|
+
if (!config.europepmcEnabled) {
|
|
222
|
+
logger.info('Europe PMC service disabled (EUROPEPMC_ENABLED=false).');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const client = new EuropePmcApiClient({
|
|
226
|
+
timeoutMs: config.europepmcTimeoutMs,
|
|
227
|
+
...(config.europepmcEmail && { email: config.europepmcEmail }),
|
|
228
|
+
});
|
|
229
|
+
const queue = new EuropePmcRequestQueue(config.europepmcRequestDelayMs);
|
|
230
|
+
_service = new EuropePmcService(client, queue, config.europepmcMaxRetries);
|
|
231
|
+
logger.info('Europe PMC service initialized.', requestContextService.createRequestContext({
|
|
232
|
+
operation: 'EuropePmcInit',
|
|
233
|
+
requestDelayMs: config.europepmcRequestDelayMs,
|
|
234
|
+
maxRetries: config.europepmcMaxRetries,
|
|
235
|
+
timeoutMs: config.europepmcTimeoutMs,
|
|
236
|
+
hasEmail: !!config.europepmcEmail,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
/** Returns the initialized service, or `undefined` when EPMC is disabled. */
|
|
240
|
+
export function getEuropePmcService() {
|
|
241
|
+
return _service;
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=europe-pmc-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"europe-pmc-service.js","sourceRoot":"","sources":["../../../src/services/europe-pmc/europe-pmc-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rate-limited request scheduler for Europe PMC calls. Caps
|
|
3
|
+
* concurrent in-flight requests and enforces a minimum start-gap between
|
|
4
|
+
* dispatches to stay polite with EBI's infrastructure. Independent rate
|
|
5
|
+
* domain from NCBI's queue — Europe PMC runs on a different host with its
|
|
6
|
+
* own limits.
|
|
7
|
+
* @module src/services/europe-pmc/request-queue
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Schedules Europe PMC requests against two independent ceilings:
|
|
11
|
+
*
|
|
12
|
+
* - **Throughput** (`minStartGapMs`): minimum delay between two consecutive
|
|
13
|
+
* dispatch times. Defaults to 200ms to be polite with EBI.
|
|
14
|
+
* - **Concurrency** (`maxConcurrent`): maximum simultaneous in-flight
|
|
15
|
+
* requests. Decouples concurrency from rate so slow upstream responses
|
|
16
|
+
* don't block new dispatches.
|
|
17
|
+
*
|
|
18
|
+
* Enqueue accepts an optional `AbortSignal` so callers can bound their total
|
|
19
|
+
* time inside the scheduler — when the signal fires, a still-waiting task
|
|
20
|
+
* rejects immediately instead of sitting behind a saturated worker.
|
|
21
|
+
*/
|
|
22
|
+
export declare class EuropePmcRequestQueue {
|
|
23
|
+
private readonly waiters;
|
|
24
|
+
private readonly minStartGapMs;
|
|
25
|
+
private readonly maxConcurrent;
|
|
26
|
+
private inFlight;
|
|
27
|
+
private lastStartTime;
|
|
28
|
+
private nextDispatchTimer;
|
|
29
|
+
constructor(minStartGapMs: number, maxConcurrent?: number);
|
|
30
|
+
enqueue<T>(task: () => Promise<T>, label: string, signal?: AbortSignal): Promise<T>;
|
|
31
|
+
private tryDispatch;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=request-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-queue.d.ts","sourceRoot":"","sources":["../../../src/services/europe-pmc/request-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH;;;;;;;;;;;;GAYG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,iBAAiB,CAA4C;gBAEzD,aAAa,EAAE,MAAM,EAAE,aAAa,GAAE,MAA+B;IAKjF,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;IA+BnF,OAAO,CAAC,WAAW;CAoDpB"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rate-limited request scheduler for Europe PMC calls. Caps
|
|
3
|
+
* concurrent in-flight requests and enforces a minimum start-gap between
|
|
4
|
+
* dispatches to stay polite with EBI's infrastructure. Independent rate
|
|
5
|
+
* domain from NCBI's queue — Europe PMC runs on a different host with its
|
|
6
|
+
* own limits.
|
|
7
|
+
* @module src/services/europe-pmc/request-queue
|
|
8
|
+
*/
|
|
9
|
+
import { logger, requestContextService } from '@cyanheads/mcp-ts-core/utils';
|
|
10
|
+
const DEFAULT_MAX_CONCURRENT = 4;
|
|
11
|
+
/**
|
|
12
|
+
* Schedules Europe PMC requests against two independent ceilings:
|
|
13
|
+
*
|
|
14
|
+
* - **Throughput** (`minStartGapMs`): minimum delay between two consecutive
|
|
15
|
+
* dispatch times. Defaults to 200ms to be polite with EBI.
|
|
16
|
+
* - **Concurrency** (`maxConcurrent`): maximum simultaneous in-flight
|
|
17
|
+
* requests. Decouples concurrency from rate so slow upstream responses
|
|
18
|
+
* don't block new dispatches.
|
|
19
|
+
*
|
|
20
|
+
* Enqueue accepts an optional `AbortSignal` so callers can bound their total
|
|
21
|
+
* time inside the scheduler — when the signal fires, a still-waiting task
|
|
22
|
+
* rejects immediately instead of sitting behind a saturated worker.
|
|
23
|
+
*/
|
|
24
|
+
export class EuropePmcRequestQueue {
|
|
25
|
+
waiters = [];
|
|
26
|
+
minStartGapMs;
|
|
27
|
+
maxConcurrent;
|
|
28
|
+
inFlight = 0;
|
|
29
|
+
lastStartTime = 0;
|
|
30
|
+
nextDispatchTimer;
|
|
31
|
+
constructor(minStartGapMs, maxConcurrent = DEFAULT_MAX_CONCURRENT) {
|
|
32
|
+
this.minStartGapMs = minStartGapMs;
|
|
33
|
+
this.maxConcurrent = maxConcurrent;
|
|
34
|
+
}
|
|
35
|
+
enqueue(task, label, signal) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
if (signal?.aborted) {
|
|
38
|
+
reject(signal.reason);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const waiter = {
|
|
42
|
+
resolve,
|
|
43
|
+
reject,
|
|
44
|
+
task,
|
|
45
|
+
label,
|
|
46
|
+
...(signal && { signal }),
|
|
47
|
+
};
|
|
48
|
+
if (signal) {
|
|
49
|
+
const onAbort = () => {
|
|
50
|
+
const idx = this.waiters.indexOf(waiter);
|
|
51
|
+
if (idx === -1)
|
|
52
|
+
return;
|
|
53
|
+
this.waiters.splice(idx, 1);
|
|
54
|
+
reject(signal.reason);
|
|
55
|
+
};
|
|
56
|
+
waiter.onAbort = onAbort;
|
|
57
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
58
|
+
}
|
|
59
|
+
this.waiters.push(waiter);
|
|
60
|
+
this.tryDispatch();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
tryDispatch() {
|
|
64
|
+
if (this.nextDispatchTimer !== undefined)
|
|
65
|
+
return;
|
|
66
|
+
while (this.inFlight < this.maxConcurrent && this.waiters.length > 0) {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const gap = this.minStartGapMs - (now - this.lastStartTime);
|
|
69
|
+
if (gap > 0) {
|
|
70
|
+
this.nextDispatchTimer = setTimeout(() => {
|
|
71
|
+
this.nextDispatchTimer = undefined;
|
|
72
|
+
this.tryDispatch();
|
|
73
|
+
}, gap);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const waiter = this.waiters.shift();
|
|
77
|
+
if (!waiter)
|
|
78
|
+
return;
|
|
79
|
+
if (waiter.signal && waiter.onAbort) {
|
|
80
|
+
waiter.signal.removeEventListener('abort', waiter.onAbort);
|
|
81
|
+
}
|
|
82
|
+
this.lastStartTime = Date.now();
|
|
83
|
+
this.inFlight += 1;
|
|
84
|
+
logger.debug(`Executing Europe PMC request via queue: ${waiter.label}`, requestContextService.createRequestContext({
|
|
85
|
+
operation: 'EuropePmcQueueDispatch',
|
|
86
|
+
label: waiter.label,
|
|
87
|
+
inFlight: this.inFlight,
|
|
88
|
+
queueDepth: this.waiters.length,
|
|
89
|
+
}));
|
|
90
|
+
Promise.resolve()
|
|
91
|
+
.then(() => waiter.task())
|
|
92
|
+
.then(waiter.resolve, (err) => {
|
|
93
|
+
logger.error('Error processing Europe PMC request from queue.', requestContextService.createRequestContext({
|
|
94
|
+
operation: 'EuropePmcQueueProcess',
|
|
95
|
+
label: waiter.label,
|
|
96
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
97
|
+
}));
|
|
98
|
+
waiter.reject(err);
|
|
99
|
+
})
|
|
100
|
+
.finally(() => {
|
|
101
|
+
this.inFlight -= 1;
|
|
102
|
+
this.tryDispatch();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=request-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-queue.js","sourceRoot":"","sources":["../../../src/services/europe-pmc/request-queue.ts"],"names":[],"mappings":"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"}
|