@cyanheads/pubmed-mcp-server 2.8.0 → 2.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.
Files changed (40) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/mcp-server/tools/definitions/find-related.tool.d.ts +40 -1
  7. package/dist/mcp-server/tools/definitions/find-related.tool.d.ts.map +1 -1
  8. package/dist/mcp-server/tools/definitions/find-related.tool.js +314 -79
  9. package/dist/mcp-server/tools/definitions/find-related.tool.js.map +1 -1
  10. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts.map +1 -1
  11. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js +17 -4
  12. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js.map +1 -1
  13. package/dist/services/error-contracts.d.ts +19 -1
  14. package/dist/services/error-contracts.d.ts.map +1 -1
  15. package/dist/services/error-contracts.js +27 -1
  16. package/dist/services/error-contracts.js.map +1 -1
  17. package/dist/services/europe-pmc/api-client.d.ts +14 -0
  18. package/dist/services/europe-pmc/api-client.d.ts.map +1 -1
  19. package/dist/services/europe-pmc/api-client.js +42 -0
  20. package/dist/services/europe-pmc/api-client.js.map +1 -1
  21. package/dist/services/europe-pmc/europe-pmc-service.d.ts +28 -1
  22. package/dist/services/europe-pmc/europe-pmc-service.d.ts.map +1 -1
  23. package/dist/services/europe-pmc/europe-pmc-service.js +59 -0
  24. package/dist/services/europe-pmc/europe-pmc-service.js.map +1 -1
  25. package/dist/services/europe-pmc/types.d.ts +31 -0
  26. package/dist/services/europe-pmc/types.d.ts.map +1 -1
  27. package/dist/services/openalex/api-client.d.ts +39 -0
  28. package/dist/services/openalex/api-client.d.ts.map +1 -0
  29. package/dist/services/openalex/api-client.js +174 -0
  30. package/dist/services/openalex/api-client.js.map +1 -0
  31. package/dist/services/openalex/openalex-service.d.ts +59 -0
  32. package/dist/services/openalex/openalex-service.d.ts.map +1 -0
  33. package/dist/services/openalex/openalex-service.js +195 -0
  34. package/dist/services/openalex/openalex-service.js.map +1 -0
  35. package/dist/services/openalex/types.d.ts +56 -0
  36. package/dist/services/openalex/types.d.ts.map +1 -0
  37. package/dist/services/openalex/types.js +15 -0
  38. package/dist/services/openalex/types.js.map +1 -0
  39. package/package.json +1 -1
  40. package/server.json +3 -3
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @fileoverview Low-level HTTP client for the OpenAlex API. Builds URLs,
3
+ * injects the optional polite-pool email, and exposes single-attempt fetch
4
+ * calls. Retry logic lives in `OpenAlexService`.
5
+ * @module src/services/openalex/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 { OPENALEX_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 OpenAlex. Single-attempt — retries upstream. */
13
+ export class OpenAlexApiClient {
14
+ config;
15
+ constructor(config) {
16
+ this.config = config;
17
+ }
18
+ /**
19
+ * Fetch a single work by PMID. Returns the work record (with related_works
20
+ * and referenced_works populated) or null when the PMID is unknown.
21
+ */
22
+ async getWorkByPmid(pmid, signal) {
23
+ const params = new URLSearchParams({
24
+ select: 'id,related_works,referenced_works',
25
+ });
26
+ if (this.config.email)
27
+ params.set('mailto', this.config.email);
28
+ const url = `${OPENALEX_API_BASE}/works/pmid:${encodeURIComponent(pmid)}?${params.toString()}`;
29
+ const ctx = requestContextService.createRequestContext({
30
+ operation: 'OpenAlexGetWorkByPmid',
31
+ pmid,
32
+ });
33
+ let response;
34
+ try {
35
+ response = await fetchWithTimeout(url, this.config.timeoutMs, ctx, {
36
+ headers: { Accept: 'application/json', 'User-Agent': USER_AGENT },
37
+ ...(signal && { signal }),
38
+ });
39
+ }
40
+ catch (error) {
41
+ if (error instanceof McpError)
42
+ throw error;
43
+ const msg = error instanceof Error ? error.message : String(error);
44
+ throw serviceUnavailable(`OpenAlex request failed: ${msg}`, { reason: 'openalex_unreachable', ...recoveryFor('openalex_unreachable') }, { cause: error });
45
+ }
46
+ if (response.status === 404)
47
+ return null;
48
+ if (!response.ok) {
49
+ throw await httpErrorFromResponse(response, {
50
+ service: 'OpenAlex',
51
+ data: { url, reason: 'openalex_unreachable', ...recoveryFor('openalex_unreachable') },
52
+ });
53
+ }
54
+ const text = await response.text();
55
+ try {
56
+ return JSON.parse(text);
57
+ }
58
+ catch (error) {
59
+ throw serviceUnavailable('OpenAlex returned a non-JSON body.', { reason: 'openalex_invalid_response', ...recoveryFor('openalex_invalid_response') }, { cause: error });
60
+ }
61
+ }
62
+ /**
63
+ * Batch-resolve a list of OpenAlex work IDs to their PMIDs.
64
+ * Uses the filter=openalex:W1|W2|... endpoint with select=id,ids.
65
+ * Returns only records that carry a `pmid` field.
66
+ */
67
+ resolveOaIdsToPmids(oaIds, signal) {
68
+ if (oaIds.length === 0)
69
+ return Promise.resolve([]);
70
+ // Strip https://openalex.org/ prefix if present — filter expects bare IDs
71
+ const bareIds = oaIds.map((id) => id.startsWith('https://openalex.org/') ? id.slice('https://openalex.org/'.length) : id);
72
+ const params = new URLSearchParams({
73
+ filter: `openalex:${bareIds.join('|')}`,
74
+ select: 'id,ids',
75
+ per_page: String(Math.min(oaIds.length, 200)),
76
+ });
77
+ if (this.config.email)
78
+ params.set('mailto', this.config.email);
79
+ const url = `${OPENALEX_API_BASE}/works?${params.toString()}`;
80
+ return this.fetchWorksList(url, 'OpenAlexResolveOaIds', signal);
81
+ }
82
+ /**
83
+ * Fetch works that cite a given OpenAlex work ID (the cited_by relationship).
84
+ * Returns up to `perPage` records with PMIDs.
85
+ */
86
+ async getCitedBy(oaId, perPage, signal) {
87
+ // Strip full URL prefix if needed
88
+ const bareId = oaId.startsWith('https://openalex.org/')
89
+ ? oaId.slice('https://openalex.org/'.length)
90
+ : oaId;
91
+ const params = new URLSearchParams({
92
+ filter: `cites:${bareId}`,
93
+ select: 'id,ids',
94
+ per_page: String(Math.min(perPage, 200)),
95
+ });
96
+ if (this.config.email)
97
+ params.set('mailto', this.config.email);
98
+ const url = `${OPENALEX_API_BASE}/works?${params.toString()}`;
99
+ const ctx = requestContextService.createRequestContext({
100
+ operation: 'OpenAlexGetCitedBy',
101
+ oaId: bareId,
102
+ });
103
+ let response;
104
+ try {
105
+ response = await fetchWithTimeout(url, this.config.timeoutMs, ctx, {
106
+ headers: { Accept: 'application/json', 'User-Agent': USER_AGENT },
107
+ ...(signal && { signal }),
108
+ });
109
+ }
110
+ catch (error) {
111
+ if (error instanceof McpError)
112
+ throw error;
113
+ const msg = error instanceof Error ? error.message : String(error);
114
+ throw serviceUnavailable(`OpenAlex request failed: ${msg}`, { reason: 'openalex_unreachable', ...recoveryFor('openalex_unreachable') }, { cause: error });
115
+ }
116
+ if (!response.ok) {
117
+ throw await httpErrorFromResponse(response, {
118
+ service: 'OpenAlex',
119
+ data: { url, reason: 'openalex_unreachable', ...recoveryFor('openalex_unreachable') },
120
+ });
121
+ }
122
+ const text = await response.text();
123
+ let parsed;
124
+ try {
125
+ parsed = JSON.parse(text);
126
+ }
127
+ catch (error) {
128
+ throw serviceUnavailable('OpenAlex returned a non-JSON body.', { reason: 'openalex_invalid_response', ...recoveryFor('openalex_invalid_response') }, { cause: error });
129
+ }
130
+ logger.debug('OpenAlex cited_by response', requestContextService.createRequestContext({
131
+ operation: 'OpenAlexGetCitedByDone',
132
+ oaId: bareId,
133
+ totalCount: parsed.meta?.count,
134
+ resultCount: parsed.results?.length,
135
+ }));
136
+ return {
137
+ works: parsed.results ?? [],
138
+ totalCount: parsed.meta?.count ?? 0,
139
+ };
140
+ }
141
+ /** Helper: fetch a /works?... URL and return parsed results. */
142
+ async fetchWorksList(url, operation, signal) {
143
+ const ctx = requestContextService.createRequestContext({ operation });
144
+ let response;
145
+ try {
146
+ response = await fetchWithTimeout(url, this.config.timeoutMs, ctx, {
147
+ headers: { Accept: 'application/json', 'User-Agent': USER_AGENT },
148
+ ...(signal && { signal }),
149
+ });
150
+ }
151
+ catch (error) {
152
+ if (error instanceof McpError)
153
+ throw error;
154
+ const msg = error instanceof Error ? error.message : String(error);
155
+ throw serviceUnavailable(`OpenAlex request failed: ${msg}`, { reason: 'openalex_unreachable', ...recoveryFor('openalex_unreachable') }, { cause: error });
156
+ }
157
+ if (!response.ok) {
158
+ throw await httpErrorFromResponse(response, {
159
+ service: 'OpenAlex',
160
+ data: { url, reason: 'openalex_unreachable', ...recoveryFor('openalex_unreachable') },
161
+ });
162
+ }
163
+ const text = await response.text();
164
+ let parsed;
165
+ try {
166
+ parsed = JSON.parse(text);
167
+ }
168
+ catch (error) {
169
+ throw serviceUnavailable('OpenAlex returned a non-JSON body.', { reason: 'openalex_invalid_response', ...recoveryFor('openalex_invalid_response') }, { cause: error });
170
+ }
171
+ return parsed.results ?? [];
172
+ }
173
+ }
174
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../../../src/services/openalex/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @fileoverview OpenAlex service for related-article fallback in `pubmed_find_related`.
3
+ * Provides three capabilities that mirror the NCBI eLink relationships:
4
+ * - `similar(pmid, n)`: related_works → PMIDs (mirrors pubmed_pubmed)
5
+ * - `citedBy(pmid, n)`: cites:W<id> filter → PMIDs (mirrors pubmed_pubmed_citedin)
6
+ * - `references(pmid, n)`: referenced_works → PMIDs (mirrors pubmed_pubmed_refs)
7
+ *
8
+ * All three methods drop records with no PMID — never mints fake IDs.
9
+ * Uses the NCBI_ADMIN_EMAIL config (adminEmail) as the OpenAlex polite-pool
10
+ * `mailto=` parameter when set; omits it when unset.
11
+ *
12
+ * @module src/services/openalex/openalex-service
13
+ */
14
+ import { OpenAlexApiClient } from './api-client.js';
15
+ /** Service facade over the OpenAlex API for the find-related provider chain. */
16
+ export declare class OpenAlexService {
17
+ private readonly client;
18
+ private readonly maxRetries;
19
+ constructor(client: OpenAlexApiClient, maxRetries: number);
20
+ /**
21
+ * Find works with similar content to the given PMID via OpenAlex `related_works`.
22
+ * Returns PMIDs only; drops any record with no PMID.
23
+ * `n` is the number of PMIDs to return (OpenAlex caps related_works at ~10).
24
+ */
25
+ similar(pmid: string, n: number, signal?: AbortSignal): Promise<{
26
+ pmids: string[];
27
+ totalCount: number;
28
+ }>;
29
+ /**
30
+ * Find works that cite the given PMID via OpenAlex `cites:W<id>` filter.
31
+ * Returns PMIDs only; drops any record with no PMID.
32
+ */
33
+ citedBy(pmid: string, n: number, signal?: AbortSignal): Promise<{
34
+ pmids: string[];
35
+ totalCount: number;
36
+ }>;
37
+ /**
38
+ * Find works referenced by the given PMID via OpenAlex `referenced_works`.
39
+ * Returns PMIDs only; drops any record with no PMID.
40
+ */
41
+ references(pmid: string, n: number, signal?: AbortSignal): Promise<{
42
+ pmids: string[];
43
+ totalCount: number;
44
+ }>;
45
+ /**
46
+ * Extract unique numeric PMIDs from a list of works, excluding the source PMID.
47
+ * Any work with no PMID is silently dropped (never minted).
48
+ */
49
+ private extractPmids;
50
+ /** Retry wrapper for transient errors, mirroring the EPMC service pattern. */
51
+ private withRetry;
52
+ }
53
+ /** Initialize the OpenAlex service. Call from `setup()` in createApp. */
54
+ export declare function initOpenAlexService(): void;
55
+ /** Get the initialized OpenAlex service. Throws if not initialized. */
56
+ export declare function getOpenAlexService(): OpenAlexService;
57
+ /** Returns the service if initialized, undefined otherwise (for optional use). */
58
+ export declare function getOpenAlexServiceOptional(): OpenAlexService | undefined;
59
+ //# sourceMappingURL=openalex-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openalex-service.d.ts","sourceRoot":"","sources":["../../../src/services/openalex/openalex-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAyCpD,gFAAgF;AAChF,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;gBADV,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,MAAM;IAGrC;;;;OAIG;IACG,OAAO,CACX,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAqBnD;;;OAGG;IACG,OAAO,CACX,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAkBnD;;;OAGG;IACG,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAqBnD;;;OAGG;IACH,OAAO,CAAC,YAAY;IAYpB,8EAA8E;YAChE,SAAS;CAqDxB;AAMD,yEAAyE;AACzE,wBAAgB,mBAAmB,IAAI,IAAI,CAiB1C;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,IAAI,eAAe,CAIpD;AAED,kFAAkF;AAClF,wBAAgB,0BAA0B,IAAI,eAAe,GAAG,SAAS,CAExE"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * @fileoverview OpenAlex service for related-article fallback in `pubmed_find_related`.
3
+ * Provides three capabilities that mirror the NCBI eLink relationships:
4
+ * - `similar(pmid, n)`: related_works → PMIDs (mirrors pubmed_pubmed)
5
+ * - `citedBy(pmid, n)`: cites:W<id> filter → PMIDs (mirrors pubmed_pubmed_citedin)
6
+ * - `references(pmid, n)`: referenced_works → PMIDs (mirrors pubmed_pubmed_refs)
7
+ *
8
+ * All three methods drop records with no PMID — never mints fake IDs.
9
+ * Uses the NCBI_ADMIN_EMAIL config (adminEmail) as the OpenAlex polite-pool
10
+ * `mailto=` parameter when set; omits it when unset.
11
+ *
12
+ * @module src/services/openalex/openalex-service
13
+ */
14
+ import { internalError, JsonRpcErrorCode, McpError } from '@cyanheads/mcp-ts-core/errors';
15
+ import { logger, requestContextService } from '@cyanheads/mcp-ts-core/utils';
16
+ import { getServerConfig } from '../../config/server-config.js';
17
+ import { recoveryFor } from '../../services/error-contracts.js';
18
+ import { OpenAlexApiClient } from './api-client.js';
19
+ /** Transient codes eligible for retry. */
20
+ const RETRYABLE_CODES = new Set([
21
+ JsonRpcErrorCode.ServiceUnavailable,
22
+ JsonRpcErrorCode.Timeout,
23
+ JsonRpcErrorCode.RateLimited,
24
+ ]);
25
+ const MAX_BACKOFF_MS = 30_000;
26
+ function abortableSleep(ms, signal) {
27
+ if (!signal)
28
+ return new Promise((r) => setTimeout(r, ms));
29
+ if (signal.aborted)
30
+ return Promise.reject(signal.reason);
31
+ return new Promise((resolve, reject) => {
32
+ const onAbort = () => {
33
+ clearTimeout(timer);
34
+ reject(signal.reason);
35
+ };
36
+ const timer = setTimeout(() => {
37
+ signal.removeEventListener('abort', onAbort);
38
+ resolve();
39
+ }, ms);
40
+ signal.addEventListener('abort', onAbort, { once: true });
41
+ });
42
+ }
43
+ /**
44
+ * Extract a PMID string from an OpenAlex Work record's `ids.pmid` field.
45
+ * OpenAlex encodes PMIDs as full URLs: "https://pubmed.ncbi.nlm.nih.gov/31295471"
46
+ * This normalizes to the bare numeric string. Returns null when absent.
47
+ */
48
+ function extractPmid(work) {
49
+ const raw = work.ids?.pmid;
50
+ if (!raw)
51
+ return null;
52
+ // Strip URL prefix if present (e.g. "https://pubmed.ncbi.nlm.nih.gov/31295471")
53
+ const match = /(\d+)\s*$/.exec(raw);
54
+ return match?.[1] ?? null;
55
+ }
56
+ /** Service facade over the OpenAlex API for the find-related provider chain. */
57
+ export class OpenAlexService {
58
+ client;
59
+ maxRetries;
60
+ constructor(client, maxRetries) {
61
+ this.client = client;
62
+ this.maxRetries = maxRetries;
63
+ }
64
+ /**
65
+ * Find works with similar content to the given PMID via OpenAlex `related_works`.
66
+ * Returns PMIDs only; drops any record with no PMID.
67
+ * `n` is the number of PMIDs to return (OpenAlex caps related_works at ~10).
68
+ */
69
+ async similar(pmid, n, signal) {
70
+ const work = await this.withRetry(() => this.client.getWorkByPmid(pmid, signal), `getWorkByPmid(${pmid})`, signal);
71
+ if (!work)
72
+ return { pmids: [], totalCount: 0 };
73
+ const relatedIds = (work.related_works ?? []).slice(0, Math.min(n * 3, 50));
74
+ if (relatedIds.length === 0)
75
+ return { pmids: [], totalCount: 0 };
76
+ const resolved = await this.withRetry(() => this.client.resolveOaIdsToPmids(relatedIds, signal), `resolveRelatedWorks(${pmid})`, signal);
77
+ const pmids = this.extractPmids(resolved, pmid).slice(0, n);
78
+ return { pmids, totalCount: work.related_works?.length ?? 0 };
79
+ }
80
+ /**
81
+ * Find works that cite the given PMID via OpenAlex `cites:W<id>` filter.
82
+ * Returns PMIDs only; drops any record with no PMID.
83
+ */
84
+ async citedBy(pmid, n, signal) {
85
+ const work = await this.withRetry(() => this.client.getWorkByPmid(pmid, signal), `getWorkByPmid(${pmid})`, signal);
86
+ if (!work)
87
+ return { pmids: [], totalCount: 0 };
88
+ const { works, totalCount } = await this.withRetry(() => this.client.getCitedBy(work.id, n, signal), `getCitedBy(${work.id})`, signal);
89
+ const pmids = this.extractPmids(works, pmid);
90
+ return { pmids, totalCount };
91
+ }
92
+ /**
93
+ * Find works referenced by the given PMID via OpenAlex `referenced_works`.
94
+ * Returns PMIDs only; drops any record with no PMID.
95
+ */
96
+ async references(pmid, n, signal) {
97
+ const work = await this.withRetry(() => this.client.getWorkByPmid(pmid, signal), `getWorkByPmid(${pmid})`, signal);
98
+ if (!work)
99
+ return { pmids: [], totalCount: 0 };
100
+ const refIds = (work.referenced_works ?? []).slice(0, Math.min(n * 3, 200));
101
+ if (refIds.length === 0)
102
+ return { pmids: [], totalCount: 0 };
103
+ const resolved = await this.withRetry(() => this.client.resolveOaIdsToPmids(refIds, signal), `resolveReferencedWorks(${pmid})`, signal);
104
+ const pmids = this.extractPmids(resolved, pmid).slice(0, n);
105
+ return { pmids, totalCount: work.referenced_works?.length ?? 0 };
106
+ }
107
+ /**
108
+ * Extract unique numeric PMIDs from a list of works, excluding the source PMID.
109
+ * Any work with no PMID is silently dropped (never minted).
110
+ */
111
+ extractPmids(works, excludePmid) {
112
+ const seen = new Set();
113
+ const result = [];
114
+ for (const w of works) {
115
+ const pmid = extractPmid(w);
116
+ if (!pmid || pmid === excludePmid || seen.has(pmid))
117
+ continue;
118
+ seen.add(pmid);
119
+ result.push(pmid);
120
+ }
121
+ return result;
122
+ }
123
+ /** Retry wrapper for transient errors, mirroring the EPMC service pattern. */
124
+ async withRetry(execute, label, signal) {
125
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
126
+ if (signal?.aborted)
127
+ throw signal.reason;
128
+ try {
129
+ return await execute();
130
+ }
131
+ catch (error) {
132
+ if (signal?.aborted)
133
+ throw signal.reason;
134
+ if (!(error instanceof McpError))
135
+ throw error;
136
+ if (!RETRYABLE_CODES.has(error.code))
137
+ throw error;
138
+ if (attempt < this.maxRetries) {
139
+ const baseDelay = Math.min(1000 * 2 ** attempt, MAX_BACKOFF_MS);
140
+ const jitter = baseDelay * (0.75 + 0.5 * Math.random());
141
+ const retryDelay = Math.round(jitter);
142
+ logger.warning(`OpenAlex ${label} failed. Retrying (${attempt + 1}/${this.maxRetries}) in ${retryDelay}ms.`, requestContextService.createRequestContext({
143
+ operation: 'OpenAlexRetry',
144
+ label,
145
+ attempt: attempt + 1,
146
+ retryDelay,
147
+ }));
148
+ await abortableSleep(retryDelay, signal);
149
+ continue;
150
+ }
151
+ const attempts = this.maxRetries + 1;
152
+ const msg = error instanceof Error ? error.message : String(error);
153
+ throw new McpError(error.code, `${msg} (failed after ${attempts} attempts)`, {
154
+ reason: 'openalex_unreachable',
155
+ label,
156
+ attempts,
157
+ ...recoveryFor('openalex_unreachable'),
158
+ }, { cause: error });
159
+ }
160
+ }
161
+ throw internalError('OpenAlex request failed after all retries.', {
162
+ reason: 'openalex_unreachable',
163
+ ...recoveryFor('openalex_unreachable'),
164
+ });
165
+ }
166
+ }
167
+ // ─── Init / Accessor ────────────────────────────────────────────────────────
168
+ let _service;
169
+ /** Initialize the OpenAlex service. Call from `setup()` in createApp. */
170
+ export function initOpenAlexService() {
171
+ const config = getServerConfig();
172
+ const client = new OpenAlexApiClient({
173
+ timeoutMs: config.europepmcTimeoutMs, // reuse EPMC timeout — same order of magnitude
174
+ ...(config.adminEmail && { email: config.adminEmail }),
175
+ });
176
+ // Reuse EPMC retry config — same upstream reliability tier
177
+ _service = new OpenAlexService(client, config.europepmcMaxRetries);
178
+ logger.info('OpenAlex service initialized.', requestContextService.createRequestContext({
179
+ operation: 'OpenAlexInit',
180
+ hasEmail: !!config.adminEmail,
181
+ maxRetries: config.europepmcMaxRetries,
182
+ timeoutMs: config.europepmcTimeoutMs,
183
+ }));
184
+ }
185
+ /** Get the initialized OpenAlex service. Throws if not initialized. */
186
+ export function getOpenAlexService() {
187
+ if (!_service)
188
+ throw new Error('OpenAlex service not initialized. Call initOpenAlexService() first.');
189
+ return _service;
190
+ }
191
+ /** Returns the service if initialized, undefined otherwise (for optional use). */
192
+ export function getOpenAlexServiceOptional() {
193
+ return _service;
194
+ }
195
+ //# sourceMappingURL=openalex-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openalex-service.js","sourceRoot":"","sources":["../../../src/services/openalex/openalex-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @fileoverview Types for the OpenAlex API. Covers the work lookup (PMID resolve),
3
+ * related/referenced works resolution, and cited-by filter endpoints.
4
+ *
5
+ * See https://docs.openalex.org/api-entities/works for the full API.
6
+ * We use three endpoints:
7
+ * - GET /works/pmid:{pmid}?select=id,related_works,referenced_works
8
+ * - GET /works?filter=openalex:{id}|{id}&select=id,ids (batch PMID resolve)
9
+ * - GET /works?filter=cites:{id}&select=id,ids (cited_by)
10
+ *
11
+ * @module src/services/openalex/types
12
+ */
13
+ /** Base URL for the OpenAlex public API. */
14
+ export declare const OPENALEX_API_BASE = "https://api.openalex.org";
15
+ /**
16
+ * Identifier block on an OpenAlex Work. Only `openalex` (the OA ID) is always
17
+ * present; `pmid` is absent for non-PubMed records. We only care about PMIDs —
18
+ * any record without one is dropped.
19
+ */
20
+ export interface OpenAlexWorkIds {
21
+ doi?: string;
22
+ mag?: string;
23
+ openalex?: string;
24
+ pmid?: string;
25
+ [key: string]: string | undefined;
26
+ }
27
+ /**
28
+ * Minimal Work record — only fields the service actually uses. We use
29
+ * `select=id,ids` on batch calls to keep responses small.
30
+ */
31
+ export interface OpenAlexWork {
32
+ /** OpenAlex canonical ID, e.g. "https://openalex.org/W2960163646". */
33
+ id: string;
34
+ ids?: OpenAlexWorkIds;
35
+ /** IDs of referenced works (this work's reference list). Only present when requested. */
36
+ referenced_works?: string[];
37
+ /** IDs of related works (content-based similarity). Only present when requested. */
38
+ related_works?: string[];
39
+ }
40
+ /** Top-level response shape from the /works collection endpoint. */
41
+ export interface OpenAlexWorksResponse {
42
+ meta?: {
43
+ count?: number;
44
+ page?: number;
45
+ per_page?: number;
46
+ next_cursor?: string;
47
+ };
48
+ results?: OpenAlexWork[];
49
+ [key: string]: unknown;
50
+ }
51
+ /** Resolved PMID list returned by service methods. */
52
+ export interface OpenAlexRelatedResult {
53
+ pmids: string[];
54
+ totalCount: number;
55
+ }
56
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/openalex/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,6BAA6B,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,eAAe,CAAC;IACtB,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,oFAAoF;IACpF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,oEAAoE;AACpE,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,sDAAsD;AACtD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview Types for the OpenAlex API. Covers the work lookup (PMID resolve),
3
+ * related/referenced works resolution, and cited-by filter endpoints.
4
+ *
5
+ * See https://docs.openalex.org/api-entities/works for the full API.
6
+ * We use three endpoints:
7
+ * - GET /works/pmid:{pmid}?select=id,related_works,referenced_works
8
+ * - GET /works?filter=openalex:{id}|{id}&select=id,ids (batch PMID resolve)
9
+ * - GET /works?filter=cites:{id}&select=id,ids (cited_by)
10
+ *
11
+ * @module src/services/openalex/types
12
+ */
13
+ /** Base URL for the OpenAlex public API. */
14
+ export const OPENALEX_API_BASE = 'https://api.openalex.org';
15
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/openalex/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,4CAA4C;AAC5C,MAAM,CAAC,MAAM,iBAAiB,GAAG,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/pubmed-mcp-server",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "mcpName": "io.github.cyanheads/pubmed-mcp-server",
5
5
  "description": "Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/cyanheads/pubmed-mcp-server",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.8.0",
9
+ "version": "2.9.0",
10
10
  "remotes": [
11
11
  {
12
12
  "type": "streamable-http",
@@ -19,7 +19,7 @@
19
19
  "registryBaseUrl": "https://registry.npmjs.org",
20
20
  "identifier": "@cyanheads/pubmed-mcp-server",
21
21
  "runtimeHint": "bun",
22
- "version": "2.8.0",
22
+ "version": "2.9.0",
23
23
  "packageArguments": [
24
24
  {
25
25
  "type": "positional",
@@ -66,7 +66,7 @@
66
66
  "registryBaseUrl": "https://registry.npmjs.org",
67
67
  "identifier": "@cyanheads/pubmed-mcp-server",
68
68
  "runtimeHint": "bun",
69
- "version": "2.8.0",
69
+ "version": "2.9.0",
70
70
  "packageArguments": [
71
71
  {
72
72
  "type": "positional",