@cyanheads/zenodo-mcp-server 0.1.1
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/AGENTS.md +435 -0
- package/CLAUDE.md +435 -0
- package/Dockerfile +140 -0
- package/LICENSE +201 -0
- package/README.md +292 -0
- package/changelog/0.1.x/0.1.1.md +21 -0
- package/changelog/template.md +151 -0
- package/dist/config/server-config.d.ts +13 -0
- package/dist/config/server-config.d.ts.map +1 -0
- package/dist/config/server-config.js +19 -0
- package/dist/config/server-config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/get-record.tool.d.ts +186 -0
- package/dist/mcp-server/tools/definitions/get-record.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/get-record.tool.js +568 -0
- package/dist/mcp-server/tools/definitions/get-record.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/index.d.ts +721 -0
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/index.js +21 -0
- package/dist/mcp-server/tools/definitions/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/list-files.tool.d.ts +108 -0
- package/dist/mcp-server/tools/definitions/list-files.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/list-files.tool.js +348 -0
- package/dist/mcp-server/tools/definitions/list-files.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/list-versions.tool.d.ts +92 -0
- package/dist/mcp-server/tools/definitions/list-versions.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/list-versions.tool.js +268 -0
- package/dist/mcp-server/tools/definitions/list-versions.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.d.ts +99 -0
- package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.js +324 -0
- package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/read-file.tool.d.ts +104 -0
- package/dist/mcp-server/tools/definitions/read-file.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/read-file.tool.js +370 -0
- package/dist/mcp-server/tools/definitions/read-file.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/search-records.tool.d.ts +181 -0
- package/dist/mcp-server/tools/definitions/search-records.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/search-records.tool.js +531 -0
- package/dist/mcp-server/tools/definitions/search-records.tool.js.map +1 -0
- package/dist/mcp-server/tools/record-miss.d.ts +37 -0
- package/dist/mcp-server/tools/record-miss.d.ts.map +1 -0
- package/dist/mcp-server/tools/record-miss.js +51 -0
- package/dist/mcp-server/tools/record-miss.js.map +1 -0
- package/dist/mcp-server/tools/render.d.ts +31 -0
- package/dist/mcp-server/tools/render.d.ts.map +1 -0
- package/dist/mcp-server/tools/render.js +53 -0
- package/dist/mcp-server/tools/render.js.map +1 -0
- package/dist/mcp-server/tools/schema-helpers.d.ts +20 -0
- package/dist/mcp-server/tools/schema-helpers.d.ts.map +1 -0
- package/dist/mcp-server/tools/schema-helpers.js +44 -0
- package/dist/mcp-server/tools/schema-helpers.js.map +1 -0
- package/dist/services/zenodo/cache.d.ts +25 -0
- package/dist/services/zenodo/cache.d.ts.map +1 -0
- package/dist/services/zenodo/cache.js +58 -0
- package/dist/services/zenodo/cache.js.map +1 -0
- package/dist/services/zenodo/html-to-text.d.ts +23 -0
- package/dist/services/zenodo/html-to-text.d.ts.map +1 -0
- package/dist/services/zenodo/html-to-text.js +148 -0
- package/dist/services/zenodo/html-to-text.js.map +1 -0
- package/dist/services/zenodo/http.d.ts +73 -0
- package/dist/services/zenodo/http.d.ts.map +1 -0
- package/dist/services/zenodo/http.js +378 -0
- package/dist/services/zenodo/http.js.map +1 -0
- package/dist/services/zenodo/identifiers.d.ts +104 -0
- package/dist/services/zenodo/identifiers.d.ts.map +1 -0
- package/dist/services/zenodo/identifiers.js +261 -0
- package/dist/services/zenodo/identifiers.js.map +1 -0
- package/dist/services/zenodo/normalize.d.ts +29 -0
- package/dist/services/zenodo/normalize.d.ts.map +1 -0
- package/dist/services/zenodo/normalize.js +324 -0
- package/dist/services/zenodo/normalize.js.map +1 -0
- package/dist/services/zenodo/query-builder.d.ts +87 -0
- package/dist/services/zenodo/query-builder.d.ts.map +1 -0
- package/dist/services/zenodo/query-builder.js +221 -0
- package/dist/services/zenodo/query-builder.js.map +1 -0
- package/dist/services/zenodo/resource-types.d.ts +34 -0
- package/dist/services/zenodo/resource-types.d.ts.map +1 -0
- package/dist/services/zenodo/resource-types.js +97 -0
- package/dist/services/zenodo/resource-types.js.map +1 -0
- package/dist/services/zenodo/text-preview.d.ts +56 -0
- package/dist/services/zenodo/text-preview.d.ts.map +1 -0
- package/dist/services/zenodo/text-preview.js +151 -0
- package/dist/services/zenodo/text-preview.js.map +1 -0
- package/dist/services/zenodo/types.d.ts +517 -0
- package/dist/services/zenodo/types.d.ts.map +1 -0
- package/dist/services/zenodo/types.js +8 -0
- package/dist/services/zenodo/types.js.map +1 -0
- package/dist/services/zenodo/zenodo-service.d.ts +98 -0
- package/dist/services/zenodo/zenodo-service.d.ts.map +1 -0
- package/dist/services/zenodo/zenodo-service.js +392 -0
- package/dist/services/zenodo/zenodo-service.js.map +1 -0
- package/package.json +110 -0
- package/server.json +113 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The single fetch boundary to the Zenodo REST API. Per-call status
|
|
3
|
+
* accept-lists (206/301/302/403/404/410/416 are outcomes here, not errors), the
|
|
4
|
+
* rate-limit header gate, two pacers (search and general buckets), and the retry
|
|
5
|
+
* matrix. Only paths the service builds are fetched, against a constant base URL,
|
|
6
|
+
* and redirects are followed only within zenodo.org.
|
|
7
|
+
* @module services/zenodo/http
|
|
8
|
+
*/
|
|
9
|
+
import { JsonRpcErrorCode, McpError, rateLimited, serviceUnavailable, timeout, } from '@cyanheads/mcp-ts-core/errors';
|
|
10
|
+
import { createPacer, httpErrorFromResponse, withRetry, } from '@cyanheads/mcp-ts-core/utils';
|
|
11
|
+
const ORIGIN = 'https://zenodo.org';
|
|
12
|
+
const BASE_URL = `${ORIGIN}/api`;
|
|
13
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
14
|
+
const MAX_REDIRECTS = 3;
|
|
15
|
+
/** Largest JSON or text body read; the heaviest upstream page observed is 8.4 MB. */
|
|
16
|
+
const MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
17
|
+
/** Per-attempt budgets. Multi-thousand-file record GETs legitimately take 32–34 s. */
|
|
18
|
+
export const JSON_ATTEMPT_MS = 45_000;
|
|
19
|
+
export const CONTENT_ATTEMPT_MS = 20_000;
|
|
20
|
+
const LADDER_DEADLINE_MS = 50_000;
|
|
21
|
+
const PACER_MAX_WAIT_MS = 20_000;
|
|
22
|
+
/** A 504 slower than this is Zenodo's ~30 s gateway cutoff — deterministic, never retried. */
|
|
23
|
+
const SLOW_GATEWAY_MS = 10_000;
|
|
24
|
+
function headerNumber(headers, name) {
|
|
25
|
+
const value = headers.get(name);
|
|
26
|
+
if (value === null || value.trim() === '')
|
|
27
|
+
return;
|
|
28
|
+
const n = Number(value);
|
|
29
|
+
return Number.isFinite(n) ? n : undefined;
|
|
30
|
+
}
|
|
31
|
+
function looksLikeHtml(text) {
|
|
32
|
+
return /^\s*<(?:!doctype\s+html|html[\s>]|head[\s>]|body[\s>])/i.test(text);
|
|
33
|
+
}
|
|
34
|
+
/** Cancels a response body that will not be read, releasing the connection. */
|
|
35
|
+
export async function discardBody(res) {
|
|
36
|
+
await res.body?.cancel().catch(() => undefined);
|
|
37
|
+
}
|
|
38
|
+
/** Reads a UTF-8 body of at most {@link MAX_BODY_BYTES}; a larger one is refused, not buffered. */
|
|
39
|
+
export async function readText(res) {
|
|
40
|
+
const { bytes, more } = await readCapped(res, MAX_BODY_BYTES);
|
|
41
|
+
if (more) {
|
|
42
|
+
throw serviceUnavailable(`Zenodo returned a response body over ${MAX_BODY_BYTES} bytes; it was not read.`, { status: res.status, retryable: false });
|
|
43
|
+
}
|
|
44
|
+
return new TextDecoder().decode(bytes);
|
|
45
|
+
}
|
|
46
|
+
/** Reads a JSON body. An HTML page or unparseable body on a 2xx is a transient upstream fault. */
|
|
47
|
+
export async function readJson(res) {
|
|
48
|
+
const text = await readText(res);
|
|
49
|
+
if (res.ok && looksLikeHtml(text)) {
|
|
50
|
+
throw serviceUnavailable('Zenodo returned an HTML page instead of JSON (edge error page).', {
|
|
51
|
+
status: res.status,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(text);
|
|
56
|
+
}
|
|
57
|
+
catch (cause) {
|
|
58
|
+
throw serviceUnavailable('Zenodo returned a body that is not valid JSON.', { status: res.status }, { cause });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Streams a body, keeping at most `maxBytes` after skipping `skipBytes`, and
|
|
63
|
+
* cancels the stream as soon as the cap is passed. `more` is true when bytes
|
|
64
|
+
* remained past the cap.
|
|
65
|
+
*/
|
|
66
|
+
export async function readCapped(res, maxBytes, skipBytes = 0) {
|
|
67
|
+
const reader = res.body?.getReader();
|
|
68
|
+
if (!reader)
|
|
69
|
+
return { bytes: new Uint8Array(0), more: false };
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let kept = 0;
|
|
72
|
+
let toSkip = skipBytes;
|
|
73
|
+
let more = false;
|
|
74
|
+
for (;;) {
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
if (done)
|
|
77
|
+
break;
|
|
78
|
+
let chunk = value;
|
|
79
|
+
if (toSkip > 0) {
|
|
80
|
+
const drop = Math.min(toSkip, chunk.length);
|
|
81
|
+
toSkip -= drop;
|
|
82
|
+
chunk = chunk.subarray(drop);
|
|
83
|
+
if (chunk.length === 0)
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (kept + chunk.length > maxBytes) {
|
|
87
|
+
chunks.push(chunk.subarray(0, maxBytes - kept));
|
|
88
|
+
kept = maxBytes;
|
|
89
|
+
more = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
chunks.push(chunk);
|
|
93
|
+
kept += chunk.length;
|
|
94
|
+
}
|
|
95
|
+
if (more)
|
|
96
|
+
await reader.cancel().catch(() => undefined);
|
|
97
|
+
const bytes = new Uint8Array(kept);
|
|
98
|
+
let offset = 0;
|
|
99
|
+
for (const c of chunks) {
|
|
100
|
+
bytes.set(c, offset);
|
|
101
|
+
offset += c.length;
|
|
102
|
+
}
|
|
103
|
+
return { bytes, more };
|
|
104
|
+
}
|
|
105
|
+
/** The HTTP client: pacing, header gate, retry ladder, and status mapping. */
|
|
106
|
+
export class ZenodoHttp {
|
|
107
|
+
#general;
|
|
108
|
+
#headers = { search: {}, general: {} };
|
|
109
|
+
#search;
|
|
110
|
+
#token;
|
|
111
|
+
#userAgent;
|
|
112
|
+
constructor(options) {
|
|
113
|
+
this.#token = options.accessToken;
|
|
114
|
+
this.#userAgent = options.userAgent;
|
|
115
|
+
const cooldown = { baseMs: 2_000, maxMs: 60_000 };
|
|
116
|
+
this.#search = createPacer({
|
|
117
|
+
name: 'zenodo-search',
|
|
118
|
+
limits: [{ requests: 25, perMs: 60_000 }],
|
|
119
|
+
maxConcurrent: 2,
|
|
120
|
+
cooldown,
|
|
121
|
+
});
|
|
122
|
+
this.#general = createPacer({
|
|
123
|
+
name: 'zenodo-general',
|
|
124
|
+
limits: this.#token
|
|
125
|
+
? [
|
|
126
|
+
{ requests: 90, perMs: 60_000 },
|
|
127
|
+
{ requests: 4_800, perMs: 3_600_000 },
|
|
128
|
+
]
|
|
129
|
+
: [
|
|
130
|
+
{ requests: 55, perMs: 60_000 },
|
|
131
|
+
{ requests: 1_900, perMs: 3_600_000 },
|
|
132
|
+
],
|
|
133
|
+
maxConcurrent: 4,
|
|
134
|
+
cooldown,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Rejects queued waiters and clears the pacers' timers. */
|
|
138
|
+
dispose() {
|
|
139
|
+
this.#search.dispose();
|
|
140
|
+
this.#general.dispose();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Runs one request through the retry ladder (outside) and the bucket's pacer
|
|
144
|
+
* (inside). `read` consumes the response of an accepted status and runs inside
|
|
145
|
+
* the attempt, so parse faults are retried with the fetch.
|
|
146
|
+
*/
|
|
147
|
+
async request(req, read, ctx) {
|
|
148
|
+
try {
|
|
149
|
+
return await withRetry((attempt) => this.#paced(req, read, ctx, attempt), {
|
|
150
|
+
maxRetries: 1,
|
|
151
|
+
baseDelayMs: 1_000,
|
|
152
|
+
maxDelayMs: 15_000,
|
|
153
|
+
deadlineMs: LADDER_DEADLINE_MS,
|
|
154
|
+
signal: ctx.signal,
|
|
155
|
+
operation: req.operation,
|
|
156
|
+
context: ctx,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (!(error instanceof McpError))
|
|
161
|
+
throw error;
|
|
162
|
+
const reason = error.data?.reason;
|
|
163
|
+
if (reason === 'pacer_shed') {
|
|
164
|
+
const retryAfter = error.data?.retryAfter;
|
|
165
|
+
throw rateLimited("Zenodo request queue is full; this server's shared rate budget is spent for now.", {
|
|
166
|
+
reason: 'rate_limited',
|
|
167
|
+
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
168
|
+
...ctx.recoveryFor('rate_limited'),
|
|
169
|
+
}, { cause: error });
|
|
170
|
+
}
|
|
171
|
+
if (reason === 'retry_deadline_exceeded')
|
|
172
|
+
throw this.#timeoutError(req, ctx, error);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
#paced(req, read, ctx, attempt) {
|
|
177
|
+
const options = { signal: attempt.signal, maxWaitMs: PACER_MAX_WAIT_MS };
|
|
178
|
+
const task = (signal) => this.#dispatch(req, read, ctx, signal, attempt.remainingMs);
|
|
179
|
+
if (req.bucket === 'general')
|
|
180
|
+
return this.#general.run(task, options);
|
|
181
|
+
// A search also counts against the global budget: reserve a general start slot,
|
|
182
|
+
// then run in the search pacer, so a search 429 closes only the search gate.
|
|
183
|
+
return this.#general
|
|
184
|
+
.run(async () => undefined, options)
|
|
185
|
+
.then(() => this.#search.run(task, options));
|
|
186
|
+
}
|
|
187
|
+
async #dispatch(req, read, ctx, signal, remainingMs) {
|
|
188
|
+
this.#checkGate(req.bucket, ctx);
|
|
189
|
+
const budget = Math.max(1, Math.min(req.attemptMs, remainingMs));
|
|
190
|
+
const deadline = new AbortController();
|
|
191
|
+
const timer = setTimeout(() => deadline.abort(), budget);
|
|
192
|
+
const started = Date.now();
|
|
193
|
+
try {
|
|
194
|
+
const res = await this.#fetchWithinZenodo(req, AbortSignal.any([signal, deadline.signal]));
|
|
195
|
+
if (!req.okStatuses.includes(res.status)) {
|
|
196
|
+
throw await this.#statusError(req, res, ctx, Date.now() - started);
|
|
197
|
+
}
|
|
198
|
+
return await read(res);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error instanceof McpError)
|
|
202
|
+
throw error;
|
|
203
|
+
if (deadline.signal.aborted && !signal.aborted)
|
|
204
|
+
throw this.#timeoutError(req, ctx, error);
|
|
205
|
+
if (signal.aborted)
|
|
206
|
+
throw error;
|
|
207
|
+
throw serviceUnavailable(`Zenodo request failed before a response arrived (${req.operation}).`, { operation: req.operation }, { cause: error });
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Fetches with `redirect: 'manual'` and follows a redirect only while its target
|
|
215
|
+
* stays on zenodo.org, so neither the request nor its Authorization header can be
|
|
216
|
+
* sent to another host. A `manual` request gets the 3xx itself.
|
|
217
|
+
*/
|
|
218
|
+
async #fetchWithinZenodo(req, signal) {
|
|
219
|
+
let url = this.#url(req);
|
|
220
|
+
for (let hops = 0;; hops++) {
|
|
221
|
+
const res = await fetch(url, {
|
|
222
|
+
headers: this.#requestHeaders(req),
|
|
223
|
+
redirect: 'manual',
|
|
224
|
+
signal,
|
|
225
|
+
});
|
|
226
|
+
this.#track(req.bucket, res.headers);
|
|
227
|
+
const location = res.headers.get('location');
|
|
228
|
+
if (req.redirect === 'manual' || !REDIRECT_STATUSES.has(res.status) || location === null) {
|
|
229
|
+
return res;
|
|
230
|
+
}
|
|
231
|
+
await discardBody(res);
|
|
232
|
+
const next = URL.parse(location, url);
|
|
233
|
+
if (next?.origin !== ORIGIN || hops === MAX_REDIRECTS) {
|
|
234
|
+
throw serviceUnavailable(hops === MAX_REDIRECTS
|
|
235
|
+
? `Zenodo redirected ${req.operation} more than ${MAX_REDIRECTS} times.`
|
|
236
|
+
: `Zenodo redirected ${req.operation} off zenodo.org; the redirect was not followed.`, { status: res.status, retryable: false });
|
|
237
|
+
}
|
|
238
|
+
url = next.href;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
#url(req) {
|
|
242
|
+
const query = req.query?.length
|
|
243
|
+
? `?${new URLSearchParams(req.query)}`
|
|
244
|
+
: '';
|
|
245
|
+
return `${BASE_URL}${req.path}${query}`;
|
|
246
|
+
}
|
|
247
|
+
#requestHeaders(req) {
|
|
248
|
+
return {
|
|
249
|
+
Accept: req.accept,
|
|
250
|
+
'User-Agent': this.#userAgent,
|
|
251
|
+
...(this.#token ? { Authorization: `Bearer ${this.#token}` } : {}),
|
|
252
|
+
...(req.range ? { Range: req.range } : {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Records `X-RateLimit-*` for the bucket — on every response, error statuses included. */
|
|
256
|
+
#track(bucket, headers) {
|
|
257
|
+
const limit = headerNumber(headers, 'x-ratelimit-limit');
|
|
258
|
+
const remaining = headerNumber(headers, 'x-ratelimit-remaining');
|
|
259
|
+
const resetAt = headerNumber(headers, 'x-ratelimit-reset');
|
|
260
|
+
if (limit === undefined && remaining === undefined && resetAt === undefined)
|
|
261
|
+
return;
|
|
262
|
+
this.#headers[bucket] = {
|
|
263
|
+
...(limit !== undefined ? { limit } : {}),
|
|
264
|
+
...(remaining !== undefined ? { remaining } : {}),
|
|
265
|
+
...(resetAt !== undefined ? { resetAt } : {}),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** Refuses to dispatch while the bucket's advertised window is spent. */
|
|
269
|
+
#checkGate(bucket, ctx) {
|
|
270
|
+
const { remaining, resetAt } = this.#headers[bucket];
|
|
271
|
+
if (remaining === undefined || resetAt === undefined || remaining > 0)
|
|
272
|
+
return;
|
|
273
|
+
const waitMs = resetAt * 1000 - Date.now();
|
|
274
|
+
if (waitMs <= 0)
|
|
275
|
+
return;
|
|
276
|
+
throw rateLimited(`Zenodo's ${bucket} rate-limit window is spent until it resets.`, {
|
|
277
|
+
reason: 'rate_limited',
|
|
278
|
+
retryAfter: Math.ceil(waitMs / 1000),
|
|
279
|
+
...ctx.recoveryFor('rate_limited'),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async #statusError(req, res, ctx, elapsedMs) {
|
|
283
|
+
const status = res.status;
|
|
284
|
+
if (status === 429) {
|
|
285
|
+
const reset = headerNumber(res.headers, 'x-ratelimit-reset');
|
|
286
|
+
const retryAfter = headerNumber(res.headers, 'retry-after') ??
|
|
287
|
+
(reset === undefined ? undefined : Math.max(1, Math.ceil(reset - Date.now() / 1000)));
|
|
288
|
+
await discardBody(res);
|
|
289
|
+
return rateLimited('Zenodo answered HTTP 429 (rate limit reached).', {
|
|
290
|
+
reason: 'rate_limited',
|
|
291
|
+
status,
|
|
292
|
+
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
293
|
+
...ctx.recoveryFor('rate_limited'),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (status >= 500) {
|
|
297
|
+
await discardBody(res);
|
|
298
|
+
if (status === 504 && elapsedMs >= SLOW_GATEWAY_MS)
|
|
299
|
+
return this.#timeoutError(req, ctx);
|
|
300
|
+
if (status === 500)
|
|
301
|
+
return this.#serverError(req, ctx);
|
|
302
|
+
return serviceUnavailable(`Zenodo answered HTTP ${status} (${req.operation}).`, { status });
|
|
303
|
+
}
|
|
304
|
+
const error = await httpErrorFromResponse(res, {
|
|
305
|
+
service: 'Zenodo',
|
|
306
|
+
data: { operation: req.operation },
|
|
307
|
+
});
|
|
308
|
+
// Zenodo sends Retry-After on every response, counting to its window reset; it
|
|
309
|
+
// means "wait" only on a 429, which is mapped above.
|
|
310
|
+
const { retryAfter: _windowReset, ...data } = error.data ?? {};
|
|
311
|
+
return new McpError(error.code, error.message, data);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* A ZIP Zenodo cannot open: its `/container` listing or member read answered 500
|
|
315
|
+
* (after the one retry) or never finished. The message names the archive's
|
|
316
|
+
* download URL, the path that still works.
|
|
317
|
+
*/
|
|
318
|
+
#archiveError(req, ctx, failure, cause) {
|
|
319
|
+
const what = failure === 'status_500' ? 'HTTP 500' : 'no complete response in time';
|
|
320
|
+
return new McpError(JsonRpcErrorCode.ServiceUnavailable, `Zenodo could not open this .zip archive (${what}); download it from ${req.downloadUrl ?? 'its download_url'} instead.`, {
|
|
321
|
+
reason: 'archive_unavailable',
|
|
322
|
+
...(failure === 'status_500' ? { status: 500 } : { retryable: false }),
|
|
323
|
+
...ctx.recoveryFor('archive_unavailable'),
|
|
324
|
+
}, cause === undefined ? undefined : { cause });
|
|
325
|
+
}
|
|
326
|
+
/** HTTP 500, per the retry matrix. */
|
|
327
|
+
#serverError(req, ctx) {
|
|
328
|
+
switch (req.endpoint) {
|
|
329
|
+
case 'search':
|
|
330
|
+
return serviceUnavailable('Zenodo answered HTTP 500 for this search query.', {
|
|
331
|
+
reason: 'query_failed',
|
|
332
|
+
status: 500,
|
|
333
|
+
retryable: false,
|
|
334
|
+
...ctx.recoveryFor('query_failed'),
|
|
335
|
+
});
|
|
336
|
+
case 'doi_lookup':
|
|
337
|
+
return serviceUnavailable('Zenodo answered HTTP 500 while resolving the DOI.', {
|
|
338
|
+
reason: 'record_unavailable',
|
|
339
|
+
status: 500,
|
|
340
|
+
retryable: false,
|
|
341
|
+
...ctx.recoveryFor('record_unavailable'),
|
|
342
|
+
});
|
|
343
|
+
case 'record':
|
|
344
|
+
case 'versions':
|
|
345
|
+
return serviceUnavailable('Zenodo answered HTTP 500 for this record.', {
|
|
346
|
+
reason: 'record_unavailable',
|
|
347
|
+
status: 500,
|
|
348
|
+
...ctx.recoveryFor('record_unavailable'),
|
|
349
|
+
});
|
|
350
|
+
case 'archive':
|
|
351
|
+
return this.#archiveError(req, ctx, 'status_500');
|
|
352
|
+
default:
|
|
353
|
+
return serviceUnavailable(`Zenodo answered HTTP 500 (${req.operation}).`, { status: 500 });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/** Slow 504, attempt timeout, or ladder deadline — deterministic for oversized payloads, never retried. */
|
|
357
|
+
#timeoutError(req, ctx, cause) {
|
|
358
|
+
const options = cause === undefined ? undefined : { cause };
|
|
359
|
+
switch (req.endpoint) {
|
|
360
|
+
case 'search':
|
|
361
|
+
return timeout('Zenodo did not finish the search page within its time budget; large file manifests in the hits make pages heavy.', { reason: 'upstream_timeout', retryable: false, ...ctx.recoveryFor('upstream_timeout') }, options);
|
|
362
|
+
case 'versions':
|
|
363
|
+
return timeout('Zenodo did not finish the versions page within its time budget; the series holds large file manifests.', { reason: 'upstream_timeout', retryable: false, ...ctx.recoveryFor('upstream_timeout') }, options);
|
|
364
|
+
case 'record':
|
|
365
|
+
case 'doi_lookup':
|
|
366
|
+
return new McpError(JsonRpcErrorCode.ServiceUnavailable, 'Zenodo did not serve this record within its time budget (gateway cutoff or timeout).', {
|
|
367
|
+
reason: 'record_unavailable',
|
|
368
|
+
retryable: false,
|
|
369
|
+
...ctx.recoveryFor('record_unavailable'),
|
|
370
|
+
}, options);
|
|
371
|
+
case 'archive':
|
|
372
|
+
return this.#archiveError(req, ctx, 'timeout', cause);
|
|
373
|
+
default:
|
|
374
|
+
return timeout(`Zenodo did not respond within the time budget (${req.operation}).`, { retryable: false }, options);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/services/zenodo/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure parsers for the identifier forms Zenodo tools accept: record
|
|
3
|
+
* references (record ids, Zenodo and external DOIs, zenodo.org and doi.org URLs),
|
|
4
|
+
* funder, award, ORCID, and community references, plus file-key path encoding.
|
|
5
|
+
* Nothing here touches the network; URLs are parsed locally and never fetched.
|
|
6
|
+
* @module services/zenodo/identifiers
|
|
7
|
+
*/
|
|
8
|
+
/** How an `id` input was written. */
|
|
9
|
+
export type InputKind = 'record_id' | 'zenodo_doi' | 'external_doi' | 'url';
|
|
10
|
+
/** A record reference resolved to a Zenodo record id without any upstream call. */
|
|
11
|
+
export interface RecidRef {
|
|
12
|
+
inputKind: InputKind;
|
|
13
|
+
kind: 'recid';
|
|
14
|
+
recid: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A DOI minted outside Zenodo's `10.5281/zenodo.N` scheme. `doi` is the form as
|
|
18
|
+
* given; `strippedDoi` is the form with trailing `.`/`,`/`;` removed, tried only
|
|
19
|
+
* when the as-given form misses.
|
|
20
|
+
*/
|
|
21
|
+
export interface ExternalDoiRef {
|
|
22
|
+
doi: string;
|
|
23
|
+
inputKind: InputKind;
|
|
24
|
+
kind: 'external_doi';
|
|
25
|
+
strippedDoi?: string;
|
|
26
|
+
}
|
|
27
|
+
/** An input that matches no accepted form. `message` says why. */
|
|
28
|
+
export interface ParseFailure {
|
|
29
|
+
kind: 'invalid';
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
export type RecordRef = RecidRef | ExternalDoiRef;
|
|
33
|
+
/**
|
|
34
|
+
* Classifies a record reference. Accepts a record id, `zenodo.N`, a Zenodo DOI
|
|
35
|
+
* (any case, optionally `doi:`-prefixed), another DOI, a doi.org URL, or a
|
|
36
|
+
* zenodo.org record/DOI/badge URL. Wrapping `<>` / quotes (with any punctuation
|
|
37
|
+
* after them) and trailing punctuation after a record id or Zenodo DOI are
|
|
38
|
+
* stripped, in whichever order they were combined. Query strings and
|
|
39
|
+
* fragments are ignored. Never fetches anything.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseRecordRef(raw: string): RecordRef | ParseFailure;
|
|
42
|
+
/** True when `raw` parses as a single DOI or URL record reference (not a bare number). */
|
|
43
|
+
export declare function isWholeIdentifier(raw: string): boolean;
|
|
44
|
+
/** A funder reference as the service resolves it. */
|
|
45
|
+
export type FunderRef = {
|
|
46
|
+
kind: 'ror';
|
|
47
|
+
ror: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'funder_doi';
|
|
50
|
+
doi: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Parses a funder reference: a ROR id (`01cwqze88`), a ROR URL, or a Crossref
|
|
54
|
+
* Funder DOI (`10.13039/100000002`, bare, `doi:`-prefixed, or as a doi.org URL).
|
|
55
|
+
* Returns `undefined` for anything else — funder names are never matched.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseFunderRef(raw: string): FunderRef | undefined;
|
|
58
|
+
/** European Commission ROR id — the funder behind CORDIS award DOIs (`10.3030/N`). */
|
|
59
|
+
export declare const EUROPEAN_COMMISSION_ROR = "00k4n6c32";
|
|
60
|
+
/** How an award reference filters: by full award id, or by grant number alone. */
|
|
61
|
+
export type AwardRef = {
|
|
62
|
+
field: 'id';
|
|
63
|
+
value: string;
|
|
64
|
+
} | {
|
|
65
|
+
field: 'number';
|
|
66
|
+
value: string;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Parses an award reference: `<funder-ror>::<number>`, a CORDIS award DOI
|
|
70
|
+
* (`10.3030/<n>` → European Commission), or a bare grant number.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseAwardRef(raw: string): AwardRef;
|
|
73
|
+
/**
|
|
74
|
+
* Normalizes an ORCID input: strips an `orcid.org/` URL prefix and uppercases a
|
|
75
|
+
* trailing check character `x`. Validation is separate ({@link isValidOrcid}).
|
|
76
|
+
*/
|
|
77
|
+
export declare function normalizeOrcid(raw: string): string;
|
|
78
|
+
/** The `0000-0000-0000-000X` shape of an ORCID iD. */
|
|
79
|
+
export declare const ORCID_PATTERN: RegExp;
|
|
80
|
+
/** True when `orcid` has the {@link ORCID_PATTERN} shape and a valid ISO 7064 mod 11-2 check digit. */
|
|
81
|
+
export declare function isValidOrcid(orcid: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Parses a community reference: a slug, a UUID, or a `zenodo.org/communities/<slug>`
|
|
84
|
+
* URL (scheme optional). Returns `undefined` when the value cannot be a
|
|
85
|
+
* community identifier, so it is never interpolated into a request path.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseCommunityRef(raw: string): string | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* True when a `/`-separated key or member path has a `.` or `..` segment.
|
|
90
|
+
* `encodeURIComponent` leaves those literal and the URL parser resolves them, so
|
|
91
|
+
* such a path would reach a different zenodo.org endpoint instead of naming a file.
|
|
92
|
+
*/
|
|
93
|
+
export declare function hasDotSegment(key: string): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Encodes a file key (or ZIP member path) for a URL path: each `/`-separated
|
|
96
|
+
* segment is percent-encoded. Request paths must reject {@link hasDotSegment}
|
|
97
|
+
* keys first, since `.` and `..` survive encoding.
|
|
98
|
+
*/
|
|
99
|
+
export declare function encodeKeySegments(key: string): string;
|
|
100
|
+
/** The public download URL for a top-level file. */
|
|
101
|
+
export declare function downloadUrl(recid: string, key: string): string;
|
|
102
|
+
/** The public landing page for a record. */
|
|
103
|
+
export declare function recordUrl(recid: string): string;
|
|
104
|
+
//# sourceMappingURL=identifiers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identifiers.d.ts","sourceRoot":"","sources":["../../../src/services/zenodo/identifiers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,qCAAqC;AACrC,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,YAAY,GAAG,cAAc,GAAG,KAAK,CAAC;AAE5E,mFAAmF;AACnF,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,EAAE,cAAc,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,cAAc,CAAC;AA4IlD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAwBpE;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAKtD;AAED,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAI3F;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAOjE;AAED,sFAAsF;AACtF,eAAO,MAAM,uBAAuB,cAAc,CAAC;AAEnD,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3F;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAOnD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED,sDAAsD;AACtD,eAAO,MAAM,aAAa,QAAmC,CAAC;AAE9D,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,oDAAoD;AACpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,4CAA4C;AAC5C,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C"}
|