@cyanheads/pubmed-mcp-server 2.6.11 → 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/CLAUDE.md +2 -2
- package/README.md +36 -14
- package/dist/config/server-config.d.ts +6 -0
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +52 -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/find-related.tool.d.ts +0 -1
- package/dist/mcp-server/tools/definitions/find-related.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/find-related.tool.js +21 -41
- package/dist/mcp-server/tools/definitions/find-related.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/ncbi-service.d.ts +6 -7
- package/dist/services/ncbi/ncbi-service.d.ts.map +1 -1
- package/dist/services/ncbi/ncbi-service.js +11 -11
- package/dist/services/ncbi/ncbi-service.js.map +1 -1
- package/dist/services/ncbi/request-queue.d.ts +26 -11
- package/dist/services/ncbi/request-queue.d.ts.map +1 -1
- package/dist/services/ncbi/request-queue.js +94 -52
- package/dist/services/ncbi/request-queue.js.map +1 -1
- 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 +7 -6
- package/server.json +4 -4
|
@@ -1,88 +1,130 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Rate-limited request scheduler for NCBI E-utility calls. Caps
|
|
3
|
+
* concurrent in-flight requests and enforces a minimum start-gap between
|
|
4
|
+
* dispatches to stay within NCBI's documented per-second ceiling. Supports
|
|
5
|
+
* abort-during-wait so callers' deadlines can bound queue time end-to-end.
|
|
4
6
|
* @module src/services/ncbi/request-queue
|
|
5
7
|
*/
|
|
6
8
|
import { JsonRpcErrorCode, McpError } from '@cyanheads/mcp-ts-core/errors';
|
|
7
9
|
import { logger, requestContextService } from '@cyanheads/mcp-ts-core/utils';
|
|
8
10
|
import { recoveryFor } from '../../services/error-contracts.js';
|
|
11
|
+
const DEFAULT_MAX_CONCURRENT = 8;
|
|
9
12
|
const DEFAULT_MAX_QUEUE_SIZE = 100;
|
|
10
13
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* Schedules NCBI API requests against two independent ceilings:
|
|
15
|
+
*
|
|
16
|
+
* - **Throughput** (`minStartGapMs`): minimum delay between two consecutive
|
|
17
|
+
* dispatch times. Matches NCBI's per-second start-rate cap (≈3/s without
|
|
18
|
+
* an API key, ≈10/s with one).
|
|
19
|
+
* - **Concurrency** (`maxConcurrent`): maximum number of requests in flight
|
|
20
|
+
* simultaneously. Decouples concurrency from rate, so slow upstream
|
|
21
|
+
* responses don't block new dispatches.
|
|
22
|
+
*
|
|
23
|
+
* Enqueue accepts an optional `AbortSignal` so callers can bound their total
|
|
24
|
+
* time inside the scheduler — when the signal fires, a still-waiting task
|
|
25
|
+
* rejects immediately instead of sitting behind a saturated worker for
|
|
26
|
+
* minutes.
|
|
13
27
|
*/
|
|
14
28
|
export class NcbiRequestQueue {
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
waiters = [];
|
|
30
|
+
minStartGapMs;
|
|
31
|
+
maxConcurrent;
|
|
17
32
|
maxQueueSize;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
inFlight = 0;
|
|
34
|
+
lastStartTime = 0;
|
|
35
|
+
nextDispatchTimer;
|
|
36
|
+
constructor(minStartGapMs, maxConcurrent = DEFAULT_MAX_CONCURRENT, maxQueueSize = DEFAULT_MAX_QUEUE_SIZE) {
|
|
37
|
+
this.minStartGapMs = minStartGapMs;
|
|
38
|
+
this.maxConcurrent = maxConcurrent;
|
|
22
39
|
this.maxQueueSize = maxQueueSize;
|
|
23
40
|
}
|
|
24
|
-
enqueue(task, endpoint, params) {
|
|
25
|
-
if (this.
|
|
41
|
+
enqueue(task, endpoint, params, signal) {
|
|
42
|
+
if (this.waiters.length >= this.maxQueueSize) {
|
|
26
43
|
return Promise.reject(new McpError(JsonRpcErrorCode.RateLimited, `NCBI request queue is full (max ${this.maxQueueSize}).`, {
|
|
27
44
|
reason: 'queue_full',
|
|
28
45
|
endpoint,
|
|
29
|
-
queueSize: this.
|
|
46
|
+
queueSize: this.waiters.length,
|
|
30
47
|
...recoveryFor('queue_full'),
|
|
31
48
|
}));
|
|
32
49
|
}
|
|
33
50
|
return new Promise((resolve, reject) => {
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
if (signal?.aborted) {
|
|
52
|
+
reject(signal.reason);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const waiter = {
|
|
56
|
+
resolve,
|
|
36
57
|
reject,
|
|
37
58
|
task,
|
|
38
59
|
endpoint,
|
|
39
60
|
params,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
...(signal && { signal }),
|
|
62
|
+
};
|
|
63
|
+
if (signal) {
|
|
64
|
+
const onAbort = () => {
|
|
65
|
+
const idx = this.waiters.indexOf(waiter);
|
|
66
|
+
if (idx === -1)
|
|
67
|
+
return; // already dispatched — the task forwards its own signal
|
|
68
|
+
this.waiters.splice(idx, 1);
|
|
69
|
+
reject(signal.reason);
|
|
70
|
+
};
|
|
71
|
+
waiter.onAbort = onAbort;
|
|
72
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
43
73
|
}
|
|
74
|
+
this.waiters.push(waiter);
|
|
75
|
+
this.tryDispatch();
|
|
44
76
|
});
|
|
45
77
|
}
|
|
46
|
-
|
|
47
|
-
if (this.
|
|
48
|
-
return;
|
|
49
|
-
this.processing = true;
|
|
50
|
-
const item = this.queue.shift();
|
|
51
|
-
if (!item) {
|
|
52
|
-
this.processing = false;
|
|
78
|
+
tryDispatch() {
|
|
79
|
+
if (this.nextDispatchTimer !== undefined)
|
|
53
80
|
return;
|
|
54
|
-
|
|
55
|
-
const { resolve, reject, task, endpoint } = item;
|
|
56
|
-
try {
|
|
81
|
+
while (this.inFlight < this.maxConcurrent && this.waiters.length > 0) {
|
|
57
82
|
const now = Date.now();
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
logger.debug(`Delaying NCBI request by ${wait}ms to respect rate limit.`, requestContextService.createRequestContext({
|
|
83
|
+
const gap = this.minStartGapMs - (now - this.lastStartTime);
|
|
84
|
+
if (gap > 0) {
|
|
85
|
+
logger.debug(`Delaying next NCBI dispatch by ${gap}ms to respect rate limit.`, requestContextService.createRequestContext({
|
|
62
86
|
operation: 'NcbiQueueWait',
|
|
63
|
-
|
|
64
|
-
delayMs: wait,
|
|
87
|
+
delayMs: gap,
|
|
65
88
|
}));
|
|
66
|
-
|
|
89
|
+
this.nextDispatchTimer = setTimeout(() => {
|
|
90
|
+
this.nextDispatchTimer = undefined;
|
|
91
|
+
this.tryDispatch();
|
|
92
|
+
}, gap);
|
|
93
|
+
return;
|
|
67
94
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
operation: 'NcbiQueueProcess',
|
|
76
|
-
endpoint,
|
|
77
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
78
|
-
}));
|
|
79
|
-
reject(error);
|
|
80
|
-
}
|
|
81
|
-
finally {
|
|
82
|
-
this.processing = false;
|
|
83
|
-
if (this.queue.length > 0) {
|
|
84
|
-
queueMicrotask(() => this.processQueue());
|
|
95
|
+
const waiter = this.waiters.shift();
|
|
96
|
+
if (!waiter)
|
|
97
|
+
return;
|
|
98
|
+
// Detach the cancel listener now that we're about to start; the task is
|
|
99
|
+
// responsible for forwarding its own signal to downstream I/O.
|
|
100
|
+
if (waiter.signal && waiter.onAbort) {
|
|
101
|
+
waiter.signal.removeEventListener('abort', waiter.onAbort);
|
|
85
102
|
}
|
|
103
|
+
this.lastStartTime = Date.now();
|
|
104
|
+
this.inFlight += 1;
|
|
105
|
+
logger.info(`Executing NCBI request via queue: ${waiter.endpoint}`, requestContextService.createRequestContext({
|
|
106
|
+
operation: 'NcbiQueueDispatch',
|
|
107
|
+
endpoint: waiter.endpoint,
|
|
108
|
+
inFlight: this.inFlight,
|
|
109
|
+
queueDepth: this.waiters.length,
|
|
110
|
+
}));
|
|
111
|
+
// `Promise.resolve().then(() => task())` converts any sync throw from
|
|
112
|
+
// `task()` into a rejected promise so the finally() bookkeeping always
|
|
113
|
+
// runs.
|
|
114
|
+
Promise.resolve()
|
|
115
|
+
.then(() => waiter.task())
|
|
116
|
+
.then(waiter.resolve, (err) => {
|
|
117
|
+
logger.error('Error processing NCBI request from queue.', requestContextService.createRequestContext({
|
|
118
|
+
operation: 'NcbiQueueProcess',
|
|
119
|
+
endpoint: waiter.endpoint,
|
|
120
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
121
|
+
}));
|
|
122
|
+
waiter.reject(err);
|
|
123
|
+
})
|
|
124
|
+
.finally(() => {
|
|
125
|
+
this.inFlight -= 1;
|
|
126
|
+
this.tryDispatch();
|
|
127
|
+
});
|
|
86
128
|
}
|
|
87
129
|
}
|
|
88
130
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-queue.js","sourceRoot":"","sources":["../../../src/services/ncbi/request-queue.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"request-queue.js","sourceRoot":"","sources":["../../../src/services/ncbi/request-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -133,7 +133,7 @@ function sanitizeNcbiError(message) {
|
|
|
133
133
|
* E-utilities use `<ERROR>` for response-level failures, while PMC EFetch
|
|
134
134
|
* uses lowercase `<error id="…">` to flag a single unavailable PMCID. The
|
|
135
135
|
* latter is data (a missing ID), not a transport error, so it falls through
|
|
136
|
-
* to the caller which reports it via `
|
|
136
|
+
* to the caller which reports it via the unified `unavailable[]` list.
|
|
137
137
|
*/
|
|
138
138
|
const ERROR_TAG_REGEX = /<ERROR(?:\s[^>]*)?>/;
|
|
139
139
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unpaywall-service.d.ts","sourceRoot":"","sources":["../../../src/services/unpaywall/unpaywall-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EAEzB,MAAM,YAAY,CAAC;AAIpB;;;GAGG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM;IAGpC;;;;OAIG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"unpaywall-service.d.ts","sourceRoot":"","sources":["../../../src/services/unpaywall/unpaywall-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EAEzB,MAAM,YAAY,CAAC;AAIpB;;;GAGG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM;IAGpC;;;;OAIG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA2D9E;;;;;OAKG;IACG,YAAY,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAsBlF,OAAO;CAkDtB;AAiBD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAc3C;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,SAAS,CAElE"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* resolution; only genuine service failures (5xx, network, timeout) throw.
|
|
8
8
|
* @module src/services/unpaywall/unpaywall-service
|
|
9
9
|
*/
|
|
10
|
-
import { serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
10
|
+
import { JsonRpcErrorCode, McpError, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
11
11
|
import { fetchWithTimeout, httpErrorFromResponse, logger, requestContextService, } from '@cyanheads/mcp-ts-core/utils';
|
|
12
12
|
import { getServerConfig } from '../../config/server-config.js';
|
|
13
13
|
import { recoveryFor } from '../../services/error-contracts.js';
|
|
@@ -35,6 +35,11 @@ export class UnpaywallService {
|
|
|
35
35
|
return { kind: 'no-oa', reason: 'Invalid DOI' };
|
|
36
36
|
const url = `${UNPAYWALL_API_BASE}/${encodeURIComponent(normalized)}?email=${encodeURIComponent(this.email)}`;
|
|
37
37
|
const ctx = requestContextService.createRequestContext({ operation: 'UnpaywallResolve', doi });
|
|
38
|
+
// `fetchWithTimeout` throws on any non-2xx response, so the explicit
|
|
39
|
+
// `response.status === 404` / `=== 422` checks the API actually returns
|
|
40
|
+
// arrive as `McpError(NotFound)` / `McpError(ValidationError)` here, not as
|
|
41
|
+
// a response object. Translate those into the documented `no-oa` outcomes
|
|
42
|
+
// before falling back to the service-unavailable wrap.
|
|
38
43
|
let response;
|
|
39
44
|
try {
|
|
40
45
|
response = await fetchWithTimeout(url, this.timeoutMs, ctx, {
|
|
@@ -43,6 +48,14 @@ export class UnpaywallService {
|
|
|
43
48
|
});
|
|
44
49
|
}
|
|
45
50
|
catch (error) {
|
|
51
|
+
if (error instanceof McpError) {
|
|
52
|
+
if (error.code === JsonRpcErrorCode.NotFound) {
|
|
53
|
+
return { kind: 'no-oa', reason: 'DOI unknown to Unpaywall' };
|
|
54
|
+
}
|
|
55
|
+
if (error.code === JsonRpcErrorCode.ValidationError) {
|
|
56
|
+
return { kind: 'no-oa', reason: 'Invalid DOI format' };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
46
59
|
const msg = error instanceof Error ? error.message : String(error);
|
|
47
60
|
throw serviceUnavailable(`Unpaywall request failed: ${msg}`, {
|
|
48
61
|
reason: 'unpaywall_unreachable',
|
|
@@ -50,18 +63,6 @@ export class UnpaywallService {
|
|
|
50
63
|
...recoveryFor('unpaywall_unreachable'),
|
|
51
64
|
}, { cause: error });
|
|
52
65
|
}
|
|
53
|
-
if (response.status === 404)
|
|
54
|
-
return { kind: 'no-oa', reason: 'DOI unknown to Unpaywall' };
|
|
55
|
-
if (response.status === 422)
|
|
56
|
-
return { kind: 'no-oa', reason: 'Invalid DOI format' };
|
|
57
|
-
if (!response.ok) {
|
|
58
|
-
const body = await response.text().catch(() => '');
|
|
59
|
-
throw await httpErrorFromResponse(response, {
|
|
60
|
-
service: 'Unpaywall',
|
|
61
|
-
captureBody: false,
|
|
62
|
-
data: { doi: normalized, body: body.substring(0, 300) },
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
66
|
const data = (await response.json());
|
|
66
67
|
if (!data.is_oa)
|
|
67
68
|
return { kind: 'no-oa', reason: 'No open-access copy indexed' };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unpaywall-service.js","sourceRoot":"","sources":["../../../src/services/unpaywall/unpaywall-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"unpaywall-service.js","sourceRoot":"","sources":["../../../src/services/unpaywall/unpaywall-service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/pubmed-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"mcpName": "io.github.cyanheads/pubmed-mcp-server",
|
|
5
|
-
"description": "
|
|
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",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"e-utilities",
|
|
41
41
|
"open-access",
|
|
42
42
|
"full-text",
|
|
43
|
-
"unpaywall"
|
|
43
|
+
"unpaywall",
|
|
44
|
+
"europe-pmc"
|
|
44
45
|
],
|
|
45
46
|
"repository": {
|
|
46
47
|
"type": "git",
|
|
@@ -71,17 +72,17 @@
|
|
|
71
72
|
"access": "public"
|
|
72
73
|
},
|
|
73
74
|
"dependencies": {
|
|
74
|
-
"@cyanheads/mcp-ts-core": "^0.9.
|
|
75
|
+
"@cyanheads/mcp-ts-core": "^0.9.1",
|
|
75
76
|
"defuddle": "^0.18.1",
|
|
76
77
|
"fast-xml-parser": "~5.7.3",
|
|
77
78
|
"linkedom": "^0.18.12",
|
|
78
79
|
"pino-pretty": "^13.1.3",
|
|
79
|
-
"sanitize-html": "^2.17.
|
|
80
|
+
"sanitize-html": "^2.17.4",
|
|
80
81
|
"unpdf": "^1.6.2"
|
|
81
82
|
},
|
|
82
83
|
"devDependencies": {
|
|
83
84
|
"@biomejs/biome": "^2.4.15",
|
|
84
|
-
"@types/node": "^25.
|
|
85
|
+
"@types/node": "^25.9.0",
|
|
85
86
|
"@types/sanitize-html": "^2.16.1",
|
|
86
87
|
"@vitest/coverage-istanbul": "^4.1.6",
|
|
87
88
|
"depcheck": "^1.4.7",
|
package/server.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.cyanheads/pubmed-mcp-server",
|
|
4
|
-
"description": "Search PubMed, fetch articles and full text,
|
|
4
|
+
"description": "Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/cyanheads/pubmed-mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.7.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.
|
|
22
|
+
"version": "2.7.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.
|
|
69
|
+
"version": "2.7.0",
|
|
70
70
|
"packageArguments": [
|
|
71
71
|
{
|
|
72
72
|
"type": "positional",
|