openalex-research-mcp 0.1.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.
- package/LICENSE +21 -0
- package/QUICKSTART.md +133 -0
- package/README.md +253 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +946 -0
- package/build/index.js.map +1 -0
- package/build/openalex-client.d.ts +91 -0
- package/build/openalex-client.d.ts.map +1 -0
- package/build/openalex-client.js +190 -0
- package/build/openalex-client.js.map +1 -0
- package/package.json +58 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { OpenAlexClient } from './openalex-client.js';
|
|
6
|
+
// Initialize OpenAlex client
|
|
7
|
+
const openAlexClient = new OpenAlexClient();
|
|
8
|
+
// Define all tools
|
|
9
|
+
const tools = [
|
|
10
|
+
// Literature Search & Discovery
|
|
11
|
+
{
|
|
12
|
+
name: 'search_works',
|
|
13
|
+
description: 'Search for scholarly works (papers, articles, books) with advanced filtering. Supports Boolean operators (AND, OR, NOT), publication year ranges, citation counts, and more. Essential for finding relevant literature.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
query: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Search query. Supports Boolean operators in uppercase (AND, OR, NOT). Example: "machine learning AND (neural networks OR deep learning)"',
|
|
20
|
+
},
|
|
21
|
+
from_publication_year: {
|
|
22
|
+
type: 'number',
|
|
23
|
+
description: 'Filter works published from this year onwards',
|
|
24
|
+
},
|
|
25
|
+
to_publication_year: {
|
|
26
|
+
type: 'number',
|
|
27
|
+
description: 'Filter works published up to this year',
|
|
28
|
+
},
|
|
29
|
+
cited_by_count: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Filter by citation count. Use >X for more than X citations, <X for less than X. Example: ">100"',
|
|
32
|
+
},
|
|
33
|
+
is_oa: {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
description: 'Filter for open access works only',
|
|
36
|
+
},
|
|
37
|
+
type: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Filter by work type: article, book, dataset, etc.',
|
|
40
|
+
},
|
|
41
|
+
sort: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Sort results. Options: relevance_score (default), cited_by_count, publication_year',
|
|
44
|
+
},
|
|
45
|
+
page: {
|
|
46
|
+
type: 'number',
|
|
47
|
+
description: 'Page number for pagination (default: 1)',
|
|
48
|
+
},
|
|
49
|
+
per_page: {
|
|
50
|
+
type: 'number',
|
|
51
|
+
description: 'Results per page, max 200 (default: 25)',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'get_work',
|
|
58
|
+
description: 'Get detailed information about a specific work by OpenAlex ID or DOI. Returns full metadata including title, authors, abstract, citations, references, and more.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
id: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'Work identifier. Can be OpenAlex ID (W2741809807), DOI (10.1371/journal.pone.0000000), or full URL',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ['id'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'get_related_works',
|
|
72
|
+
description: 'Find works related to a given work based on shared topics, citations, and references. Useful for discovering similar papers in a research area.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
id: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Work identifier (OpenAlex ID, DOI, or URL)',
|
|
79
|
+
},
|
|
80
|
+
per_page: {
|
|
81
|
+
type: 'number',
|
|
82
|
+
description: 'Number of related works to return (default: 25, max: 200)',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ['id'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'search_by_topic',
|
|
90
|
+
description: 'Search for works within specific research topics or domains. Use this to explore literature in a particular field or subfield.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
topic: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'Topic name or keywords to search for (e.g., "artificial intelligence", "climate change", "quantum computing")',
|
|
97
|
+
},
|
|
98
|
+
from_year: {
|
|
99
|
+
type: 'number',
|
|
100
|
+
description: 'Filter works from this year onwards',
|
|
101
|
+
},
|
|
102
|
+
to_year: {
|
|
103
|
+
type: 'number',
|
|
104
|
+
description: 'Filter works up to this year',
|
|
105
|
+
},
|
|
106
|
+
sort: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'Sort by: cited_by_count, publication_year, relevance_score (default)',
|
|
109
|
+
},
|
|
110
|
+
per_page: {
|
|
111
|
+
type: 'number',
|
|
112
|
+
description: 'Results per page (default: 25, max: 200)',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
required: ['topic'],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'autocomplete_search',
|
|
120
|
+
description: 'Fast autocomplete/typeahead search for works, authors, institutions, or other entities. Returns quick suggestions for partial queries.',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
query: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'Partial search query',
|
|
127
|
+
},
|
|
128
|
+
entity_type: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'Type of entity to search: works, authors, institutions, sources, topics, publishers, funders',
|
|
131
|
+
enum: ['works', 'authors', 'institutions', 'sources', 'topics', 'publishers', 'funders'],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
required: ['query', 'entity_type'],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
// Citation Analysis
|
|
138
|
+
{
|
|
139
|
+
name: 'get_work_citations',
|
|
140
|
+
description: 'Get all works that cite a given work. Essential for forward citation analysis and understanding research impact.',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
id: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description: 'Work identifier (OpenAlex ID, DOI, or URL)',
|
|
147
|
+
},
|
|
148
|
+
page: {
|
|
149
|
+
type: 'number',
|
|
150
|
+
description: 'Page number for pagination',
|
|
151
|
+
},
|
|
152
|
+
per_page: {
|
|
153
|
+
type: 'number',
|
|
154
|
+
description: 'Citations per page (default: 25, max: 200)',
|
|
155
|
+
},
|
|
156
|
+
sort: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'Sort by: publication_year, cited_by_count',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
required: ['id'],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'get_work_references',
|
|
166
|
+
description: 'Get all works referenced/cited by a given work. Essential for backward citation analysis and finding foundational papers.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
id: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'Work identifier (OpenAlex ID, DOI, or URL)',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
required: ['id'],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'get_citation_network',
|
|
180
|
+
description: 'Get a citation network for a work including both citing works (forward) and referenced works (backward). Returns structured data for network visualization and analysis.',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
id: {
|
|
185
|
+
type: 'string',
|
|
186
|
+
description: 'Work identifier (OpenAlex ID, DOI, or URL)',
|
|
187
|
+
},
|
|
188
|
+
depth: {
|
|
189
|
+
type: 'number',
|
|
190
|
+
description: 'Network depth: 1 = immediate citations/references only, 2 = second-order connections (default: 1)',
|
|
191
|
+
},
|
|
192
|
+
max_citing: {
|
|
193
|
+
type: 'number',
|
|
194
|
+
description: 'Maximum number of citing works to include (default: 50)',
|
|
195
|
+
},
|
|
196
|
+
max_references: {
|
|
197
|
+
type: 'number',
|
|
198
|
+
description: 'Maximum number of referenced works to include (default: 50)',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
required: ['id'],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'get_top_cited_works',
|
|
206
|
+
description: 'Find the most highly cited works in a research area or matching specific criteria. Identifies influential and seminal papers.',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
query: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'Search query to filter works (optional)',
|
|
213
|
+
},
|
|
214
|
+
topic: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'Filter by research topic',
|
|
217
|
+
},
|
|
218
|
+
from_year: {
|
|
219
|
+
type: 'number',
|
|
220
|
+
description: 'Consider works from this year onwards',
|
|
221
|
+
},
|
|
222
|
+
to_year: {
|
|
223
|
+
type: 'number',
|
|
224
|
+
description: 'Consider works up to this year',
|
|
225
|
+
},
|
|
226
|
+
per_page: {
|
|
227
|
+
type: 'number',
|
|
228
|
+
description: 'Number of top works to return (default: 25, max: 200)',
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
// Author & Institution Analysis
|
|
234
|
+
{
|
|
235
|
+
name: 'search_authors',
|
|
236
|
+
description: 'Search for authors/researchers with filters for publication count, citations, affiliations, and more. Find experts in specific research areas.',
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
query: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
description: 'Author name or search query',
|
|
243
|
+
},
|
|
244
|
+
works_count: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
description: 'Filter by number of works. Use >X or <X. Example: ">50"',
|
|
247
|
+
},
|
|
248
|
+
cited_by_count: {
|
|
249
|
+
type: 'string',
|
|
250
|
+
description: 'Filter by citation count. Use >X or <X. Example: ">1000"',
|
|
251
|
+
},
|
|
252
|
+
institution: {
|
|
253
|
+
type: 'string',
|
|
254
|
+
description: 'Filter by institution name or ID',
|
|
255
|
+
},
|
|
256
|
+
per_page: {
|
|
257
|
+
type: 'number',
|
|
258
|
+
description: 'Results per page (default: 25, max: 200)',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'get_author_works',
|
|
265
|
+
description: "Get all publications by a specific author over time. Useful for analyzing an author's research trajectory and productivity.",
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
properties: {
|
|
269
|
+
author_id: {
|
|
270
|
+
type: 'string',
|
|
271
|
+
description: 'Author identifier (OpenAlex ID, ORCID, or URL)',
|
|
272
|
+
},
|
|
273
|
+
from_year: {
|
|
274
|
+
type: 'number',
|
|
275
|
+
description: 'Get works from this year onwards',
|
|
276
|
+
},
|
|
277
|
+
to_year: {
|
|
278
|
+
type: 'number',
|
|
279
|
+
description: 'Get works up to this year',
|
|
280
|
+
},
|
|
281
|
+
sort: {
|
|
282
|
+
type: 'string',
|
|
283
|
+
description: 'Sort by: publication_year, cited_by_count',
|
|
284
|
+
},
|
|
285
|
+
per_page: {
|
|
286
|
+
type: 'number',
|
|
287
|
+
description: 'Works per page (default: 25, max: 200)',
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
required: ['author_id'],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'get_author_collaborators',
|
|
295
|
+
description: 'Analyze an author\'s co-authorship network. Returns frequent collaborators and collaboration statistics.',
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: {
|
|
299
|
+
author_id: {
|
|
300
|
+
type: 'string',
|
|
301
|
+
description: 'Author identifier (OpenAlex ID, ORCID, or URL)',
|
|
302
|
+
},
|
|
303
|
+
min_collaborations: {
|
|
304
|
+
type: 'number',
|
|
305
|
+
description: 'Minimum number of co-authored papers to include (default: 1)',
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
required: ['author_id'],
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'search_institutions',
|
|
313
|
+
description: 'Search for academic institutions with filters for research output, citations, and geographical location. Find leading institutions in specific areas.',
|
|
314
|
+
inputSchema: {
|
|
315
|
+
type: 'object',
|
|
316
|
+
properties: {
|
|
317
|
+
query: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
description: 'Institution name or search query',
|
|
320
|
+
},
|
|
321
|
+
country_code: {
|
|
322
|
+
type: 'string',
|
|
323
|
+
description: 'Filter by ISO 3166-1 alpha-2 country code (e.g., "US", "GB", "CN")',
|
|
324
|
+
},
|
|
325
|
+
type: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
description: 'Institution type: education, healthcare, company, archive, nonprofit, government, facility, other',
|
|
328
|
+
},
|
|
329
|
+
works_count: {
|
|
330
|
+
type: 'string',
|
|
331
|
+
description: 'Filter by number of works. Use >X or <X',
|
|
332
|
+
},
|
|
333
|
+
per_page: {
|
|
334
|
+
type: 'number',
|
|
335
|
+
description: 'Results per page (default: 25, max: 200)',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
// Research Landscape & Trends
|
|
341
|
+
{
|
|
342
|
+
name: 'analyze_topic_trends',
|
|
343
|
+
description: 'Analyze publication trends over time for specific topics or queries. Returns works grouped by year to show research evolution and growth.',
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: 'object',
|
|
346
|
+
properties: {
|
|
347
|
+
query: {
|
|
348
|
+
type: 'string',
|
|
349
|
+
description: 'Search query or topic to analyze',
|
|
350
|
+
},
|
|
351
|
+
from_year: {
|
|
352
|
+
type: 'number',
|
|
353
|
+
description: 'Start year for trend analysis',
|
|
354
|
+
},
|
|
355
|
+
to_year: {
|
|
356
|
+
type: 'number',
|
|
357
|
+
description: 'End year for trend analysis',
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
required: ['query'],
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'compare_research_areas',
|
|
365
|
+
description: 'Compare publication volume and citation metrics across different research topics or queries. Useful for understanding relative activity in different fields.',
|
|
366
|
+
inputSchema: {
|
|
367
|
+
type: 'object',
|
|
368
|
+
properties: {
|
|
369
|
+
topics: {
|
|
370
|
+
type: 'array',
|
|
371
|
+
items: { type: 'string' },
|
|
372
|
+
description: 'Array of topics/queries to compare (2-5 recommended)',
|
|
373
|
+
},
|
|
374
|
+
from_year: {
|
|
375
|
+
type: 'number',
|
|
376
|
+
description: 'Compare from this year onwards',
|
|
377
|
+
},
|
|
378
|
+
to_year: {
|
|
379
|
+
type: 'number',
|
|
380
|
+
description: 'Compare up to this year',
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
required: ['topics'],
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'get_trending_topics',
|
|
388
|
+
description: 'Discover emerging and trending research topics based on recent publication activity. Identifies fast-growing research areas.',
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: 'object',
|
|
391
|
+
properties: {
|
|
392
|
+
min_works: {
|
|
393
|
+
type: 'number',
|
|
394
|
+
description: 'Minimum number of recent works for a topic to be considered trending (default: 100)',
|
|
395
|
+
},
|
|
396
|
+
time_period_years: {
|
|
397
|
+
type: 'number',
|
|
398
|
+
description: 'Consider works from the last N years (default: 3)',
|
|
399
|
+
},
|
|
400
|
+
per_page: {
|
|
401
|
+
type: 'number',
|
|
402
|
+
description: 'Number of trending topics to return (default: 25)',
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: 'analyze_geographic_distribution',
|
|
409
|
+
description: 'Analyze the geographical distribution of research activity for a topic or query. Shows which countries and institutions are most active.',
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: 'object',
|
|
412
|
+
properties: {
|
|
413
|
+
query: {
|
|
414
|
+
type: 'string',
|
|
415
|
+
description: 'Search query or topic to analyze',
|
|
416
|
+
},
|
|
417
|
+
from_year: {
|
|
418
|
+
type: 'number',
|
|
419
|
+
description: 'Analyze from this year onwards',
|
|
420
|
+
},
|
|
421
|
+
to_year: {
|
|
422
|
+
type: 'number',
|
|
423
|
+
description: 'Analyze up to this year',
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
required: ['query'],
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
// Entity Lookup
|
|
430
|
+
{
|
|
431
|
+
name: 'get_entity',
|
|
432
|
+
description: 'Get detailed information about any OpenAlex entity by ID. Supports works, authors, sources, institutions, topics, publishers, and funders.',
|
|
433
|
+
inputSchema: {
|
|
434
|
+
type: 'object',
|
|
435
|
+
properties: {
|
|
436
|
+
entity_type: {
|
|
437
|
+
type: 'string',
|
|
438
|
+
description: 'Type of entity',
|
|
439
|
+
enum: ['works', 'authors', 'sources', 'institutions', 'topics', 'publishers', 'funders'],
|
|
440
|
+
},
|
|
441
|
+
id: {
|
|
442
|
+
type: 'string',
|
|
443
|
+
description: 'Entity identifier (OpenAlex ID, DOI, ORCID, or other supported ID)',
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
required: ['entity_type', 'id'],
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: 'search_sources',
|
|
451
|
+
description: 'Search for journals, conferences, and other publication sources. Find venue information including impact metrics and open access policies.',
|
|
452
|
+
inputSchema: {
|
|
453
|
+
type: 'object',
|
|
454
|
+
properties: {
|
|
455
|
+
query: {
|
|
456
|
+
type: 'string',
|
|
457
|
+
description: 'Source name or search query',
|
|
458
|
+
},
|
|
459
|
+
type: {
|
|
460
|
+
type: 'string',
|
|
461
|
+
description: 'Source type: journal, conference, repository, ebook platform, book series',
|
|
462
|
+
},
|
|
463
|
+
is_oa: {
|
|
464
|
+
type: 'boolean',
|
|
465
|
+
description: 'Filter for open access sources only',
|
|
466
|
+
},
|
|
467
|
+
works_count: {
|
|
468
|
+
type: 'string',
|
|
469
|
+
description: 'Filter by number of works published. Use >X or <X',
|
|
470
|
+
},
|
|
471
|
+
per_page: {
|
|
472
|
+
type: 'number',
|
|
473
|
+
description: 'Results per page (default: 25, max: 200)',
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
];
|
|
479
|
+
// Create server instance
|
|
480
|
+
const server = new Server({
|
|
481
|
+
name: 'openalex-mcp',
|
|
482
|
+
version: '1.0.0',
|
|
483
|
+
}, {
|
|
484
|
+
capabilities: {
|
|
485
|
+
tools: {},
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
// Helper function to build filter object
|
|
489
|
+
function buildFilter(params) {
|
|
490
|
+
const filter = {};
|
|
491
|
+
// Map common parameters to OpenAlex filter format
|
|
492
|
+
if (params.from_publication_year) {
|
|
493
|
+
filter['from_publication_date'] = params.from_publication_year;
|
|
494
|
+
}
|
|
495
|
+
if (params.to_publication_year) {
|
|
496
|
+
filter['to_publication_date'] = params.to_publication_year;
|
|
497
|
+
}
|
|
498
|
+
if (params.from_year) {
|
|
499
|
+
filter['from_publication_date'] = params.from_year;
|
|
500
|
+
}
|
|
501
|
+
if (params.to_year) {
|
|
502
|
+
filter['to_publication_date'] = params.to_year;
|
|
503
|
+
}
|
|
504
|
+
if (params.cited_by_count) {
|
|
505
|
+
filter['cited_by_count'] = params.cited_by_count;
|
|
506
|
+
}
|
|
507
|
+
if (params.is_oa !== undefined) {
|
|
508
|
+
filter['is_oa'] = params.is_oa;
|
|
509
|
+
}
|
|
510
|
+
if (params.type) {
|
|
511
|
+
filter['type'] = params.type;
|
|
512
|
+
}
|
|
513
|
+
if (params.works_count) {
|
|
514
|
+
filter['works_count'] = params.works_count;
|
|
515
|
+
}
|
|
516
|
+
if (params.country_code) {
|
|
517
|
+
filter['country_code'] = params.country_code;
|
|
518
|
+
}
|
|
519
|
+
if (params.institution) {
|
|
520
|
+
filter['institutions.display_name'] = params.institution;
|
|
521
|
+
}
|
|
522
|
+
return filter;
|
|
523
|
+
}
|
|
524
|
+
// List available tools
|
|
525
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
526
|
+
return { tools };
|
|
527
|
+
});
|
|
528
|
+
// Handle tool calls
|
|
529
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
530
|
+
const { name, arguments: args } = request.params;
|
|
531
|
+
try {
|
|
532
|
+
// Type assertion for args
|
|
533
|
+
const params = args;
|
|
534
|
+
switch (name) {
|
|
535
|
+
case 'search_works': {
|
|
536
|
+
const filter = buildFilter(params);
|
|
537
|
+
const options = {
|
|
538
|
+
search: params.query,
|
|
539
|
+
filter,
|
|
540
|
+
sort: params.sort,
|
|
541
|
+
page: params.page || 1,
|
|
542
|
+
perPage: params.per_page || 25,
|
|
543
|
+
};
|
|
544
|
+
const results = await openAlexClient.getWorks(options);
|
|
545
|
+
return {
|
|
546
|
+
content: [
|
|
547
|
+
{
|
|
548
|
+
type: 'text',
|
|
549
|
+
text: JSON.stringify(results, null, 2),
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
case 'get_work': {
|
|
555
|
+
const work = await openAlexClient.getWork(params.id);
|
|
556
|
+
return {
|
|
557
|
+
content: [
|
|
558
|
+
{
|
|
559
|
+
type: 'text',
|
|
560
|
+
text: JSON.stringify(work, null, 2),
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
case 'get_related_works': {
|
|
566
|
+
const work = await openAlexClient.getWork(params.id);
|
|
567
|
+
const relatedIds = work.related_works || [];
|
|
568
|
+
// Fetch related works
|
|
569
|
+
const relatedWorks = [];
|
|
570
|
+
const limit = Math.min(params.per_page || 25, relatedIds.length);
|
|
571
|
+
for (let i = 0; i < limit; i++) {
|
|
572
|
+
try {
|
|
573
|
+
const relatedWork = await openAlexClient.getWork(relatedIds[i]);
|
|
574
|
+
relatedWorks.push(relatedWork);
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
// Skip if work not found
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
content: [
|
|
583
|
+
{
|
|
584
|
+
type: 'text',
|
|
585
|
+
text: JSON.stringify({ related_works: relatedWorks }, null, 2),
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
case 'search_by_topic': {
|
|
591
|
+
const filter = buildFilter(args);
|
|
592
|
+
const options = {
|
|
593
|
+
search: params.topic,
|
|
594
|
+
filter,
|
|
595
|
+
sort: params.sort || 'relevance_score',
|
|
596
|
+
perPage: params.per_page || 25,
|
|
597
|
+
};
|
|
598
|
+
const results = await openAlexClient.getWorks(options);
|
|
599
|
+
return {
|
|
600
|
+
content: [
|
|
601
|
+
{
|
|
602
|
+
type: 'text',
|
|
603
|
+
text: JSON.stringify(results, null, 2),
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
case 'autocomplete_search': {
|
|
609
|
+
const results = await openAlexClient.autocomplete(params.entity_type, params.query);
|
|
610
|
+
return {
|
|
611
|
+
content: [
|
|
612
|
+
{
|
|
613
|
+
type: 'text',
|
|
614
|
+
text: JSON.stringify(results, null, 2),
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
case 'get_work_citations': {
|
|
620
|
+
const filter = {
|
|
621
|
+
'cites': params.id,
|
|
622
|
+
};
|
|
623
|
+
const options = {
|
|
624
|
+
filter,
|
|
625
|
+
page: params.page || 1,
|
|
626
|
+
perPage: params.per_page || 25,
|
|
627
|
+
sort: params.sort,
|
|
628
|
+
};
|
|
629
|
+
const results = await openAlexClient.getWorks(options);
|
|
630
|
+
return {
|
|
631
|
+
content: [
|
|
632
|
+
{
|
|
633
|
+
type: 'text',
|
|
634
|
+
text: JSON.stringify(results, null, 2),
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
case 'get_work_references': {
|
|
640
|
+
const work = await openAlexClient.getWork(params.id);
|
|
641
|
+
const referenceIds = work.referenced_works || [];
|
|
642
|
+
return {
|
|
643
|
+
content: [
|
|
644
|
+
{
|
|
645
|
+
type: 'text',
|
|
646
|
+
text: JSON.stringify({
|
|
647
|
+
count: referenceIds.length,
|
|
648
|
+
referenced_works: referenceIds,
|
|
649
|
+
}, null, 2),
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
case 'get_citation_network': {
|
|
655
|
+
const work = await openAlexClient.getWork(params.id);
|
|
656
|
+
const maxCiting = params.max_citing || 50;
|
|
657
|
+
const maxReferences = params.max_references || 50;
|
|
658
|
+
// Get citing works
|
|
659
|
+
const citingFilter = { 'cites': params.id };
|
|
660
|
+
const citingResults = await openAlexClient.getWorks({
|
|
661
|
+
filter: citingFilter,
|
|
662
|
+
perPage: maxCiting,
|
|
663
|
+
});
|
|
664
|
+
// Get referenced works
|
|
665
|
+
const referenceIds = (work.referenced_works || []).slice(0, maxReferences);
|
|
666
|
+
return {
|
|
667
|
+
content: [
|
|
668
|
+
{
|
|
669
|
+
type: 'text',
|
|
670
|
+
text: JSON.stringify({
|
|
671
|
+
central_work: {
|
|
672
|
+
id: work.id,
|
|
673
|
+
title: work.title,
|
|
674
|
+
publication_year: work.publication_year,
|
|
675
|
+
cited_by_count: work.cited_by_count,
|
|
676
|
+
},
|
|
677
|
+
citing_works: {
|
|
678
|
+
count: citingResults.meta.count,
|
|
679
|
+
works: citingResults.results,
|
|
680
|
+
},
|
|
681
|
+
referenced_works: {
|
|
682
|
+
count: referenceIds.length,
|
|
683
|
+
work_ids: referenceIds,
|
|
684
|
+
},
|
|
685
|
+
}, null, 2),
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
case 'get_top_cited_works': {
|
|
691
|
+
const filter = buildFilter(args);
|
|
692
|
+
const options = {
|
|
693
|
+
search: params.query,
|
|
694
|
+
filter,
|
|
695
|
+
sort: 'cited_by_count:desc',
|
|
696
|
+
perPage: params.per_page || 25,
|
|
697
|
+
};
|
|
698
|
+
const results = await openAlexClient.getWorks(options);
|
|
699
|
+
return {
|
|
700
|
+
content: [
|
|
701
|
+
{
|
|
702
|
+
type: 'text',
|
|
703
|
+
text: JSON.stringify(results, null, 2),
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
case 'search_authors': {
|
|
709
|
+
const filter = buildFilter(args);
|
|
710
|
+
const options = {
|
|
711
|
+
search: params.query,
|
|
712
|
+
filter,
|
|
713
|
+
perPage: params.per_page || 25,
|
|
714
|
+
};
|
|
715
|
+
const results = await openAlexClient.getAuthors(options);
|
|
716
|
+
return {
|
|
717
|
+
content: [
|
|
718
|
+
{
|
|
719
|
+
type: 'text',
|
|
720
|
+
text: JSON.stringify(results, null, 2),
|
|
721
|
+
},
|
|
722
|
+
],
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
case 'get_author_works': {
|
|
726
|
+
const filter = {
|
|
727
|
+
'authorships.author.id': params.author_id,
|
|
728
|
+
};
|
|
729
|
+
if (params.from_year)
|
|
730
|
+
filter['from_publication_date'] = params.from_year;
|
|
731
|
+
if (params.to_year)
|
|
732
|
+
filter['to_publication_date'] = params.to_year;
|
|
733
|
+
const options = {
|
|
734
|
+
filter,
|
|
735
|
+
sort: params.sort,
|
|
736
|
+
perPage: params.per_page || 25,
|
|
737
|
+
};
|
|
738
|
+
const results = await openAlexClient.getWorks(options);
|
|
739
|
+
return {
|
|
740
|
+
content: [
|
|
741
|
+
{
|
|
742
|
+
type: 'text',
|
|
743
|
+
text: JSON.stringify(results, null, 2),
|
|
744
|
+
},
|
|
745
|
+
],
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
case 'get_author_collaborators': {
|
|
749
|
+
// Get author's works
|
|
750
|
+
const filter = {
|
|
751
|
+
'authorships.author.id': params.author_id,
|
|
752
|
+
};
|
|
753
|
+
const works = await openAlexClient.getWorks({
|
|
754
|
+
filter,
|
|
755
|
+
perPage: 200,
|
|
756
|
+
});
|
|
757
|
+
// Count collaborators
|
|
758
|
+
const collaboratorCounts = {};
|
|
759
|
+
for (const work of works.results) {
|
|
760
|
+
if (work.authorships) {
|
|
761
|
+
for (const authorship of work.authorships) {
|
|
762
|
+
const coauthorId = authorship.author?.id;
|
|
763
|
+
if (coauthorId && coauthorId !== params.author_id) {
|
|
764
|
+
if (!collaboratorCounts[coauthorId]) {
|
|
765
|
+
collaboratorCounts[coauthorId] = {
|
|
766
|
+
count: 0,
|
|
767
|
+
name: authorship.author?.display_name || 'Unknown',
|
|
768
|
+
id: coauthorId,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
collaboratorCounts[coauthorId].count++;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// Filter and sort
|
|
777
|
+
const minCollabs = params.min_collaborations || 1;
|
|
778
|
+
const collaborators = Object.values(collaboratorCounts)
|
|
779
|
+
.filter(c => c.count >= minCollabs)
|
|
780
|
+
.sort((a, b) => b.count - a.count);
|
|
781
|
+
return {
|
|
782
|
+
content: [
|
|
783
|
+
{
|
|
784
|
+
type: 'text',
|
|
785
|
+
text: JSON.stringify({
|
|
786
|
+
author_id: params.author_id,
|
|
787
|
+
total_works_analyzed: works.results.length,
|
|
788
|
+
collaborators,
|
|
789
|
+
}, null, 2),
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
case 'search_institutions': {
|
|
795
|
+
const filter = buildFilter(args);
|
|
796
|
+
const options = {
|
|
797
|
+
search: params.query,
|
|
798
|
+
filter,
|
|
799
|
+
perPage: params.per_page || 25,
|
|
800
|
+
};
|
|
801
|
+
const results = await openAlexClient.getInstitutions(options);
|
|
802
|
+
return {
|
|
803
|
+
content: [
|
|
804
|
+
{
|
|
805
|
+
type: 'text',
|
|
806
|
+
text: JSON.stringify(results, null, 2),
|
|
807
|
+
},
|
|
808
|
+
],
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
case 'analyze_topic_trends': {
|
|
812
|
+
const filter = buildFilter(args);
|
|
813
|
+
const options = {
|
|
814
|
+
search: params.query,
|
|
815
|
+
filter,
|
|
816
|
+
groupBy: 'publication_year',
|
|
817
|
+
};
|
|
818
|
+
const results = await openAlexClient.getWorks(options);
|
|
819
|
+
return {
|
|
820
|
+
content: [
|
|
821
|
+
{
|
|
822
|
+
type: 'text',
|
|
823
|
+
text: JSON.stringify(results, null, 2),
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
case 'compare_research_areas': {
|
|
829
|
+
const comparisons = [];
|
|
830
|
+
for (const topic of params.topics) {
|
|
831
|
+
const filter = buildFilter(args);
|
|
832
|
+
const options = {
|
|
833
|
+
search: topic,
|
|
834
|
+
filter,
|
|
835
|
+
perPage: 1,
|
|
836
|
+
};
|
|
837
|
+
const results = await openAlexClient.getWorks(options);
|
|
838
|
+
comparisons.push({
|
|
839
|
+
topic,
|
|
840
|
+
total_works: results.meta.count,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
content: [
|
|
845
|
+
{
|
|
846
|
+
type: 'text',
|
|
847
|
+
text: JSON.stringify({ comparisons }, null, 2),
|
|
848
|
+
},
|
|
849
|
+
],
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
case 'get_trending_topics': {
|
|
853
|
+
const currentYear = new Date().getFullYear();
|
|
854
|
+
const yearsBack = params.time_period_years || 3;
|
|
855
|
+
const fromYear = currentYear - yearsBack;
|
|
856
|
+
const filter = {
|
|
857
|
+
'from_publication_date': fromYear,
|
|
858
|
+
};
|
|
859
|
+
const options = {
|
|
860
|
+
filter,
|
|
861
|
+
groupBy: 'topics.id',
|
|
862
|
+
perPage: params.per_page || 25,
|
|
863
|
+
};
|
|
864
|
+
const results = await openAlexClient.getWorks(options);
|
|
865
|
+
return {
|
|
866
|
+
content: [
|
|
867
|
+
{
|
|
868
|
+
type: 'text',
|
|
869
|
+
text: JSON.stringify(results, null, 2),
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
case 'analyze_geographic_distribution': {
|
|
875
|
+
const filter = buildFilter(args);
|
|
876
|
+
const options = {
|
|
877
|
+
search: params.query,
|
|
878
|
+
filter,
|
|
879
|
+
groupBy: 'institutions.country_code',
|
|
880
|
+
};
|
|
881
|
+
const results = await openAlexClient.getWorks(options);
|
|
882
|
+
return {
|
|
883
|
+
content: [
|
|
884
|
+
{
|
|
885
|
+
type: 'text',
|
|
886
|
+
text: JSON.stringify(results, null, 2),
|
|
887
|
+
},
|
|
888
|
+
],
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
case 'get_entity': {
|
|
892
|
+
const entity = await openAlexClient.getEntity(params.entity_type, params.id);
|
|
893
|
+
return {
|
|
894
|
+
content: [
|
|
895
|
+
{
|
|
896
|
+
type: 'text',
|
|
897
|
+
text: JSON.stringify(entity, null, 2),
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
case 'search_sources': {
|
|
903
|
+
const filter = buildFilter(args);
|
|
904
|
+
const options = {
|
|
905
|
+
search: params.query,
|
|
906
|
+
filter,
|
|
907
|
+
perPage: params.per_page || 25,
|
|
908
|
+
};
|
|
909
|
+
const results = await openAlexClient.getSources(options);
|
|
910
|
+
return {
|
|
911
|
+
content: [
|
|
912
|
+
{
|
|
913
|
+
type: 'text',
|
|
914
|
+
text: JSON.stringify(results, null, 2),
|
|
915
|
+
},
|
|
916
|
+
],
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
default:
|
|
920
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
925
|
+
return {
|
|
926
|
+
content: [
|
|
927
|
+
{
|
|
928
|
+
type: 'text',
|
|
929
|
+
text: `Error: ${errorMessage}`,
|
|
930
|
+
},
|
|
931
|
+
],
|
|
932
|
+
isError: true,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
// Start server
|
|
937
|
+
async function main() {
|
|
938
|
+
const transport = new StdioServerTransport();
|
|
939
|
+
await server.connect(transport);
|
|
940
|
+
console.error('OpenAlex MCP Server running on stdio');
|
|
941
|
+
}
|
|
942
|
+
main().catch((error) => {
|
|
943
|
+
console.error('Fatal error:', error);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
});
|
|
946
|
+
//# sourceMappingURL=index.js.map
|