@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.
Files changed (97) hide show
  1. package/AGENTS.md +435 -0
  2. package/CLAUDE.md +435 -0
  3. package/Dockerfile +140 -0
  4. package/LICENSE +201 -0
  5. package/README.md +292 -0
  6. package/changelog/0.1.x/0.1.1.md +21 -0
  7. package/changelog/template.md +151 -0
  8. package/dist/config/server-config.d.ts +13 -0
  9. package/dist/config/server-config.d.ts.map +1 -0
  10. package/dist/config/server-config.js +19 -0
  11. package/dist/config/server-config.js.map +1 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +21 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/mcp-server/tools/definitions/get-record.tool.d.ts +186 -0
  17. package/dist/mcp-server/tools/definitions/get-record.tool.d.ts.map +1 -0
  18. package/dist/mcp-server/tools/definitions/get-record.tool.js +568 -0
  19. package/dist/mcp-server/tools/definitions/get-record.tool.js.map +1 -0
  20. package/dist/mcp-server/tools/definitions/index.d.ts +721 -0
  21. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
  22. package/dist/mcp-server/tools/definitions/index.js +21 -0
  23. package/dist/mcp-server/tools/definitions/index.js.map +1 -0
  24. package/dist/mcp-server/tools/definitions/list-files.tool.d.ts +108 -0
  25. package/dist/mcp-server/tools/definitions/list-files.tool.d.ts.map +1 -0
  26. package/dist/mcp-server/tools/definitions/list-files.tool.js +348 -0
  27. package/dist/mcp-server/tools/definitions/list-files.tool.js.map +1 -0
  28. package/dist/mcp-server/tools/definitions/list-versions.tool.d.ts +92 -0
  29. package/dist/mcp-server/tools/definitions/list-versions.tool.d.ts.map +1 -0
  30. package/dist/mcp-server/tools/definitions/list-versions.tool.js +268 -0
  31. package/dist/mcp-server/tools/definitions/list-versions.tool.js.map +1 -0
  32. package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.d.ts +99 -0
  33. package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.d.ts.map +1 -0
  34. package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.js +324 -0
  35. package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.js.map +1 -0
  36. package/dist/mcp-server/tools/definitions/read-file.tool.d.ts +104 -0
  37. package/dist/mcp-server/tools/definitions/read-file.tool.d.ts.map +1 -0
  38. package/dist/mcp-server/tools/definitions/read-file.tool.js +370 -0
  39. package/dist/mcp-server/tools/definitions/read-file.tool.js.map +1 -0
  40. package/dist/mcp-server/tools/definitions/search-records.tool.d.ts +181 -0
  41. package/dist/mcp-server/tools/definitions/search-records.tool.d.ts.map +1 -0
  42. package/dist/mcp-server/tools/definitions/search-records.tool.js +531 -0
  43. package/dist/mcp-server/tools/definitions/search-records.tool.js.map +1 -0
  44. package/dist/mcp-server/tools/record-miss.d.ts +37 -0
  45. package/dist/mcp-server/tools/record-miss.d.ts.map +1 -0
  46. package/dist/mcp-server/tools/record-miss.js +51 -0
  47. package/dist/mcp-server/tools/record-miss.js.map +1 -0
  48. package/dist/mcp-server/tools/render.d.ts +31 -0
  49. package/dist/mcp-server/tools/render.d.ts.map +1 -0
  50. package/dist/mcp-server/tools/render.js +53 -0
  51. package/dist/mcp-server/tools/render.js.map +1 -0
  52. package/dist/mcp-server/tools/schema-helpers.d.ts +20 -0
  53. package/dist/mcp-server/tools/schema-helpers.d.ts.map +1 -0
  54. package/dist/mcp-server/tools/schema-helpers.js +44 -0
  55. package/dist/mcp-server/tools/schema-helpers.js.map +1 -0
  56. package/dist/services/zenodo/cache.d.ts +25 -0
  57. package/dist/services/zenodo/cache.d.ts.map +1 -0
  58. package/dist/services/zenodo/cache.js +58 -0
  59. package/dist/services/zenodo/cache.js.map +1 -0
  60. package/dist/services/zenodo/html-to-text.d.ts +23 -0
  61. package/dist/services/zenodo/html-to-text.d.ts.map +1 -0
  62. package/dist/services/zenodo/html-to-text.js +148 -0
  63. package/dist/services/zenodo/html-to-text.js.map +1 -0
  64. package/dist/services/zenodo/http.d.ts +73 -0
  65. package/dist/services/zenodo/http.d.ts.map +1 -0
  66. package/dist/services/zenodo/http.js +378 -0
  67. package/dist/services/zenodo/http.js.map +1 -0
  68. package/dist/services/zenodo/identifiers.d.ts +104 -0
  69. package/dist/services/zenodo/identifiers.d.ts.map +1 -0
  70. package/dist/services/zenodo/identifiers.js +261 -0
  71. package/dist/services/zenodo/identifiers.js.map +1 -0
  72. package/dist/services/zenodo/normalize.d.ts +29 -0
  73. package/dist/services/zenodo/normalize.d.ts.map +1 -0
  74. package/dist/services/zenodo/normalize.js +324 -0
  75. package/dist/services/zenodo/normalize.js.map +1 -0
  76. package/dist/services/zenodo/query-builder.d.ts +87 -0
  77. package/dist/services/zenodo/query-builder.d.ts.map +1 -0
  78. package/dist/services/zenodo/query-builder.js +221 -0
  79. package/dist/services/zenodo/query-builder.js.map +1 -0
  80. package/dist/services/zenodo/resource-types.d.ts +34 -0
  81. package/dist/services/zenodo/resource-types.d.ts.map +1 -0
  82. package/dist/services/zenodo/resource-types.js +97 -0
  83. package/dist/services/zenodo/resource-types.js.map +1 -0
  84. package/dist/services/zenodo/text-preview.d.ts +56 -0
  85. package/dist/services/zenodo/text-preview.d.ts.map +1 -0
  86. package/dist/services/zenodo/text-preview.js +151 -0
  87. package/dist/services/zenodo/text-preview.js.map +1 -0
  88. package/dist/services/zenodo/types.d.ts +517 -0
  89. package/dist/services/zenodo/types.d.ts.map +1 -0
  90. package/dist/services/zenodo/types.js +8 -0
  91. package/dist/services/zenodo/types.js.map +1 -0
  92. package/dist/services/zenodo/zenodo-service.d.ts +98 -0
  93. package/dist/services/zenodo/zenodo-service.d.ts.map +1 -0
  94. package/dist/services/zenodo/zenodo-service.js +392 -0
  95. package/dist/services/zenodo/zenodo-service.js.map +1 -0
  96. package/package.json +110 -0
  97. package/server.json +113 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @fileoverview The `found: false` outcome zenodo_get_record and zenodo_list_versions
3
+ * share: the removal-tombstone schema and the `miss_kind` + guidance for each way an
4
+ * id can fail to resolve, so both tools describe a miss identically.
5
+ * @module mcp-server/tools/record-miss
6
+ */
7
+ import { z } from '@cyanheads/mcp-ts-core';
8
+ /** Output schema of a deleted record's removal tombstone. */
9
+ export const TombstoneSchema = z
10
+ .object({
11
+ removal_date: z.string().optional().describe('When the record was removed (ISO 8601).'),
12
+ removal_reason: z.string().optional().describe('Removal reason id, e.g. spam or retracted.'),
13
+ note: z.string().optional().describe('Removal note, when non-empty.'),
14
+ citation_text: z
15
+ .string()
16
+ .optional()
17
+ .describe('The citation the tombstone still serves — the only bibliographic data a deleted record keeps.'),
18
+ })
19
+ .describe('Removal tombstone of a deleted record.');
20
+ /** `miss_kind`, guidance, and (for a deleted record) the tombstone of a record GET that found nothing. */
21
+ export function recordMiss(recid, lookup) {
22
+ switch (lookup.status) {
23
+ case 'not_found':
24
+ return {
25
+ miss_kind: 'not_found',
26
+ guidance: `No Zenodo record has id ${recid}; it may never have existed. Search by title with zenodo_search_records, or by query doi:"10.5281/zenodo.${recid}" with all_versions true.`,
27
+ };
28
+ case 'deleted': {
29
+ const { tombstone } = lookup;
30
+ const date = tombstone.removal_date?.slice(0, 10) ?? 'an unrecorded date';
31
+ return {
32
+ miss_kind: 'deleted',
33
+ guidance: `Record ${recid} was removed from Zenodo on ${date} (reason: ${tombstone.removal_reason ?? 'not given'}); only its tombstone citation remains. Find a replacement or another version by title with zenodo_search_records.`,
34
+ tombstone,
35
+ };
36
+ }
37
+ case 'restricted':
38
+ return {
39
+ miss_kind: 'restricted',
40
+ guidance: `Record ${recid} exists but its metadata is restricted to authorized users and cannot be read anonymously.`,
41
+ };
42
+ }
43
+ }
44
+ /** `miss_kind` and guidance for a non-Zenodo DOI that no Zenodo record carries. */
45
+ export function notOnZenodoMiss(doi) {
46
+ return {
47
+ miss_kind: 'not_on_zenodo',
48
+ guidance: `DOI ${doi} is not registered to a Zenodo record. It resolves elsewhere at https://doi.org/${doi}; to find a related deposit, search by title with zenodo_search_records.`,
49
+ };
50
+ }
51
+ //# sourceMappingURL=record-miss.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-miss.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/record-miss.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,6DAA6D;AAC7D,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,CAAC;IACN,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;IACvF,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4CAA4C,CAAC;IAC5F,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;IACrE,aAAa,EAAE,CAAC;SACb,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,+FAA+F,CAChG;CACJ,CAAC;KACD,QAAQ,CAAC,wCAAwC,CAAC,CAAC;AAEtD,0GAA0G;AAC1G,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,MAAkD;IAC1F,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,WAAW;YACd,OAAO;gBACL,SAAS,EAAE,WAAoB;gBAC/B,QAAQ,EAAE,2BAA2B,KAAK,4GAA4G,KAAK,2BAA2B;aACvL,CAAC;QACJ,KAAK,SAAS,EAAE,CAAC;YACf,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;YAC7B,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,oBAAoB,CAAC;YAC1E,OAAO;gBACL,SAAS,EAAE,SAAkB;gBAC7B,QAAQ,EAAE,UAAU,KAAK,+BAA+B,IAAI,aAAa,SAAS,CAAC,cAAc,IAAI,WAAW,oHAAoH;gBACpO,SAAS;aACV,CAAC;QACJ,CAAC;QACD,KAAK,YAAY;YACf,OAAO;gBACL,SAAS,EAAE,YAAqB;gBAChC,QAAQ,EAAE,UAAU,KAAK,4FAA4F;aACtH,CAAC;IACN,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,OAAO;QACL,SAAS,EAAE,eAAwB;QACnC,QAAQ,EAAE,OAAO,GAAG,mFAAmF,GAAG,0EAA0E;KACrL,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @fileoverview Render helpers every Zenodo tool's `format()` uses for untrusted,
3
+ * depositor-supplied text: `inline` for single-line slots, `quoteBlock` for
4
+ * multi-line fields, `fence` for file previews. They keep upstream text readable as
5
+ * data and stop it from breaking out of the slot it is rendered in.
6
+ * @module mcp-server/tools/render
7
+ */
8
+ /**
9
+ * Flattens a string for an inline slot (a title, name, keyword, file key): every
10
+ * run of CR, LF, NEL, U+2028, or U+2029 becomes one space.
11
+ */
12
+ export declare function inline(s: string): string;
13
+ /** True when `s` holds a line break {@link inline} would flatten \u2014 the cue to render it with {@link quoteBlock}. */
14
+ export declare function hasLineBreak(s: string): boolean;
15
+ /**
16
+ * Renders multi-line untrusted text as a blockquote under a lead line. Every line —
17
+ * split on CRLF, CR, LF, NEL, U+2028, and U+2029 — gets a `> ` prefix, so nothing
18
+ * in the text can end the quote.
19
+ */
20
+ export declare function quoteBlock(text: string, lead?: string): string;
21
+ /**
22
+ * Renders file content inside a code fence whose backtick run is one longer than
23
+ * the longest run in the content (minimum 3), preceded by a line naming the source
24
+ * as untrusted data. Lines are split on CR, LF, and CRLF.
25
+ */
26
+ export declare function fence(text: string, source: {
27
+ key: string;
28
+ lang?: string;
29
+ recid: string;
30
+ }): string;
31
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/render.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH;;;GAGG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAExC;AAED,yHAAyH;AACzH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAyC,GAAG,MAAM,CAG9F;AASD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CASjG"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @fileoverview Render helpers every Zenodo tool's `format()` uses for untrusted,
3
+ * depositor-supplied text: `inline` for single-line slots, `quoteBlock` for
4
+ * multi-line fields, `fence` for file previews. They keep upstream text readable as
5
+ * data and stop it from breaking out of the slot it is rendered in.
6
+ * @module mcp-server/tools/render
7
+ */
8
+ const INLINE_BREAKS = /[\r\n\u0085\u2028\u2029]+/g;
9
+ const LINE_BREAK = /[\r\n\u0085\u2028\u2029]/;
10
+ const LINE_SPLIT = /\r\n|[\r\n\u0085\u2028\u2029]/;
11
+ /**
12
+ * Flattens a string for an inline slot (a title, name, keyword, file key): every
13
+ * run of CR, LF, NEL, U+2028, or U+2029 becomes one space.
14
+ */
15
+ export function inline(s) {
16
+ return s.replace(INLINE_BREAKS, ' ');
17
+ }
18
+ /** True when `s` holds a line break {@link inline} would flatten \u2014 the cue to render it with {@link quoteBlock}. */
19
+ export function hasLineBreak(s) {
20
+ return LINE_BREAK.test(s);
21
+ }
22
+ /**
23
+ * Renders multi-line untrusted text as a blockquote under a lead line. Every line —
24
+ * split on CRLF, CR, LF, NEL, U+2028, and U+2029 — gets a `> ` prefix, so nothing
25
+ * in the text can end the quote.
26
+ */
27
+ export function quoteBlock(text, lead = 'Depositor-supplied text (untrusted):') {
28
+ const lines = text.split(LINE_SPLIT).map((line) => (line ? `> ${line}` : '>'));
29
+ return [lead, ...lines].join('\n');
30
+ }
31
+ /** Longest run of consecutive backticks in `text`. */
32
+ function longestBacktickRun(text) {
33
+ let longest = 0;
34
+ for (const run of text.match(/`+/g) ?? [])
35
+ longest = Math.max(longest, run.length);
36
+ return longest;
37
+ }
38
+ /**
39
+ * Renders file content inside a code fence whose backtick run is one longer than
40
+ * the longest run in the content (minimum 3), preceded by a line naming the source
41
+ * as untrusted data. Lines are split on CR, LF, and CRLF.
42
+ */
43
+ export function fence(text, source) {
44
+ const ticks = '`'.repeat(Math.max(3, longestBacktickRun(text) + 1));
45
+ const body = text.split(/\r\n|[\r\n]/).join('\n');
46
+ return [
47
+ `Untrusted file content from Zenodo record ${source.recid}, ${inline(source.key)} — shown as data:`,
48
+ `${ticks}${source.lang ?? ''}`,
49
+ body,
50
+ ticks,
51
+ ].join('\n');
52
+ }
53
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/render.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,aAAa,GAAG,4BAA4B,CAAC;AACnD,MAAM,UAAU,GAAG,0BAA0B,CAAC;AAC9C,MAAM,UAAU,GAAG,+BAA+B,CAAC;AAEnD;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,CAAS;IAC9B,OAAO,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;AACvC,CAAC;AAED,yHAAyH;AACzH,MAAM,UAAU,YAAY,CAAC,CAAS;IACpC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,IAAI,GAAG,sCAAsC;IACpF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/E,OAAO,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrC,CAAC;AAED,sDAAsD;AACtD,SAAS,kBAAkB,CAAC,IAAY;IACtC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE;QAAE,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,IAAY,EAAE,MAAqD;IACvF,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACpE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClD,OAAO;QACL,6CAA6C,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,mBAAmB;QACnG,GAAG,KAAK,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE,EAAE;QAC9B,IAAI;QACJ,KAAK;KACN,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @fileoverview Input preprocessors shared by the Zenodo tools. Form clients submit
3
+ * every optional field, blank ones as `''`; these make a blank value read as unset
4
+ * so no optional field needs `.min(1)`. Enum inputs also fold case and separators
5
+ * and accept one-to-one aliases before the enum check.
6
+ * @module mcp-server/tools/schema-helpers
7
+ */
8
+ /** Trims a string; `''` or whitespace-only → `undefined`, so a blank form field reads as "not set". */
9
+ export declare const blankToUndefined: (v: unknown) => unknown;
10
+ /** A lone string becomes a one-element array; blank elements are dropped, and an array left empty reads as unset. */
11
+ export declare const toOptionalArray: (v: unknown) => unknown;
12
+ /**
13
+ * Preprocess for an enum input. A blank value reads as unset; a value whose case- and
14
+ * separator-folded form names exactly one option, or one of the listed aliases, maps
15
+ * to that option. Anything else passes through unchanged, so the enum still rejects
16
+ * it with the valid options listed. The advertised JSON Schema keeps the canonical
17
+ * enum: the mapping runs before validation and changes nothing a client sees.
18
+ */
19
+ export declare function enumPreprocess<const T extends readonly [string, ...string[]]>(options: T, aliases?: Readonly<Record<string, T[number]>>): (v: unknown) => unknown;
20
+ //# sourceMappingURL=schema-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-helpers.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/schema-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,uGAAuG;AACvG,eAAO,MAAM,gBAAgB,MAAO,OAAO,KAAG,OAI7C,CAAC;AAEF,qHAAqH;AACrH,eAAO,MAAM,eAAe,MAAO,OAAO,KAAG,OAM5C,CAAC;AAKF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,CAAC,CAAC,SAAS,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,EAC3E,OAAO,EAAE,CAAC,EACV,OAAO,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAM,GAChD,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAQzB"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @fileoverview Input preprocessors shared by the Zenodo tools. Form clients submit
3
+ * every optional field, blank ones as `''`; these make a blank value read as unset
4
+ * so no optional field needs `.min(1)`. Enum inputs also fold case and separators
5
+ * and accept one-to-one aliases before the enum check.
6
+ * @module mcp-server/tools/schema-helpers
7
+ */
8
+ /** Trims a string; `''` or whitespace-only → `undefined`, so a blank form field reads as "not set". */
9
+ export const blankToUndefined = (v) => {
10
+ if (typeof v !== 'string')
11
+ return v;
12
+ const trimmed = v.trim();
13
+ return trimmed === '' ? undefined : trimmed;
14
+ };
15
+ /** A lone string becomes a one-element array; blank elements are dropped, and an array left empty reads as unset. */
16
+ export const toOptionalArray = (v) => {
17
+ const b = blankToUndefined(v);
18
+ const arr = typeof b === 'string' ? [b] : b;
19
+ if (!Array.isArray(arr))
20
+ return arr;
21
+ const kept = arr.map(blankToUndefined).filter((x) => x !== undefined);
22
+ return kept.length ? kept : undefined;
23
+ };
24
+ /** Folds case and separators, so `Metadata Only`, `metadata_only`, and `METADATA-ONLY` compare equal. */
25
+ const foldEnumKey = (s) => s.toLowerCase().replace(/[\s_-]+/g, '');
26
+ /**
27
+ * Preprocess for an enum input. A blank value reads as unset; a value whose case- and
28
+ * separator-folded form names exactly one option, or one of the listed aliases, maps
29
+ * to that option. Anything else passes through unchanged, so the enum still rejects
30
+ * it with the valid options listed. The advertised JSON Schema keeps the canonical
31
+ * enum: the mapping runs before validation and changes nothing a client sees.
32
+ */
33
+ export function enumPreprocess(options, aliases = {}) {
34
+ const table = new Map();
35
+ for (const [alias, option] of Object.entries(aliases))
36
+ table.set(foldEnumKey(alias), option);
37
+ for (const option of options)
38
+ table.set(foldEnumKey(option), option);
39
+ return (v) => {
40
+ const b = blankToUndefined(v);
41
+ return typeof b === 'string' ? (table.get(foldEnumKey(b)) ?? b) : b;
42
+ };
43
+ }
44
+ //# sourceMappingURL=schema-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-helpers.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/schema-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,uGAAuG;AACvG,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAU,EAAW,EAAE;IACtD,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACzB,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC,CAAC;AAEF,qHAAqH;AACrH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAU,EAAW,EAAE;IACrD,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAC9B,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACxC,CAAC,CAAC;AAEF,yGAAyG;AACzG,MAAM,WAAW,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAEnF;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAU,EACV,OAAO,GAAwC,EAAE;IAEjD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;IAC7F,KAAK,MAAM,MAAM,IAAI,OAAO;QAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;IACrE,OAAO,CAAC,CAAC,EAAE,EAAE;QACX,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;QAC9B,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtE,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @fileoverview Process-local TTL LRU cache with an approximate byte budget. Zenodo
3
+ * data is public, so one cache is shared across tenants; hits save the shared
4
+ * egress IP's rate budget.
5
+ * @module services/zenodo/cache
6
+ */
7
+ /** Budget options for {@link TtlLruCache}. */
8
+ export interface CacheOptions {
9
+ /** Approximate total budget across all entries. */
10
+ maxBytes: number;
11
+ /** Entries estimated above this size are not stored. */
12
+ maxEntryBytes: number;
13
+ }
14
+ /** In-memory TTL cache, least-recently-used eviction by approximate byte size. */
15
+ export declare class TtlLruCache {
16
+ #private;
17
+ constructor(options: CacheOptions);
18
+ /** Returns the live value for `key` (refreshing its recency), or `undefined`. */
19
+ get<T>(key: string): T | undefined;
20
+ /** Stores `value` for `ttlMs`. Oversized values are skipped; older entries are evicted to fit. */
21
+ set(key: string, value: unknown, ttlMs: number): void;
22
+ /** Drops every entry. */
23
+ clear(): void;
24
+ }
25
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../src/services/zenodo/cache.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,8CAA8C;AAC9C,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,aAAa,EAAE,MAAM,CAAC;CACvB;AAOD,kFAAkF;AAClF,qBAAa,WAAW;;IAKtB,YAAY,OAAO,EAAE,YAAY,EAEhC;IAED,iFAAiF;IACjF,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAUjC;IAED,kGAAkG;IAClG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAWpD;IAED,yBAAyB;IACzB,KAAK,IAAI,IAAI,CAGZ;CAMF"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @fileoverview Process-local TTL LRU cache with an approximate byte budget. Zenodo
3
+ * data is public, so one cache is shared across tenants; hits save the shared
4
+ * egress IP's rate budget.
5
+ * @module services/zenodo/cache
6
+ */
7
+ /** Approximate size of a JSON-shaped value: its serialized length. */
8
+ function estimateBytes(value) {
9
+ return value === undefined ? 0 : (JSON.stringify(value)?.length ?? 0);
10
+ }
11
+ /** In-memory TTL cache, least-recently-used eviction by approximate byte size. */
12
+ export class TtlLruCache {
13
+ #entries = new Map();
14
+ #options;
15
+ #bytes = 0;
16
+ constructor(options) {
17
+ this.#options = options;
18
+ }
19
+ /** Returns the live value for `key` (refreshing its recency), or `undefined`. */
20
+ get(key) {
21
+ const entry = this.#entries.get(key);
22
+ if (!entry)
23
+ return;
24
+ if (entry.expiresAt <= Date.now()) {
25
+ this.#delete(key, entry);
26
+ return;
27
+ }
28
+ this.#entries.delete(key);
29
+ this.#entries.set(key, entry);
30
+ return entry.value;
31
+ }
32
+ /** Stores `value` for `ttlMs`. Oversized values are skipped; older entries are evicted to fit. */
33
+ set(key, value, ttlMs) {
34
+ const bytes = estimateBytes(value);
35
+ const existing = this.#entries.get(key);
36
+ if (existing)
37
+ this.#delete(key, existing);
38
+ if (bytes > this.#options.maxEntryBytes)
39
+ return;
40
+ this.#entries.set(key, { bytes, expiresAt: Date.now() + ttlMs, value });
41
+ this.#bytes += bytes;
42
+ for (const [oldKey, oldEntry] of this.#entries) {
43
+ if (this.#bytes <= this.#options.maxBytes)
44
+ break;
45
+ this.#delete(oldKey, oldEntry);
46
+ }
47
+ }
48
+ /** Drops every entry. */
49
+ clear() {
50
+ this.#entries.clear();
51
+ this.#bytes = 0;
52
+ }
53
+ #delete(key, entry) {
54
+ this.#entries.delete(key);
55
+ this.#bytes -= entry.bytes;
56
+ }
57
+ }
58
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../../src/services/zenodo/cache.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH,sEAAsE;AACtE,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,kFAAkF;AAClF,MAAM,OAAO,WAAW;IACb,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IACpC,QAAQ,CAAe;IAChC,MAAM,GAAG,CAAC,CAAC;IAEX,YAAY,OAAqB;QAC/B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,iFAAiF;IACjF,GAAG,CAAI,GAAW;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAClC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC9B,OAAO,KAAK,CAAC,KAAU,CAAC;IAC1B,CAAC;IAED,kGAAkG;IAClG,GAAG,CAAC,GAAW,EAAE,KAAc,EAAE,KAAa;QAC5C,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,QAAQ;YAAE,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC1C,IAAI,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa;YAAE,OAAO;QAChD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;QACrB,KAAK,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC/C,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ;gBAAE,MAAM;YACjD,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAW,EAAE,KAAY;QAC/B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC;IAC7B,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview Deterministic HTML → plain-text conversion for depositor-supplied
3
+ * record descriptions. The one documented transformation of upstream text: drop
4
+ * script/style, turn block ends and line breaks into newlines, inline link targets,
5
+ * strip tags, decode entities, normalize whitespace.
6
+ * @module services/zenodo/html-to-text
7
+ */
8
+ /**
9
+ * Decodes numeric (decimal and hex) entities, the fixed named table (any case), and
10
+ * the Latin-1 named entities (exact case). Unknown names stay as written.
11
+ */
12
+ export declare function decodeEntities(text: string): string;
13
+ /**
14
+ * Converts an HTML fragment to plain text:
15
+ * 1. drops `<script>`/`<style>` elements with their content (and comments);
16
+ * 2. `<br>` → newline, closing block tags → newline, `<li>` → `- `;
17
+ * 3. `<a href="X">T</a>` → `T (X)` when X is http(s) and differs from T;
18
+ * 4. strips remaining tags; 5. decodes entities;
19
+ * 6. normalizes CRLF/CR to LF, trims trailing spaces per line, collapses 3+
20
+ * newlines to 2, and trims the result.
21
+ */
22
+ export declare function htmlToText(html: string): string;
23
+ //# sourceMappingURL=html-to-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-to-text.d.ts","sourceRoot":"","sources":["../../../src/services/zenodo/html-to-text.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA4CH;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWnD;AAgED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmB/C"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @fileoverview Deterministic HTML → plain-text conversion for depositor-supplied
3
+ * record descriptions. The one documented transformation of upstream text: drop
4
+ * script/style, turn block ends and line breaks into newlines, inline link targets,
5
+ * strip tags, decode entities, normalize whitespace.
6
+ * @module services/zenodo/html-to-text
7
+ */
8
+ const NAMED_ENTITIES = {
9
+ amp: '&',
10
+ lt: '<',
11
+ gt: '>',
12
+ quot: '"',
13
+ apos: "'",
14
+ nbsp: ' ',
15
+ ndash: '–',
16
+ mdash: '—',
17
+ hellip: '…',
18
+ lsquo: '‘',
19
+ rsquo: '’',
20
+ ldquo: '“',
21
+ rdquo: '”',
22
+ copy: '©',
23
+ reg: '®',
24
+ deg: '°',
25
+ };
26
+ /**
27
+ * The HTML 4 Latin-1 entities (U+00A0–U+00FF), case-sensitive. Zenodo's
28
+ * description editor stores accented letters as named entities (`n&uacute;meros`),
29
+ * so non-English descriptions stay unreadable without them.
30
+ */
31
+ const LATIN1_ENTITIES = Object.fromEntries('AElig:198 Aacute:193 Acirc:194 Agrave:192 Aring:197 Atilde:195 Auml:196 Ccedil:199 ETH:208 Eacute:201 Ecirc:202 Egrave:200 Euml:203 Iacute:205 Icirc:206 Igrave:204 Iuml:207 Ntilde:209 Oacute:211 Ocirc:212 Ograve:210 Oslash:216 Otilde:213 Ouml:214 THORN:222 Uacute:218 Ucirc:219 Ugrave:217 Uuml:220 Yacute:221 aacute:225 acirc:226 acute:180 aelig:230 agrave:224 aring:229 atilde:227 auml:228 brvbar:166 ccedil:231 cedil:184 cent:162 curren:164 divide:247 eacute:233 ecirc:234 egrave:232 eth:240 euml:235 frac12:189 frac14:188 frac34:190 iacute:237 icirc:238 iexcl:161 igrave:236 iquest:191 iuml:239 laquo:171 macr:175 micro:181 middot:183 not:172 ntilde:241 oacute:243 ocirc:244 ograve:242 ordf:170 ordm:186 oslash:248 otilde:245 ouml:246 para:182 plusmn:177 pound:163 raquo:187 sect:167 shy:173 sup1:185 sup2:178 sup3:179 szlig:223 thorn:254 times:215 uacute:250 ucirc:251 ugrave:249 uml:168 uuml:252 yacute:253 yen:165 yuml:255'
32
+ .split(' ')
33
+ .map((pair) => {
34
+ const [name, code] = pair.split(':');
35
+ return [name, Number(code)];
36
+ }));
37
+ function codePointToString(codePoint) {
38
+ return Number.isInteger(codePoint) &&
39
+ codePoint > 0 &&
40
+ codePoint <= 0x10ffff &&
41
+ (codePoint < 0xd800 || codePoint > 0xdfff)
42
+ ? String.fromCodePoint(codePoint)
43
+ : '\uFFFD';
44
+ }
45
+ /**
46
+ * Decodes numeric (decimal and hex) entities, the fixed named table (any case), and
47
+ * the Latin-1 named entities (exact case). Unknown names stay as written.
48
+ */
49
+ export function decodeEntities(text) {
50
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]*);/gi, (match, body) => {
51
+ if (body[0] === '#') {
52
+ const hex = body[1] === 'x' || body[1] === 'X';
53
+ return codePointToString(Number.parseInt(body.slice(hex ? 2 : 1), hex ? 16 : 10));
54
+ }
55
+ const named = NAMED_ENTITIES[body.toLowerCase()];
56
+ if (named !== undefined)
57
+ return named;
58
+ const latin1 = LATIN1_ENTITIES[body];
59
+ return latin1 === undefined ? match : String.fromCodePoint(latin1);
60
+ });
61
+ }
62
+ /** Re-escapes the characters entity decoding would otherwise read twice. */
63
+ function escapeForDecode(text) {
64
+ return text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
65
+ }
66
+ /**
67
+ * Every pattern and scan below stays linear on hostile input: descriptions are
68
+ * depositor-supplied and converted for every search hit, so a pattern that rescans
69
+ * to the end of the text from each `<` (or each space) would let one deposit stall
70
+ * the server. Tag patterns stop at the next `<` or `>` instead of scanning past it.
71
+ */
72
+ /** Drops every `<…>` tag: each `<` through the next `>`, as `/<[^>]*>/g` would, in one pass. */
73
+ function stripTags(html) {
74
+ let out = '';
75
+ let from = 0;
76
+ for (;;) {
77
+ const open = html.indexOf('<', from);
78
+ const close = open === -1 ? -1 : html.indexOf('>', open);
79
+ if (close === -1)
80
+ return out + html.slice(from);
81
+ out += html.slice(from, open);
82
+ from = close + 1;
83
+ }
84
+ }
85
+ const HREF = /\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
86
+ /**
87
+ * `<a href="X">T</a>` → `T (X)` when X is http(s) and differs from T, else `T`.
88
+ * Each closing tag is searched for once, from its opening tag; with none left, the
89
+ * remaining links stay as they are.
90
+ */
91
+ function inlineLinks(html) {
92
+ const openTag = /<a\b([^<>]*)>/gi;
93
+ const closeTag = /<\/a\s*>/gi;
94
+ let out = '';
95
+ let from = 0;
96
+ for (let open = openTag.exec(html); open; open = openTag.exec(html)) {
97
+ const attr = HREF.exec(open[1] ?? '');
98
+ if (!attr)
99
+ continue;
100
+ closeTag.lastIndex = openTag.lastIndex;
101
+ const close = closeTag.exec(html);
102
+ if (!close)
103
+ break;
104
+ const inner = html.slice(openTag.lastIndex, close.index);
105
+ const href = decodeEntities(attr[1] ?? attr[2] ?? attr[3] ?? '').trim();
106
+ const text = decodeEntities(stripTags(inner)).trim();
107
+ out +=
108
+ html.slice(from, open.index) +
109
+ (/^https?:\/\//i.test(href) && href !== text ? `${inner} (${escapeForDecode(href)})` : inner);
110
+ from = close.index + close[0].length;
111
+ openTag.lastIndex = from;
112
+ }
113
+ return out + html.slice(from);
114
+ }
115
+ /** `line` without trailing spaces and tabs. */
116
+ function trimTrailingBlanks(line) {
117
+ let end = line.length;
118
+ while (end > 0 && (line[end - 1] === ' ' || line[end - 1] === '\t'))
119
+ end--;
120
+ return line.slice(0, end);
121
+ }
122
+ /**
123
+ * Converts an HTML fragment to plain text:
124
+ * 1. drops `<script>`/`<style>` elements with their content (and comments);
125
+ * 2. `<br>` → newline, closing block tags → newline, `<li>` → `- `;
126
+ * 3. `<a href="X">T</a>` → `T (X)` when X is http(s) and differs from T;
127
+ * 4. strips remaining tags; 5. decodes entities;
128
+ * 6. normalizes CRLF/CR to LF, trims trailing spaces per line, collapses 3+
129
+ * newlines to 2, and trims the result.
130
+ */
131
+ export function htmlToText(html) {
132
+ let s = html
133
+ .replace(/<!--[\s\S]*?(?:-->|$)/g, '')
134
+ .replace(/<(script|style)\b[^<>]*>[\s\S]*?(?:<\/\1\s*>|$)/gi, '');
135
+ s = s
136
+ .replace(/<br\s*\/?>/gi, '\n')
137
+ .replace(/<\/(?:p|div|h[1-6]|li|tr|blockquote|pre|ul|ol|table)\s*>/gi, '\n')
138
+ .replace(/<li\b[^<>]*>/gi, '- ');
139
+ s = decodeEntities(stripTags(inlineLinks(s)));
140
+ return s
141
+ .replace(/\r\n?/g, '\n')
142
+ .split('\n')
143
+ .map(trimTrailingBlanks)
144
+ .join('\n')
145
+ .replace(/\n{3,}/g, '\n\n')
146
+ .trim();
147
+ }
148
+ //# sourceMappingURL=html-to-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-to-text.js","sourceRoot":"","sources":["../../../src/services/zenodo/html-to-text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
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 type { Context } from '@cyanheads/mcp-ts-core';
10
+ /** Upstream rate-limit bucket. `search` is the record-search list only (30/min). */
11
+ export type Bucket = 'search' | 'general';
12
+ /**
13
+ * Which row of the retry matrix a call follows: `search` (record search),
14
+ * `doi_lookup` (the server-built DOI search), `record` (record GET), `versions`
15
+ * (`/versions`), `archive` (a ZIP's `/container` listing or member read), `other`
16
+ * (file content, `versions/latest`, citations, vocabularies).
17
+ */
18
+ export type EndpointKind = 'search' | 'doi_lookup' | 'record' | 'versions' | 'archive' | 'other';
19
+ /** One upstream request as the service builds it. */
20
+ export interface ZenodoRequest {
21
+ accept: string;
22
+ /** Per-attempt time budget (capped by the ladder's remaining budget). */
23
+ attemptMs: number;
24
+ bucket: Bucket;
25
+ /** `archive` endpoints: the archive's download URL, named in a typed failure. */
26
+ downloadUrl?: string;
27
+ endpoint: EndpointKind;
28
+ /** Statuses returned to the caller as results rather than mapped to errors. */
29
+ okStatuses: readonly number[];
30
+ /** Log label. */
31
+ operation: string;
32
+ /** Path under `/api`, starting with `/`, already percent-encoded. */
33
+ path: string;
34
+ query?: readonly (readonly [string, string])[];
35
+ range?: string;
36
+ /** `follow` (default): follow redirects within zenodo.org. `manual`: return the 3xx itself. */
37
+ redirect?: 'follow' | 'manual';
38
+ }
39
+ /** Per-attempt budgets. Multi-thousand-file record GETs legitimately take 32–34 s. */
40
+ export declare const JSON_ATTEMPT_MS = 45000;
41
+ export declare const CONTENT_ATTEMPT_MS = 20000;
42
+ /** Cancels a response body that will not be read, releasing the connection. */
43
+ export declare function discardBody(res: Response): Promise<void>;
44
+ /** Reads a UTF-8 body of at most {@link MAX_BODY_BYTES}; a larger one is refused, not buffered. */
45
+ export declare function readText(res: Response): Promise<string>;
46
+ /** Reads a JSON body. An HTML page or unparseable body on a 2xx is a transient upstream fault. */
47
+ export declare function readJson<T>(res: Response): Promise<T>;
48
+ /**
49
+ * Streams a body, keeping at most `maxBytes` after skipping `skipBytes`, and
50
+ * cancels the stream as soon as the cap is passed. `more` is true when bytes
51
+ * remained past the cap.
52
+ */
53
+ export declare function readCapped(res: Response, maxBytes: number, skipBytes?: number): Promise<{
54
+ bytes: Uint8Array;
55
+ more: boolean;
56
+ }>;
57
+ /** The HTTP client: pacing, header gate, retry ladder, and status mapping. */
58
+ export declare class ZenodoHttp {
59
+ #private;
60
+ constructor(options: {
61
+ accessToken?: string | undefined;
62
+ userAgent: string;
63
+ });
64
+ /** Rejects queued waiters and clears the pacers' timers. */
65
+ dispose(): void;
66
+ /**
67
+ * Runs one request through the retry ladder (outside) and the bucket's pacer
68
+ * (inside). `read` consumes the response of an accepted status and runs inside
69
+ * the attempt, so parse faults are retried with the fetch.
70
+ */
71
+ request<T>(req: ZenodoRequest, read: (res: Response) => Promise<T>, ctx: Context): Promise<T>;
72
+ }
73
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/services/zenodo/http.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAuBtD,oFAAoF;AACpF,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAEjG,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,YAAY,CAAC;IACvB,+EAA+E;IAC/E,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,iBAAiB;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAChC;AAED,sFAAsF;AACtF,eAAO,MAAM,eAAe,QAAS,CAAC;AACtC,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAyBzC,+EAA+E;AAC/E,wBAAsB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAED,mGAAmG;AACnG,wBAAsB,QAAQ,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAS7D;AAED,kGAAkG;AAClG,wBAAsB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAgB3D;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,QAAQ,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAI,GACZ,OAAO,CAAC;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,CAkC/C;AAED,8EAA8E;AAC9E,qBAAa,UAAU;;IAOrB,YAAY,OAAO,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAwB3E;IAED,4DAA4D;IAC5D,OAAO,IAAI,IAAI,CAGd;IAED;;;;OAIG;IACG,OAAO,CAAC,CAAC,EACb,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,EACnC,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,CAAC,CAAC,CA6BZ;CAiQF"}