@cyanheads/openalex-mcp-server 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/AGENTS.md +14 -11
  2. package/CLAUDE.md +14 -11
  3. package/README.md +34 -16
  4. package/changelog/0.11.x/0.11.0.md +41 -0
  5. package/dist/config/server-config.d.ts.map +1 -1
  6. package/dist/config/server-config.js +1 -1
  7. package/dist/config/server-config.js.map +1 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp-server/prompts/definitions/literature-review.prompt.js +1 -1
  11. package/dist/mcp-server/prompts/definitions/research-landscape.prompt.js +1 -1
  12. package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts +1 -0
  13. package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts.map +1 -1
  14. package/dist/mcp-server/tools/definitions/analyze-trends.tool.js +10 -5
  15. package/dist/mcp-server/tools/definitions/analyze-trends.tool.js.map +1 -1
  16. package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts +6 -9
  17. package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/citation-graph.tool.js +43 -33
  19. package/dist/mcp-server/tools/definitions/citation-graph.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/definitions/describe-fields.tool.d.ts +1 -0
  21. package/dist/mcp-server/tools/definitions/describe-fields.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/describe-fields.tool.js +25 -1
  23. package/dist/mcp-server/tools/definitions/describe-fields.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/resolve-name.tool.js +44 -16
  26. package/dist/mcp-server/tools/definitions/resolve-name.tool.js.map +1 -1
  27. package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts +22 -18
  28. package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/definitions/search-entities.tool.js +194 -85
  30. package/dist/mcp-server/tools/definitions/search-entities.tool.js.map +1 -1
  31. package/dist/mcp-server/tools/escape-markdown.d.ts +26 -11
  32. package/dist/mcp-server/tools/escape-markdown.d.ts.map +1 -1
  33. package/dist/mcp-server/tools/escape-markdown.js +95 -26
  34. package/dist/mcp-server/tools/escape-markdown.js.map +1 -1
  35. package/dist/mcp-server/tools/page-size.d.ts +38 -7
  36. package/dist/mcp-server/tools/page-size.d.ts.map +1 -1
  37. package/dist/mcp-server/tools/page-size.js +37 -11
  38. package/dist/mcp-server/tools/page-size.js.map +1 -1
  39. package/dist/mcp-server/tools/record-page.d.ts +174 -0
  40. package/dist/mcp-server/tools/record-page.d.ts.map +1 -0
  41. package/dist/mcp-server/tools/record-page.js +392 -0
  42. package/dist/mcp-server/tools/record-page.js.map +1 -0
  43. package/dist/mcp-server/tools/render-entity-record.d.ts +3 -1
  44. package/dist/mcp-server/tools/render-entity-record.d.ts.map +1 -1
  45. package/dist/mcp-server/tools/render-entity-record.js +7 -2
  46. package/dist/mcp-server/tools/render-entity-record.js.map +1 -1
  47. package/dist/services/openalex/field-catalog.json +107 -0
  48. package/dist/services/openalex/openalex-service.d.ts +18 -7
  49. package/dist/services/openalex/openalex-service.d.ts.map +1 -1
  50. package/dist/services/openalex/openalex-service.js +84 -63
  51. package/dist/services/openalex/openalex-service.js.map +1 -1
  52. package/dist/services/openalex/types.d.ts +13 -2
  53. package/dist/services/openalex/types.d.ts.map +1 -1
  54. package/dist/services/openalex/types.js +28 -1
  55. package/dist/services/openalex/types.js.map +1 -1
  56. package/package.json +1 -1
  57. package/server.json +5 -5
  58. package/skills/openalex-guide/SKILL.md +77 -0
  59. package/dist/mcp-server/tools/response-budget.d.ts +0 -160
  60. package/dist/mcp-server/tools/response-budget.d.ts.map +0 -1
  61. package/dist/mcp-server/tools/response-budget.js +0 -536
  62. package/dist/mcp-server/tools/response-budget.js.map +0 -1
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @fileoverview Record pages for the tools that return OpenAlex entity records
3
+ * (`openalex_search_entities`, `openalex_get_citation_graph`). A page holds every record of the
4
+ * upstream page, and an `id` lookup every selected array whole. `windows` discloses OpenAlex's
5
+ * author caps — 100 authorships on a list record, 5,000 stored on a singleton — and every
6
+ * count-based `slice` window, each with the call that continues it. A response whose
7
+ * `structuredContent` or `content[]` text would exceed 8,000,000 bytes is refused, never
8
+ * shortened, so both surfaces and the JSON-RPC envelope stay under the 16 MiB an HTTP client
9
+ * such as Claude Code reads from one response.
10
+ * @module mcp-server/tools/record-page
11
+ */
12
+ import { z } from '@cyanheads/mcp-ts-core';
13
+ import type { EntityRecord, EntityType } from '../../services/openalex/types.js';
14
+ /** Per-surface ceiling: serialized `structuredContent`, and summed `content[]` text. */
15
+ export declare const RESPONSE_CEILING_BYTES = 8000000;
16
+ /**
17
+ * Bytes reserved on each surface for the `budget` enrichment field (the OpenAlex usage budget,
18
+ * not a size limit). The service writes it from OpenAlex's rate-limit headers through
19
+ * `ctx.enrich`, so its value is not in the handler's hands when the response is measured. The
20
+ * allowance covers its widest realistic form — every number at full double precision, with a
21
+ * prepaid balance — measured at 176 bytes as a JSON member and 108 as a trailer line with its
22
+ * separator.
23
+ */
24
+ export declare const BUDGET_FIELD_ALLOWANCE_BYTES = 256;
25
+ declare const arrayWindowSchema: z.ZodObject<{
26
+ id: z.ZodString;
27
+ field: z.ZodString;
28
+ offset: z.ZodNumber;
29
+ shown: z.ZodNumber;
30
+ total: z.ZodNumber;
31
+ possibly_capped: z.ZodBoolean;
32
+ next: z.ZodNullable<z.ZodObject<{
33
+ tool: z.ZodLiteral<"openalex_search_entities">;
34
+ arguments: z.ZodObject<{}, z.core.$loose>;
35
+ }, z.core.$strip>>;
36
+ }, z.core.$strip>;
37
+ /** Output field: arrays returned in part, or possibly in part, each with its continuation. */
38
+ export declare const windowsOutputSchema: z.ZodOptional<z.ZodArray<z.ZodObject<{
39
+ id: z.ZodString;
40
+ field: z.ZodString;
41
+ offset: z.ZodNumber;
42
+ shown: z.ZodNumber;
43
+ total: z.ZodNumber;
44
+ possibly_capped: z.ZodBoolean;
45
+ next: z.ZodNullable<z.ZodObject<{
46
+ tool: z.ZodLiteral<"openalex_search_entities">;
47
+ arguments: z.ZodObject<{}, z.core.$loose>;
48
+ }, z.core.$strip>>;
49
+ }, z.core.$strip>>>;
50
+ type ArrayWindow = z.infer<typeof arrayWindowSchema>;
51
+ /** A record page as both tools return it: upstream meta, records, and any windows. */
52
+ export interface RecordPage {
53
+ meta: {
54
+ count: number;
55
+ next_cursor: string | null;
56
+ per_page: number;
57
+ };
58
+ results: EntityRecord[];
59
+ windows?: ArrayWindow[] | undefined;
60
+ }
61
+ /** Enrichment the handler writes, which the framework renders into the `content[]` trailer. */
62
+ export interface PageEnrichment {
63
+ echo: string;
64
+ notice: string | undefined;
65
+ totalCount: number;
66
+ /** The tool's `enrichmentTrailer.totalCount.label`. */
67
+ totalLabel: string;
68
+ }
69
+ /** `https://openalex.org/W123` → `W123`. Any other form (a keyword URL) passes through whole. */
70
+ export declare function bareId(id: string): string;
71
+ /** Join notice sentences, dropping empty ones; undefined when none remain. */
72
+ export declare function joinNotices(parts: readonly (string | undefined)[]): string | undefined;
73
+ /**
74
+ * `format()` for a record page: a count header, each record, then each window.
75
+ * `noun` names what `meta.count` counts ("result(s)", "edge(s)").
76
+ */
77
+ export declare function formatRecordPage(page: RecordPage, noun: string): {
78
+ type: 'text';
79
+ text: string;
80
+ }[];
81
+ /**
82
+ * Byte-exact copy of the framework's inline code span (`codeSpan` in mcp-ts-core's formatting
83
+ * utils, which the package does not export), the form a one-line trailer string takes: the
84
+ * fence runs one backtick past the value's longest backtick run, and a space pads each side when
85
+ * a backtick touches an end or the value both begins and ends with a space. The echo is always
86
+ * one line — caller text in it is JSON-encoded — so the fenced-block form never applies here.
87
+ * Authored code spans around one-line values use it too, so a backtick in the value cannot close
88
+ * the span early.
89
+ */
90
+ export declare function codeSpan(value: string): string;
91
+ /**
92
+ * The enrichment trailer's bytes, minus the service-written `budget` line. Fields render as the
93
+ * framework renders them for these tools: `echo` as a code span under its `Query` label,
94
+ * `totalCount` under `totalLabel`, and `notice` as a block quote with every line quoted, joined
95
+ * after a leading blank line.
96
+ */
97
+ export declare function trailerTextBytes(enrichment: PageEnrichment): number;
98
+ /** What a record page is built from, besides its records. */
99
+ interface PageContext {
100
+ echo: string;
101
+ entityType: EntityType;
102
+ meta: RecordPage['meta'];
103
+ /** The tool's own notice sentences, joined ahead of the page's. */
104
+ notices: readonly (string | undefined)[];
105
+ /** What `meta.count` counts in the rendered header ("result(s)", "edge(s)"). */
106
+ noun: string;
107
+ totalCount: number;
108
+ /** The tool's `enrichmentTrailer.totalCount.label`. */
109
+ totalLabel: string;
110
+ }
111
+ /** A page within the ceiling, or the typed refusal of one that is not. */
112
+ export type PageOutcome = {
113
+ kind: 'page';
114
+ page: RecordPage;
115
+ notice: string | undefined;
116
+ } | {
117
+ kind: 'too_large';
118
+ message: string;
119
+ data: TooLargeData;
120
+ };
121
+ /** `data` for a `response_too_large` failure. */
122
+ export interface TooLargeData {
123
+ ceilingBytes: number;
124
+ /** The largest value of the named input whose response fits; absent when none does. */
125
+ largestFitting?: {
126
+ input: 'per_page' | 'slice.limit';
127
+ value: number;
128
+ };
129
+ recovery: {
130
+ hint: string;
131
+ };
132
+ structuredBytes: number;
133
+ textBytes: number;
134
+ [key: string]: unknown;
135
+ }
136
+ export interface RecordPageRequest extends PageContext {
137
+ /**
138
+ * How the listing continues past this page, for the hint naming a smaller `per_page`: a
139
+ * `cursor`, or the next `page` of a semantic search or a sample.
140
+ */
141
+ continuation: 'cursor' | 'page';
142
+ /** The 1-based `page` of a listing that continues by `page`, whose boundaries move with per_page. */
143
+ page?: number;
144
+ /**
145
+ * `list` checks the 100-author list cap; a `lookup` holds one record and checks the
146
+ * 5,000-author storage cap.
147
+ */
148
+ path: 'list' | 'lookup';
149
+ records: readonly EntityRecord[];
150
+ }
151
+ /**
152
+ * Every record of the upstream page, with the windows that disclose OpenAlex's author caps —
153
+ * or, when that response would run over the ceiling on either surface, its refusal naming the
154
+ * largest page that fits. Returns new arrays, never the caller's mutated.
155
+ */
156
+ export declare function buildRecordPage(request: RecordPageRequest): PageOutcome;
157
+ export interface SlicePageRequest extends PageContext {
158
+ /** Record key of the array to page (a `select` alias already resolved); must hold an array. */
159
+ field: string;
160
+ /** Elements to return, already clamped. */
161
+ limit: number;
162
+ /** Requested start index; past the end yields an empty, final window. */
163
+ offset: number;
164
+ /** The record from a one-field projection: `id`, `display_name`, and `field`. */
165
+ record: EntityRecord;
166
+ }
167
+ /**
168
+ * One count-based window of an `id` lookup's array: `limit` elements of `field` from `offset`,
169
+ * fewer only at the array end, whose `next` repeats `limit` — or, when that response would run
170
+ * over the ceiling, its refusal naming the largest `slice.limit` that fits.
171
+ */
172
+ export declare function buildSlicePage(request: SlicePageRequest): PageOutcome;
173
+ export {};
174
+ //# sourceMappingURL=record-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-page.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/record-page.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,392 @@
1
+ /**
2
+ * @fileoverview Record pages for the tools that return OpenAlex entity records
3
+ * (`openalex_search_entities`, `openalex_get_citation_graph`). A page holds every record of the
4
+ * upstream page, and an `id` lookup every selected array whole. `windows` discloses OpenAlex's
5
+ * author caps — 100 authorships on a list record, 5,000 stored on a singleton — and every
6
+ * count-based `slice` window, each with the call that continues it. A response whose
7
+ * `structuredContent` or `content[]` text would exceed 8,000,000 bytes is refused, never
8
+ * shortened, so both surfaces and the JSON-RPC envelope stay under the 16 MiB an HTTP client
9
+ * such as Claude Code reads from one response.
10
+ * @module mcp-server/tools/record-page
11
+ */
12
+ import { z } from '@cyanheads/mcp-ts-core';
13
+ import { escapeMarkdown } from './escape-markdown.js';
14
+ import { renderEntityRecord, } from './render-entity-record.js';
15
+ /** Per-surface ceiling: serialized `structuredContent`, and summed `content[]` text. */
16
+ export const RESPONSE_CEILING_BYTES = 8_000_000;
17
+ const CEILING_LABEL = `${RESPONSE_CEILING_BYTES.toLocaleString('en-US')}-byte`;
18
+ /**
19
+ * The most authorships OpenAlex returns on a list record. An `id` lookup can return more,
20
+ * up to the separate storage ceiling.
21
+ */
22
+ const LIST_AUTHORSHIPS_CAP = 100;
23
+ const STORED_AUTHORSHIPS_CAP = 5000;
24
+ const STORAGE_CAP_NOTICE = 'OpenAlex stores at most 5,000 authorships per work in singleton records and snapshots; this author array may omit part of the byline. `total` counts stored entries, not the complete byline. Further OpenAlex fetches or snapshots cannot recover authors beyond this storage ceiling.';
25
+ /** Record arrays the cap applies to: `authorships`, and the `author_list` derived from it. */
26
+ const CAPPED_AUTHOR_FIELDS = ['authorships', 'author_list'];
27
+ /**
28
+ * Bytes reserved on each surface for the `budget` enrichment field (the OpenAlex usage budget,
29
+ * not a size limit). The service writes it from OpenAlex's rate-limit headers through
30
+ * `ctx.enrich`, so its value is not in the handler's hands when the response is measured. The
31
+ * allowance covers its widest realistic form — every number at full double precision, with a
32
+ * prepaid balance — measured at 176 bytes as a JSON member and 108 as a trailer line with its
33
+ * separator.
34
+ */
35
+ export const BUDGET_FIELD_ALLOWANCE_BYTES = 256;
36
+ const CONTINUATION_TOOL = 'openalex_search_entities';
37
+ const OPENALEX_ID_PREFIX = 'https://openalex.org/';
38
+ const NATIVE_OPENALEX_ID = /^[A-Z]\d+$/;
39
+ /**
40
+ * The call that continues a window. Its arguments are data to pass back unchanged, so the
41
+ * schema leaves them loosely typed rather than spelling out the search tool's input a second
42
+ * time.
43
+ */
44
+ const continuationCallSchema = z.object({
45
+ tool: z.literal(CONTINUATION_TOOL).describe('Tool to call.'),
46
+ arguments: z.object({}).loose().describe('Arguments to pass as-is.'),
47
+ });
48
+ const arrayWindowSchema = z
49
+ .object({
50
+ id: z.string().describe('Bare ID of the record.'),
51
+ field: z.string().describe('Array field.'),
52
+ offset: z.number().describe('Index of the first element shown.'),
53
+ shown: z.number().describe('Elements shown.'),
54
+ total: z
55
+ .number()
56
+ .describe('Stored array length returned to this call, not a complete byline total.'),
57
+ possibly_capped: z
58
+ .boolean()
59
+ .describe('True for exactly 100 authorships or author_list entries on a list record, or 5,000 on a singleton lookup: the byline may be longer than the returned array.'),
60
+ next: continuationCallSchema
61
+ .nullable()
62
+ .describe('`id` + `slice` call for the next elements; null at the array end.'),
63
+ })
64
+ .describe('An array returned in part, or possibly in part.');
65
+ /** Output field: arrays returned in part, or possibly in part, each with its continuation. */
66
+ export const windowsOutputSchema = z
67
+ .array(arrayWindowSchema)
68
+ .optional()
69
+ .describe("Author arrays possibly capped at 100 on a list record or 5,000 stored on a singleton, and the window of every `slice` call — present even when the slice holds the whole array. A slice window's `next` repeats its `slice.limit` from the following offset; `next` is null at the stored-array end, including a possibly capped singleton.");
70
+ const utf8Bytes = (text) => Buffer.byteLength(text, 'utf8');
71
+ const count = (n) => n.toLocaleString('en-US');
72
+ /** `https://openalex.org/W123` → `W123`. Any other form (a keyword URL) passes through whole. */
73
+ export function bareId(id) {
74
+ const tail = id.startsWith(OPENALEX_ID_PREFIX) ? id.slice(OPENALEX_ID_PREFIX.length) : id;
75
+ return NATIVE_OPENALEX_ID.test(tail) ? tail : id;
76
+ }
77
+ /** Join notice sentences, dropping empty ones; undefined when none remain. */
78
+ export function joinNotices(parts) {
79
+ return parts.filter(Boolean).join(' ') || undefined;
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // Rendering — shared by both tools' format() and by the ceiling's measurement
83
+ // ---------------------------------------------------------------------------
84
+ /** A continuation call as code spans, which hold any backtick its arguments carry. */
85
+ const renderCall = (call) => `${codeSpan(call.tool)} ${codeSpan(JSON.stringify(call.arguments))}`;
86
+ function renderWindow(window) {
87
+ const capped = window.possibly_capped
88
+ ? window.total >= STORED_AUTHORSHIPS_CAP
89
+ ? ' — possibly capped: OpenAlex stores at most 5,000 authorships per work; total counts stored entries, not the complete byline'
90
+ : ` — possibly capped: OpenAlex returns at most ${LIST_AUTHORSHIPS_CAP} authorships on a list record`
91
+ : '';
92
+ const next = window.next ? `next: ${renderCall(window.next)}` : 'end of array';
93
+ return `**Window:** ${escapeMarkdown(window.id)} ${codeSpan(window.field)} — offset ${window.offset}, ${window.shown} shown of ${window.total}${capped} — ${next}`;
94
+ }
95
+ /**
96
+ * Each windowed array's place in its full array, per record ID, so a slice numbers its items
97
+ * at their real index and its line says how much of the array it shows.
98
+ */
99
+ function windowViewsByRecord(windows) {
100
+ const views = new Map();
101
+ for (const window of windows ?? []) {
102
+ const fields = views.get(window.id) ?? new Map();
103
+ fields.set(window.field, { offset: window.offset, total: window.total });
104
+ views.set(window.id, fields);
105
+ }
106
+ return views;
107
+ }
108
+ /**
109
+ * `format()` for a record page: a count header, each record, then each window.
110
+ * `noun` names what `meta.count` counts ("result(s)", "edge(s)").
111
+ */
112
+ export function formatRecordPage(page, noun) {
113
+ const countLabel = `${page.meta.count} ${noun} — ${page.meta.per_page} per page`;
114
+ const lines = [
115
+ page.meta.next_cursor
116
+ ? `**${countLabel}** — next cursor: \`${page.meta.next_cursor}\``
117
+ : `**${countLabel}**`,
118
+ ];
119
+ const views = windowViewsByRecord(page.windows);
120
+ for (const record of page.results) {
121
+ lines.push(...renderEntityRecord(record, views.get(bareId(record.id))));
122
+ }
123
+ if (page.windows?.length)
124
+ lines.push('', ...page.windows.map(renderWindow));
125
+ return [{ type: 'text', text: lines.join('\n') }];
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Measurement — both surfaces as the framework emits them
129
+ // ---------------------------------------------------------------------------
130
+ /**
131
+ * Byte-exact copy of the framework's trailer join (`joinTrailerFields` in mcp-ts-core's tool
132
+ * handler factory, which the package does not export — cyanheads/mcp-ts-core#538). A line opening
133
+ * a block quote or list item is followed by a blank line instead of a single newline. The
134
+ * record-page test compares the modelled trailer with the one `runToolContract` renders.
135
+ */
136
+ const OPENS_LAZY_CONTAINER = /^ {0,3}(?:>|(?:[-+*]|\d{1,9}[.)])(?: |$))/;
137
+ function joinTrailerFields(fields) {
138
+ let text = '';
139
+ for (const field of fields) {
140
+ if (text.length > 0) {
141
+ text += OPENS_LAZY_CONTAINER.test(text.slice(text.lastIndexOf('\n') + 1)) ? '\n\n' : '\n';
142
+ }
143
+ text += field;
144
+ }
145
+ return text;
146
+ }
147
+ /**
148
+ * Byte-exact copy of the framework's inline code span (`codeSpan` in mcp-ts-core's formatting
149
+ * utils, which the package does not export), the form a one-line trailer string takes: the
150
+ * fence runs one backtick past the value's longest backtick run, and a space pads each side when
151
+ * a backtick touches an end or the value both begins and ends with a space. The echo is always
152
+ * one line — caller text in it is JSON-encoded — so the fenced-block form never applies here.
153
+ * Authored code spans around one-line values use it too, so a backtick in the value cannot close
154
+ * the span early.
155
+ */
156
+ export function codeSpan(value) {
157
+ let longestRun = 0;
158
+ for (const [run] of value.matchAll(/`+/g))
159
+ longestRun = Math.max(longestRun, run.length);
160
+ const fence = '`'.repeat(longestRun + 1);
161
+ const pad = value.startsWith('`') ||
162
+ value.endsWith('`') ||
163
+ (value.startsWith(' ') && value.endsWith(' ') && /[^ ]/.test(value))
164
+ ? ' '
165
+ : '';
166
+ return `${fence}${pad}${value}${pad}${fence}`;
167
+ }
168
+ /**
169
+ * The enrichment trailer's bytes, minus the service-written `budget` line. Fields render as the
170
+ * framework renders them for these tools: `echo` as a code span under its `Query` label,
171
+ * `totalCount` under `totalLabel`, and `notice` as a block quote with every line quoted, joined
172
+ * after a leading blank line.
173
+ */
174
+ export function trailerTextBytes(enrichment) {
175
+ const fields = [
176
+ `**Query:** ${codeSpan(enrichment.echo)}`,
177
+ `**${enrichment.totalLabel}:** ${enrichment.totalCount}`,
178
+ ...(enrichment.notice === undefined
179
+ ? []
180
+ : [
181
+ enrichment.notice
182
+ .split(/\r\n|\r|\n/)
183
+ .map((line) => (line === '' ? '>' : `> ${line}`))
184
+ .join('\n'),
185
+ ]),
186
+ ];
187
+ return utf8Bytes(`\n\n${joinTrailerFields(fields)}`);
188
+ }
189
+ /**
190
+ * Both surfaces' bytes for a page as the client receives it: `structuredContent` with the
191
+ * enrichment the handler writes, `content[]` with the trailer the framework renders, and on
192
+ * each the allowance for the service's `budget` field.
193
+ */
194
+ function measurePage(page, noun, enrichment) {
195
+ const { echo, notice, totalCount } = enrichment;
196
+ const structured = utf8Bytes(JSON.stringify({ ...page, echo, totalCount, ...(notice !== undefined && { notice }) }));
197
+ const text = formatRecordPage(page, noun).reduce((sum, block) => sum + utf8Bytes(block.text), 0);
198
+ return {
199
+ structured: structured + BUDGET_FIELD_ALLOWANCE_BYTES,
200
+ text: text + trailerTextBytes(enrichment) + BUDGET_FIELD_ALLOWANCE_BYTES,
201
+ };
202
+ }
203
+ const withinCeiling = (bytes) => bytes.structured <= RESPONSE_CEILING_BYTES && bytes.text <= RESPONSE_CEILING_BYTES;
204
+ /**
205
+ * Largest n in [0, limit] with `fits(n)`, given `fits(0)` holds and `fits` stays false once
206
+ * it turns false. Gallops up from 0 before bisecting, so each probe measures about as many
207
+ * elements as the answer holds rather than half the array.
208
+ */
209
+ function largestFitting(limit, fits) {
210
+ let low = 0;
211
+ let high = 1;
212
+ while (high <= limit && fits(high)) {
213
+ low = high;
214
+ high *= 2;
215
+ }
216
+ high = Math.min(high, limit + 1);
217
+ while (high - low > 1) {
218
+ const mid = Math.floor((low + high) / 2);
219
+ if (fits(mid))
220
+ low = mid;
221
+ else
222
+ high = mid;
223
+ }
224
+ return low;
225
+ }
226
+ function arrayWindow(entityType, record, field, view) {
227
+ const id = bareId(record.id);
228
+ const end = Math.min(view.offset, view.total) + view.shown;
229
+ return {
230
+ id,
231
+ field,
232
+ offset: view.offset,
233
+ shown: view.shown,
234
+ total: view.total,
235
+ possibly_capped: view.possiblyCapped,
236
+ next: end < view.total || view.recoverable
237
+ ? {
238
+ tool: CONTINUATION_TOOL,
239
+ arguments: {
240
+ entity_type: entityType,
241
+ id,
242
+ slice: { field, offset: end, ...(view.limit !== undefined && { limit: view.limit }) },
243
+ },
244
+ }
245
+ : null,
246
+ };
247
+ }
248
+ /** Author arrays holding exactly the cap for this request path. */
249
+ function cappedAuthorFields(record, cap) {
250
+ if (cap === undefined)
251
+ return [];
252
+ return CAPPED_AUTHOR_FIELDS.filter((field) => {
253
+ const items = record[field];
254
+ return Array.isArray(items) && items.length === cap;
255
+ });
256
+ }
257
+ function capWindow(entityType, record, field) {
258
+ const total = record[field].length;
259
+ return arrayWindow(entityType, record, field, {
260
+ offset: 0,
261
+ shown: total,
262
+ total,
263
+ possiblyCapped: true,
264
+ recoverable: total === LIST_AUTHORSHIPS_CAP,
265
+ });
266
+ }
267
+ function capNotice(listCapped, windows) {
268
+ return joinNotices([
269
+ listCapped > 0
270
+ ? `${listCapped} record(s) carry exactly ${LIST_AUTHORSHIPS_CAP} authorships or author_list entries, the most OpenAlex returns on a list record, so the author list may be longer; each such array's \`windows\` entry gives the \`id\` + \`slice\` call for more stored authors, up to the singleton storage ceiling of 5,000.`
271
+ : undefined,
272
+ windows.some((window) => window.possibly_capped && window.total >= STORED_AUTHORSHIPS_CAP)
273
+ ? STORAGE_CAP_NOTICE
274
+ : undefined,
275
+ ]);
276
+ }
277
+ function tooLarge(subject, bytes, hint, largest) {
278
+ return {
279
+ kind: 'too_large',
280
+ message: `${subject} would return ${count(bytes.structured)} bytes of structuredContent and ${count(bytes.text)} bytes of content[] text, over the ${CEILING_LABEL} response ceiling.`,
281
+ data: {
282
+ structuredBytes: bytes.structured,
283
+ textBytes: bytes.text,
284
+ ceilingBytes: RESPONSE_CEILING_BYTES,
285
+ ...(largest && { largestFitting: largest }),
286
+ recovery: { hint },
287
+ },
288
+ };
289
+ }
290
+ /**
291
+ * Every record of the upstream page, with the windows that disclose OpenAlex's author caps —
292
+ * or, when that response would run over the ceiling on either surface, its refusal naming the
293
+ * largest page that fits. Returns new arrays, never the caller's mutated.
294
+ */
295
+ export function buildRecordPage(request) {
296
+ const { continuation, entityType, meta, noun, path, records } = request;
297
+ const cap = entityType === 'works'
298
+ ? path === 'list'
299
+ ? LIST_AUTHORSHIPS_CAP
300
+ : STORED_AUTHORSHIPS_CAP
301
+ : undefined;
302
+ const pageFor = (kept) => {
303
+ const results = records.slice(0, kept);
304
+ const capped = results
305
+ .map((record) => ({ record, fields: cappedAuthorFields(record, cap) }))
306
+ .filter(({ fields }) => fields.length > 0);
307
+ const windows = capped.flatMap(({ record, fields }) => fields.map((field) => capWindow(entityType, record, field)));
308
+ return {
309
+ page: { meta, results, ...(windows.length > 0 && { windows }) },
310
+ notice: joinNotices([
311
+ ...request.notices,
312
+ capNotice(path === 'list' ? capped.length : 0, windows),
313
+ ]),
314
+ };
315
+ };
316
+ /**
317
+ * Each record's bytes on each surface plus the separator joining it to its neighbour: a comma
318
+ * in `results`, a newline in the rendered text. `format()` renders a page record with no
319
+ * window note — a cap window shows its whole array — so the bare render is the one it emits.
320
+ */
321
+ const sizes = records.map((record) => ({
322
+ structured: utf8Bytes(JSON.stringify(record)) + 1,
323
+ text: utf8Bytes(renderEntityRecord(record).join('\n')) + 1,
324
+ }));
325
+ const bytesFor = (kept) => {
326
+ const { page, notice } = pageFor(kept);
327
+ const frame = measurePage({ ...page, results: [] }, noun, { ...request, notice });
328
+ // `kept` records carry `kept - 1` commas, not `kept`.
329
+ const bytes = { structured: frame.structured - (kept > 0 ? 1 : 0), text: frame.text };
330
+ for (const size of sizes.slice(0, kept)) {
331
+ bytes.structured += size.structured;
332
+ bytes.text += size.text;
333
+ }
334
+ return bytes;
335
+ };
336
+ const bytes = bytesFor(records.length);
337
+ if (withinCeiling(bytes))
338
+ return { kind: 'page', ...pageFor(records.length) };
339
+ const [record] = records;
340
+ if (path === 'lookup' && record) {
341
+ const largest = Object.entries(record)
342
+ .filter((entry) => Array.isArray(entry[1]))
343
+ .map(([field, items]) => ({ field, items, bytes: utf8Bytes(JSON.stringify(items)) }))
344
+ .sort((a, b) => b.bytes - a.bytes)[0];
345
+ return tooLarge(`The record at \`${bareId(record.id)}\``, bytes, largest
346
+ ? `Narrow \`select\` to the fields you need, or page \`${largest.field}\` (${count(largest.items.length)} elements, its largest array) with \`slice\`.`
347
+ : 'Narrow `select` to the fields you need.');
348
+ }
349
+ const fits = largestFitting(records.length - 1, (kept) => withinCeiling(bytesFor(kept)));
350
+ const subject = records.length === 1 ? 'This record' : `These ${records.length} records`;
351
+ if (fits === 0) {
352
+ return tooLarge(subject, bytes, 'The first record alone exceeds the ceiling. Narrow `select` to the fields you need, or fetch the record by `id` and page its largest array with `slice`.');
353
+ }
354
+ // Past page 1, the same `page` at a smaller per_page holds other records, so the named size
355
+ // holds only from page 1.
356
+ const page = request.page ?? 1;
357
+ return tooLarge(subject, bytes, continuation === 'page' && page > 1
358
+ ? `per_page ${fits} is the most of these records that fit, but page boundaries move with per_page, so \`page: ${page}\` at that size holds other records; walk the listing from page 1 at per_page ${fits}, or narrow \`select\` to the fields you need.`
359
+ : `Rerun with per_page ${fits}, the most of these records that fit, and continue ${continuation === 'cursor' ? 'from the cursor that page returns' : 'with the next `page`'}; or narrow \`select\` to the fields you need.`, { input: 'per_page', value: fits });
360
+ }
361
+ /**
362
+ * One count-based window of an `id` lookup's array: `limit` elements of `field` from `offset`,
363
+ * fewer only at the array end, whose `next` repeats `limit` — or, when that response would run
364
+ * over the ceiling, its refusal naming the largest `slice.limit` that fits.
365
+ */
366
+ export function buildSlicePage(request) {
367
+ const { entityType, field, limit, meta, noun, offset, record } = request;
368
+ const items = record[field];
369
+ const total = items.length;
370
+ const start = Math.min(offset, total);
371
+ const possiblyCapped = cappedAuthorFields(record, entityType === 'works' ? STORED_AUTHORSHIPS_CAP : undefined).includes(field);
372
+ const notice = joinNotices([...request.notices, possiblyCapped ? STORAGE_CAP_NOTICE : undefined]);
373
+ const pageFor = (shown) => ({
374
+ meta,
375
+ results: [{ ...record, [field]: items.slice(start, start + shown) }],
376
+ windows: [
377
+ arrayWindow(entityType, record, field, { offset, shown, total, possiblyCapped, limit }),
378
+ ],
379
+ });
380
+ const bytesFor = (shown) => measurePage(pageFor(shown), noun, { ...request, notice });
381
+ const shown = Math.min(limit, total - start);
382
+ const bytes = bytesFor(shown);
383
+ if (withinCeiling(bytes))
384
+ return { kind: 'page', page: pageFor(shown), notice };
385
+ const fits = largestFitting(shown - 1, (n) => withinCeiling(bytesFor(n)));
386
+ const subject = `This slice of ${shown} \`${field}\` elements`;
387
+ if (fits === 0) {
388
+ return tooLarge(subject, bytes, `The element at offset ${start} alone exceeds the ceiling; continue past it with slice.offset ${start + 1}.`);
389
+ }
390
+ return tooLarge(subject, bytes, `Rerun with slice.limit ${fits}, the most of these elements that fit, and continue from the offset its window's \`next\` names.`, { input: 'slice.limit', value: fits });
391
+ }
392
+ //# sourceMappingURL=record-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-page.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/record-page.ts"],"names":[],"mappings":"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"}
@@ -17,7 +17,9 @@ export interface ArrayWindowView {
17
17
  * Render an OpenAlex entity record as markdown lines: a `### display_name` header,
18
18
  * the ID, then each remaining field as a labeled line. Caller joins the lines into
19
19
  * the final text block. Provider text is escaped for Markdown; field names are OpenAlex
20
- * schema keys and render as they are.
20
+ * schema keys and render as they are. A heading holds one line, so a `display_name` with a
21
+ * line break heads the record with the ID, as a missing one does, and takes a
22
+ * `**Display Name:**` line of its own.
21
23
  *
22
24
  * `windows` maps an array field returned in part to where it sits in the full array: its items
23
25
  * are numbered from `offset`, and its line says how many of `total` it shows, so a window never
@@ -1 +1 @@
1
- {"version":3,"file":"render-entity-record.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/render-entity-record.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAqFjE,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;CACf;AAgCD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAC7C,MAAM,EAAE,CAWV"}
1
+ {"version":3,"file":"render-entity-record.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/render-entity-record.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAqFjE,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;CACf;AAgCD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAC7C,MAAM,EAAE,CAaV"}
@@ -115,7 +115,9 @@ function renderField(field, value, view) {
115
115
  * Render an OpenAlex entity record as markdown lines: a `### display_name` header,
116
116
  * the ID, then each remaining field as a labeled line. Caller joins the lines into
117
117
  * the final text block. Provider text is escaped for Markdown; field names are OpenAlex
118
- * schema keys and render as they are.
118
+ * schema keys and render as they are. A heading holds one line, so a `display_name` with a
119
+ * line break heads the record with the ID, as a missing one does, and takes a
120
+ * `**Display Name:**` line of its own.
119
121
  *
120
122
  * `windows` maps an array field returned in part to where it sits in the full array: its items
121
123
  * are numbered from `offset`, and its line says how many of `total` it shows, so a window never
@@ -123,11 +125,14 @@ function renderField(field, value, view) {
123
125
  */
124
126
  export function renderEntityRecord(record, windows) {
125
127
  const { id, display_name, ...rest } = record;
128
+ const multiLineName = display_name != null && /[\r\n]/.test(display_name);
126
129
  const lines = [
127
130
  '',
128
- `### ${escapeMarkdown(display_name || id, 'heading')}`,
131
+ `### ${escapeMarkdown((!multiLineName && display_name) || id, 'heading')}`,
129
132
  `**ID:** ${escapeMarkdown(id)}`,
130
133
  ];
134
+ if (multiLineName)
135
+ lines.push(renderField('display_name', display_name, undefined));
131
136
  for (const [field, value] of Object.entries(rest)) {
132
137
  lines.push(renderField(field, value, windows?.get(field)));
133
138
  }