@cyanheads/zenodo-mcp-server 0.1.2 → 0.1.3
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 +22 -15
- package/CLAUDE.md +22 -15
- package/Dockerfile +3 -3
- package/README.md +6 -4
- package/changelog/0.1.x/0.1.3.md +35 -0
- package/dist/mcp-server/tools/definitions/get-record.tool.d.ts +106 -2
- package/dist/mcp-server/tools/definitions/get-record.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-record.tool.js +452 -106
- package/dist/mcp-server/tools/definitions/get-record.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +283 -180
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/list-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/list-files.tool.js +21 -15
- package/dist/mcp-server/tools/definitions/list-files.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/list-versions.tool.d.ts +1 -1
- package/dist/mcp-server/tools/definitions/list-versions.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/list-versions.tool.js +29 -20
- package/dist/mcp-server/tools/definitions/list-versions.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.d.ts +2 -1
- package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.js +39 -22
- package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/read-file.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/read-file.tool.js +16 -14
- package/dist/mcp-server/tools/definitions/read-file.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-records.tool.d.ts +3 -3
- package/dist/mcp-server/tools/definitions/search-records.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-records.tool.js +138 -29
- package/dist/mcp-server/tools/definitions/search-records.tool.js.map +1 -1
- package/dist/mcp-server/tools/paging.d.ts +42 -0
- package/dist/mcp-server/tools/paging.d.ts.map +1 -0
- package/dist/mcp-server/tools/paging.js +44 -0
- package/dist/mcp-server/tools/paging.js.map +1 -0
- package/dist/mcp-server/tools/record-miss.d.ts +5 -1
- package/dist/mcp-server/tools/record-miss.d.ts.map +1 -1
- package/dist/mcp-server/tools/record-miss.js +6 -2
- package/dist/mcp-server/tools/record-miss.js.map +1 -1
- package/dist/services/zenodo/html-to-text.d.ts +19 -8
- package/dist/services/zenodo/html-to-text.d.ts.map +1 -1
- package/dist/services/zenodo/html-to-text.js +175 -48
- package/dist/services/zenodo/html-to-text.js.map +1 -1
- package/dist/services/zenodo/http.js +1 -1
- package/dist/services/zenodo/http.js.map +1 -1
- package/dist/services/zenodo/normalize.d.ts.map +1 -1
- package/dist/services/zenodo/normalize.js +5 -2
- package/dist/services/zenodo/normalize.js.map +1 -1
- package/dist/services/zenodo/query-builder.d.ts +84 -8
- package/dist/services/zenodo/query-builder.d.ts.map +1 -1
- package/dist/services/zenodo/query-builder.js +484 -21
- package/dist/services/zenodo/query-builder.js.map +1 -1
- package/dist/services/zenodo/text-preview.d.ts +1 -1
- package/dist/services/zenodo/text-preview.d.ts.map +1 -1
- package/dist/services/zenodo/text-preview.js +38 -38
- package/dist/services/zenodo/text-preview.js.map +1 -1
- package/package.json +7 -7
- package/server.json +3 -3
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview zenodo_get_record — resolves one Zenodo deposit from a record id,
|
|
3
3
|
* Zenodo or external DOI, or zenodo.org/doi.org URL and returns its full metadata,
|
|
4
|
-
* the first 25 files, and optionally a formatted citation.
|
|
5
|
-
*
|
|
4
|
+
* the first 25 files, and optionally a formatted citation. Fields cut at a display
|
|
5
|
+
* cap are listed in `continuations` with a cursor; passing one back returns the
|
|
6
|
+
* next window of that field. A miss (unknown id, deleted record, restricted
|
|
7
|
+
* metadata, DOI not on Zenodo) is a result, not an error.
|
|
6
8
|
* @module mcp-server/tools/definitions/get-record
|
|
7
9
|
*/
|
|
8
10
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
9
11
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
12
|
+
import { decodeCursor, encodeCursor } from '@cyanheads/mcp-ts-core/utils';
|
|
10
13
|
import { parseRecordRef, recordUrl } from '../../../services/zenodo/identifiers.js';
|
|
11
14
|
import { getZenodoService } from '../../../services/zenodo/zenodo-service.js';
|
|
12
15
|
import { notOnZenodoMiss, recordMiss, TombstoneSchema } from '../record-miss.js';
|
|
13
16
|
import { hasLineBreak, inline, quoteBlock } from '../render.js';
|
|
14
|
-
import { enumPreprocess } from '../schema-helpers.js';
|
|
17
|
+
import { blankToUndefined, enumPreprocess } from '../schema-helpers.js';
|
|
15
18
|
const CITATION_STYLES = [
|
|
16
19
|
'bibtex',
|
|
17
20
|
'csl-json',
|
|
@@ -29,6 +32,29 @@ const COMMUNITIES_CAP = 25;
|
|
|
29
32
|
const DESCRIPTION_CAP = 4_000;
|
|
30
33
|
const ADDITIONAL_DESCRIPTIONS_CAP = 5;
|
|
31
34
|
const ADDITIONAL_DESCRIPTION_TEXT_CAP = 1_000;
|
|
35
|
+
/** Characters per text window in cursor mode. */
|
|
36
|
+
const TEXT_WINDOW = 8_000;
|
|
37
|
+
/** The list fields a cursor can page, in record order. */
|
|
38
|
+
const LIST_FIELDS = [
|
|
39
|
+
'additional_descriptions',
|
|
40
|
+
'creators',
|
|
41
|
+
'contributors',
|
|
42
|
+
'keywords',
|
|
43
|
+
'funding',
|
|
44
|
+
'related_identifiers',
|
|
45
|
+
'communities',
|
|
46
|
+
];
|
|
47
|
+
const FieldSchema = z.enum(['description', ...LIST_FIELDS]);
|
|
48
|
+
const UnitSchema = z.enum(['characters', 'entries']);
|
|
49
|
+
/** The state a cursor carries: framework pagination state bound to one record revision. */
|
|
50
|
+
const CursorStateSchema = z.object({
|
|
51
|
+
offset: z.number().int().min(0),
|
|
52
|
+
limit: z.number().int().min(1),
|
|
53
|
+
recid: z.string(),
|
|
54
|
+
rev: z.number().int().optional(),
|
|
55
|
+
field: FieldSchema,
|
|
56
|
+
index: z.number().int().min(0).optional(),
|
|
57
|
+
});
|
|
32
58
|
const PersonSchema = z
|
|
33
59
|
.object({
|
|
34
60
|
name: z.string().describe('Display name as deposited.'),
|
|
@@ -57,6 +83,46 @@ const UsageSchema = z
|
|
|
57
83
|
.describe('Unique downloads, as zenodo.org displays them; absent when not reported.'),
|
|
58
84
|
})
|
|
59
85
|
.describe('Usage counts.');
|
|
86
|
+
const AdditionalDescriptionSchema = z
|
|
87
|
+
.object({
|
|
88
|
+
type: z.string().optional().describe('Description type, e.g. Notes or Methods.'),
|
|
89
|
+
text: z.string().describe('Plain text, first 1,000 characters.'),
|
|
90
|
+
length: z.number().describe('Full plain-text length in characters.'),
|
|
91
|
+
truncated: z
|
|
92
|
+
.boolean()
|
|
93
|
+
.describe('True when text was cut at 1,000 characters; continuations carries a cursor for the rest.'),
|
|
94
|
+
})
|
|
95
|
+
.describe('One additional description.');
|
|
96
|
+
const FundingSchema = z
|
|
97
|
+
.object({
|
|
98
|
+
funder_id: z.string().optional().describe('Funder ROR id.'),
|
|
99
|
+
funder_name: z.string().optional().describe('Funder name.'),
|
|
100
|
+
award_id: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Award id (<funder-ror>::<number>) — feeds zenodo_search_records award.'),
|
|
104
|
+
award_number: z.string().optional().describe('Grant number.'),
|
|
105
|
+
award_acronym: z.string().optional().describe('Award acronym.'),
|
|
106
|
+
award_title: z.string().optional().describe('Award title.'),
|
|
107
|
+
award_program: z.string().optional().describe('Funding program.'),
|
|
108
|
+
award_doi: z.string().optional().describe('Award DOI, e.g. 10.3030/101135562.'),
|
|
109
|
+
award_url: z.string().optional().describe('Award landing page URL.'),
|
|
110
|
+
})
|
|
111
|
+
.describe('One funding entry.');
|
|
112
|
+
const RelatedIdentifierSchema = z
|
|
113
|
+
.object({
|
|
114
|
+
identifier: z.string().describe('The related identifier (DOI, arXiv id, URL, PMID, …).'),
|
|
115
|
+
scheme: z.string().optional().describe('Identifier scheme, e.g. doi, arxiv, url.'),
|
|
116
|
+
relation: z.string().optional().describe('Relation type id, e.g. issupplementto or cites.'),
|
|
117
|
+
resource_type: z.string().optional().describe('Resource type id of the related item.'),
|
|
118
|
+
})
|
|
119
|
+
.describe('One related identifier.');
|
|
120
|
+
const CommunitySchema = z
|
|
121
|
+
.object({
|
|
122
|
+
slug: z.string().describe('Community slug — feeds zenodo_search_records community.'),
|
|
123
|
+
title: z.string().optional().describe('Community title.'),
|
|
124
|
+
})
|
|
125
|
+
.describe('One community.');
|
|
60
126
|
const RecordSchema = z
|
|
61
127
|
.object({
|
|
62
128
|
recid: z.string().describe('Record id of this version.'),
|
|
@@ -97,28 +163,25 @@ const RecordSchema = z
|
|
|
97
163
|
description: z
|
|
98
164
|
.string()
|
|
99
165
|
.optional()
|
|
100
|
-
.describe('Description converted from HTML to plain text,
|
|
166
|
+
.describe('Description converted from HTML to plain text, first 4,000 characters; absent when the record has none.'),
|
|
101
167
|
description_truncated: z
|
|
102
168
|
.boolean()
|
|
103
169
|
.optional()
|
|
104
|
-
.describe('True when the description was cut at 4,000 characters. Present with description.'),
|
|
170
|
+
.describe('True when the description was cut at 4,000 characters; continuations carries a cursor for the rest. Present with description.'),
|
|
105
171
|
description_length: z
|
|
106
172
|
.number()
|
|
107
173
|
.optional()
|
|
108
174
|
.describe('Full plain-text length of the description in characters. Present with description.'),
|
|
109
175
|
additional_descriptions: z
|
|
110
|
-
.array(
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
text: z.string().describe('Plain text, capped at 1,000 characters.'),
|
|
114
|
-
})
|
|
115
|
-
.describe('One additional description.'))
|
|
116
|
-
.describe('Up to 5 additional descriptions.'),
|
|
176
|
+
.array(AdditionalDescriptionSchema)
|
|
177
|
+
.describe('First 5 additional descriptions, in record order (index 0 first).'),
|
|
178
|
+
additional_description_count: z.number().describe('Total additional descriptions.'),
|
|
117
179
|
creators: z.array(PersonSchema).describe('First 25 creators.'),
|
|
118
180
|
creator_count: z.number().describe('Total creators.'),
|
|
119
181
|
contributors: z.array(PersonSchema).describe('First 25 contributors.'),
|
|
120
182
|
contributor_count: z.number().describe('Total contributors.'),
|
|
121
|
-
keywords: z.array(z.string()).describe('
|
|
183
|
+
keywords: z.array(z.string()).describe('First 50 subject keywords.'),
|
|
184
|
+
keyword_count: z.number().describe('Total subject keywords.'),
|
|
122
185
|
rights: z
|
|
123
186
|
.array(z
|
|
124
187
|
.object({
|
|
@@ -146,52 +209,16 @@ const RecordSchema = z
|
|
|
146
209
|
embargo_reason: z.string().optional().describe('Embargo reason, when active.'),
|
|
147
210
|
})
|
|
148
211
|
.describe('Access status and embargo.'),
|
|
149
|
-
funding: z
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
funder_id: z.string().optional().describe('Funder ROR id.'),
|
|
153
|
-
funder_name: z.string().optional().describe('Funder name.'),
|
|
154
|
-
award_id: z
|
|
155
|
-
.string()
|
|
156
|
-
.optional()
|
|
157
|
-
.describe('Award id (<funder-ror>::<number>) — feeds zenodo_search_records award.'),
|
|
158
|
-
award_number: z.string().optional().describe('Grant number.'),
|
|
159
|
-
award_acronym: z.string().optional().describe('Award acronym.'),
|
|
160
|
-
award_title: z.string().optional().describe('Award title.'),
|
|
161
|
-
award_program: z.string().optional().describe('Funding program.'),
|
|
162
|
-
award_doi: z.string().optional().describe('Award DOI, e.g. 10.3030/101135562.'),
|
|
163
|
-
award_url: z.string().optional().describe('Award landing page URL.'),
|
|
164
|
-
})
|
|
165
|
-
.describe('One funding entry.'))
|
|
166
|
-
.describe('Up to 50 funding entries (funders and grants).'),
|
|
167
|
-
related_identifiers: z
|
|
168
|
-
.array(z
|
|
169
|
-
.object({
|
|
170
|
-
identifier: z
|
|
171
|
-
.string()
|
|
172
|
-
.describe('The related identifier (DOI, arXiv id, URL, PMID, …).'),
|
|
173
|
-
scheme: z.string().optional().describe('Identifier scheme, e.g. doi, arxiv, url.'),
|
|
174
|
-
relation: z
|
|
175
|
-
.string()
|
|
176
|
-
.optional()
|
|
177
|
-
.describe('Relation type id, e.g. issupplementto or cites.'),
|
|
178
|
-
resource_type: z.string().optional().describe('Resource type id of the related item.'),
|
|
179
|
-
})
|
|
180
|
-
.describe('One related identifier.'))
|
|
181
|
-
.describe('First 50 related identifiers.'),
|
|
212
|
+
funding: z.array(FundingSchema).describe('First 50 funding entries (funders and grants).'),
|
|
213
|
+
funding_count: z.number().describe('Total funding entries.'),
|
|
214
|
+
related_identifiers: z.array(RelatedIdentifierSchema).describe('First 50 related identifiers.'),
|
|
182
215
|
related_identifier_count: z.number().describe('Total related identifiers.'),
|
|
183
216
|
code_repository: z
|
|
184
217
|
.string()
|
|
185
218
|
.optional()
|
|
186
219
|
.describe('Source code repository URL; absent when none is linked.'),
|
|
187
|
-
communities: z
|
|
188
|
-
|
|
189
|
-
.object({
|
|
190
|
-
slug: z.string().describe('Community slug — feeds zenodo_search_records community.'),
|
|
191
|
-
title: z.string().optional().describe('Community title.'),
|
|
192
|
-
})
|
|
193
|
-
.describe('One community.'))
|
|
194
|
-
.describe('Up to 25 communities holding this record.'),
|
|
220
|
+
communities: z.array(CommunitySchema).describe('First 25 communities holding this record.'),
|
|
221
|
+
community_count: z.number().describe('Total communities holding this record.'),
|
|
195
222
|
versions: z
|
|
196
223
|
.object({
|
|
197
224
|
index: z
|
|
@@ -244,6 +271,225 @@ const RecordSchema = z
|
|
|
244
271
|
zenodo_url: z.string().describe('Record landing page on zenodo.org.'),
|
|
245
272
|
})
|
|
246
273
|
.describe('The deposit’s metadata.');
|
|
274
|
+
const ContinuationSchema = z
|
|
275
|
+
.object({
|
|
276
|
+
field: FieldSchema.describe('The field that was cut.'),
|
|
277
|
+
index: z
|
|
278
|
+
.number()
|
|
279
|
+
.optional()
|
|
280
|
+
.describe('Position of the additional description whose text was cut (0 = first). Present only for an additional description’s text.'),
|
|
281
|
+
unit: UnitSchema.describe('characters for description text, entries for a list (additional_descriptions without index is the list).'),
|
|
282
|
+
shown: z
|
|
283
|
+
.number()
|
|
284
|
+
.describe('Characters or entries of the field returned so far, counted from its start; the cursor resumes here.'),
|
|
285
|
+
total: z.number().describe('Full length of the field in characters, or its total entries.'),
|
|
286
|
+
id: z
|
|
287
|
+
.string()
|
|
288
|
+
.describe('Record id the cursor is bound to; pass it as id beside the cursor, whatever id form this call took.'),
|
|
289
|
+
cursor: z
|
|
290
|
+
.string()
|
|
291
|
+
.describe('Pass as cursor, with this entry’s id, to get the next window of this field.'),
|
|
292
|
+
})
|
|
293
|
+
.describe('One field cut short in this response.');
|
|
294
|
+
const WindowSchema = z
|
|
295
|
+
.object({
|
|
296
|
+
field: FieldSchema.describe('The field this window continues.'),
|
|
297
|
+
index: z
|
|
298
|
+
.number()
|
|
299
|
+
.optional()
|
|
300
|
+
.describe('Position of the additional description whose text this window continues (0 = first). Present only for an additional description’s text.'),
|
|
301
|
+
unit: UnitSchema.describe('characters for a text window, entries for a list window.'),
|
|
302
|
+
offset: z.number().describe('Characters or entries of the field before this window.'),
|
|
303
|
+
shown: z.number().describe('Characters or entries in this window.'),
|
|
304
|
+
total: z.number().describe('Full length of the field in characters, or its total entries.'),
|
|
305
|
+
text: z
|
|
306
|
+
.string()
|
|
307
|
+
.optional()
|
|
308
|
+
.describe('Up to 8,000 characters of the field’s plain text, starting at offset. Present when unit is characters.'),
|
|
309
|
+
additional_descriptions: z
|
|
310
|
+
.array(AdditionalDescriptionSchema)
|
|
311
|
+
.optional()
|
|
312
|
+
.describe('Up to 5 additional descriptions starting at offset, each text cut at 1,000 characters. Present when field is additional_descriptions and unit is entries.'),
|
|
313
|
+
creators: z
|
|
314
|
+
.array(PersonSchema)
|
|
315
|
+
.optional()
|
|
316
|
+
.describe('Up to 25 creators starting at offset. Present when field is creators.'),
|
|
317
|
+
contributors: z
|
|
318
|
+
.array(PersonSchema)
|
|
319
|
+
.optional()
|
|
320
|
+
.describe('Up to 25 contributors starting at offset. Present when field is contributors.'),
|
|
321
|
+
keywords: z
|
|
322
|
+
.array(z.string())
|
|
323
|
+
.optional()
|
|
324
|
+
.describe('Up to 50 subject keywords starting at offset. Present when field is keywords.'),
|
|
325
|
+
funding: z
|
|
326
|
+
.array(FundingSchema)
|
|
327
|
+
.optional()
|
|
328
|
+
.describe('Up to 50 funding entries starting at offset. Present when field is funding.'),
|
|
329
|
+
related_identifiers: z
|
|
330
|
+
.array(RelatedIdentifierSchema)
|
|
331
|
+
.optional()
|
|
332
|
+
.describe('Up to 50 related identifiers starting at offset. Present when field is related_identifiers.'),
|
|
333
|
+
communities: z
|
|
334
|
+
.array(CommunitySchema)
|
|
335
|
+
.optional()
|
|
336
|
+
.describe('Up to 25 communities starting at offset. Present when field is communities.'),
|
|
337
|
+
})
|
|
338
|
+
.describe('One window of a field a previous response cut short.');
|
|
339
|
+
const isHighSurrogate = (code) => code >= 0xd800 && code <= 0xdbff;
|
|
340
|
+
/** `n`, or `n − 1` when cutting `text` at `n` would split a surrogate pair. */
|
|
341
|
+
const cutAt = (text, n) => n < text.length && isHighSurrogate(text.charCodeAt(n - 1)) ? n - 1 : n;
|
|
342
|
+
function additionalDescriptionView(d) {
|
|
343
|
+
return {
|
|
344
|
+
...(d.type ? { type: d.type } : {}),
|
|
345
|
+
text: d.text.slice(0, cutAt(d.text, ADDITIONAL_DESCRIPTION_TEXT_CAP)),
|
|
346
|
+
length: d.text.length,
|
|
347
|
+
truncated: d.text.length > ADDITIONAL_DESCRIPTION_TEXT_CAP,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/** A continuation for `field` resuming at `shown`, its cursor bound to this record revision. */
|
|
351
|
+
function continuation(record, cut, limit) {
|
|
352
|
+
return {
|
|
353
|
+
...cut,
|
|
354
|
+
id: record.recid,
|
|
355
|
+
cursor: encodeCursor({
|
|
356
|
+
offset: cut.shown,
|
|
357
|
+
limit,
|
|
358
|
+
recid: record.recid,
|
|
359
|
+
...(record.revision !== undefined ? { rev: record.revision } : {}),
|
|
360
|
+
field: cut.field,
|
|
361
|
+
...(cut.index !== undefined ? { index: cut.index } : {}),
|
|
362
|
+
}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/** One page of a list field from `offset`, at the field's display cap. */
|
|
366
|
+
function listPage(record, field, offset) {
|
|
367
|
+
const page = (all, size, arm) => {
|
|
368
|
+
const items = all.slice(offset, offset + size);
|
|
369
|
+
return { arm: arm(items), size, shown: items.length, total: all.length };
|
|
370
|
+
};
|
|
371
|
+
switch (field) {
|
|
372
|
+
case 'additional_descriptions':
|
|
373
|
+
return page(record.additional_descriptions, ADDITIONAL_DESCRIPTIONS_CAP, (items) => ({
|
|
374
|
+
additional_descriptions: items.map(additionalDescriptionView),
|
|
375
|
+
}));
|
|
376
|
+
case 'creators':
|
|
377
|
+
return page(record.creators, PEOPLE_CAP, (creators) => ({ creators }));
|
|
378
|
+
case 'contributors':
|
|
379
|
+
return page(record.contributors, PEOPLE_CAP, (contributors) => ({ contributors }));
|
|
380
|
+
case 'keywords':
|
|
381
|
+
return page(record.keywords, LIST_CAP, (keywords) => ({ keywords }));
|
|
382
|
+
case 'funding':
|
|
383
|
+
return page(record.funding, LIST_CAP, (funding) => ({ funding }));
|
|
384
|
+
case 'related_identifiers':
|
|
385
|
+
return page(record.related_identifiers, LIST_CAP, (related_identifiers) => ({
|
|
386
|
+
related_identifiers,
|
|
387
|
+
}));
|
|
388
|
+
case 'communities':
|
|
389
|
+
return page(record.communities, COMMUNITIES_CAP, (communities) => ({ communities }));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** Continuations for the cut texts among additional descriptions `from`…`to - 1`. */
|
|
393
|
+
function additionalTextCuts(record, from, to) {
|
|
394
|
+
return record.additional_descriptions.slice(from, to).flatMap((d, i) => d.text.length > ADDITIONAL_DESCRIPTION_TEXT_CAP
|
|
395
|
+
? [
|
|
396
|
+
continuation(record, {
|
|
397
|
+
field: 'additional_descriptions',
|
|
398
|
+
index: from + i,
|
|
399
|
+
unit: 'characters',
|
|
400
|
+
shown: cutAt(d.text, ADDITIONAL_DESCRIPTION_TEXT_CAP),
|
|
401
|
+
total: d.text.length,
|
|
402
|
+
}, TEXT_WINDOW),
|
|
403
|
+
]
|
|
404
|
+
: []);
|
|
405
|
+
}
|
|
406
|
+
/** Every field the record view cuts, in record order; files page through zenodo_list_files instead. */
|
|
407
|
+
function recordContinuations(record) {
|
|
408
|
+
const cuts = [];
|
|
409
|
+
const description = record.description;
|
|
410
|
+
if (description && description.length > DESCRIPTION_CAP) {
|
|
411
|
+
cuts.push(continuation(record, {
|
|
412
|
+
field: 'description',
|
|
413
|
+
unit: 'characters',
|
|
414
|
+
shown: cutAt(description, DESCRIPTION_CAP),
|
|
415
|
+
total: description.length,
|
|
416
|
+
}, TEXT_WINDOW));
|
|
417
|
+
}
|
|
418
|
+
for (const field of LIST_FIELDS) {
|
|
419
|
+
const { size, shown, total } = listPage(record, field, 0);
|
|
420
|
+
if (shown < total) {
|
|
421
|
+
cuts.push(continuation(record, { field, unit: 'entries', shown, total }, size));
|
|
422
|
+
}
|
|
423
|
+
if (field === 'additional_descriptions')
|
|
424
|
+
cuts.push(...additionalTextCuts(record, 0, shown));
|
|
425
|
+
}
|
|
426
|
+
return cuts;
|
|
427
|
+
}
|
|
428
|
+
/** The window a cursor names in `record`, or why it names none. */
|
|
429
|
+
function openWindow(record, state) {
|
|
430
|
+
const revision = (rev) => (rev === undefined ? '' : ` revision ${rev}`);
|
|
431
|
+
if (state.recid !== record.recid || state.rev !== record.revision) {
|
|
432
|
+
return `The cursor continues record ${state.recid}${revision(state.rev)}, but id resolves to record ${record.recid}${revision(record.revision)}; the cursor belongs to another record or the record has changed since it was issued.`;
|
|
433
|
+
}
|
|
434
|
+
const { field, index, offset } = state;
|
|
435
|
+
const name = `${field}${index === undefined ? '' : ` index ${index}`}`;
|
|
436
|
+
const pastEnd = `The cursor points past the end of ${name} in record ${record.recid}.`;
|
|
437
|
+
const noSuchField = `Record ${record.recid} has no ${name} for the cursor to continue.`;
|
|
438
|
+
const text = field === 'description' && index === undefined
|
|
439
|
+
? record.description
|
|
440
|
+
: field === 'additional_descriptions' && index !== undefined
|
|
441
|
+
? record.additional_descriptions[index]?.text
|
|
442
|
+
: undefined;
|
|
443
|
+
if (text !== undefined) {
|
|
444
|
+
if (offset >= text.length)
|
|
445
|
+
return pastEnd;
|
|
446
|
+
const end = cutAt(text, Math.min(offset + TEXT_WINDOW, text.length));
|
|
447
|
+
const cut = { field, ...(index === undefined ? {} : { index }), unit: 'characters' };
|
|
448
|
+
return {
|
|
449
|
+
window: {
|
|
450
|
+
...cut,
|
|
451
|
+
offset,
|
|
452
|
+
shown: end - offset,
|
|
453
|
+
total: text.length,
|
|
454
|
+
text: text.slice(offset, end),
|
|
455
|
+
},
|
|
456
|
+
continuations: end < text.length
|
|
457
|
+
? [continuation(record, { ...cut, shown: end, total: text.length }, TEXT_WINDOW)]
|
|
458
|
+
: [],
|
|
459
|
+
size: TEXT_WINDOW,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (field === 'description' || index !== undefined)
|
|
463
|
+
return noSuchField;
|
|
464
|
+
const page = listPage(record, field, offset);
|
|
465
|
+
if (offset >= page.total)
|
|
466
|
+
return pastEnd;
|
|
467
|
+
const end = offset + page.shown;
|
|
468
|
+
return {
|
|
469
|
+
window: { field, unit: 'entries', offset, shown: page.shown, total: page.total, ...page.arm },
|
|
470
|
+
continuations: [
|
|
471
|
+
...(end < page.total
|
|
472
|
+
? [
|
|
473
|
+
continuation(record, { field, unit: 'entries', shown: end, total: page.total }, page.size),
|
|
474
|
+
]
|
|
475
|
+
: []),
|
|
476
|
+
...(field === 'additional_descriptions' ? additionalTextCuts(record, offset, end) : []),
|
|
477
|
+
],
|
|
478
|
+
size: page.size,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/** The continuation state `cursor` carries, or undefined when it is not one this tool issued. */
|
|
482
|
+
function readCursor(cursor, ctx) {
|
|
483
|
+
let decoded;
|
|
484
|
+
try {
|
|
485
|
+
decoded = decodeCursor(cursor, ctx);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const state = CursorStateSchema.safeParse(decoded);
|
|
491
|
+
return state.success ? state.data : undefined;
|
|
492
|
+
}
|
|
247
493
|
/** Applies get_record's display caps to a normalized record. */
|
|
248
494
|
function toRecordView(record, latestRecid) {
|
|
249
495
|
const description = record.description;
|
|
@@ -262,29 +508,30 @@ function toRecordView(record, latestRecid) {
|
|
|
262
508
|
...(record.resource_type ? { resource_type: record.resource_type } : {}),
|
|
263
509
|
...(description
|
|
264
510
|
? {
|
|
265
|
-
description: description.slice(0, DESCRIPTION_CAP),
|
|
511
|
+
description: description.slice(0, cutAt(description, DESCRIPTION_CAP)),
|
|
266
512
|
description_truncated: description.length > DESCRIPTION_CAP,
|
|
267
513
|
description_length: description.length,
|
|
268
514
|
}
|
|
269
515
|
: {}),
|
|
270
516
|
additional_descriptions: record.additional_descriptions
|
|
271
517
|
.slice(0, ADDITIONAL_DESCRIPTIONS_CAP)
|
|
272
|
-
.map(
|
|
273
|
-
|
|
274
|
-
text: d.text.slice(0, ADDITIONAL_DESCRIPTION_TEXT_CAP),
|
|
275
|
-
})),
|
|
518
|
+
.map(additionalDescriptionView),
|
|
519
|
+
additional_description_count: record.additional_descriptions.length,
|
|
276
520
|
creators: record.creators.slice(0, PEOPLE_CAP),
|
|
277
521
|
creator_count: record.creators.length,
|
|
278
522
|
contributors: record.contributors.slice(0, PEOPLE_CAP),
|
|
279
523
|
contributor_count: record.contributors.length,
|
|
280
524
|
keywords: record.keywords.slice(0, LIST_CAP),
|
|
525
|
+
keyword_count: record.keywords.length,
|
|
281
526
|
rights: record.rights,
|
|
282
527
|
access: record.access,
|
|
283
528
|
funding: record.funding.slice(0, LIST_CAP),
|
|
529
|
+
funding_count: record.funding.length,
|
|
284
530
|
related_identifiers: record.related_identifiers.slice(0, LIST_CAP),
|
|
285
531
|
related_identifier_count: record.related_identifiers.length,
|
|
286
532
|
...(record.code_repository ? { code_repository: record.code_repository } : {}),
|
|
287
533
|
communities: record.communities.slice(0, COMMUNITIES_CAP),
|
|
534
|
+
community_count: record.communities.length,
|
|
288
535
|
versions: { ...record.versions, ...(latestRecid ? { latest_recid: latestRecid } : {}) },
|
|
289
536
|
...(record.stats ? { stats: record.stats } : {}),
|
|
290
537
|
files: {
|
|
@@ -315,9 +562,84 @@ function renderPerson(p) {
|
|
|
315
562
|
parts.push(`— ${affiliations.join('; ')}`);
|
|
316
563
|
return `- ${parts.join(' ')}`;
|
|
317
564
|
}
|
|
565
|
+
function renderFunding(f) {
|
|
566
|
+
const funder = [
|
|
567
|
+
f.funder_name ? inline(f.funder_name) : undefined,
|
|
568
|
+
f.funder_id ? `(${inline(f.funder_id)})` : undefined,
|
|
569
|
+
]
|
|
570
|
+
.filter(Boolean)
|
|
571
|
+
.join(' ');
|
|
572
|
+
const award = [
|
|
573
|
+
f.award_acronym ? inline(f.award_acronym) : undefined,
|
|
574
|
+
f.award_number ? `no. ${inline(f.award_number)}` : undefined,
|
|
575
|
+
f.award_id ? `[award id ${inline(f.award_id)}]` : undefined,
|
|
576
|
+
f.award_program ? `program ${inline(f.award_program)}` : undefined,
|
|
577
|
+
f.award_doi ? `DOI ${inline(f.award_doi)}` : undefined,
|
|
578
|
+
f.award_url ? inline(f.award_url) : undefined,
|
|
579
|
+
]
|
|
580
|
+
.filter(Boolean)
|
|
581
|
+
.join(', ');
|
|
582
|
+
const lines = [`- ${funder || 'Unnamed funder'}${award ? ` — ${award}` : ''}`];
|
|
583
|
+
if (f.award_title) {
|
|
584
|
+
lines.push(hasLineBreak(f.award_title)
|
|
585
|
+
? quoteBlock(f.award_title, ' Award title (untrusted):')
|
|
586
|
+
: ` Award title: ${f.award_title}`);
|
|
587
|
+
}
|
|
588
|
+
return lines;
|
|
589
|
+
}
|
|
590
|
+
function renderRelated(x) {
|
|
591
|
+
return `- ${inline(x.relation ?? 'related')}: ${inline(x.identifier)}${x.scheme ? ` (${inline(x.scheme)})` : ''}${x.resource_type ? ` [${inline(x.resource_type)}]` : ''}`;
|
|
592
|
+
}
|
|
593
|
+
function renderCommunities(communities) {
|
|
594
|
+
return communities
|
|
595
|
+
.map((c) => `${inline(c.slug)}${c.title ? ` (${inline(c.title)})` : ''}`)
|
|
596
|
+
.join('; ');
|
|
597
|
+
}
|
|
598
|
+
/** One additional description at its position in the record, with its length and any cut. */
|
|
599
|
+
function renderAdditionalDescription(d, index, total) {
|
|
600
|
+
return [
|
|
601
|
+
'',
|
|
602
|
+
`### Additional description${d.type ? ` — ${inline(d.type)}` : ''} (index ${index} of ${total}: ${d.length} chars${d.truncated ? ', truncated to 1,000' : ''})`,
|
|
603
|
+
quoteBlock(d.text),
|
|
604
|
+
];
|
|
605
|
+
}
|
|
606
|
+
/** Every continuation of one response names the same record id, so the heading names it once. */
|
|
607
|
+
function renderContinuations(continuations) {
|
|
608
|
+
return [
|
|
609
|
+
'',
|
|
610
|
+
`### Continuations (pass a cursor with id ${continuations[0]?.id} for the next window)`,
|
|
611
|
+
...continuations.map((c) => `- ${c.field}${c.index === undefined ? '' : ` index ${c.index}`}: ${c.shown} of ${c.total} ${c.unit} returned so far — cursor: \`${c.cursor}\``),
|
|
612
|
+
];
|
|
613
|
+
}
|
|
614
|
+
function renderWindow(w) {
|
|
615
|
+
const lines = [
|
|
616
|
+
'',
|
|
617
|
+
`### Window: ${w.field}${w.index === undefined ? '' : ` index ${w.index}`} — ${w.unit} from offset ${w.offset}, ${w.shown} shown of ${w.total}`,
|
|
618
|
+
];
|
|
619
|
+
if (w.text !== undefined)
|
|
620
|
+
lines.push(quoteBlock(w.text));
|
|
621
|
+
if (w.additional_descriptions) {
|
|
622
|
+
w.additional_descriptions.forEach((d, i) => {
|
|
623
|
+
lines.push(...renderAdditionalDescription(d, w.offset + i, w.total));
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (w.creators)
|
|
627
|
+
lines.push(...w.creators.map(renderPerson));
|
|
628
|
+
if (w.contributors)
|
|
629
|
+
lines.push(...w.contributors.map(renderPerson));
|
|
630
|
+
if (w.keywords)
|
|
631
|
+
lines.push(`**Keywords:** ${w.keywords.map(inline).join('; ')}`);
|
|
632
|
+
if (w.funding)
|
|
633
|
+
lines.push(...w.funding.flatMap(renderFunding));
|
|
634
|
+
if (w.related_identifiers)
|
|
635
|
+
lines.push(...w.related_identifiers.map(renderRelated));
|
|
636
|
+
if (w.communities)
|
|
637
|
+
lines.push(`**Communities:** ${renderCommunities(w.communities)}`);
|
|
638
|
+
return lines;
|
|
639
|
+
}
|
|
318
640
|
export const getRecord = tool('zenodo_get_record', {
|
|
319
641
|
title: 'Get a Zenodo record',
|
|
320
|
-
description: 'Resolve one Zenodo deposit from a record id, a Zenodo DOI (10.5281/zenodo.N), a concept DOI or concept record id (resolves to the latest version), another DOI deposited on Zenodo, or a zenodo.org or doi.org URL, and return its full metadata: description, creators with ORCIDs and ROR affiliations, license, access and embargo, funding and grants, related identifiers, communities, version position, usage counts, and the first 25 files. Optionally includes a formatted citation. A miss (an unknown id, a deleted record with its removal tombstone, restricted metadata, or a DOI not registered on Zenodo) returns found: false with guidance instead of an error.',
|
|
642
|
+
description: 'Resolve one Zenodo deposit from a record id, a Zenodo DOI (10.5281/zenodo.N), a concept DOI or concept record id (resolves to the latest version), another DOI deposited on Zenodo, or a zenodo.org or doi.org URL, and return its full metadata: description, creators with ORCIDs and ROR affiliations, license, access and embargo, funding and grants, related identifiers, communities, version position, usage counts, and the first 25 files. Optionally includes a formatted citation. Long text and lists are cut at display caps with their totals reported; every cut except the file list (paged with zenodo_list_files) is listed in continuations with a cursor and the record id it is bound to, and passing both back returns the next window of that field (8,000 characters of text, or the next page of entries) in place of the record. A miss (an unknown id, a deleted record with its removal tombstone, restricted metadata, or a DOI not registered on Zenodo) returns found: false with guidance instead of an error.',
|
|
321
643
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
322
644
|
input: z.object({
|
|
323
645
|
id: z
|
|
@@ -329,6 +651,9 @@ export const getRecord = tool('zenodo_get_record', {
|
|
|
329
651
|
citation_style: z
|
|
330
652
|
.preprocess(enumPreprocess(CITATION_STYLES), z.enum(CITATION_STYLES).optional())
|
|
331
653
|
.describe('Include a formatted citation of the resolved version: bibtex, csl-json, or a text style (apa, chicago-author-date, harvard-cite-them-right, ieee, modern-language-association, nature). Case, spaces, hyphens, and underscores are ignored when matching (BibTeX, APA, CSL JSON). Omit for none.'),
|
|
654
|
+
cursor: z
|
|
655
|
+
.preprocess(blankToUndefined, z.string().max(500).optional())
|
|
656
|
+
.describe('A continuations[].cursor from an earlier zenodo_get_record response, passed unchanged with that continuation’s id as id. Returns that field’s next window (8,000 characters of text, or the next page of entries) as window, in place of record. Omit to get the record.'),
|
|
332
657
|
}),
|
|
333
658
|
output: z.object({
|
|
334
659
|
found: z.boolean().describe('True when the record resolved.'),
|
|
@@ -345,7 +670,12 @@ export const getRecord = tool('zenodo_get_record', {
|
|
|
345
670
|
.describe('Why nothing resolved. Present when not found.'),
|
|
346
671
|
guidance: z.string().optional().describe('What to do next. Present when not found.'),
|
|
347
672
|
tombstone: TombstoneSchema.optional().describe('Removal tombstone. Present when miss_kind is deleted.'),
|
|
348
|
-
record: RecordSchema.optional().describe('The record. Present when found.'),
|
|
673
|
+
record: RecordSchema.optional().describe('The record. Present when found and no cursor was passed.'),
|
|
674
|
+
window: WindowSchema.optional().describe('The next window of the field the cursor continues. Present when found and a cursor was passed, in place of record.'),
|
|
675
|
+
continuations: z
|
|
676
|
+
.array(ContinuationSchema)
|
|
677
|
+
.optional()
|
|
678
|
+
.describe('Every field this response cut short, each with a cursor for the rest. Absent when nothing was cut; files are paged with zenodo_list_files instead.'),
|
|
349
679
|
citation: z
|
|
350
680
|
.string()
|
|
351
681
|
.optional()
|
|
@@ -356,9 +686,15 @@ export const getRecord = tool('zenodo_get_record', {
|
|
|
356
686
|
.describe('The citation style used. Present with citation.'),
|
|
357
687
|
}),
|
|
358
688
|
enrichment: {
|
|
359
|
-
truncated: z
|
|
360
|
-
|
|
361
|
-
|
|
689
|
+
truncated: z
|
|
690
|
+
.boolean()
|
|
691
|
+
.describe('Without cursor: true when anything was cut, files past the 25 shown or any field in continuations. With cursor: true when continuations lists a next step, because the field continues past this window or an additional description on this page was cut.'),
|
|
692
|
+
shown: z
|
|
693
|
+
.number()
|
|
694
|
+
.describe('Without cursor: file entries shown. With cursor: characters or entries in the window.'),
|
|
695
|
+
cap: z
|
|
696
|
+
.number()
|
|
697
|
+
.describe('Without cursor: maximum file entries shown (25). With cursor: the window size (8,000 characters, or the field’s page of entries).'),
|
|
362
698
|
notice: z.string().optional().describe('File access notes or a pointer to zenodo_list_files.'),
|
|
363
699
|
},
|
|
364
700
|
errors: [
|
|
@@ -368,6 +704,12 @@ export const getRecord = tool('zenodo_get_record', {
|
|
|
368
704
|
when: 'id matches no accepted form, or is a GitHub-badge latestdoi id or a non-zenodo.org host',
|
|
369
705
|
recovery: 'Pass a Zenodo record id (22705923), a DOI (10.5281/zenodo.22705923), or a zenodo.org/records URL as id; for a title or keyword, use zenodo_search_records.',
|
|
370
706
|
},
|
|
707
|
+
{
|
|
708
|
+
reason: 'invalid_cursor',
|
|
709
|
+
code: JsonRpcErrorCode.ValidationError,
|
|
710
|
+
when: 'cursor is not one this tool returned in continuations, belongs to another record, or the record was edited after the cursor was issued',
|
|
711
|
+
recovery: 'Pass the cursor with the record id the message names as id to continue that record; otherwise call zenodo_get_record with this id and no cursor for this record’s own continuations, and pass one of those cursors unchanged with the id it lists.',
|
|
712
|
+
},
|
|
371
713
|
{
|
|
372
714
|
reason: 'record_unavailable',
|
|
373
715
|
code: JsonRpcErrorCode.ServiceUnavailable,
|
|
@@ -390,6 +732,10 @@ export const getRecord = tool('zenodo_get_record', {
|
|
|
390
732
|
if (ref.kind === 'invalid') {
|
|
391
733
|
throw ctx.fail('invalid_identifier', ref.message);
|
|
392
734
|
}
|
|
735
|
+
const cursor = input.cursor === undefined ? undefined : readCursor(input.cursor, ctx);
|
|
736
|
+
if (input.cursor !== undefined && !cursor) {
|
|
737
|
+
throw ctx.fail('invalid_cursor', 'The cursor is not a zenodo_get_record continuation; it may be cut short or altered.');
|
|
738
|
+
}
|
|
393
739
|
const service = getZenodoService();
|
|
394
740
|
const inputKind = ref.inputKind;
|
|
395
741
|
let record;
|
|
@@ -414,12 +760,32 @@ export const getRecord = tool('zenodo_get_record', {
|
|
|
414
760
|
record = lookup.record;
|
|
415
761
|
resolvedFrom = record.concept_recid === ref.recid ? 'concept_to_latest' : 'version';
|
|
416
762
|
}
|
|
763
|
+
const opened = cursor && openWindow(record, cursor);
|
|
764
|
+
if (typeof opened === 'string')
|
|
765
|
+
throw ctx.fail('invalid_cursor', opened);
|
|
417
766
|
const [latestRecid, citation] = await Promise.all([
|
|
418
|
-
record.versions.is_latest === false
|
|
767
|
+
!opened && record.versions.is_latest === false
|
|
768
|
+
? service.latestRecid(record.recid, ctx)
|
|
769
|
+
: undefined,
|
|
419
770
|
input.citation_style
|
|
420
771
|
? service.getCitation(record.recid, input.citation_style, ctx)
|
|
421
772
|
: undefined,
|
|
422
773
|
]);
|
|
774
|
+
const cited = citation !== undefined && input.citation_style
|
|
775
|
+
? { citation, citation_style: input.citation_style }
|
|
776
|
+
: {};
|
|
777
|
+
if (opened) {
|
|
778
|
+
const { window, continuations, size } = opened;
|
|
779
|
+
ctx.enrich({ truncated: continuations.length > 0, shown: window.shown, cap: size });
|
|
780
|
+
return {
|
|
781
|
+
found: true,
|
|
782
|
+
input_kind: inputKind,
|
|
783
|
+
resolved_from: resolvedFrom,
|
|
784
|
+
window,
|
|
785
|
+
...(continuations.length ? { continuations } : {}),
|
|
786
|
+
...cited,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
423
789
|
const view = toRecordView(record, latestRecid);
|
|
424
790
|
ctx.enrich({ shown: view.files.shown });
|
|
425
791
|
const fileCount = record.files.count ?? record.files.entries.length;
|
|
@@ -431,17 +797,19 @@ export const getRecord = tool('zenodo_get_record', {
|
|
|
431
797
|
ctx.enrich.truncated({
|
|
432
798
|
shown: view.files.shown,
|
|
433
799
|
cap: FILES_CAP,
|
|
434
|
-
guidance: `Showing ${FILES_CAP} of ${fileCount} files; page the rest with zenodo_list_files.`,
|
|
800
|
+
guidance: `Showing ${FILES_CAP} of ${fileCount.toLocaleString('en-US')} files; page the rest with zenodo_list_files.`,
|
|
435
801
|
});
|
|
436
802
|
}
|
|
803
|
+
const continuations = recordContinuations(record);
|
|
804
|
+
if (continuations.length)
|
|
805
|
+
ctx.enrich({ truncated: true });
|
|
437
806
|
return {
|
|
438
807
|
found: true,
|
|
439
808
|
input_kind: inputKind,
|
|
440
809
|
resolved_from: resolvedFrom,
|
|
441
810
|
record: view,
|
|
442
|
-
...(
|
|
443
|
-
|
|
444
|
-
: {}),
|
|
811
|
+
...(continuations.length ? { continuations } : {}),
|
|
812
|
+
...cited,
|
|
445
813
|
};
|
|
446
814
|
},
|
|
447
815
|
format: (result) => {
|
|
@@ -503,43 +871,17 @@ export const getRecord = tool('zenodo_get_record', {
|
|
|
503
871
|
if (r.contributors.length || r.contributor_count) {
|
|
504
872
|
lines.push('', `### Contributors (${r.contributor_count})`, ...r.contributors.map(renderPerson));
|
|
505
873
|
}
|
|
506
|
-
if (r.keywords.length)
|
|
507
|
-
lines.push('', `**Keywords:** ${r.keywords.map(inline).join('; ')}`);
|
|
508
|
-
if (r.communities.length) {
|
|
509
|
-
lines.push(`**Communities:** ${r.communities.map((c) => `${inline(c.slug)}${c.title ? ` (${inline(c.title)})` : ''}`).join('; ')}`);
|
|
874
|
+
if (r.keywords.length || r.keyword_count) {
|
|
875
|
+
lines.push('', `**Keywords (${r.keyword_count}):** ${r.keywords.map(inline).join('; ')}`);
|
|
510
876
|
}
|
|
511
|
-
if (r.
|
|
512
|
-
lines.push(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
f.funder_id ? `(${inline(f.funder_id)})` : undefined,
|
|
517
|
-
]
|
|
518
|
-
.filter(Boolean)
|
|
519
|
-
.join(' ');
|
|
520
|
-
const award = [
|
|
521
|
-
f.award_acronym ? inline(f.award_acronym) : undefined,
|
|
522
|
-
f.award_number ? `no. ${inline(f.award_number)}` : undefined,
|
|
523
|
-
f.award_id ? `[award id ${inline(f.award_id)}]` : undefined,
|
|
524
|
-
f.award_program ? `program ${inline(f.award_program)}` : undefined,
|
|
525
|
-
f.award_doi ? `DOI ${inline(f.award_doi)}` : undefined,
|
|
526
|
-
f.award_url ? inline(f.award_url) : undefined,
|
|
527
|
-
]
|
|
528
|
-
.filter(Boolean)
|
|
529
|
-
.join(', ');
|
|
530
|
-
lines.push(`- ${funder || 'Unnamed funder'}${award ? ` — ${award}` : ''}`);
|
|
531
|
-
if (f.award_title) {
|
|
532
|
-
lines.push(hasLineBreak(f.award_title)
|
|
533
|
-
? quoteBlock(f.award_title, ' Award title (untrusted):')
|
|
534
|
-
: ` Award title: ${f.award_title}`);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
877
|
+
if (r.communities.length || r.community_count) {
|
|
878
|
+
lines.push(`**Communities (${r.community_count}):** ${renderCommunities(r.communities)}`);
|
|
879
|
+
}
|
|
880
|
+
if (r.funding.length || r.funding_count) {
|
|
881
|
+
lines.push('', `### Funding (${r.funding_count})`, ...r.funding.flatMap(renderFunding));
|
|
537
882
|
}
|
|
538
883
|
if (r.related_identifiers.length || r.related_identifier_count) {
|
|
539
|
-
lines.push('', `### Related identifiers (${r.related_identifier_count})
|
|
540
|
-
for (const x of r.related_identifiers) {
|
|
541
|
-
lines.push(`- ${inline(x.relation ?? 'related')}: ${inline(x.identifier)}${x.scheme ? ` (${inline(x.scheme)})` : ''}${x.resource_type ? ` [${inline(x.resource_type)}]` : ''}`);
|
|
542
|
-
}
|
|
884
|
+
lines.push('', `### Related identifiers (${r.related_identifier_count})`, ...r.related_identifiers.map(renderRelated));
|
|
543
885
|
}
|
|
544
886
|
if (r.description) {
|
|
545
887
|
lines.push('', `### Description (${r.description_length ?? r.description.length} chars${r.description_truncated ? ', truncated to 4,000' : ''})`, quoteBlock(r.description));
|
|
@@ -547,15 +889,19 @@ export const getRecord = tool('zenodo_get_record', {
|
|
|
547
889
|
else if (r.description_length !== undefined || r.description_truncated !== undefined) {
|
|
548
890
|
lines.push(`Description length: ${r.description_length}, truncated: ${r.description_truncated}`);
|
|
549
891
|
}
|
|
550
|
-
|
|
551
|
-
lines.push(
|
|
552
|
-
}
|
|
892
|
+
r.additional_descriptions.forEach((d, i) => {
|
|
893
|
+
lines.push(...renderAdditionalDescription(d, i, r.additional_description_count));
|
|
894
|
+
});
|
|
553
895
|
const files = r.files;
|
|
554
896
|
lines.push('', `### Files (enabled: ${files.enabled}, count: ${files.count ?? 'not disclosed'}, total bytes: ${files.total_bytes ?? 'not disclosed'}, shown: ${files.shown})`);
|
|
555
897
|
for (const f of files.entries) {
|
|
556
898
|
lines.push(`- ${inline(f.key)} — ${f.size ?? '?'} bytes${f.mimetype ? `, ${inline(f.mimetype)}` : ''}${f.md5 ? `, md5 ${inline(f.md5)}` : ''} — ${f.download_url}`);
|
|
557
899
|
}
|
|
558
900
|
}
|
|
901
|
+
if (result.window)
|
|
902
|
+
lines.push(...renderWindow(result.window));
|
|
903
|
+
if (result.continuations)
|
|
904
|
+
lines.push(...renderContinuations(result.continuations));
|
|
559
905
|
if (result.citation) {
|
|
560
906
|
lines.push('', quoteBlock(result.citation, `Citation (${result.citation_style ?? 'requested style'}, untrusted):`));
|
|
561
907
|
}
|