@cyanheads/openalex-mcp-server 0.7.13 → 0.7.15

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 (36) hide show
  1. package/AGENTS.md +25 -10
  2. package/CLAUDE.md +25 -10
  3. package/README.md +167 -88
  4. package/changelog/0.7.x/0.7.14.md +28 -0
  5. package/changelog/0.7.x/0.7.15.md +33 -0
  6. package/changelog/template.md +7 -7
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp-server/prompts/definitions/literature-review.prompt.js +1 -1
  10. package/dist/mcp-server/prompts/definitions/literature-review.prompt.js.map +1 -1
  11. package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts +19 -0
  12. package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/analyze-trends.tool.js +32 -3
  14. package/dist/mcp-server/tools/definitions/analyze-trends.tool.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts +12 -1
  16. package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/citation-graph.tool.js +55 -33
  18. package/dist/mcp-server/tools/definitions/citation-graph.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts +11 -0
  20. package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/resolve-name.tool.js +13 -1
  22. package/dist/mcp-server/tools/definitions/resolve-name.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts +40 -0
  24. package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/search-entities.tool.js +132 -20
  26. package/dist/mcp-server/tools/definitions/search-entities.tool.js.map +1 -1
  27. package/dist/services/openalex/openalex-service.d.ts +16 -0
  28. package/dist/services/openalex/openalex-service.d.ts.map +1 -1
  29. package/dist/services/openalex/openalex-service.js +105 -29
  30. package/dist/services/openalex/openalex-service.js.map +1 -1
  31. package/dist/services/openalex/types.d.ts +11 -0
  32. package/dist/services/openalex/types.d.ts.map +1 -1
  33. package/dist/services/openalex/types.js +7 -0
  34. package/dist/services/openalex/types.js.map +1 -1
  35. package/package.json +7 -7
  36. package/server.json +3 -3
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.7.13-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/openalex-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/openalex-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/openalex-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.7.15-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/openalex-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/openalex-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/openalex-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -27,105 +27,112 @@
27
27
 
28
28
  ---
29
29
 
30
- ## Tools
30
+ ## Overview
31
31
 
32
- Five tools for querying the [OpenAlex](https://openalex.org) academic research catalog:
32
+ Scholarly catalog data from [OpenAlex](https://openalex.org) — 270M+ works, 90M+ authors, 100K+ sources, plus institutions, topics, keywords, publishers, and funders. Search, filter, and aggregate across all eight entity types, resolve ambiguous names to canonical IDs, and walk the citation graph one hop at a time. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
33
33
 
34
- | Tool Name | Description |
35
- |:----------|:------------|
36
- | `openalex_search_entities` | Search, filter, sort, or retrieve by ID across all 8 entity types. |
37
- | `openalex_analyze_trends` | Group-by aggregation for trend and distribution analysis. |
38
- | `openalex_resolve_name` | Resolve a name or an identifier (DOI, ORCID, ROR, PMID, ISSN, OpenAlex ID) to an OpenAlex ID. |
39
- | `openalex_get_citation_graph` | Walk the citation graph one hop from a seed work: cites, cited_by, or related_to. |
40
- | `openalex_describe_fields` | List valid filter, group_by, and select field names for an entity type — call before building a query to avoid invalid-field errors. |
34
+ ### Tools
41
35
 
42
- ### `openalex_search_entities`
36
+ | Tool | Description |
37
+ |:---|:---|
38
+ | `openalex_search_entities` | Search, filter, sort, or retrieve by ID across all 8 entity types |
39
+ | `openalex_analyze_trends` | Group-by aggregation for trend and distribution analysis |
40
+ | `openalex_resolve_name` | Resolve a name or an identifier (DOI, ORCID, ROR, PMID, ISSN, OpenAlex ID) to an OpenAlex ID |
41
+ | `openalex_get_citation_graph` | Walk the citation graph one hop from a seed work: `cites`, `cited_by`, or `related_to` |
42
+ | `openalex_describe_fields` | List valid filter, group_by, and select field names for an entity type |
43
43
 
44
- Primary discovery and lookup tool. Covers all OpenAlex entity types (works, authors, sources, institutions, topics, keywords, publishers, funders).
44
+ ### Prompts
45
45
 
46
- - Retrieve a single entity by ID (OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN), bare or in URL form, with the scheme prefix accepted in any case. A PMCID is recognized as well, bare or as a PubMed Central URL, but OpenAlex indexes no PMCIDs, so it resolves nothing — pass the work's PMID or DOI instead. `id` takes precedence: search criteria passed alongside it are not applied, and the response says which ones were dropped rather than echoing them back as though they ran. The search-only validations (semantic page cap, `sample` with `cursor`, `seed` without `sample`) are skipped too — a lookup is never rejected over parameters it ignores
47
- - Keyword search with boolean operators, quoted phrases, wildcards, and fuzzy matching
48
- - Exact and AI semantic search modes
49
- - Rich filter syntax: AND across fields, OR within fields (`us|gb`), NOT (`!us`), ranges (`2020-2024`), comparisons (`>100`)
50
- - Sensible default field selection per entity type, applied to both searches and ID lookups — prevents oversized responses; pass `select` to choose fields, or `["*"]` for the full record
51
- - Invalid `select` field names produce an error listing the valid fields for that entity type
52
- - Formatted MCP output is a generic markdown renderer — every returned field is surfaced without per-entity-type hard-coding
53
- - Cursor pagination and up to 100 results per page; `sort` takes a single key or a comma-separated list, with the `-` descending prefix applied per key
54
- - `display_name` is nullable — OpenAlex holds no title for paratext and other untitled records, which pass through instead of failing the whole page
55
-
56
- ---
46
+ | Prompt | Description |
47
+ |:-------|:------------|
48
+ | `openalex_literature_review` | Guides a systematic literature search: formulate query, search, filter, analyze citation network, synthesize findings |
49
+ | `openalex_research_landscape` | Analyzes the research landscape for a topic: volume trends, top authors/institutions, open access rates, funding sources |
57
50
 
58
- ### `openalex_analyze_trends`
51
+ ## Capability reference
59
52
 
60
- Aggregate entities into groups and count them for trend, distribution, and comparative analysis.
53
+ ### `openalex_search_entities` <sub>tool</sub>
61
54
 
62
- - Group by any supported field (publication year, OA status, institution, country, topic, etc.)
63
- - Combine with filters to scope the population before aggregation
64
- - Up to 200 groups per page with cursor pagination
65
- - Supports `include_unknown` to show entities with no value for the grouped field
55
+ - Retrieve a single entity by ID — OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or PMCID (bare or URL form), or a keyword by its slug or keyword URL. `id` takes precedence: search parameters passed alongside it are dropped, and the response names which ones. A PMCID resolves nothing (OpenAlex indexes none) — use the work's PMID or DOI instead
56
+ - Keyword search (boolean operators, quoted phrases, wildcards, fuzzy match) plus `exact` and `semantic` search modes — semantic ranks at most 50 candidates at ~1 req/sec, and its `meta.count` reports that ceiling rather than a match total
57
+ - Rich filter syntax: AND across fields, OR within a field (`|`), NOT (`!`), ranges, comparisons; a comma inside a filter value is rejected (use `|`, or a `.search` filter for free text)
58
+ - `select` returns a curated per-entity-type default unless overridden, or `["*"]` for the full record; invalid field names error with the valid set
59
+ - Cursor pagination for keyword and exact search, up to 100 results per page (default 25); semantic search walks its candidates with `page` (1-based) instead, and mixing either knob with the wrong `search_mode` is rejected before the upstream call
60
+ - `sample` (up to 100, single page only — neither `cursor` nor `page` applies) plus a deterministic `seed` for reproducible random sampling
61
+ - `display_name` is nullable for untitled records; every call reports OpenAlex daily-budget cost and remaining balance
66
62
 
67
63
  ---
68
64
 
69
- ### `openalex_resolve_name`
65
+ ### `openalex_analyze_trends` <sub>tool</sub>
70
66
 
71
- The front door for turning anything you have into an OpenAlex ID. **Always use this before filtering by entity** — names are ambiguous, IDs are not.
67
+ - Group any supported field for trend, distribution, or comparative analysis; combine with `filters` to scope the population before aggregation
68
+ - Up to 200 groups per page (default). `order: "count"` (default) returns the top-N by count with no further pages; `order: "key"` enumerates all distinct values key-ascending with cursor pagination
69
+ - `include_unknown` (default `false`) adds a group for entities with no value for the grouped field
70
+ - Not every field is groupable — raw date fields, `.search` operators, and `from_*`/`to_*` range modifiers are rejected; check with `openalex_describe_fields(entity_type, "group_by")`
71
+ - Reports OpenAlex daily-budget cost and remaining balance — aggregation is priced far below paging the same entities
72
72
 
73
- - A name or partial name runs an autocomplete search: up to 10 matches with disambiguation hints, ~200ms
74
- - An identifier resolves deterministically to the single record it addresses — OpenAlex ID, DOI, ORCID, ROR, PMID, or ISSN, bare or in URL form, with the scheme prefix accepted in any case. No `entity_type` needed: the identifier determines its own
75
- - A PMCID is recognized, bare or as a PubMed Central URL, but OpenAlex indexes no PMCIDs, so it resolves nothing — the empty result says so and points at converting it to a PMID or DOI
76
- - An identifier that matches nothing returns an empty result naming the scheme, not name-search advice
77
- - Optional entity type filter and field-level filters, applied to name queries
73
+ ---
74
+
75
+ ### `openalex_resolve_name` <sub>tool</sub>
76
+
77
+ - A name or partial name runs an autocomplete search: up to 10 matches with disambiguation hints (last institution, host organization, place, etc.)
78
+ - An identifier — OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or keyword URL, bare or in URL form — resolves directly to the one record it addresses; no `entity_type` needed, since the identifier determines its own. A PMCID is recognized but resolves nothing — OpenAlex indexes none
79
+ - `filters` narrows autocomplete only; on an identifier lookup they're ignored and named in a notice
80
+ - Reports OpenAlex daily-budget cost and remaining balance
78
81
 
79
82
  ---
80
83
 
81
- ### `openalex_get_citation_graph`
84
+ ### `openalex_get_citation_graph` <sub>tool</sub>
82
85
 
83
- One-hop citation graph traversal from a seed work. Wraps the OpenAlex `cites`/`cited_by`/`related_to` filters behind an explicit `direction` argument so callers do not have to know the filter names.
86
+ - `direction` sets the edge: `cites` (works citing the seed), `cited_by` (the seed's own reference list), `related_to` (OpenAlex's algorithmic related works, ~8-30 typical, may be empty)
87
+ - `seed_id` accepts an OpenAlex ID, DOI, or PMID (PMCID recognized but resolves nothing); validated against a live lookup first, so a non-existent seed fails as `NotFound` rather than returning an empty graph
88
+ - Stacks with `filters`/`sort`/`select` to narrow the graph; `filters` cannot set `cites`/`cited_by`/`related_to`, nor an alias of one such as `cited_works` — those keys are reserved for `direction`
89
+ - Cursor pagination, up to 100 results per page (default 25)
90
+ - Reports OpenAlex daily-budget cost, covering both the seed-validation lookup and the graph page, plus remaining balance
84
91
 
85
- - `cites`: works that cite the seed (incoming citations)
86
- - `cited_by`: works the seed cites (its reference list)
87
- - `related_to`: OpenAlex algorithmic "related works" (~8-30 typical, may be empty for less-cited seeds)
88
- - Accepts OpenAlex IDs, DOIs, and PMIDs as `seed_id`, bare or in URL form; validates the seed via a singleton `/works/{id}` lookup before walking, so non-existent seeds surface as `NotFound`. A PMCID seed is recognized but OpenAlex indexes no PMCIDs, so it resolves nothing — the `NotFound` says so and points at converting it to a PMID or DOI
89
- - Stacks with `filters`/`sort`/`select` to narrow the graph (e.g., `publication_year=">2020"`, `is_oa="true"`)
92
+ ---
93
+
94
+ ### `openalex_describe_fields` <sub>tool</sub>
95
+
96
+ - Lists every valid field name for an entity type + context (`filter`, `group_by`, `select`) — the complete pool, never truncated
97
+ - `group_by` is the filter set minus raw date fields, `.search`/`.search.exact` operators, and `from_*`/`to_*` range modifiers, which OpenAlex rejects as aggregation keys
98
+ - Optional `query` reorders results by name similarity without dropping any field — a nested value's parent object stays reachable further down the list
99
+ - Backed by a generated field catalog — no live API calls
90
100
 
91
101
  ---
92
102
 
93
- ### `openalex_describe_fields`
103
+ ### `openalex_literature_review` <sub>prompt</sub>
94
104
 
95
- Discover valid field names before constructing a query — avoids invalid-field 400 errors. Backed by a catalog generated from OpenAlex's own field validation.
105
+ - Arguments: `topic` required; `scope` (`narrow` / `broad`) optional, defaults to `narrow`
106
+ - Returns one user message walking a 6-step workflow: resolve entities, search literature, identify key papers, trace citations, analyze the landscape, synthesize findings
107
+ - `scope` changes the search step: `narrow` favors exact search with tight topic filters; `broad` adds semantic search across multiple related topic IDs
96
108
 
97
- - List valid fields for any entity type and context (`filter`, `group_by`, or `select`)
98
- - `group_by` returns the subset of the `filter` set OpenAlex can aggregate — raw date fields, `*.search` operators, and `from_*`/`to_*` range modifiers are excluded
99
- - Pass `query` (a partial or guessed name) to rank results by name similarity — surfaces the right field when you only know roughly what you want. Ranking reorders the list without shortening it, so a nested value's parent object is still reachable further down
100
- - Complements the ranked "did you mean" suggestions now appended to invalid-field errors on the search, trends, and citation-graph tools
109
+ ---
101
110
 
102
- ## Prompts
111
+ ### `openalex_research_landscape` <sub>prompt</sub>
103
112
 
104
- | Prompt | Description |
105
- |:-------|:------------|
106
- | `openalex_literature_review` | Guides a systematic literature search: formulate query, search, filter, analyze citation network, synthesize findings. |
107
- | `openalex_research_landscape` | Analyzes the research landscape for a topic: volume trends, top authors/institutions, open access rates, funding sources. |
113
+ - Arguments: `topic` required
114
+ - Returns one user message walking a 7-step quantitative workflow: resolve the topic ID, volume trends, top contributors (institutions/countries/journals), open access rate, funding sources, most-cited works, emerging fronts
115
+ - The funding step groups by `awards.funder_id` (resolve names via `openalex_resolve_name`) or `awards.funder_display_name` for readable labels in a single hop
108
116
 
109
117
  ## Features
110
118
 
111
- Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core):
112
-
113
- - Declarative tool definitions — single file per tool, framework handles registration and validation
114
- - Unified error handling across all tools
115
- - Pluggable auth (`none`, `jwt`, `oauth`)
116
- - Swappable storage backends via the framework (not currently used by this server)
117
- - Structured logging with optional OpenTelemetry tracing
118
- - Runs locally (stdio/HTTP) or in Docker from the same codebase
119
+ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
119
120
 
120
121
  OpenAlex-specific:
121
122
 
122
- - Typed API client with automatic ID normalization (DOI, ORCID, ROR, PMID, PMCID, ISSN, OpenAlex and PubMed/PubMed Central URLs). A PMCID normalizes but resolves nothing — OpenAlex indexes none — so use the work's PMID or DOI
123
- - Abstract reconstruction from inverted indices — plaintext instead of OpenAlex's position-keyed encoding
124
- - HTTP status codes mapped to specific MCP error classes (400 → InvalidParams, 422 → ValidationError, 429 → RateLimited, etc.) with upstream messages surfaced
125
- - Every API-calling tool reports what the call spent against the OpenAlex daily budget and what is left of it, so a paginated sweep can be priced before it runs instead of ending in a 429. An account holding prepaid balance sees that too, since it keeps serving once the day's allowance is gone
123
+ - Typed API client with automatic ID normalization (DOI, ORCID, ROR, PMID, PMCID, ISSN, OpenAlex and PubMed/PubMed Central URLs); a PMCID normalizes but resolves nothing since OpenAlex indexes none
124
+ - Keyless by default — an optional API key raises rate and daily-budget limits, and an optional `mailto` identifies the caller to OpenAlex's polite pool
125
+ - HTTP status codes mapped to specific MCP error classes (400 → InvalidParams, 422 → ValidationError, 429 → RateLimited) with upstream messages surfaced
126
126
  - Timeout-aware request retries and cancellation support via `AbortSignal`
127
127
 
128
- ## Getting Started
128
+ Agent-friendly output:
129
+
130
+ - Provenance — every API-calling tool reports OpenAlex daily-budget cost, remaining balance, and reset time (`budget.costUsd`, `remainingUsd`, `resetsInSeconds`)
131
+ - Effective-query echo — search, trends, and citation-graph responses echo the criteria that actually ran, so an empty result is diagnosable without re-reading the request
132
+ - Discriminated output contracts — typed error reasons (`entity_not_found`, `upstream_budget_exhausted`, `semantic_per_page_cap`, `reserved_filter_key`, and more) each carrying an explicit recovery hint
133
+ - Response shaping — abstracts are reconstructed from OpenAlex's inverted-index encoding into plaintext, and `display_name` stays `null` for untitled or paratext records instead of being backfilled
134
+
135
+ ## Getting started
129
136
 
130
137
  ### Public Hosted Instance
131
138
 
@@ -144,7 +151,26 @@ A public instance is available at `https://openalex.caseyjhand.com/mcp` — no i
144
151
 
145
152
  ### Self-Hosted / Local
146
153
 
147
- Add to your MCP client config (e.g., `claude_desktop_config.json`):
154
+ Add the following to your MCP client configuration file.
155
+
156
+ ```json
157
+ {
158
+ "mcpServers": {
159
+ "openalex-mcp-server": {
160
+ "type": "stdio",
161
+ "command": "bunx",
162
+ "args": ["@cyanheads/openalex-mcp-server@latest"],
163
+ "env": {
164
+ "MCP_TRANSPORT_TYPE": "stdio",
165
+ "MCP_LOG_LEVEL": "info",
166
+ "OPENALEX_API_KEY": "your-api-key"
167
+ }
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ Or with npx (no Bun required):
148
174
 
149
175
  ```json
150
176
  {
@@ -152,94 +178,147 @@ Add to your MCP client config (e.g., `claude_desktop_config.json`):
152
178
  "openalex-mcp-server": {
153
179
  "type": "stdio",
154
180
  "command": "npx",
155
- "args": ["-y", "@cyanheads/openalex-mcp-server"],
181
+ "args": ["-y", "@cyanheads/openalex-mcp-server@latest"],
156
182
  "env": {
157
- "OPENALEX_API_KEY": "your-openalex-api-key"
183
+ "MCP_TRANSPORT_TYPE": "stdio",
184
+ "MCP_LOG_LEVEL": "info",
185
+ "OPENALEX_API_KEY": "your-api-key"
158
186
  }
159
187
  }
160
188
  }
161
189
  }
162
190
  ```
163
191
 
192
+ Or with Docker:
193
+
194
+ ```json
195
+ {
196
+ "mcpServers": {
197
+ "openalex-mcp-server": {
198
+ "type": "stdio",
199
+ "command": "docker",
200
+ "args": [
201
+ "run", "-i", "--rm",
202
+ "-e", "MCP_TRANSPORT_TYPE=stdio",
203
+ "-e", "OPENALEX_API_KEY=your-api-key",
204
+ "ghcr.io/cyanheads/openalex-mcp-server:latest"
205
+ ]
206
+ }
207
+ }
208
+ }
209
+ ```
210
+
211
+ For Streamable HTTP, set the transport and start the server:
212
+
213
+ ```sh
214
+ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 OPENALEX_API_KEY=... bun run start:http
215
+ # Server listens at http://localhost:3010/mcp
216
+ ```
217
+
164
218
  `OPENALEX_API_KEY` is optional — set it to a free [OpenAlex account key](https://openalex.org/settings/api) for keyed rate limits and budget under OpenAlex's usage-based pricing, or omit it for anonymous access. Set `OPENALEX_MAILTO` to an email if you want to identify yourself to OpenAlex (the [polite pool](https://developers.openalex.org/guides/authentication)).
165
219
 
166
220
  ### Prerequisites
167
221
 
168
- - [Bun v1.4.0](https://bun.sh/) or higher (for development)
222
+ - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
223
+ - Optional: an [OpenAlex account API key](https://openalex.org/settings/api) for keyed rate limits and budget — omit for anonymous access.
169
224
 
170
225
  ### Installation
171
226
 
172
227
  1. **Clone the repository:**
228
+
173
229
  ```sh
174
230
  git clone https://github.com/cyanheads/openalex-mcp-server.git
175
231
  ```
176
232
 
177
233
  2. **Navigate into the directory:**
234
+
178
235
  ```sh
179
236
  cd openalex-mcp-server
180
237
  ```
181
238
 
182
239
  3. **Install dependencies:**
240
+
183
241
  ```sh
184
242
  bun install
185
243
  ```
186
244
 
245
+ 4. **Configure environment:**
246
+
247
+ ```sh
248
+ cp .env.example .env
249
+ # edit .env and set required vars
250
+ ```
251
+
187
252
  ## Configuration
188
253
 
189
254
  | Variable | Description | Default |
190
255
  |:---------|:------------|:--------|
191
- | `OPENALEX_API_KEY` | **Optional.** OpenAlex account API key, sent upstream as `api_key=` (free from [openalex.org/settings/api](https://openalex.org/settings/api)). Without it, anonymous rate limits apply. | — |
192
- | `OPENALEX_MAILTO` | **Optional.** Email sent upstream as `mailto=` to identify yourself to OpenAlex (the [polite pool](https://developers.openalex.org/guides/authentication)). A courtesy identifier, separate from the API key. | — |
193
- | `OPENALEX_BASE_URL` | OpenAlex API base URL. | `https://api.openalex.org` |
194
256
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
195
257
  | `MCP_HTTP_PORT` | Port for HTTP server. | `3010` |
258
+ | `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto` (resolves to `stateful`). The server declares `stateless` in code; an explicit value overrides it. | `stateless` |
196
259
  | `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
197
- | `MCP_SESSION_MODE` | HTTP session handling: `auto`, `stateful`, or `stateless`. This server ships `stateless` — set by `Dockerfile` and by `.env.example`. | `auto` (resolves to `stateful`) |
198
260
  | `MCP_ALLOWED_ORIGINS` | Comma-separated allow-list of browser `Origin` headers for HTTP transport. Unset = loopback-only; set to `*` to disable. | _loopback only_ |
199
261
  | `MCP_LOG_LEVEL` | Log level (RFC 5424). | `debug` |
200
262
  | `LOGS_DIR` | Directory for log files (Node.js only). | `<project-root>/logs` |
263
+ | `STORAGE_PROVIDER_TYPE` | Storage backend. | `in-memory` |
264
+ | `OPENALEX_API_KEY` | OpenAlex account API key, sent upstream as `api_key=` (free from [openalex.org/settings/api](https://openalex.org/settings/api)). Without it, anonymous rate limits apply. | — |
265
+ | `OPENALEX_MAILTO` | Email sent upstream as `mailto=` to identify yourself to OpenAlex (the "polite pool"); a courtesy identifier, separate from the API key. | — |
266
+ | `OPENALEX_BASE_URL` | OpenAlex API base URL. | `https://api.openalex.org` |
201
267
  | `OTEL_ENABLED` | Enable [OpenTelemetry instrumentation](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) (spans, metrics, completion logs). | `false` |
202
268
 
203
- ## Running the Server
269
+ See [`.env.example`](./.env.example) for the full list of optional overrides.
270
+
271
+ ## Running the server
272
+
273
+ ### Local development
204
274
 
205
- ### Local Development
275
+ - **Build and run:**
206
276
 
207
- - **Build and run the production version:**
208
277
  ```sh
209
- bun run build
210
- bun run start:http # or start:stdio
278
+ # One-time build
279
+ bun run rebuild
280
+
281
+ # Run the built server
282
+ bun run start:stdio
283
+ # or
284
+ bun run start:http
211
285
  ```
212
286
 
213
287
  - **Run checks and tests:**
288
+
214
289
  ```sh
215
- bun run devcheck # Lints, formats, type-checks
216
- bun run test # Runs test suite
290
+ bun run devcheck # Lints, formats, type-checks
291
+ bun run test # Runs the test suite
217
292
  ```
218
293
 
219
294
  ### Docker
220
295
 
221
296
  ```sh
222
297
  docker build -t openalex-mcp-server .
223
- docker run -e OPENALEX_API_KEY=your-key -p 3010:3010 openalex-mcp-server
298
+ docker run --rm -e OPENALEX_API_KEY=your-key -p 3010:3010 openalex-mcp-server
224
299
  ```
225
300
 
226
- ## Project Structure
301
+ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/openalex-mcp-server`. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
302
+
303
+ ## Project structure
227
304
 
228
305
  | Directory | Purpose |
229
306
  |:----------|:--------|
307
+ | `src/index.ts` | `createApp()` entry point — registers tools and prompts. |
308
+ | `src/config/` | Server-specific environment variable parsing and validation with Zod. |
230
309
  | `src/mcp-server/tools/definitions/` | Tool definitions (`*.tool.ts`). |
231
310
  | `src/mcp-server/prompts/definitions/` | Prompt definitions (`*.prompt.ts`). |
232
- | `src/services/openalex/` | OpenAlex API client service and domain types. |
233
- | `src/config/` | Environment variable parsing and validation with Zod. |
311
+ | `src/services/openalex/` | OpenAlex API client, field catalog, and domain types. |
234
312
  | `tests/` | Unit and integration tests, mirroring the `src/` structure. |
235
313
 
236
- ## Development Guide
314
+ ## Development guide
237
315
 
238
316
  See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
239
317
 
240
318
  - Handlers throw, framework catches — no `try/catch` in tool logic
241
319
  - Use `ctx.log` for logging, `ctx.state` for storage
242
- - Always resolve names to IDs via `openalex_resolve_name` before using them in filters
320
+ - Wrap OpenAlex responses: validate the raw payload → normalize to a domain type → return the output schema; never fabricate missing fields
321
+ - Always resolve names to IDs via `openalex_resolve_name` before filtering by entity
243
322
 
244
323
  ## Contributing
245
324
 
@@ -0,0 +1,28 @@
1
+ ---
2
+ summary: "Input aliases for filter/search/name parameters, mcp-ts-core 0.13.x error-text and validation upgrades, and a code-declared stateless session default."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.7.14 — 2026-09-19
8
+
9
+ ## Added
10
+
11
+ - **Input aliases** — `filter` → `filters` on `openalex_search_entities`, `openalex_resolve_name`, `openalex_analyze_trends`, and `openalex_get_citation_graph`; `search` → `query` on `openalex_search_entities`; `name` → `query` on `openalex_resolve_name`. Rewritten only when the declared key is absent and not advertised in `inputSchema`.
12
+
13
+ ## Changed
14
+
15
+ - **Session mode** — the server now declares `stateless` as its HTTP session-mode default in code; an explicitly set `MCP_SESSION_MODE` still overrides it. The Docker image and `.env.example` already pinned `stateless`.
16
+ - **Tool error `content[]` text** now ends with `(reason <reason> · not retryable)` (or `· retryable`) whenever the error carries a `data.reason` or a boolean `data.retryable`. The `Recovery:` line is dropped when the message already contains the hint word for word. `structuredContent` is unchanged.
17
+ - **Argument-validation rejections** carry `data.reason: "invalid_arguments"` and a `Recovery:` hint derived from the schema, and an omitted required field reads as missing rather than as a wrong choice.
18
+ - **Tool arguments now pass a pre-validation step** before schema validation: undeclared root keys a client adds are dropped, key spellings differing only by case/`-`/`_` are rewritten, and a JSON-stringified array is repaired once after a failed parse.
19
+ - Framework skills, scripts, issue templates, and CI workflow synced with mcp-ts-core 0.13.1–0.13.5 (adds `.github/workflows/codeql.yml`, a README version-badge packaging check, and `config/`-aware build tsconfig probing); `withRetry`'s per-attempt signal now reaches `fetch`, and two handlers' precondition checks moved into module-level helpers.
20
+
21
+ ## Dependencies
22
+
23
+ - `@cyanheads/mcp-ts-core` ^0.13.0 → ^0.13.5
24
+ - `zod` ^4.6.1 → ^4.6.5
25
+ - `@biomejs/biome` ^2.5.13 → ^2.5.14
26
+ - `@types/node` ^26.5.1 → ^26.6.1
27
+ - `tsc-alias` ^1.9.4 → ^1.9.5
28
+ - `vitest` ^5.0.0 → ^5.0.1
@@ -0,0 +1,33 @@
1
+ ---
2
+ summary: "Keyword identifiers, semantic-mode `page` pagination, AND'd filter aliases, and honest pagination/notice handling across the paginating tools."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.7.15 — 2026-09-22
8
+
9
+ ## Added
10
+
11
+ - **Keyword identifiers** resolve via their OpenAlex URL or bare slug (`https://openalex.org/keywords/groundwater` or `groundwater`) — keywords never had a native alphanumeric ID, so the removed `K`-prefixed routing arm 404'd. ([#68](https://github.com/cyanheads/openalex-mcp-server/issues/68))
12
+ - **`page`** input on `openalex_search_entities` paginates semantic search (1-based), the one mode OpenAlex rejects `cursor` on. Combining `page`/`cursor`/`sample` with the wrong mode now throws `semantic_with_cursor`, `page_without_semantic`, or `sample_with_page`. ([#71](https://github.com/cyanheads/openalex-mcp-server/issues/71))
13
+ - **`best_oa_location`** joins the default `works` projection, naming a green-OA work's readable copy (`pdf_url`, `license`, `version`, hosting source) instead of leaving only the closed publisher location. ([#79](https://github.com/cyanheads/openalex-mcp-server/issues/79))
14
+ - **`query_too_long`** error reason on `openalex_search_entities` — an over-length `query` (HTTP 400) now maps to its own contract entry instead of the generic invalid-params one. ([#71](https://github.com/cyanheads/openalex-mcp-server/issues/71))
15
+
16
+ ## Changed
17
+
18
+ - **Filter aliases** resolving to the same upstream key are now AND'd instead of silently overwriting — `{year: "2020", publication_year: "2024"}` now matches works in both years, matching OpenAlex's own repeated-key behavior. ([#70](https://github.com/cyanheads/openalex-mcp-server/issues/70))
19
+ - **`openalex_get_citation_graph`'s reserved-key guard** now checks the *translated* filter key, so an alias of `cites`/`cited_by`/`related_to` (e.g. `cited_works`) is caught instead of colliding silently with `direction`. ([#70](https://github.com/cyanheads/openalex-mcp-server/issues/70))
20
+ - **Pagination-exhausted notices** — `openalex_search_entities`, `openalex_analyze_trends`, and `openalex_get_citation_graph` now report the traversal as finished on an empty page reached via `cursor`, instead of advising a broader query. ([#74](https://github.com/cyanheads/openalex-mcp-server/issues/74))
21
+ - **`meta.per_page`** on `openalex_search_entities` and `openalex_get_citation_graph` now documents itself as the requested page size, not the record count returned. ([#74](https://github.com/cyanheads/openalex-mcp-server/issues/74))
22
+ - **`meta.count`** under `search_mode: "semantic"` is disclosed as the capped candidate ceiling (≤50), not an exhaustive match total. ([#71](https://github.com/cyanheads/openalex-mcp-server/issues/71))
23
+
24
+ ## Fixed
25
+
26
+ - **Blank `id`/`query`/`cursor`/`group_by` inputs** on the paginating tools are now rejected at the schema, instead of running as an unfiltered listing or an accepted "exhausted" page. ([#69](https://github.com/cyanheads/openalex-mcp-server/issues/69), [#80](https://github.com/cyanheads/openalex-mcp-server/issues/80))
27
+ - **`openalex_analyze_trends`** no longer reports a successful empty aggregation when OpenAlex answers with its plain list shape (no `group_by` key) — it now throws `upstream_missing_group_by`. ([#69](https://github.com/cyanheads/openalex-mcp-server/issues/69))
28
+ - **`openalex_literature_review`** prompt's step 3 now re-runs the step-2 filtered search sorted by `-cited_by_count` instead of an unfiltered search, and names the fields to `select` on the detail lookup. ([#77](https://github.com/cyanheads/openalex-mcp-server/issues/77))
29
+
30
+ ## Dependencies
31
+
32
+ - `@cyanheads/mcp-ts-core` ^0.13.5 → ^0.13.6
33
+ - `@types/node` ^26.6.1 → ^26.6.2
@@ -6,8 +6,8 @@
6
6
 
7
7
  # Required. One-line GitHub Release-style headline. 350 character cap — a
8
8
  # ceiling, not a target. Default short and scannable. Don't pad, don't stitch
9
- # unrelated changes with commas/semicolons into an inventory — pick the
10
- # headline, like a tag's theme line. Quotes required: unquoted YAML treats
9
+ # unrelated changes with commas/semicolons into an inventory — pick the one
10
+ # headline the release is about. Quotes required: unquoted YAML treats
11
11
  # `: ` inside the value as a key separator and fails GitHub's strict parser.
12
12
  summary: ""
13
13
 
@@ -119,11 +119,11 @@ security: false
119
119
  TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
120
120
  via `gh release create --notes-from-tag`. It is a condensed digest of this
121
121
  entry, never a copy, and its format is owned by the `release-and-publish`
122
- skill (step 4, "Create the annotated tag"): the entry's `summary:` as the
123
- theme line without the version, flat headline bullets — no Keep-a-Changelog
124
- section headers, no gates line — at most one deps line, issue backlinks,
125
- and the changelog link last. In release-PR mode the `git-wrapup` skill
126
- authors that digest as the PR body's `## Changes` and the tag copies it.
122
+ skill (step 4, "Create the annotated tag"): a short subject line without the
123
+ version, flat headline bullets — no Keep-a-Changelog section headers, no
124
+ gates line — at most one deps line, issue backlinks, and the changelog link
125
+ last. In release-PR mode the `git-wrapup` skill authors those bullets as the
126
+ PR body's `## Changes` and the tag copies them.
127
127
  -->
128
128
 
129
129
  ## Added
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ import { initOpenAlexService } from './services/openalex/openalex-service.js';
15
15
  await createApp({
16
16
  name: 'openalex-mcp-server',
17
17
  title: 'openalex-mcp-server',
18
+ // No tool gates on ctx.requestInput, so every call completes in a single round trip.
19
+ sessionMode: 'stateless',
18
20
  tools: [
19
21
  resolveNameTool,
20
22
  searchEntitiesTool,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,8DAA8D,CAAC;AACtG,OAAO,EAAE,uBAAuB,EAAE,MAAM,+DAA+D,CAAC;AACxG,OAAO,EAAE,iBAAiB,EAAE,MAAM,uDAAuD,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,uDAAuD,CAAC;AAC7F,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAC5F,OAAO,EAAE,eAAe,EAAE,MAAM,qDAAqD,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAC5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAE9E,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,qBAAqB;IAC3B,KAAK,EAAE,qBAAqB;IAC5B,KAAK,EAAE;QACL,eAAe;QACf,kBAAkB;QAClB,iBAAiB;QACjB,oBAAoB;QACpB,kBAAkB;KACnB;IACD,OAAO,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IAC1D,YAAY,EACV,iUAAiU;IACnU,OAAO,EAAE;QACP,OAAO,EAAE,yEAAyE;QAClF,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,kDAAkD;QAC5D,UAAU,EAAE;YACV,gBAAgB,EAAE,uBAAuB;SAC1C;QACD,KAAK,EAAE;YACL,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,sBAAsB,EAAE;YACnD,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,iCAAiC,EAAE;SAC/D;KACF;IACD,KAAK;QACH,mBAAmB,EAAE,CAAC;IACxB,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,8DAA8D,CAAC;AACtG,OAAO,EAAE,uBAAuB,EAAE,MAAM,+DAA+D,CAAC;AACxG,OAAO,EAAE,iBAAiB,EAAE,MAAM,uDAAuD,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,uDAAuD,CAAC;AAC7F,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAC5F,OAAO,EAAE,eAAe,EAAE,MAAM,qDAAqD,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAC5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAE9E,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,qBAAqB;IAC3B,KAAK,EAAE,qBAAqB;IAC5B,qFAAqF;IACrF,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE;QACL,eAAe;QACf,kBAAkB;QAClB,iBAAiB;QACjB,oBAAoB;QACpB,kBAAkB;KACnB;IACD,OAAO,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IAC1D,YAAY,EACV,iUAAiU;IACnU,OAAO,EAAE;QACP,OAAO,EAAE,yEAAyE;QAClF,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,kDAAkD;QAC5D,UAAU,EAAE;YACV,gBAAgB,EAAE,uBAAuB;SAC1C;QACD,KAAK,EAAE;YACL,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,sBAAsB,EAAE;YACnD,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,iCAAiC,EAAE;SAC/D;KACF;IACD,KAAK;QACH,mBAAmB,EAAE,CAAC;IACxB,CAAC;CACF,CAAC,CAAC"}
@@ -41,7 +41,7 @@ Follow this workflow using the OpenAlex tools:
41
41
  - Search with multiple related topic IDs to capture adjacent areas`}
42
42
  Use select to keep payloads manageable.
43
43
 
44
- 3. **Identify key papers** — Sort by cited_by_count to find landmark works. Use openalex_search_entities with id to get full details on the most important ones.
44
+ 3. **Identify key papers** — Re-run the step 2 search with sort: "-cited_by_count" to surface landmark works, keeping the topical filters in place: an explicit sort replaces relevance ranking, so without them you get the most-cited works in the catalog rather than in "${args.topic}". Then call openalex_search_entities with id and a select naming the fields you need (e.g. ["abstract", "authorships", "primary_location"]) for the most important ones — an id lookup on its own returns the curated default projection.
45
45
 
46
46
  4. **Trace citations** — For each key paper, use openalex_get_citation_graph with direction "cites" to find subsequent work that cites it, and direction "cited_by" to find foundational work it references.
47
47
 
@@ -1 +1 @@
1
- {"version":3,"file":"literature-review.prompt.js","sourceRoot":"","sources":["../../../../src/mcp-server/prompts/definitions/literature-review.prompt.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEnD,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,4BAA4B,EAAE;IACzE,WAAW,EACT,wHAAwH;IAC1H,SAAS,EACP,2HAA2H;IAC7H,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,KAAK,EAAE,CAAC;aACL,MAAM,EAAE;aACR,QAAQ,CACP,+FAA+F,CAChG;QACH,KAAK,EAAE,CAAC;aACL,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;aACzB,QAAQ,EAAE;aACV,QAAQ,CACP,wGAAwG,CACzG;KACJ,CAAC;IACF,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;QACjB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC;QACrC,OAAO;YACL;gBACE,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;oBACP,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,+CAA+C,IAAI,CAAC,KAAK;;SAEhE,KAAK;;;;mHAIqG,IAAI,CAAC,KAAK;;iFAGjH,KAAK,KAAK,QAAQ;wBAChB,CAAC,CAAC;;;qCAGqB;wBACvB,CAAC,CAAC;;;sEAIN;;;;;;;;;;;;+IAYqI;iBACtI;aACF;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"literature-review.prompt.js","sourceRoot":"","sources":["../../../../src/mcp-server/prompts/definitions/literature-review.prompt.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEnD,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,4BAA4B,EAAE;IACzE,WAAW,EACT,wHAAwH;IAC1H,SAAS,EACP,2HAA2H;IAC7H,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,KAAK,EAAE,CAAC;aACL,MAAM,EAAE;aACR,QAAQ,CACP,+FAA+F,CAChG;QACH,KAAK,EAAE,CAAC;aACL,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;aACzB,QAAQ,EAAE;aACV,QAAQ,CACP,wGAAwG,CACzG;KACJ,CAAC;IACF,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;QACjB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC;QACrC,OAAO;YACL;gBACE,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;oBACP,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,+CAA+C,IAAI,CAAC,KAAK;;SAEhE,KAAK;;;;mHAIqG,IAAI,CAAC,KAAK;;iFAGjH,KAAK,KAAK,QAAQ;wBAChB,CAAC,CAAC;;;qCAGqB;wBACvB,CAAC,CAAC;;;sEAIN;;;+QAGqQ,IAAI,CAAC,KAAK;;;;;;;;;+IAS1I;iBACtI;aACF;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -41,64 +41,83 @@ export declare const analyzeTrendsTool: import("@cyanheads/mcp-ts-core").ToolDef
41
41
  readonly when: "OpenAlex throttled the request for exceeding its per-second ceiling (HTTP 429).";
42
42
  readonly retryable: true;
43
43
  readonly recovery: "Wait several seconds and retry; consider lowering request frequency for this caller.";
44
+ readonly thrownBy: "service";
44
45
  }, {
45
46
  readonly reason: "upstream_budget_exhausted";
46
47
  readonly code: JsonRpcErrorCode.RateLimited;
47
48
  readonly when: "The OpenAlex daily usage budget is spent (HTTP 429).";
48
49
  readonly retryable: false;
49
50
  readonly recovery: "The daily budget refills at midnight UTC — retrying sooner will not succeed. Set OPENALEX_API_KEY to a free key (https://openalex.org/settings/api) for a larger daily budget than anonymous access, or wait for the reset.";
51
+ readonly thrownBy: "service";
50
52
  }, {
51
53
  readonly reason: "upstream_timeout";
52
54
  readonly code: JsonRpcErrorCode.Timeout;
53
55
  readonly when: "OpenAlex did not respond within the request deadline.";
54
56
  readonly retryable: true;
55
57
  readonly recovery: "Retry after a short delay; if timeouts persist, narrow the request with tighter filters to reduce upstream load.";
58
+ readonly thrownBy: "service";
56
59
  }, {
57
60
  readonly reason: "upstream_unavailable";
58
61
  readonly code: JsonRpcErrorCode.ServiceUnavailable;
59
62
  readonly when: "OpenAlex was unreachable or unusable — HTTP 503, a connection failure, or a body that was empty, HTML, or unparseable JSON.";
60
63
  readonly retryable: true;
61
64
  readonly recovery: "Wait and retry; check https://openalex.org for service status if the outage persists.";
65
+ readonly thrownBy: "service";
62
66
  }, {
63
67
  readonly reason: "upstream_unauthorized";
64
68
  readonly code: JsonRpcErrorCode.Unauthorized;
65
69
  readonly when: "OpenAlex rejected the API key (HTTP 401).";
66
70
  readonly recovery: "Check that OPENALEX_API_KEY is set to a valid OpenAlex account API key (free from https://openalex.org/settings/api).";
71
+ readonly thrownBy: "service";
67
72
  }, {
68
73
  readonly reason: "upstream_forbidden";
69
74
  readonly code: JsonRpcErrorCode.Forbidden;
70
75
  readonly when: "OpenAlex denied access to the requested resource (HTTP 403).";
71
76
  readonly recovery: "Confirm the API key has access to this entity type or endpoint, then retry the request.";
77
+ readonly thrownBy: "service";
72
78
  }, {
73
79
  readonly reason: "comma_in_filter_value";
74
80
  readonly code: JsonRpcErrorCode.InvalidParams;
75
81
  readonly when: "A filter value contains a comma, which collides with the OpenAlex filter separator.";
76
82
  readonly recovery: "Use `|` for OR within a filter value (e.g. \"2020|2021\"), or use a `.search` filter or the `query` parameter for free-text phrases that contain commas.";
83
+ readonly thrownBy: "service";
77
84
  }, {
78
85
  readonly reason: "upstream_invalid_params";
79
86
  readonly code: JsonRpcErrorCode.InvalidParams;
80
87
  readonly when: "OpenAlex rejected an invalid group_by or filter field name (HTTP 400).";
81
88
  readonly recovery: "The upstream message names the rejected key and suggests close matches. Use openalex_describe_fields(entity_type, \"group_by\") to browse valid group_by fields, or openalex_describe_fields(entity_type, \"filter\") for filter fields.";
89
+ readonly thrownBy: "service";
82
90
  }, {
83
91
  readonly reason: "upstream_invalid_id_value";
84
92
  readonly code: JsonRpcErrorCode.InvalidParams;
85
93
  readonly when: "An entity-ID filter received a value that is not an OpenAlex ID — usually a name (HTTP 400).";
86
94
  readonly recovery: "Call openalex_resolve_name to turn the name into an OpenAlex ID, then filter by that ID. Entity filters such as authorships.author.id and primary_topic.id accept IDs only.";
95
+ readonly thrownBy: "service";
87
96
  }, {
88
97
  readonly reason: "upstream_ungroupable_group_by";
89
98
  readonly code: JsonRpcErrorCode.InvalidParams;
90
99
  readonly when: "group_by targets a raw date, float, or *.search field OpenAlex cannot aggregate (HTTP 400).";
91
100
  readonly recovery: "Group by a categorical or year field (e.g. publication_year, type, oa_status, or an integer count field) — raw date fields and *.search operators cannot be grouped. Call openalex_describe_fields(entity_type, \"group_by\") for the groupable set.";
101
+ readonly thrownBy: "service";
92
102
  }, {
93
103
  readonly reason: "upstream_invalid_params_other";
94
104
  readonly code: JsonRpcErrorCode.InvalidParams;
95
105
  readonly when: "OpenAlex rejected the request (HTTP 400) for a reason other than an invalid field name.";
96
106
  readonly recovery: "Read the upstream message in the error above and adjust the request — check filter operators, value formats, and the group_by field.";
107
+ readonly thrownBy: "service";
97
108
  }, {
98
109
  readonly reason: "upstream_validation_failed";
99
110
  readonly code: JsonRpcErrorCode.ValidationError;
100
111
  readonly when: "OpenAlex rejected the request as semantically invalid (HTTP 422).";
101
112
  readonly recovery: "Read the upstream message for the specific field, then adjust the request to satisfy validation.";
113
+ readonly thrownBy: "service";
114
+ }, {
115
+ readonly reason: "upstream_missing_group_by";
116
+ readonly code: JsonRpcErrorCode.ServiceUnavailable;
117
+ readonly when: "OpenAlex answered with its plain list shape, carrying no aggregation for the requested group_by.";
118
+ readonly retryable: true;
119
+ readonly recovery: "Retry the request; if it repeats, confirm group_by names a field OpenAlex can aggregate with openalex_describe_fields(entity_type, \"group_by\").";
120
+ readonly thrownBy: "service";
102
121
  }], {
103
122
  readonly echo: z.ZodString;
104
123
  readonly totalCount: z.ZodNumber;
@@ -1 +1 @@
1
- {"version":3,"file":"analyze-trends.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/analyze-trends.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8S5B,CAAC"}
1
+ {"version":3,"file":"analyze-trends.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/analyze-trends.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4U5B,CAAC"}