@cyanheads/openalex-mcp-server 0.2.7

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 (42) hide show
  1. package/CLAUDE.md +282 -0
  2. package/Dockerfile +99 -0
  3. package/LICENSE +190 -0
  4. package/README.md +188 -0
  5. package/dist/config/server-config.d.ts +13 -0
  6. package/dist/config/server-config.d.ts.map +1 -0
  7. package/dist/config/server-config.js +18 -0
  8. package/dist/config/server-config.js.map +1 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +20 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/mcp-server/prompts/definitions/literature-review.prompt.d.ts +13 -0
  14. package/dist/mcp-server/prompts/definitions/literature-review.prompt.d.ts.map +1 -0
  15. package/dist/mcp-server/prompts/definitions/literature-review.prompt.js +53 -0
  16. package/dist/mcp-server/prompts/definitions/literature-review.prompt.js.map +1 -0
  17. package/dist/mcp-server/prompts/definitions/research-landscape.prompt.d.ts +9 -0
  18. package/dist/mcp-server/prompts/definitions/research-landscape.prompt.d.ts.map +1 -0
  19. package/dist/mcp-server/prompts/definitions/research-landscape.prompt.js +42 -0
  20. package/dist/mcp-server/prompts/definitions/research-landscape.prompt.js.map +1 -0
  21. package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts +33 -0
  22. package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts.map +1 -0
  23. package/dist/mcp-server/tools/definitions/analyze-trends.tool.js +82 -0
  24. package/dist/mcp-server/tools/definitions/analyze-trends.tool.js.map +1 -0
  25. package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts +30 -0
  26. package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts.map +1 -0
  27. package/dist/mcp-server/tools/definitions/resolve-name.tool.js +73 -0
  28. package/dist/mcp-server/tools/definitions/resolve-name.tool.js.map +1 -0
  29. package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts +40 -0
  30. package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts.map +1 -0
  31. package/dist/mcp-server/tools/definitions/search-entities.tool.js +113 -0
  32. package/dist/mcp-server/tools/definitions/search-entities.tool.js.map +1 -0
  33. package/dist/services/openalex/openalex-service.d.ts +23 -0
  34. package/dist/services/openalex/openalex-service.d.ts.map +1 -0
  35. package/dist/services/openalex/openalex-service.js +254 -0
  36. package/dist/services/openalex/openalex-service.js.map +1 -0
  37. package/dist/services/openalex/types.d.ts +68 -0
  38. package/dist/services/openalex/types.d.ts.map +1 -0
  39. package/dist/services/openalex/types.js +15 -0
  40. package/dist/services/openalex/types.js.map +1 -0
  41. package/package.json +73 -0
  42. package/server.json +125 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @fileoverview Primary discovery and lookup tool for OpenAlex entities.
3
+ * @module mcp-server/tools/definitions/search-entities.tool
4
+ */
5
+ import { z } from '@cyanheads/mcp-ts-core';
6
+ export declare const searchEntitiesTool: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
7
+ entity_type: z.ZodEnum<{
8
+ works: "works";
9
+ authors: "authors";
10
+ sources: "sources";
11
+ institutions: "institutions";
12
+ topics: "topics";
13
+ keywords: "keywords";
14
+ publishers: "publishers";
15
+ funders: "funders";
16
+ }>;
17
+ id: z.ZodOptional<z.ZodString>;
18
+ query: z.ZodOptional<z.ZodString>;
19
+ search_mode: z.ZodDefault<z.ZodEnum<{
20
+ keyword: "keyword";
21
+ exact: "exact";
22
+ semantic: "semantic";
23
+ }>>;
24
+ filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
25
+ sort: z.ZodOptional<z.ZodString>;
26
+ select: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
+ per_page: z.ZodDefault<z.ZodNumber>;
28
+ cursor: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$strip>, z.ZodObject<{
30
+ meta: z.ZodObject<{
31
+ count: z.ZodNumber;
32
+ per_page: z.ZodNumber;
33
+ next_cursor: z.ZodNullable<z.ZodString>;
34
+ }, z.core.$strip>;
35
+ results: z.ZodArray<z.ZodObject<{
36
+ id: z.ZodString;
37
+ display_name: z.ZodString;
38
+ }, z.core.$loose>>;
39
+ }, z.core.$strip>>;
40
+ //# sourceMappingURL=search-entities.tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-entities.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/search-entities.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAIjD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA4H7B,CAAC"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @fileoverview Primary discovery and lookup tool for OpenAlex entities.
3
+ * @module mcp-server/tools/definitions/search-entities.tool
4
+ */
5
+ import { tool, z } from '@cyanheads/mcp-ts-core';
6
+ import { getOpenAlexService } from '../../../services/openalex/openalex-service.js';
7
+ import { ENTITY_TYPES } from '../../../services/openalex/types.js';
8
+ export const searchEntitiesTool = tool('openalex_search_entities', {
9
+ description: 'Search, filter, sort, or retrieve by ID. Covers all OpenAlex entity types (works, authors, sources, institutions, topics, keywords, publishers, funders). Pass `id` to retrieve a single entity (free, unlimited API calls). Otherwise, use `query` and/or `filters` for discovery. Supports keyword search with boolean operators, exact phrase matching, and AI semantic search. Use openalex_resolve_name to resolve names to IDs before filtering. Use the `select` parameter to reduce payload size — works (~70KB) and institutions (~20KB) are especially large without it.',
10
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
11
+ input: z.object({
12
+ entity_type: z.enum(ENTITY_TYPES).describe('Type of scholarly entity to search.'),
13
+ id: z
14
+ .string()
15
+ .optional()
16
+ .describe('Retrieve a single entity by ID. Supports: OpenAlex ID ("W2741809807"), DOI ("10.1038/nature12373"), ORCID ("0000-0002-1825-0097"), ROR ("https://ror.org/00hx57361"), PMID ("12345678"), PMCID ("PMC1234567"), ISSN ("1234-5678"). When provided, other search/filter/sort params are ignored. Use openalex_resolve_name to find the ID if unknown.'),
17
+ query: z
18
+ .string()
19
+ .optional()
20
+ .describe('Text search query. Supports boolean operators (AND, OR, NOT), quoted phrases ("exact match"), wildcards (machin*), fuzzy matching (machin~1), and proximity ("climate change"~5). Omit for filter-only queries.'),
21
+ search_mode: z
22
+ .enum(['keyword', 'exact', 'semantic'])
23
+ .default('keyword')
24
+ .describe('Search strategy. "keyword": stemmed full-text (default). "exact": no stemming, matches individual words (use quoted phrases for multi-word exact match). "semantic": AI embedding similarity (max 50 results, 1 req/sec).'),
25
+ filters: z
26
+ .record(z.string(), z.string())
27
+ .optional()
28
+ .describe('Filter criteria as field:value pairs. AND across fields (multiple keys). OR within field: pipe-separate ("us|gb"). NOT: prefix "!" ("!us"). Range: "2020-2024". Comparison: ">100", "<50". AND within same field: "+"-separate. Use OpenAlex IDs (not names) for entity filters — resolve names first.'),
29
+ sort: z
30
+ .string()
31
+ .optional()
32
+ .describe('Sort field. Prefix with "-" for descending. Common: "cited_by_count", "-publication_date", "relevance_score" (default when query present).'),
33
+ select: z
34
+ .array(z.string())
35
+ .optional()
36
+ .describe('Fields to return (reduces payload). Top-level fields only. STRONGLY RECOMMENDED for works and institutions — full records are 20-70KB each. Example: ["id", "doi", "display_name", "publication_year", "cited_by_count", "primary_topic"].'),
37
+ per_page: z
38
+ .number()
39
+ .int()
40
+ .min(1)
41
+ .max(100)
42
+ .default(25)
43
+ .describe('Results per page (1-100). Default 25.'),
44
+ cursor: z
45
+ .string()
46
+ .optional()
47
+ .describe('Pagination cursor from a previous response. Pass to get the next page.'),
48
+ }),
49
+ output: z.object({
50
+ meta: z
51
+ .object({
52
+ count: z.number().describe('Total results matching the query/filters.'),
53
+ per_page: z.number().describe('Results on this page.'),
54
+ next_cursor: z
55
+ .string()
56
+ .nullable()
57
+ .describe('Cursor for next page. null if no more results.'),
58
+ })
59
+ .describe('Result metadata including pagination.'),
60
+ results: z
61
+ .array(z
62
+ .object({
63
+ id: z.string().describe('OpenAlex ID (e.g., "W2741809807", "A1234567890").'),
64
+ display_name: z.string().describe('Entity name or work title.'),
65
+ })
66
+ .passthrough())
67
+ .describe('Entity objects. Additional fields depend on entity_type and select.'),
68
+ }),
69
+ async handler(input, ctx) {
70
+ const service = getOpenAlexService();
71
+ const result = await service.search({
72
+ entityType: input.entity_type,
73
+ id: input.id,
74
+ query: input.query,
75
+ searchMode: input.search_mode,
76
+ filters: input.filters,
77
+ sort: input.sort,
78
+ select: input.select,
79
+ perPage: input.per_page,
80
+ cursor: input.cursor,
81
+ }, ctx);
82
+ ctx.log.info('Search completed', {
83
+ entityType: input.entity_type,
84
+ id: input.id,
85
+ query: input.query,
86
+ resultCount: result.results.length,
87
+ totalCount: result.meta.count,
88
+ });
89
+ return result;
90
+ },
91
+ format: (result) => {
92
+ const header = `Found ${result.meta.count} result(s).`;
93
+ if (result.results.length === 0) {
94
+ return [{ type: 'text', text: header }];
95
+ }
96
+ const lines = result.results.map((r) => {
97
+ const parts = [r.display_name];
98
+ const rec = r;
99
+ if (rec.publication_year)
100
+ parts.push(String(rec.publication_year));
101
+ if (typeof rec.cited_by_count === 'number')
102
+ parts.push(`${rec.cited_by_count.toLocaleString()} citations`);
103
+ if (rec.doi)
104
+ parts.push(String(rec.doi));
105
+ return `- ${parts[0]} (${r.id})${parts.length > 1 ? ` — ${parts.slice(1).join(', ')}` : ''}`;
106
+ });
107
+ const footer = result.meta.next_cursor
108
+ ? `\n[More results available — pass cursor to paginate]`
109
+ : '';
110
+ return [{ type: 'text', text: `${header}\n\n${lines.join('\n')}${footer}` }];
111
+ },
112
+ });
113
+ //# sourceMappingURL=search-entities.tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-entities.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/search-entities.tool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview OpenAlex API client service. Handles all communication with the OpenAlex REST API.
3
+ * @module services/openalex/openalex-service
4
+ */
5
+ import type { Context } from '@cyanheads/mcp-ts-core';
6
+ import { type AnalyzeParams, type AnalyzeResult, type AutocompleteParams, type AutocompleteResult, type SearchParams, type SearchResult } from './types.js';
7
+ declare class OpenAlexService {
8
+ private readonly baseUrl;
9
+ private readonly apiKey;
10
+ constructor();
11
+ /** Execute an HTTP request against the OpenAlex API with retry on transient failures. */
12
+ private request;
13
+ /** Search/filter/sort entities, or retrieve a single entity by ID. */
14
+ search(params: SearchParams, ctx: Context): Promise<SearchResult>;
15
+ /** Group-by aggregation. */
16
+ analyze(params: AnalyzeParams, ctx: Context): Promise<AnalyzeResult>;
17
+ /** Autocomplete name resolution. */
18
+ autocomplete(params: AutocompleteParams, ctx: Context): Promise<AutocompleteResult>;
19
+ }
20
+ export declare function initOpenAlexService(): void;
21
+ export declare function getOpenAlexService(): OpenAlexService;
22
+ export {};
23
+ //# sourceMappingURL=openalex-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openalex-service.d.ts","sourceRoot":"","sources":["../../../src/services/openalex/openalex-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAKtD,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EAGvB,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AA8GpB,cAAM,eAAe;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;IAQhC,yFAAyF;YAC3E,OAAO;IAwDrB,sEAAsE;IAChE,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC;IAqEvE,4BAA4B;IACtB,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC;IAgC1E,oCAAoC;IAC9B,YAAY,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAwB1F;AAID,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED,wBAAgB,kBAAkB,IAAI,eAAe,CAIpD"}
@@ -0,0 +1,254 @@
1
+ /**
2
+ * @fileoverview OpenAlex API client service. Handles all communication with the OpenAlex REST API.
3
+ * @module services/openalex/openalex-service
4
+ */
5
+ import { notFound, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
6
+ import { getServerConfig } from '../../config/server-config.js';
7
+ import { ENTITY_TYPES, } from './types.js';
8
+ /**
9
+ * Build an OpenAlex filter string from a key-value record.
10
+ * Input: { "cited_by_count": ">100", "is_oa": "true" }
11
+ * Output: "cited_by_count:>100,is_oa:true"
12
+ */
13
+ function buildFilterString(filters) {
14
+ return Object.entries(filters)
15
+ .map(([key, value]) => `${key}:${value}`)
16
+ .join(',');
17
+ }
18
+ /**
19
+ * Detect ID format and return the API path segment.
20
+ * "10.1038/nature12373" → "doi:10.1038/nature12373"
21
+ * "https://doi.org/10.1038/nature12373" → "doi:10.1038/nature12373"
22
+ * "0000-0002-1825-0097" → "orcid:0000-0002-1825-0097"
23
+ * "https://ror.org/00hx57361" → "ror:https://ror.org/00hx57361"
24
+ * "PMC1234567" → "pmcid:PMC1234567"
25
+ * "W2741809807" → "W2741809807"
26
+ */
27
+ function normalizeId(id) {
28
+ const trimmed = id.trim();
29
+ // Full OpenAlex URL
30
+ if (trimmed.startsWith('https://openalex.org/')) {
31
+ return trimmed.replace('https://openalex.org/', '');
32
+ }
33
+ // DOI URL
34
+ if (trimmed.startsWith('https://doi.org/')) {
35
+ return `doi:${trimmed.replace('https://doi.org/', '')}`;
36
+ }
37
+ // DOI pattern (10.xxxx/...)
38
+ if (/^10\.\d{4,}\//.test(trimmed)) {
39
+ return `doi:${trimmed}`;
40
+ }
41
+ // ROR URL
42
+ if (trimmed.startsWith('https://ror.org/')) {
43
+ return `ror:${trimmed}`;
44
+ }
45
+ // ORCID pattern (0000-0000-0000-0000)
46
+ if (/^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$/i.test(trimmed)) {
47
+ return `orcid:${trimmed}`;
48
+ }
49
+ // ISSN pattern (0000-0000)
50
+ if (/^\d{4}-\d{3}[\dX]$/i.test(trimmed)) {
51
+ return `issn:${trimmed}`;
52
+ }
53
+ // PMCID
54
+ if (/^PMC\d+$/i.test(trimmed)) {
55
+ return `pmcid:${trimmed}`;
56
+ }
57
+ // Pure numeric → PMID
58
+ if (/^\d{5,}$/.test(trimmed)) {
59
+ return `pmid:${trimmed}`;
60
+ }
61
+ // OpenAlex ID or already prefixed — pass through
62
+ return trimmed;
63
+ }
64
+ /**
65
+ * Reconstruct abstract text from OpenAlex's inverted index format.
66
+ * The API stores abstracts as { word: [position, ...] } — we reverse this to plaintext.
67
+ */
68
+ function reconstructAbstract(invertedIndex) {
69
+ const words = [];
70
+ for (const [word, positions] of Object.entries(invertedIndex)) {
71
+ for (const pos of positions) {
72
+ words.push([pos, word]);
73
+ }
74
+ }
75
+ words.sort((a, b) => a[0] - b[0]);
76
+ return words.map(([, word]) => word).join(' ');
77
+ }
78
+ /**
79
+ * Post-process entity records: reconstruct abstracts from inverted indices.
80
+ */
81
+ function processResults(results) {
82
+ for (const record of results) {
83
+ if (record.abstract_inverted_index &&
84
+ typeof record.abstract_inverted_index === 'object' &&
85
+ !Array.isArray(record.abstract_inverted_index)) {
86
+ record.abstract = reconstructAbstract(record.abstract_inverted_index);
87
+ delete record.abstract_inverted_index;
88
+ }
89
+ }
90
+ return results;
91
+ }
92
+ const MAX_RETRIES = 3;
93
+ const BASE_BACKOFF_MS = 1000;
94
+ function sleep(ms) {
95
+ return new Promise((resolve) => setTimeout(resolve, ms));
96
+ }
97
+ class OpenAlexService {
98
+ baseUrl;
99
+ apiKey;
100
+ constructor() {
101
+ const config = getServerConfig();
102
+ this.baseUrl = config.baseUrl.replace(/\/+$/, '');
103
+ this.apiKey = config.apiKey;
104
+ }
105
+ /** Execute an HTTP request against the OpenAlex API with retry on transient failures. */
106
+ async request(path, params, ctx) {
107
+ const url = new URL(`${this.baseUrl}${path}`);
108
+ url.searchParams.set('mailto', this.apiKey);
109
+ for (const [key, value] of Object.entries(params)) {
110
+ if (value)
111
+ url.searchParams.set(key, value);
112
+ }
113
+ ctx.log.debug('OpenAlex request', { path, params: Object.keys(params) });
114
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
115
+ const response = await fetch(url.toString(), {
116
+ headers: { Accept: 'application/json' },
117
+ signal: ctx.signal,
118
+ });
119
+ if (response.ok)
120
+ return response.json();
121
+ if (response.status === 404) {
122
+ throw notFound(`Entity not found at ${path}`, { path, status: 404 });
123
+ }
124
+ const retryable = response.status === 429 ||
125
+ response.status === 500 ||
126
+ response.status === 502 ||
127
+ response.status === 503 ||
128
+ response.status === 504;
129
+ if (retryable && attempt < MAX_RETRIES - 1) {
130
+ await sleep(BASE_BACKOFF_MS * 2 ** attempt);
131
+ continue;
132
+ }
133
+ const body = await response.text().catch(() => '');
134
+ let detail = '';
135
+ try {
136
+ const parsed = JSON.parse(body);
137
+ detail = parsed.message ?? '';
138
+ }
139
+ catch {
140
+ detail = body
141
+ .replace(/<[^>]*>/g, '')
142
+ .trim()
143
+ .slice(0, 200);
144
+ }
145
+ const message = detail
146
+ ? `OpenAlex API error (${response.status}): ${detail}`
147
+ : `OpenAlex API error: ${response.status} ${response.statusText}`;
148
+ throw serviceUnavailable(message, { path, status: response.status });
149
+ }
150
+ throw serviceUnavailable(`OpenAlex API request failed after ${MAX_RETRIES} attempts`, { path });
151
+ }
152
+ /** Search/filter/sort entities, or retrieve a single entity by ID. */
153
+ async search(params, ctx) {
154
+ // Singleton lookup by ID
155
+ if (params.id) {
156
+ const normalizedId = normalizeId(params.id);
157
+ const queryParams = {};
158
+ if (params.select?.length) {
159
+ queryParams.select = params.select.join(',');
160
+ }
161
+ const data = (await this.request(`/${params.entityType}/${normalizedId}`, queryParams, ctx));
162
+ const results = processResults([data]);
163
+ return {
164
+ meta: { count: 1, per_page: 1, next_cursor: null },
165
+ results,
166
+ };
167
+ }
168
+ // Search/filter/list
169
+ const queryParams = {};
170
+ if (params.query) {
171
+ const searchKey = params.searchMode === 'exact'
172
+ ? 'search.exact'
173
+ : params.searchMode === 'semantic'
174
+ ? 'search.semantic'
175
+ : 'search';
176
+ queryParams[searchKey] = params.query;
177
+ }
178
+ if (params.filters && Object.keys(params.filters).length > 0) {
179
+ queryParams.filter = buildFilterString(params.filters);
180
+ }
181
+ if (params.sort) {
182
+ // Translate "-field" prefix to "field:desc" (OpenAlex API syntax)
183
+ queryParams.sort = params.sort.startsWith('-') ? `${params.sort.slice(1)}:desc` : params.sort;
184
+ }
185
+ if (params.select?.length) {
186
+ queryParams.select = params.select.join(',');
187
+ }
188
+ queryParams.per_page = String(params.perPage ?? 25);
189
+ // Semantic search doesn't support cursor pagination — use page/per_page only
190
+ if (params.searchMode !== 'semantic') {
191
+ queryParams.cursor = params.cursor ?? '*';
192
+ }
193
+ const data = (await this.request(`/${params.entityType}`, queryParams, ctx));
194
+ return {
195
+ meta: {
196
+ ...data.meta,
197
+ next_cursor: data.meta.next_cursor ?? null,
198
+ },
199
+ results: processResults(data.results),
200
+ };
201
+ }
202
+ /** Group-by aggregation. */
203
+ async analyze(params, ctx) {
204
+ const queryParams = {};
205
+ queryParams.group_by = params.includeUnknown
206
+ ? `${params.groupBy}:include_unknown`
207
+ : params.groupBy;
208
+ if (params.filters && Object.keys(params.filters).length > 0) {
209
+ queryParams.filter = buildFilterString(params.filters);
210
+ }
211
+ // Only send cursor when explicitly provided — boolean group_by fields
212
+ // (is_retracted, has_orcid, etc.) reject cursor pagination entirely.
213
+ if (params.cursor) {
214
+ queryParams.cursor = params.cursor;
215
+ }
216
+ const data = (await this.request(`/${params.entityType}`, queryParams, ctx));
217
+ return {
218
+ meta: {
219
+ count: data.meta.count,
220
+ groups_count: data.meta.groups_count ?? data.group_by?.length ?? null,
221
+ next_cursor: data.meta.next_cursor ?? null,
222
+ },
223
+ groups: data.group_by ?? [],
224
+ };
225
+ }
226
+ /** Autocomplete name resolution. */
227
+ async autocomplete(params, ctx) {
228
+ const path = params.entityType ? `/autocomplete/${params.entityType}` : '/autocomplete';
229
+ const queryParams = {
230
+ q: params.query,
231
+ };
232
+ if (params.filters && Object.keys(params.filters).length > 0) {
233
+ queryParams.filter = buildFilterString(params.filters);
234
+ }
235
+ const data = (await this.request(path, queryParams, ctx));
236
+ // Cross-entity autocomplete can return types not in our enum (country, license, etc.).
237
+ // Filter to known entity types so callers can use results in other tools directly.
238
+ const knownTypes = new Set(ENTITY_TYPES.map((t) => t.replace(/s$/, '')));
239
+ const results = params.entityType
240
+ ? data.results
241
+ : data.results.filter((r) => knownTypes.has(r.entity_type));
242
+ return { results };
243
+ }
244
+ }
245
+ let _service;
246
+ export function initOpenAlexService() {
247
+ _service = new OpenAlexService();
248
+ }
249
+ export function getOpenAlexService() {
250
+ if (!_service)
251
+ throw new Error('OpenAlexService not initialized — call initOpenAlexService() in setup()');
252
+ return _service;
253
+ }
254
+ //# sourceMappingURL=openalex-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openalex-service.js","sourceRoot":"","sources":["../../../src/services/openalex/openalex-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @fileoverview Domain types for OpenAlex API interactions.
3
+ * @module services/openalex/types
4
+ */
5
+ export declare const ENTITY_TYPES: readonly ["works", "authors", "sources", "institutions", "topics", "keywords", "publishers", "funders"];
6
+ export type EntityType = (typeof ENTITY_TYPES)[number];
7
+ export interface SearchParams {
8
+ cursor?: string | undefined;
9
+ entityType: EntityType;
10
+ filters?: Record<string, string> | undefined;
11
+ id?: string | undefined;
12
+ perPage?: number | undefined;
13
+ query?: string | undefined;
14
+ searchMode?: 'keyword' | 'exact' | 'semantic' | undefined;
15
+ select?: string[] | undefined;
16
+ sort?: string | undefined;
17
+ }
18
+ export interface SearchResult {
19
+ meta: {
20
+ count: number;
21
+ per_page: number;
22
+ next_cursor: string | null;
23
+ };
24
+ results: EntityRecord[];
25
+ }
26
+ export interface AnalyzeParams {
27
+ cursor?: string | undefined;
28
+ entityType: EntityType;
29
+ filters?: Record<string, string> | undefined;
30
+ groupBy: string;
31
+ includeUnknown?: boolean | undefined;
32
+ }
33
+ export interface AnalyzeResult {
34
+ groups: GroupRecord[];
35
+ meta: {
36
+ count: number;
37
+ groups_count: number | null;
38
+ next_cursor: string | null;
39
+ };
40
+ }
41
+ export interface AutocompleteParams {
42
+ entityType?: EntityType | undefined;
43
+ filters?: Record<string, string> | undefined;
44
+ query: string;
45
+ }
46
+ export interface AutocompleteResult {
47
+ results: AutocompleteRecord[];
48
+ }
49
+ export interface EntityRecord {
50
+ display_name: string;
51
+ id: string;
52
+ [key: string]: unknown;
53
+ }
54
+ export interface GroupRecord {
55
+ count: number;
56
+ key: string;
57
+ key_display_name: string;
58
+ }
59
+ export interface AutocompleteRecord {
60
+ cited_by_count: number;
61
+ display_name: string;
62
+ entity_type: string;
63
+ external_id: string | null;
64
+ hint: string | null;
65
+ id: string;
66
+ works_count: number | null;
67
+ }
68
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/openalex/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,YAAY,yGASf,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC7C,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IAC1D,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,CAAC;IACF,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC7C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,kBAAkB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview Domain types for OpenAlex API interactions.
3
+ * @module services/openalex/types
4
+ */
5
+ export const ENTITY_TYPES = [
6
+ 'works',
7
+ 'authors',
8
+ 'sources',
9
+ 'institutions',
10
+ 'topics',
11
+ 'keywords',
12
+ 'publishers',
13
+ 'funders',
14
+ ];
15
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/openalex/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,OAAO;IACP,SAAS;IACT,SAAS;IACT,cAAc;IACd,QAAQ;IACR,UAAU;IACV,YAAY;IACZ,SAAS;CACD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@cyanheads/openalex-mcp-server",
3
+ "version": "0.2.7",
4
+ "description": "Access the OpenAlex academic research catalog - 270M+ publications through MCP. STDIO & Streamable HTTP.",
5
+ "mcpName": "io.github.cyanheads/openalex-mcp-server",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "bin": {
10
+ "openalex-mcp-server": "dist/index.js"
11
+ },
12
+ "files": ["dist/", "README.md", "LICENSE", "CLAUDE.md", "Dockerfile", "server.json"],
13
+ "scripts": {
14
+ "build": "bun scripts/build.ts",
15
+ "rebuild": "bun scripts/clean.ts && bun scripts/build.ts",
16
+ "clean": "bun scripts/clean.ts",
17
+ "devcheck": "bun scripts/devcheck.ts",
18
+ "tree": "bun scripts/tree.ts",
19
+ "format": "biome check --write --unsafe .",
20
+ "lint:mcp": "bun scripts/lint-mcp.ts",
21
+ "test": "vitest run",
22
+ "dev:stdio": "MCP_TRANSPORT_TYPE=stdio bun --watch src/index.ts",
23
+ "dev:http": "MCP_TRANSPORT_TYPE=http bun --watch src/index.ts",
24
+ "start:stdio": "MCP_TRANSPORT_TYPE=stdio node dist/index.js",
25
+ "start:http": "MCP_TRANSPORT_TYPE=http node dist/index.js"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "mcp-server",
30
+ "model-context-protocol",
31
+ "openalex",
32
+ "academic-research",
33
+ "academic-search",
34
+ "bibliometrics",
35
+ "citation-analysis",
36
+ "literature-search",
37
+ "scholarly-data",
38
+ "scholarly-research",
39
+ "typescript"
40
+ ],
41
+ "author": "cyanheads (https://github.com/cyanheads)",
42
+ "homepage": "https://github.com/cyanheads/openalex-mcp-server#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/cyanheads/openalex-mcp-server/issues"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/cyanheads/openalex-mcp-server.git"
49
+ },
50
+ "license": "Apache-2.0",
51
+ "engines": {
52
+ "node": ">=22.0.0",
53
+ "bun": ">=1.3.0"
54
+ },
55
+ "packageManager": "bun@1.3.11",
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "dependencies": {
60
+ "@cyanheads/mcp-ts-core": "^0.1.28",
61
+ "pino-pretty": "^13.1.3"
62
+ },
63
+ "devDependencies": {
64
+ "@biomejs/biome": "^2.4.8",
65
+ "@types/node": "^25.5.0",
66
+ "depcheck": "^1.4.7",
67
+ "ignore": "^7.0.5",
68
+ "tsc-alias": "^1.8.16",
69
+ "tsx": "^4.21.0",
70
+ "typescript": "^6.0.2",
71
+ "vitest": "^4.1.1"
72
+ }
73
+ }