@cyanheads/openalex-mcp-server 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +26 -9
- package/CLAUDE.md +26 -9
- package/Dockerfile +9 -3
- package/README.md +31 -104
- package/changelog/0.10.x/0.10.0.md +30 -0
- package/changelog/0.10.x/0.10.1.md +28 -0
- package/dist/config/server-config.d.ts +2 -0
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +10 -0
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts +8 -2
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.js +53 -38
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts +7 -1
- package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/citation-graph.tool.js +17 -12
- package/dist/mcp-server/tools/definitions/citation-graph.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts +6 -0
- package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/resolve-name.tool.js +8 -1
- package/dist/mcp-server/tools/definitions/resolve-name.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts +23 -2
- package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-entities.tool.js +70 -15
- package/dist/mcp-server/tools/definitions/search-entities.tool.js.map +1 -1
- package/dist/mcp-server/tools/page-size.d.ts +18 -0
- package/dist/mcp-server/tools/page-size.d.ts.map +1 -0
- package/dist/mcp-server/tools/page-size.js +28 -0
- package/dist/mcp-server/tools/page-size.js.map +1 -0
- package/dist/mcp-server/tools/response-budget.d.ts +6 -6
- package/dist/mcp-server/tools/response-budget.d.ts.map +1 -1
- package/dist/mcp-server/tools/response-budget.js +77 -30
- package/dist/mcp-server/tools/response-budget.js.map +1 -1
- package/dist/services/openalex/filter-values.d.ts +2 -0
- package/dist/services/openalex/filter-values.d.ts.map +1 -1
- package/dist/services/openalex/filter-values.js +56 -0
- package/dist/services/openalex/filter-values.js.map +1 -1
- package/dist/services/openalex/openalex-service.d.ts +6 -2
- package/dist/services/openalex/openalex-service.d.ts.map +1 -1
- package/dist/services/openalex/openalex-service.js +15 -9
- package/dist/services/openalex/openalex-service.js.map +1 -1
- package/dist/services/openalex/types.d.ts +5 -2
- package/dist/services/openalex/types.d.ts.map +1 -1
- package/package.json +9 -8
- package/server.json +31 -3
package/AGENTS.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** openalex-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.10.1
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
|
|
8
8
|
**Zod:** ^4.6.7
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
@@ -42,6 +42,8 @@ No resources — entity lookups need `select` for payload control, which fits to
|
|
|
42
42
|
| `OPENALEX_API_KEY` | No | OpenAlex account API key, sent as `api_key=` (free from https://openalex.org/settings/api); omit for anonymous access |
|
|
43
43
|
| `OPENALEX_MAILTO` | No | Email sent as `mailto=` to identify yourself to OpenAlex (the "polite pool"); a courtesy identifier, separate from the API key |
|
|
44
44
|
| `OPENALEX_BASE_URL` | No | Default: `https://api.openalex.org` |
|
|
45
|
+
| `OPENALEX_FILTER_STRING_INPUT` | No | Default: `true`; accept unambiguous native filter strings on `openalex_search_entities`; `false` restores record-only input. Strict boolean parsing rejects invalid values. |
|
|
46
|
+
| `OPENALEX_CLAMP_PER_PAGE` | No | Default: `true`; clamp positive integer `per_page` to 100 for search/citation and 200 for analysis, with a notice, log, and counter. `false` restores rejecting advertised maxima. Strict boolean parsing; semantic cap 50 and sample maxima still reject. |
|
|
45
47
|
|
|
46
48
|
**Session mode:** `createApp({ sessionMode: 'stateless' })` in `src/index.ts` — no tool gates on `ctx.requestInput`, so every call completes in one round trip. An explicit `MCP_SESSION_MODE` still overrides it; `.env.example` and the `Dockerfile` set the same value. No service holds a watcher, socket, or ref'd timer, so there is no `teardown` hook.
|
|
47
49
|
|
|
@@ -189,6 +191,8 @@ async handler(input, ctx) {
|
|
|
189
191
|
|
|
190
192
|
**Declare contracts inline on each tool, even when similar across tools.** The contract is part of the tool's documented public surface — reading one tool definition file should give the full picture. Don't extract a shared `errors[]` constant; per-tool repetition is the intended cost of locality.
|
|
191
193
|
|
|
194
|
+
Native-filter parser failures raised during schema preprocessing carry explicit recovery guidance: the handler boundary has not run yet, so handler-contract hint filling cannot supply it.
|
|
195
|
+
|
|
192
196
|
The OpenAlex service throws factory errors (`notFound`, `rateLimited`, etc.) based on upstream status codes, carrying the contract `reason` on `data.reason` and nothing else of the contract; the framework fills the calling tool's `recovery` for that reason at the handler boundary, and the errors bubble through and are auto-classified. Three service throws set their own `recovery: { hint }`, which wins over the fill: the PMCID 404, since the generic "verify the ID" recovery would send the caller back to retry an identifier OpenAlex never indexes; `upstream_budget_exhausted` on a server with no `OPENALEX_API_KEY`, whose hint adds the free-key advice to the contract's key-neutral midnight-UTC recovery (a keyed server's budget is already the larger one, so it gets the contract text alone); and `upstream_unavailable` on a semantic search — including the HTTP 400 `Failed to embed query:` and `Semantic search is not configured` bodies, relabelled from caller errors to that outage — whose hint adds the keyword-search fallback the generic outage recovery cannot name. One status is read past its code: an HTTP 429 whose body `error` is `Plan upgrade required` — a filter OpenAlex serves only on a paid plan — is thrown as `Forbidden` with `upstream_plan_required` and `retryable: false` under a message of the server's own, so it fails on its first request — no wait clears a plan gate. Every service-thrown entry carries `thrownBy: 'service'` — lint-only metadata that tells `error-contract-unthrown`, whose scan reads the handler body alone, to skip a reason produced a layer below it. Handler-local precondition throws stay inline and unmarked so the rule keeps checking them: `semantic_without_query`, `semantic_per_page_cap`, `semantic_with_cursor`, `page_without_semantic`, `sample_with_cursor`, `sample_with_page`, `sample_with_semantic`, `sample_with_sort`, `seed_without_sample`, `slice_without_id`, and `slice_field_not_array` on `openalex_search_entities`; `reserved_filter_key` and the seed-lookup `entity_not_found` on `openalex_get_citation_graph`; `filters_without_entity_type` on `openalex_resolve_name`. Recovery assertions belong on the contract path (`runToolContract`), where the fill applies — a direct `definition.handler(...)` or service throw carries only what the throw site wrote.
|
|
193
197
|
|
|
194
198
|
**Fallback for ad-hoc throws:** error factories or plain `Error`.
|
|
@@ -210,12 +214,12 @@ See framework CLAUDE.md for the full auto-classification table and all available
|
|
|
210
214
|
src/
|
|
211
215
|
index.ts # createApp() entry point
|
|
212
216
|
config/
|
|
213
|
-
server-config.ts #
|
|
217
|
+
server-config.ts # OpenAlex API access and input compatibility switches
|
|
214
218
|
services/
|
|
215
219
|
openalex/
|
|
216
220
|
openalex-service.ts # API client (init/accessor pattern)
|
|
217
221
|
types.ts # Domain types
|
|
218
|
-
filter-values.ts #
|
|
222
|
+
filter-values.ts # Native filter-string parsing and filter values: country/language codes, quoted phrases, `+` as AND
|
|
219
223
|
iso-codes.json # ISO 3166-1 / 639-2 lookups (generated by scripts/generate-iso-codes.ts)
|
|
220
224
|
provider-text.ts # Cleanup of provider text: entities, markup, MathType blobs
|
|
221
225
|
html-entities.json # WHATWG named-entity table provider-text.ts decodes against
|
|
@@ -229,6 +233,7 @@ src/
|
|
|
229
233
|
render-entity-record.ts # Entity records as content[] markdown
|
|
230
234
|
render-budget.ts # The budget trailer line
|
|
231
235
|
response-budget.ts # 64,000-byte response budget and its disclosures
|
|
236
|
+
page-size.ts # Configurable page-size validation and disclosed clamping
|
|
232
237
|
tools/definitions/
|
|
233
238
|
search-entities.tool.ts # openalex_search_entities
|
|
234
239
|
analyze-trends.tool.ts # openalex_analyze_trends
|
|
@@ -255,9 +260,18 @@ src/
|
|
|
255
260
|
|
|
256
261
|
## Skills
|
|
257
262
|
|
|
258
|
-
|
|
263
|
+
Two skill trees live at the project root, written for different readers:
|
|
264
|
+
|
|
265
|
+
| Tree | For | Written by |
|
|
266
|
+
|:-----|:----|:-----------|
|
|
267
|
+
| `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
|
|
268
|
+
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Absent by default; usually none is needed |
|
|
269
|
+
|
|
270
|
+
Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
|
|
271
|
+
|
|
272
|
+
A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image, and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.
|
|
259
273
|
|
|
260
|
-
**Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
274
|
+
**Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
261
275
|
|
|
262
276
|
Available skills:
|
|
263
277
|
|
|
@@ -269,6 +283,7 @@ Available skills:
|
|
|
269
283
|
| `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
|
|
270
284
|
| `add-resource` | Scaffold a new resource definition |
|
|
271
285
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
286
|
+
| `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
|
|
272
287
|
| `add-service` | Scaffold a new service integration |
|
|
273
288
|
| `add-test` | Scaffold test file for a tool, resource, or service |
|
|
274
289
|
| `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
|
|
@@ -317,7 +332,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
317
332
|
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
318
333
|
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
319
334
|
| `bun run lint:mcp` | Validate MCP tool/prompt definitions (rule catalog: `api-linter` skill) |
|
|
320
|
-
| `bun run lint:packaging` | Packaging surface checks — env-var parity, plugin
|
|
335
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `.mcpbignore` coverage, plugin manifests, README version badge (run by devcheck) |
|
|
321
336
|
| `bun run list-skills` | List available skills |
|
|
322
337
|
| `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
|
|
323
338
|
| `bun run start:stdio` | Production mode (stdio, after `rebuild`) |
|
|
@@ -333,7 +348,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
333
348
|
|
|
334
349
|
## Bundling
|
|
335
350
|
|
|
336
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. MCPB is stdio-only — HTTP deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
|
|
351
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line, and a root path of the server's own (`/claude-plans/`) is added by hand. MCPB is stdio-only — HTTP deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
|
|
337
352
|
|
|
338
353
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.
|
|
339
354
|
Every bundle option must be wired as `${user_config.<option>}`; optional strings need a default. Claude plugins use the same options under `userConfig`, while Codex forwards user variables through `env_vars`. Never overwrite a user's exported value with an empty plugin `env` entry.
|
|
@@ -397,4 +412,6 @@ import { getOpenAlexService } from '@/services/openalex/openalex-service.js';
|
|
|
397
412
|
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
|
|
398
413
|
- [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
|
|
399
414
|
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
|
|
415
|
+
- [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name; `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y <full package name>`, and the same fixed `env` as `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
|
|
416
|
+
- [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass
|
|
400
417
|
- [ ] `bun run devcheck` passes
|
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** openalex-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.10.1
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
|
|
8
8
|
**Zod:** ^4.6.7
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
@@ -42,6 +42,8 @@ No resources — entity lookups need `select` for payload control, which fits to
|
|
|
42
42
|
| `OPENALEX_API_KEY` | No | OpenAlex account API key, sent as `api_key=` (free from https://openalex.org/settings/api); omit for anonymous access |
|
|
43
43
|
| `OPENALEX_MAILTO` | No | Email sent as `mailto=` to identify yourself to OpenAlex (the "polite pool"); a courtesy identifier, separate from the API key |
|
|
44
44
|
| `OPENALEX_BASE_URL` | No | Default: `https://api.openalex.org` |
|
|
45
|
+
| `OPENALEX_FILTER_STRING_INPUT` | No | Default: `true`; accept unambiguous native filter strings on `openalex_search_entities`; `false` restores record-only input. Strict boolean parsing rejects invalid values. |
|
|
46
|
+
| `OPENALEX_CLAMP_PER_PAGE` | No | Default: `true`; clamp positive integer `per_page` to 100 for search/citation and 200 for analysis, with a notice, log, and counter. `false` restores rejecting advertised maxima. Strict boolean parsing; semantic cap 50 and sample maxima still reject. |
|
|
45
47
|
|
|
46
48
|
**Session mode:** `createApp({ sessionMode: 'stateless' })` in `src/index.ts` — no tool gates on `ctx.requestInput`, so every call completes in one round trip. An explicit `MCP_SESSION_MODE` still overrides it; `.env.example` and the `Dockerfile` set the same value. No service holds a watcher, socket, or ref'd timer, so there is no `teardown` hook.
|
|
47
49
|
|
|
@@ -189,6 +191,8 @@ async handler(input, ctx) {
|
|
|
189
191
|
|
|
190
192
|
**Declare contracts inline on each tool, even when similar across tools.** The contract is part of the tool's documented public surface — reading one tool definition file should give the full picture. Don't extract a shared `errors[]` constant; per-tool repetition is the intended cost of locality.
|
|
191
193
|
|
|
194
|
+
Native-filter parser failures raised during schema preprocessing carry explicit recovery guidance: the handler boundary has not run yet, so handler-contract hint filling cannot supply it.
|
|
195
|
+
|
|
192
196
|
The OpenAlex service throws factory errors (`notFound`, `rateLimited`, etc.) based on upstream status codes, carrying the contract `reason` on `data.reason` and nothing else of the contract; the framework fills the calling tool's `recovery` for that reason at the handler boundary, and the errors bubble through and are auto-classified. Three service throws set their own `recovery: { hint }`, which wins over the fill: the PMCID 404, since the generic "verify the ID" recovery would send the caller back to retry an identifier OpenAlex never indexes; `upstream_budget_exhausted` on a server with no `OPENALEX_API_KEY`, whose hint adds the free-key advice to the contract's key-neutral midnight-UTC recovery (a keyed server's budget is already the larger one, so it gets the contract text alone); and `upstream_unavailable` on a semantic search — including the HTTP 400 `Failed to embed query:` and `Semantic search is not configured` bodies, relabelled from caller errors to that outage — whose hint adds the keyword-search fallback the generic outage recovery cannot name. One status is read past its code: an HTTP 429 whose body `error` is `Plan upgrade required` — a filter OpenAlex serves only on a paid plan — is thrown as `Forbidden` with `upstream_plan_required` and `retryable: false` under a message of the server's own, so it fails on its first request — no wait clears a plan gate. Every service-thrown entry carries `thrownBy: 'service'` — lint-only metadata that tells `error-contract-unthrown`, whose scan reads the handler body alone, to skip a reason produced a layer below it. Handler-local precondition throws stay inline and unmarked so the rule keeps checking them: `semantic_without_query`, `semantic_per_page_cap`, `semantic_with_cursor`, `page_without_semantic`, `sample_with_cursor`, `sample_with_page`, `sample_with_semantic`, `sample_with_sort`, `seed_without_sample`, `slice_without_id`, and `slice_field_not_array` on `openalex_search_entities`; `reserved_filter_key` and the seed-lookup `entity_not_found` on `openalex_get_citation_graph`; `filters_without_entity_type` on `openalex_resolve_name`. Recovery assertions belong on the contract path (`runToolContract`), where the fill applies — a direct `definition.handler(...)` or service throw carries only what the throw site wrote.
|
|
193
197
|
|
|
194
198
|
**Fallback for ad-hoc throws:** error factories or plain `Error`.
|
|
@@ -210,12 +214,12 @@ See framework CLAUDE.md for the full auto-classification table and all available
|
|
|
210
214
|
src/
|
|
211
215
|
index.ts # createApp() entry point
|
|
212
216
|
config/
|
|
213
|
-
server-config.ts #
|
|
217
|
+
server-config.ts # OpenAlex API access and input compatibility switches
|
|
214
218
|
services/
|
|
215
219
|
openalex/
|
|
216
220
|
openalex-service.ts # API client (init/accessor pattern)
|
|
217
221
|
types.ts # Domain types
|
|
218
|
-
filter-values.ts #
|
|
222
|
+
filter-values.ts # Native filter-string parsing and filter values: country/language codes, quoted phrases, `+` as AND
|
|
219
223
|
iso-codes.json # ISO 3166-1 / 639-2 lookups (generated by scripts/generate-iso-codes.ts)
|
|
220
224
|
provider-text.ts # Cleanup of provider text: entities, markup, MathType blobs
|
|
221
225
|
html-entities.json # WHATWG named-entity table provider-text.ts decodes against
|
|
@@ -229,6 +233,7 @@ src/
|
|
|
229
233
|
render-entity-record.ts # Entity records as content[] markdown
|
|
230
234
|
render-budget.ts # The budget trailer line
|
|
231
235
|
response-budget.ts # 64,000-byte response budget and its disclosures
|
|
236
|
+
page-size.ts # Configurable page-size validation and disclosed clamping
|
|
232
237
|
tools/definitions/
|
|
233
238
|
search-entities.tool.ts # openalex_search_entities
|
|
234
239
|
analyze-trends.tool.ts # openalex_analyze_trends
|
|
@@ -255,9 +260,18 @@ src/
|
|
|
255
260
|
|
|
256
261
|
## Skills
|
|
257
262
|
|
|
258
|
-
|
|
263
|
+
Two skill trees live at the project root, written for different readers:
|
|
264
|
+
|
|
265
|
+
| Tree | For | Written by |
|
|
266
|
+
|:-----|:----|:-----------|
|
|
267
|
+
| `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
|
|
268
|
+
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Absent by default; usually none is needed |
|
|
269
|
+
|
|
270
|
+
Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
|
|
271
|
+
|
|
272
|
+
A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image, and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.
|
|
259
273
|
|
|
260
|
-
**Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
274
|
+
**Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
261
275
|
|
|
262
276
|
Available skills:
|
|
263
277
|
|
|
@@ -269,6 +283,7 @@ Available skills:
|
|
|
269
283
|
| `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
|
|
270
284
|
| `add-resource` | Scaffold a new resource definition |
|
|
271
285
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
286
|
+
| `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
|
|
272
287
|
| `add-service` | Scaffold a new service integration |
|
|
273
288
|
| `add-test` | Scaffold test file for a tool, resource, or service |
|
|
274
289
|
| `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
|
|
@@ -317,7 +332,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
317
332
|
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
318
333
|
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
319
334
|
| `bun run lint:mcp` | Validate MCP tool/prompt definitions (rule catalog: `api-linter` skill) |
|
|
320
|
-
| `bun run lint:packaging` | Packaging surface checks — env-var parity, plugin
|
|
335
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `.mcpbignore` coverage, plugin manifests, README version badge (run by devcheck) |
|
|
321
336
|
| `bun run list-skills` | List available skills |
|
|
322
337
|
| `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
|
|
323
338
|
| `bun run start:stdio` | Production mode (stdio, after `rebuild`) |
|
|
@@ -333,7 +348,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
333
348
|
|
|
334
349
|
## Bundling
|
|
335
350
|
|
|
336
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. MCPB is stdio-only — HTTP deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
|
|
351
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line, and a root path of the server's own (`/claude-plans/`) is added by hand. MCPB is stdio-only — HTTP deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
|
|
337
352
|
|
|
338
353
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.
|
|
339
354
|
Every bundle option must be wired as `${user_config.<option>}`; optional strings need a default. Claude plugins use the same options under `userConfig`, while Codex forwards user variables through `env_vars`. Never overwrite a user's exported value with an empty plugin `env` entry.
|
|
@@ -397,4 +412,6 @@ import { getOpenAlexService } from '@/services/openalex/openalex-service.js';
|
|
|
397
412
|
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
|
|
398
413
|
- [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
|
|
399
414
|
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
|
|
415
|
+
- [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name; `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y <full package name>`, and the same fixed `env` as `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
|
|
416
|
+
- [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass
|
|
400
417
|
- [ ] `bun run devcheck` passes
|
package/Dockerfile
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# output. A stage that compiles a native addon needs the target-arch toolchain
|
|
15
15
|
# and cannot cross-compile this way — drop the flag there.
|
|
16
16
|
# ==============================================================================
|
|
17
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
17
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS build
|
|
18
18
|
|
|
19
19
|
WORKDIR /usr/src/app
|
|
20
20
|
|
|
@@ -29,6 +29,11 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
|
29
29
|
# Copy the rest of the source code
|
|
30
30
|
COPY . .
|
|
31
31
|
|
|
32
|
+
# The skills a server publishes (`createApp({ skills: 'skills' })`) are read at
|
|
33
|
+
# runtime from the application root. Created when the project has none, so the
|
|
34
|
+
# production stage's copy below holds with or without one.
|
|
35
|
+
RUN mkdir -p skills
|
|
36
|
+
|
|
32
37
|
# Build the application
|
|
33
38
|
RUN bun run build
|
|
34
39
|
|
|
@@ -46,7 +51,7 @@ RUN bun run build
|
|
|
46
51
|
# A clean image rather than `FROM build`: the build stage's node_modules holds
|
|
47
52
|
# devDependencies.
|
|
48
53
|
# ==============================================================================
|
|
49
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
54
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS deps
|
|
50
55
|
|
|
51
56
|
WORKDIR /usr/src/app
|
|
52
57
|
|
|
@@ -114,7 +119,7 @@ RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
|
114
119
|
# dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
|
|
115
120
|
# and CMD, which run on the real target at container start.
|
|
116
121
|
# ==============================================================================
|
|
117
|
-
FROM oven/bun:1.4.
|
|
122
|
+
FROM oven/bun:1.4.4-slim AS production
|
|
118
123
|
|
|
119
124
|
WORKDIR /usr/src/app
|
|
120
125
|
|
|
@@ -136,6 +141,7 @@ COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
|
136
141
|
|
|
137
142
|
# Copy the compiled application code from the build stage
|
|
138
143
|
COPY --from=build /usr/src/app/dist ./dist
|
|
144
|
+
COPY --from=build /usr/src/app/skills ./skills
|
|
139
145
|
|
|
140
146
|
# The 'oven/bun' image already provides a non-root user named 'bun'.
|
|
141
147
|
# We will use this existing user for enhanced security.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/openalex-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/openalex-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -54,13 +54,16 @@ Scholarly catalog data from [OpenAlex](https://openalex.org) — 270M+ works, 90
|
|
|
54
54
|
|
|
55
55
|
- Retrieve a single entity by ID — OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or PMCID (bare or URL form, including OpenAlex record-page and API URLs such as `https://openalex.org/works/W2741809807`), or a keyword by its slug or keyword URL. Under `entity_type: "funders"`, a funder's Crossref Funder Registry DOI (`10.13039/…`) resolves through a one-result list request. OpenAlex does not bill a lookup by ID, with three exceptions it bills like one list call: a keyword looked up by its slug or URL, an `id` carrying characters past the identifier itself (`W2741809807#x` still returns W2741809807), and that funder-DOI request. `id` takes precedence: search parameters passed alongside it are dropped, and the response names which ones. An OpenAlex ID whose letter names another entity type than `entity_type` fails as `id_type_mismatch` before any upstream call, and an `id` holding a `.` or `..` path segment, plain or percent-encoded, fails as `id_dot_segment`; a `?`, `#`, `\`, tab, or line break inside an identifier (SICI DOIs end in `#`) is sent percent-encoded as part of it. A PMCID resolves nothing (OpenAlex indexes none) — use the work's PMID or DOI instead
|
|
56
56
|
- Keyword search (boolean operators, quoted phrases, wildcards, fuzzy match) plus `exact` and `semantic` search modes — semantic ranks a query-dependent candidate set at ~1 req/sec (up to 50 per page), and its `meta.count` reports the candidate count rather than a match total
|
|
57
|
+
- `filters` accepts a record object or an unambiguous native string (`"issn:0920-8542"`), including through the `filter` alias. Conversion is disclosed in `notice`; commas outside paired double quotes separate clauses, and the first colon separates a key from its value, preserving DOI/URL colons and quoted comma phrases. Exact duplicate keys, bare fragments, and unmatched quotes are rejected with record-form recovery guidance. A record holds one value per key; distinct aliases of the same upstream field still AND together. Set `OPENALEX_FILTER_STRING_INPUT=false` to require record objects. Native strings apply only to this tool's filters, with the record form remaining the advertised schema
|
|
58
|
+
- Filter values sent unquoted must have paired double quotes on all four filter-taking tools (`unmatched_quote_in_filter_value`); citation filters are checked before the seed lookup. Balanced search phrases such as `"graph neural" networks` remain valid, including `.search.exact`. A value sent double-quoted still cannot contain its own interior double quote (`quote_in_filter_value`)
|
|
57
59
|
- Rich filter syntax: AND across fields, OR within a field (`|`), AND within a field (`+`), NOT (`!`), ranges, comparisons. One value cannot hold `|` beside an AND operator — a `+`, or a space between values on an ID, code, or `language` key (a space inside a language name is part of the name, and on a country field a space is always part of a country name, never AND: `DE US|FR` fails as `invalid_country_code`): OpenAlex splits a value on `|` first and drops every alternative that ANDs more than one value, so `DE|FR+US` and `authorships.author.id: A1 A2|A3` both fail as `mixed_and_or_in_filter_value` before any request — put the AND in a second key instead (`institutions.country_code: US` beside `authorships.institutions.country_code: DE|FR` on works). A comma inside a free-text search filter (any `.search` key — `title.search`, `abstract.search.exact`, `title_abstract_keywords.search`) or a field OpenAlex matches as whole text (listed below) is sent double-quoted as part of the phrase, while a comma in any other filter value fails as `comma_in_filter_value` (use `|` for OR); inside a `.search` value, a DOI, a date (a timestamp's `+00:00` offset), a URL (`landing_page_url`; not the boolean `has_pdf_url`), an exactly matched name (`display_name`, `awards.funder_display_name` — `C++`, `Erasmus+`), a `biblio` field (issue `1+2`), or `awards.funder_award_id`, a `+` is a literal character; a value with spaces on a field OpenAlex matches as whole text (`display_name`, `awards.funder_display_name`, `awards.funder_award_id`, `geo.city`, `geo.region`, `raw_affiliation_strings`, `block_key`, a `biblio` field) is sent double-quoted, so it matches as one phrase (`awards.funder_display_name: National Science Foundation`) — `!` and `|` work with it (`geo.city: New York|Boston`), but `+` cannot AND it with another value in the same field. A comma is part of such a value too (`raw_affiliation_strings: Associazione Meteonetwork OdV, Milano, Italy`): a value holding one goes out quoted with its `+` as a literal character. A double quote inside the outer pair of any value sent quoted this way, or of a value you wrap in double quotes on any filter but a free-text search, fails as `quote_in_filter_value`
|
|
58
60
|
- Country and language values are converted to the codes OpenAlex stores, here and in every other tool that takes `filters`: on a country field (a key ending in `country_code`, `country_codes`, or `countries`), an ISO 3166-1 alpha-3 code, numeric code, or English name — ISO's, or the one OpenAlex's own country facets display (`Russia`, `Ivory Coast`, `Trinidad & Tobago`) — becomes its alpha-2 code (`DEU`, `276`, `Germany` → `DE`) and `UK` becomes `GB`, while a value that cannot be a country code (`Germanyy`, `999`, `unknown`, or two codes joined by a space, `DE US`) fails as `invalid_country_code` without the filter being sent; on `language`, an ISO 639-2 code becomes its ISO 639-1 code and an English name becomes its code (`eng`, `English` → `en`; `Cebuano` → `ceb`). Each `|`/`+` segment is trimmed and converted on its own, so a multi-word name is one value (`South Korea|Japan` → `KR|JP`, `Scottish Gaelic|English` → `gd|en`), and anything else — `null`, `XK`, `ceb`, `zh-cn`, the `countries/DEU` path form — is sent as given
|
|
59
61
|
- `select` returns a curated per-entity-type default unless overridden, or `["*"]` for the full record; invalid field names error with the valid set. On works, `author_list` returns each author in authorship order as `{id, display_name, orcid}` — about a tenth the size of `authorships`, which adds institutions and affiliations — and is never part of the default or `["*"]`
|
|
60
62
|
- 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
|
|
61
63
|
- `sample` (up to 100, single page only — neither `cursor` nor `page` applies) plus a deterministic `seed` for reproducible random sampling; keyword and exact modes only, and never alongside `sort` — OpenAlex does not sample a semantic search and refuses a sorted sample, so both combinations are rejected before the upstream call
|
|
62
64
|
- Each response holds to 64,000 bytes per surface (`structuredContent`, and `content[]` with its trailer) unless the least it can return is larger, which it flags as `over_budget`: a page that would run over keeps its leading records whole and names the rest in `omitted` with the one call that returns them (as a set, in OpenAlex's order), without disturbing `next_cursor`; a record too large on its own comes back with its largest arrays windowed, filled in turn so each shows what fits and labeled with how much of the array it holds, and `slice: {field, offset}` on an `id` lookup pages any array to the end
|
|
63
|
-
- A list record carrying exactly 100 `authorships` or `author_list` entries
|
|
65
|
+
- A list record carrying exactly 100 `authorships` or `author_list` entries is flagged as possibly capped, with an `id` + `slice` call to retrieve more stored authors. Singleton lookups and snapshots retain at most 5,000 authorships per work; at that ceiling both author fields remain possibly capped on every slice. `total` counts stored entries rather than a complete byline, and `next` becomes null when those entries are exhausted. Another OpenAlex fetch or snapshot cannot recover authors beyond the storage ceiling
|
|
66
|
+
- Positive integer `per_page` values above 100 for search/citation or 200 for analysis clamp to the maximum, with requested/applied sizes in `notice`, a log, and a counter. Set `OPENALEX_CLAMP_PER_PAGE=false` to restore rejecting advertised maxima. Zero, negatives, fractions, semantic search above 50, and oversized samples still reject. An `id` ignores page size and `sample` overrides it
|
|
64
67
|
- `display_name` is nullable for untitled records; every call reports OpenAlex daily-budget cost and remaining balance
|
|
65
68
|
|
|
66
69
|
---
|
|
@@ -68,8 +71,9 @@ Scholarly catalog data from [OpenAlex](https://openalex.org) — 270M+ works, 90
|
|
|
68
71
|
### `openalex_analyze_trends` <sub>tool</sub>
|
|
69
72
|
|
|
70
73
|
- Group any supported field for trend, distribution, or comparative analysis; combine with `filters` to scope the population before aggregation
|
|
71
|
-
- Up to 200 groups per page (default). `order: "count"` (default) returns the top-N by count with no further pages; `order: "key"` enumerates
|
|
72
|
-
- `include_unknown` (default `false`) adds a group for entities with no value for the grouped field, flagged `is_unknown: true` and labeled as unknown in the text output. OpenAlex keys it `-111`/`-111.0` (numeric fields), `unknown` (text fields and `order: "key"`), or an ID ending in `/unknown`; the key is a sentinel, not a filter value. Boolean fields have no such group — a missing value counts as `false`
|
|
74
|
+
- Up to 200 groups per page (default). Without a cursor, `order: "count"` (default) returns the top-N by count with no further pages; `order: "key"` enumerates distinct values key-ascending with cursor pagination. Boolean fields keep count order even with `"key"` and do not support cursor pagination. A supplied `cursor` selects key order even when `order` is omitted or `"count"`. OpenAlex can wrap a key-ordered walk to earlier groups: stop when a page's first key repeats an earlier key, or when a page is short or empty, even if `next_cursor` is still present
|
|
75
|
+
- `include_unknown` (default `false`) adds a group for entities with no value for the grouped field, flagged `is_unknown: true` and labeled as unknown in the text output. The native `:include_unknown` suffix on `group_by` also enables it, even with an explicit `false`; passing both adds the suffix once. OpenAlex keys it `-111`/`-111.0` (numeric fields), `unknown` (text fields and `order: "key"`), or an ID ending in `/unknown`; the key is a sentinel, not a filter value. Boolean fields have no such group — a missing value counts as `false`
|
|
76
|
+
- `key_display_name` remains present but can be `null` when OpenAlex supplies no label; text output renders the group key in that case, including unknown buckets
|
|
73
77
|
- Not every field is groupable — raw date fields, `.search` operators, `from_*`/`to_*` range modifiers, decimal scores such as `fwci`, `display_name`, external-ID fields such as `doi`, and fields OpenAlex looks up from a related entity such as `primary_location.source.country_code` are rejected; `openalex_describe_fields(entity_type, "group_by")` lists the fields that group
|
|
74
78
|
- A works group_by on `authorships.countries` says in its notice that OpenAlex counts only each work's first 100 authorships for that field, and names `authorships.institutions.country_code` as the field that counts all of them
|
|
75
79
|
- Reports OpenAlex daily-budget cost and remaining balance — aggregation is priced far below paging the same entities
|
|
@@ -143,120 +147,32 @@ Agent-friendly output:
|
|
|
143
147
|
|
|
144
148
|
## Getting started
|
|
145
149
|
|
|
146
|
-
|
|
150
|
+
Connect to the hosted instance, no install:
|
|
147
151
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
```json
|
|
151
|
-
{
|
|
152
|
-
"mcpServers": {
|
|
153
|
-
"openalex-mcp-server": {
|
|
154
|
-
"type": "streamable-http",
|
|
155
|
-
"url": "https://openalex.caseyjhand.com/mcp"
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
152
|
+
```sh
|
|
153
|
+
claude mcp add --transport http openalex-mcp-server https://openalex.caseyjhand.com/mcp
|
|
159
154
|
```
|
|
160
155
|
|
|
161
|
-
|
|
156
|
+
Or run it locally:
|
|
162
157
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
```json
|
|
166
|
-
{
|
|
167
|
-
"mcpServers": {
|
|
168
|
-
"openalex-mcp-server": {
|
|
169
|
-
"type": "stdio",
|
|
170
|
-
"command": "bunx",
|
|
171
|
-
"args": ["@cyanheads/openalex-mcp-server@latest"],
|
|
172
|
-
"env": {
|
|
173
|
-
"MCP_TRANSPORT_TYPE": "stdio",
|
|
174
|
-
"MCP_LOG_LEVEL": "info",
|
|
175
|
-
"OPENALEX_API_KEY": "your-api-key"
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
158
|
+
```sh
|
|
159
|
+
claude mcp add openalex-mcp-server -- npx -y @cyanheads/openalex-mcp-server
|
|
180
160
|
```
|
|
181
161
|
|
|
182
|
-
|
|
162
|
+
For other clients, add this to the MCP config file:
|
|
183
163
|
|
|
184
164
|
```json
|
|
185
165
|
{
|
|
186
166
|
"mcpServers": {
|
|
187
167
|
"openalex-mcp-server": {
|
|
188
|
-
"type": "stdio",
|
|
189
168
|
"command": "npx",
|
|
190
|
-
"args": ["-y", "@cyanheads/openalex-mcp-server
|
|
191
|
-
"env": {
|
|
192
|
-
"MCP_TRANSPORT_TYPE": "stdio",
|
|
193
|
-
"MCP_LOG_LEVEL": "info",
|
|
194
|
-
"OPENALEX_API_KEY": "your-api-key"
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Or with Docker:
|
|
202
|
-
|
|
203
|
-
```json
|
|
204
|
-
{
|
|
205
|
-
"mcpServers": {
|
|
206
|
-
"openalex-mcp-server": {
|
|
207
|
-
"type": "stdio",
|
|
208
|
-
"command": "docker",
|
|
209
|
-
"args": [
|
|
210
|
-
"run", "-i", "--rm",
|
|
211
|
-
"-e", "MCP_TRANSPORT_TYPE=stdio",
|
|
212
|
-
"-e", "OPENALEX_API_KEY=your-api-key",
|
|
213
|
-
"ghcr.io/cyanheads/openalex-mcp-server:latest"
|
|
214
|
-
]
|
|
169
|
+
"args": ["-y", "@cyanheads/openalex-mcp-server"]
|
|
215
170
|
}
|
|
216
171
|
}
|
|
217
172
|
}
|
|
218
173
|
```
|
|
219
174
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
```sh
|
|
223
|
-
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 OPENALEX_API_KEY=... bun run start:http
|
|
224
|
-
# Server listens at http://localhost:3010/mcp
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
`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)).
|
|
228
|
-
|
|
229
|
-
### Prerequisites
|
|
230
|
-
|
|
231
|
-
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
|
|
232
|
-
- Optional: an [OpenAlex account API key](https://openalex.org/settings/api) for keyed rate limits and budget — omit for anonymous access.
|
|
233
|
-
|
|
234
|
-
### Installation
|
|
235
|
-
|
|
236
|
-
1. **Clone the repository:**
|
|
237
|
-
|
|
238
|
-
```sh
|
|
239
|
-
git clone https://github.com/cyanheads/openalex-mcp-server.git
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
2. **Navigate into the directory:**
|
|
243
|
-
|
|
244
|
-
```sh
|
|
245
|
-
cd openalex-mcp-server
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
3. **Install dependencies:**
|
|
249
|
-
|
|
250
|
-
```sh
|
|
251
|
-
bun install
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
4. **Configure environment:**
|
|
255
|
-
|
|
256
|
-
```sh
|
|
257
|
-
cp .env.example .env
|
|
258
|
-
# edit .env and set required vars
|
|
259
|
-
```
|
|
175
|
+
An optional `OPENALEX_API_KEY` — a free [OpenAlex account key](https://openalex.org/settings/api) — raises the rate limits and daily budget over anonymous access; `OPENALEX_MAILTO` identifies you to OpenAlex's [polite pool](https://developers.openalex.org/guides/authentication). For Docker or a source build, see [Running the server](#running-the-server).
|
|
260
176
|
|
|
261
177
|
## Configuration
|
|
262
178
|
|
|
@@ -273,6 +189,8 @@ cp .env.example .env
|
|
|
273
189
|
| `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. | — |
|
|
274
190
|
| `OPENALEX_MAILTO` | Email sent upstream as `mailto=` to identify yourself to OpenAlex (the "polite pool"); a courtesy identifier, separate from the API key. | — |
|
|
275
191
|
| `OPENALEX_BASE_URL` | OpenAlex API base URL. | `https://api.openalex.org` |
|
|
192
|
+
| `OPENALEX_FILTER_STRING_INPUT` | Accept unambiguous native filter strings on `openalex_search_entities`. Set `false` to require record objects; invalid boolean values fail startup. | `true` |
|
|
193
|
+
| `OPENALEX_CLAMP_PER_PAGE` | Clamp oversized positive integer `per_page` with a requested/applied notice (100 search/citation, 200 analysis). Set `false` to restore rejecting advertised maxima; invalid boolean values fail startup. Semantic and sample caps still reject. | `true` |
|
|
276
194
|
| `OTEL_ENABLED` | Enable [OpenTelemetry instrumentation](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) (spans, metrics, completion logs). | `false` |
|
|
277
195
|
|
|
278
196
|
See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
@@ -281,6 +199,16 @@ See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
|
281
199
|
|
|
282
200
|
### Local development
|
|
283
201
|
|
|
202
|
+
Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
|
|
203
|
+
|
|
204
|
+
- **Clone and install:**
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
git clone https://github.com/cyanheads/openalex-mcp-server.git
|
|
208
|
+
cd openalex-mcp-server && bun install
|
|
209
|
+
cp .env.example .env # optionally set OPENALEX_API_KEY
|
|
210
|
+
```
|
|
211
|
+
|
|
284
212
|
- **Build and run:**
|
|
285
213
|
|
|
286
214
|
```sh
|
|
@@ -303,11 +231,10 @@ See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
|
303
231
|
### Docker
|
|
304
232
|
|
|
305
233
|
```sh
|
|
306
|
-
docker
|
|
307
|
-
docker run --rm -e OPENALEX_API_KEY=your-key -p 3010:3010 openalex-mcp-server
|
|
234
|
+
docker run --rm -e OPENALEX_API_KEY=your-key -p 3010:3010 ghcr.io/cyanheads/openalex-mcp-server:latest
|
|
308
235
|
```
|
|
309
236
|
|
|
310
|
-
The
|
|
237
|
+
The image defaults to HTTP transport, stateless session mode, and logs to `/var/log/openalex-mcp-server`. To run it over stdio from an MCP client, use `"command": "docker"` with `"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/openalex-mcp-server:latest"]`. To build locally, `docker build -t openalex-mcp-server .`; OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
|
|
311
238
|
|
|
312
239
|
## Project structure
|
|
313
240
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Preserve nullable aggregation labels, correct filter handling, and disclose pagination and author-storage limits."
|
|
3
|
+
breaking: true
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.10.0 — 2026-10-22
|
|
8
|
+
|
|
9
|
+
## Changed
|
|
10
|
+
|
|
11
|
+
- Allow required `key_display_name` to be `null` in aggregation results, rendering the group key when its label is absent. (#119)
|
|
12
|
+
- **Native filter strings** are accepted by `openalex_search_entities`, with conversion disclosed in `notice`; set `OPENALEX_FILTER_STRING_INPUT=false` to require record objects. ([#118](https://github.com/cyanheads/openalex-mcp-server/issues/118))
|
|
13
|
+
- **Ordinary read pages** clamp oversized `per_page` to 100 for search/citation or 200 for analysis, with requested/applied notices; `OPENALEX_CLAMP_PER_PAGE=false` restores rejection. Semantic search retains its 50-result cap; `id` and `sample` take precedence. ([#121](https://github.com/cyanheads/openalex-mcp-server/issues/121))
|
|
14
|
+
- **Bun** 1.4.3 → 1.4.4 for package management, container images, and the README badge.
|
|
15
|
+
- **Regression coverage** checks filter scanner growth and stored author sequences; entity-like Markdown escaping is measured with thread CPU time.
|
|
16
|
+
|
|
17
|
+
## Fixed
|
|
18
|
+
|
|
19
|
+
- **Aggregation ordering** notices and echoes follow supplied cursors and the boolean-field count-order exception. ([#41](https://github.com/cyanheads/openalex-mcp-server/issues/41))
|
|
20
|
+
- **Unknown aggregation groups** are flagged when `group_by` carries `:include_unknown`; combining the suffix and boolean sends the suffix once. ([#75](https://github.com/cyanheads/openalex-mcp-server/issues/75))
|
|
21
|
+
- **Key-ordered pagination** warns that OpenAlex can wrap and directs callers to stop on a repeated first key or a short/empty page. ([#108](https://github.com/cyanheads/openalex-mcp-server/issues/108))
|
|
22
|
+
- **Unmatched filter quotes** fail before any upstream request on all four filter-taking tools, including the citation seed lookup, with recovery guidance. ([#116](https://github.com/cyanheads/openalex-mcp-server/issues/116))
|
|
23
|
+
- **Author windows** disclose OpenAlex's 5,000-author storage ceiling on singleton lookups and slices, retaining `possibly_capped:true` while ending continuation at the stored-array boundary. ([#120](https://github.com/cyanheads/openalex-mcp-server/issues/120))
|
|
24
|
+
|
|
25
|
+
## Dependencies
|
|
26
|
+
|
|
27
|
+
- `ignore` ^7.0.12 → ^7.0.13.
|
|
28
|
+
- `tsc-alias` ^1.9.7 → ^1.9.8.
|
|
29
|
+
- `vitest` ^5.0.4 → ^5.0.5.
|
|
30
|
+
- Lockfile: `postcss` 8.5.30 → 8.5.31.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "mcp-ts-core 0.13.18: query echoes render as code spans, error text in content[] escapes Markdown, invalid framework config fails startup naming the variable, and the root Agent Plugins pair ships."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.10.1 — 2026-10-25
|
|
8
|
+
|
|
9
|
+
## Changed
|
|
10
|
+
|
|
11
|
+
- **Query echo** — the enrichment trailer's `Query:` line renders as an inline code span, so a copied query reads back with no backslash escapes (mcp-ts-core 0.13.18, [#785](https://github.com/cyanheads/mcp-ts-core/issues/785)).
|
|
12
|
+
- **Error text in `content[]`** — the `Error:` message, `Recovery:` hint, and reason escape Markdown metacharacters with backslashes; `structuredContent` keeps the original strings (mcp-ts-core 0.13.16, [#760](https://github.com/cyanheads/mcp-ts-core/issues/760)).
|
|
13
|
+
- **Framework configuration** — a non-integer or out-of-range count or timer variable stops startup with a message naming the variable, e.g. `MCP_HTTP_PORT (mcpHttpPort): Invalid input: expected int, received number` (mcp-ts-core 0.13.16).
|
|
14
|
+
- **Response budget** — `trailerTextBytes` counts the echo's code-span fence and the `> ` prefix on every line of a notice, so the 64,000-byte per-surface budget measures what `content[]` carries under the new rendering.
|
|
15
|
+
- **Packaging** — root Agent Plugins `plugin.json` and `mcp.json` (a stdio entry running `npx -y @cyanheads/openalex-mcp-server`), root-anchored `.mcpbignore` lines for source, tests, docs, changelog, plugin manifests, and agent docs, and the framework's bundle cleaner dropping its own build-only files (`changelog/`, `config/`, `scripts/`, `templates/`, `CLAUDE.md`, `AGENTS.md`) from the `.mcpb` (mcp-ts-core 0.13.18).
|
|
16
|
+
- **`skills/`** — the Dockerfile creates and copies an empty `skills/` directory and `package.json` `files` lists it, for template alignment; this server publishes no skills.
|
|
17
|
+
- **Framework skills and scripts** re-synced to mcp-ts-core 0.13.18, adding the `add-skill` skill; `CLAUDE.md` and `AGENTS.md` follow (skills section, MCP SDK `^2.4.0`).
|
|
18
|
+
- **README** — getting started condensed to hosted and local installs, with Docker and source-build detail under Running the server.
|
|
19
|
+
- **`.env.example`** — the `MCP_SESSION_MODE` comment is rewritten; the value stays `stateless`.
|
|
20
|
+
- **Issue forms** — the bug report takes one Environment table in place of separate version, runtime, transport, and OS fields; the feature request's `breaking-change` label no longer says it requires a major bump.
|
|
21
|
+
- **`bunfig.toml`** — `minimumReleaseAge` 3 days → 1 day.
|
|
22
|
+
|
|
23
|
+
## Dependencies
|
|
24
|
+
|
|
25
|
+
- `@cyanheads/mcp-ts-core` ^0.13.15 → ^0.13.18 (brings MCP SDK `@modelcontextprotocol/server` 2.3.1 → 2.4.0).
|
|
26
|
+
- `@biomejs/biome` ^2.5.15 → ^2.6.3 (dev).
|
|
27
|
+
- `ignore` ^7.0.13 → ^7.0.14 (dev).
|
|
28
|
+
- `tsc-alias` ^1.9.8 → ^1.9.9 (dev).
|
|
@@ -7,6 +7,8 @@ declare const ServerConfigSchema: z.ZodObject<{
|
|
|
7
7
|
apiKey: z.ZodDefault<z.ZodString>;
|
|
8
8
|
mailto: z.ZodDefault<z.ZodString>;
|
|
9
9
|
baseUrl: z.ZodDefault<z.ZodString>;
|
|
10
|
+
filterStringInput: z.ZodDefault<z.ZodCodec<z.ZodString, z.ZodBoolean>>;
|
|
11
|
+
clampPerPage: z.ZodDefault<z.ZodCodec<z.ZodString, z.ZodBoolean>>;
|
|
10
12
|
}, z.core.$strip>;
|
|
11
13
|
export type ServerConfig = z.infer<typeof ServerConfigSchema>;
|
|
12
14
|
export declare function getServerConfig(): ServerConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;;iBAsBtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAS9C"}
|
|
@@ -14,6 +14,14 @@ const ServerConfigSchema = z.object({
|
|
|
14
14
|
.default('')
|
|
15
15
|
.describe('Optional email sent upstream as mailto= to identify yourself to OpenAlex (the "polite pool"); a courtesy identifier, separate from and not a substitute for the API key'),
|
|
16
16
|
baseUrl: z.string().url().default('https://api.openalex.org').describe('OpenAlex API base URL'),
|
|
17
|
+
filterStringInput: z
|
|
18
|
+
.stringbool()
|
|
19
|
+
.default(true)
|
|
20
|
+
.describe('Accept unambiguous native filter strings on openalex_search_entities'),
|
|
21
|
+
clampPerPage: z
|
|
22
|
+
.stringbool()
|
|
23
|
+
.default(true)
|
|
24
|
+
.describe('Clamp oversized per_page values to each tool maximum, with a notice'),
|
|
17
25
|
});
|
|
18
26
|
let _config;
|
|
19
27
|
export function getServerConfig() {
|
|
@@ -21,6 +29,8 @@ export function getServerConfig() {
|
|
|
21
29
|
apiKey: 'OPENALEX_API_KEY',
|
|
22
30
|
mailto: 'OPENALEX_MAILTO',
|
|
23
31
|
baseUrl: 'OPENALEX_BASE_URL',
|
|
32
|
+
filterStringInput: 'OPENALEX_FILTER_STRING_INPUT',
|
|
33
|
+
clampPerPage: 'OPENALEX_CLAMP_PER_PAGE',
|
|
24
34
|
});
|
|
25
35
|
return _config;
|
|
26
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC;SACN,MAAM,EAAE;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CACP,sMAAsM,CACvM;IACH,MAAM,EAAE,CAAC;SACN,MAAM,EAAE;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CACP,yKAAyK,CAC1K;IACH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,0BAA0B,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC;SACN,MAAM,EAAE;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CACP,sMAAsM,CACvM;IACH,MAAM,EAAE,CAAC;SACN,MAAM,EAAE;SACR,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CACP,yKAAyK,CAC1K;IACH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,0BAA0B,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC;IAC/F,iBAAiB,EAAE,CAAC;SACjB,UAAU,EAAE;SACZ,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CAAC,sEAAsE,CAAC;IACnF,YAAY,EAAE,CAAC;SACZ,UAAU,EAAE;SACZ,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CAAC,qEAAqE,CAAC;CACnF,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,MAAM,EAAE,kBAAkB;QAC1B,MAAM,EAAE,iBAAiB;QACzB,OAAO,EAAE,mBAAmB;QAC5B,iBAAiB,EAAE,8BAA8B;QACjD,YAAY,EAAE,yBAAyB;KACxC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
|