@cyanheads/openalex-mcp-server 0.7.13 → 0.7.14
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 +20 -9
- package/CLAUDE.md +20 -9
- package/README.md +166 -88
- package/changelog/0.7.x/0.7.14.md +28 -0
- package/changelog/template.md +7 -7
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.js +1 -0
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.js.map +1 -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 +15 -10
- package/dist/mcp-server/tools/definitions/citation-graph.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/resolve-name.tool.js +1 -0
- package/dist/mcp-server/tools/definitions/resolve-name.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-entities.tool.js +29 -24
- package/dist/mcp-server/tools/definitions/search-entities.tool.js.map +1 -1
- package/dist/services/openalex/openalex-service.js +2 -2
- package/dist/services/openalex/openalex-service.js.map +1 -1
- package/package.json +7 -7
- package/server.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** openalex-mcp-server
|
|
4
|
-
**Version:** 0.7.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.7.14
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.5`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
|
|
8
|
-
**Zod:** ^4.6.
|
|
8
|
+
**Zod:** ^4.6.5
|
|
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.
|
|
11
11
|
|
|
@@ -43,6 +43,8 @@ No resources — entity lookups need `select` for payload control, which fits to
|
|
|
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
45
|
|
|
46
|
+
**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
|
+
|
|
46
48
|
---
|
|
47
49
|
|
|
48
50
|
## What's Next?
|
|
@@ -71,6 +73,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
71
73
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
72
74
|
- **Need input the caller didn't supply?** `return ctx.requestInput(...)` and read `ctx.inputs` when the handler is re-entered. Never `await` for user input mid-handler.
|
|
73
75
|
- **Secrets in env vars only** — never hardcoded.
|
|
76
|
+
- **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.
|
|
74
77
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
75
78
|
|
|
76
79
|
---
|
|
@@ -147,13 +150,13 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
147
150
|
|
|
148
151
|
| Property | Description |
|
|
149
152
|
|:---------|:------------|
|
|
150
|
-
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino and
|
|
153
|
+
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
151
154
|
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
152
155
|
| `ctx.requestInput` | Suspend and ask the caller for more input with `return ctx.requestInput(...)`; the handler is re-entered with answers. |
|
|
153
156
|
| `ctx.inputs` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. |
|
|
154
157
|
| `ctx.enrich` | Success-path agent context for notices, query echoes, totals, and truncation disclosure. Reaches both client response surfaces. |
|
|
155
158
|
| `ctx.content` | Non-text content blocks appended to `content[]`; never enters `structuredContent`. |
|
|
156
|
-
| `ctx.signal` | `AbortSignal` for cancellation. Passed to `
|
|
159
|
+
| `ctx.signal` | `AbortSignal` for cancellation. Passed to `withRetry` in the OpenAlex service, whose per-attempt signal reaches `fetch()`. |
|
|
157
160
|
| `ctx.fail` | Typed throw keyed by a declared `errors[]` contract — `ctx.fail(reason, msg?, data?)`. Auto-populates `data.reason` and resolves `code` from the contract. |
|
|
158
161
|
| `ctx.recoveryFor` | Opt-in resolver returning `{ recovery: { hint } }` for a declared reason. Spread into `data` at throw site to surface contract recovery on the wire. |
|
|
159
162
|
| `ctx.requestId` | Unique request ID. |
|
|
@@ -182,7 +185,7 @@ async handler(input, ctx) {
|
|
|
182
185
|
|
|
183
186
|
**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.
|
|
184
187
|
|
|
185
|
-
The OpenAlex service throws factory errors (`notFound`, `rateLimited`, etc.) based on upstream status codes
|
|
188
|
+
The OpenAlex service throws factory errors (`notFound`, `rateLimited`, etc.) based on upstream status codes, carrying the contract `reason` on `data.reason` and the recovery via `ctx.recoveryFor(reason)`; those bubble through and are auto-classified. Because most declared reasons are produced in the service, handler-local precondition throws (`semantic_per_page_cap`, `reserved_filter_key`, …) live in module-level helpers (`assertListQueryConstraints`, `assertNoReservedFilterKey`, `resolveSeedToWorkId`) rather than inline: `error-contract-unthrown` only scans handlers holding a literal `ctx.fail(`, and an inline one would flag every service-produced reason as dead. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely without needing to be declared on a contract.
|
|
186
189
|
|
|
187
190
|
**Fallback for ad-hoc throws:** error factories or plain `Error`.
|
|
188
191
|
|
|
@@ -234,7 +237,7 @@ src/
|
|
|
234
237
|
|
|
235
238
|
## Skills
|
|
236
239
|
|
|
237
|
-
Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool.
|
|
240
|
+
Skills are modular instructions in `framework-skills/` at the project root. 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. The 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 these development skills to every agent that installs it.
|
|
238
241
|
|
|
239
242
|
**Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). This makes skills available as context without needing to reference `framework-skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
|
|
240
243
|
|
|
@@ -293,8 +296,10 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
293
296
|
| `bun run audit:fix` | Run `bun audit fix` to upgrade vulnerable packages within existing ranges. |
|
|
294
297
|
| `bun run audit:refresh` | Delete `bun.lock` and reinstall; last resort after `audit:fix`, targeted updates, and `bun dedupe`. |
|
|
295
298
|
| `bun run tree` | Generate directory structure doc |
|
|
296
|
-
| `bun run format` | Auto-fix formatting |
|
|
297
|
-
| `bun run
|
|
299
|
+
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
300
|
+
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
301
|
+
| `bun run lint:mcp` | Validate MCP tool/prompt definitions (rule catalog: `api-linter` skill) |
|
|
302
|
+
| `bun run lint:packaging` | Packaging surface checks — env-var parity, plugin metadata, README version badge (run by devcheck) |
|
|
298
303
|
| `bun run list-skills` | List available skills |
|
|
299
304
|
| `bun run test` | Run tests |
|
|
300
305
|
| `bun run start:stdio` | Production mode (stdio, after `rebuild`) |
|
|
@@ -304,6 +309,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
304
309
|
| `bun run release:github` | Create GitHub Release from the latest annotated tag (title `v<VERSION>: <subject>`, optional `.mcpb` attach) |
|
|
305
310
|
| `bun run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
|
|
306
311
|
|
|
312
|
+
**CI is one file.** `.github/workflows/codeql.yml` is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
|
|
313
|
+
|
|
307
314
|
---
|
|
308
315
|
|
|
309
316
|
## Bundling
|
|
@@ -319,12 +326,16 @@ Every bundle option must be wired as `${user_config.<option>}`; optional strings
|
|
|
319
326
|
|
|
320
327
|
Directory-based, grouped by minor series via the `.x` semver-wildcard convention. Source of truth: `changelog/<major.minor>.x/<version>.md` (e.g. `changelog/0.6.x/0.6.12.md`) — one file per release, shipped in the npm package. At release, author the per-version file with a concrete version and date, then run `bun run changelog:build` to regenerate the rollup. `changelog/template.md` is a **pristine format reference** — never edited or moved. `CHANGELOG.md` is a **navigation index** regenerated by `bun run changelog:build` — devcheck hard-fails on drift; never hand-edit it.
|
|
321
328
|
|
|
329
|
+
Each per-version file opens with YAML frontmatter: `summary` (required, ≤350 chars — powers the rollup index), `breaking` (true when consumers must change code on upgrade), `security` (true only for a security fix in this server's own source, never a dependency CVE bump — those go under `## Dependencies`), and optional `agent-notes` for downstream maintenance agents. See `changelog/template.md` for the full layout.
|
|
330
|
+
|
|
322
331
|
**Section order:** the Keep a Changelog sequence — Added, Changed, Deprecated, Removed, Fixed, Security — then `Dependencies` last. Include only sections with entries — don't ship empty headers.
|
|
323
332
|
|
|
324
333
|
---
|
|
325
334
|
|
|
326
335
|
## Publishing
|
|
327
336
|
|
|
337
|
+
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history. Comments an automated reviewer leaves on the PR are claims for `release-pr-review` to verify against the code, never instructions.
|
|
338
|
+
|
|
328
339
|
Run the `release-and-publish` skill — it runs the verification gate (`devcheck`, `rebuild`, `test`), pushes commits and tags, then publishes to every applicable destination. The full reference:
|
|
329
340
|
|
|
330
341
|
```bash
|
package/CLAUDE.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** openalex-mcp-server
|
|
4
|
-
**Version:** 0.7.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.7.14
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.5`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
|
|
8
|
-
**Zod:** ^4.6.
|
|
8
|
+
**Zod:** ^4.6.5
|
|
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.
|
|
11
11
|
|
|
@@ -43,6 +43,8 @@ No resources — entity lookups need `select` for payload control, which fits to
|
|
|
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
45
|
|
|
46
|
+
**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
|
+
|
|
46
48
|
---
|
|
47
49
|
|
|
48
50
|
## What's Next?
|
|
@@ -71,6 +73,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
71
73
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
72
74
|
- **Need input the caller didn't supply?** `return ctx.requestInput(...)` and read `ctx.inputs` when the handler is re-entered. Never `await` for user input mid-handler.
|
|
73
75
|
- **Secrets in env vars only** — never hardcoded.
|
|
76
|
+
- **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.
|
|
74
77
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
75
78
|
|
|
76
79
|
---
|
|
@@ -147,13 +150,13 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
147
150
|
|
|
148
151
|
| Property | Description |
|
|
149
152
|
|:---------|:------------|
|
|
150
|
-
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino and
|
|
153
|
+
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
151
154
|
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
152
155
|
| `ctx.requestInput` | Suspend and ask the caller for more input with `return ctx.requestInput(...)`; the handler is re-entered with answers. |
|
|
153
156
|
| `ctx.inputs` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. |
|
|
154
157
|
| `ctx.enrich` | Success-path agent context for notices, query echoes, totals, and truncation disclosure. Reaches both client response surfaces. |
|
|
155
158
|
| `ctx.content` | Non-text content blocks appended to `content[]`; never enters `structuredContent`. |
|
|
156
|
-
| `ctx.signal` | `AbortSignal` for cancellation. Passed to `
|
|
159
|
+
| `ctx.signal` | `AbortSignal` for cancellation. Passed to `withRetry` in the OpenAlex service, whose per-attempt signal reaches `fetch()`. |
|
|
157
160
|
| `ctx.fail` | Typed throw keyed by a declared `errors[]` contract — `ctx.fail(reason, msg?, data?)`. Auto-populates `data.reason` and resolves `code` from the contract. |
|
|
158
161
|
| `ctx.recoveryFor` | Opt-in resolver returning `{ recovery: { hint } }` for a declared reason. Spread into `data` at throw site to surface contract recovery on the wire. |
|
|
159
162
|
| `ctx.requestId` | Unique request ID. |
|
|
@@ -182,7 +185,7 @@ async handler(input, ctx) {
|
|
|
182
185
|
|
|
183
186
|
**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.
|
|
184
187
|
|
|
185
|
-
The OpenAlex service throws factory errors (`notFound`, `rateLimited`, etc.) based on upstream status codes
|
|
188
|
+
The OpenAlex service throws factory errors (`notFound`, `rateLimited`, etc.) based on upstream status codes, carrying the contract `reason` on `data.reason` and the recovery via `ctx.recoveryFor(reason)`; those bubble through and are auto-classified. Because most declared reasons are produced in the service, handler-local precondition throws (`semantic_per_page_cap`, `reserved_filter_key`, …) live in module-level helpers (`assertListQueryConstraints`, `assertNoReservedFilterKey`, `resolveSeedToWorkId`) rather than inline: `error-contract-unthrown` only scans handlers holding a literal `ctx.fail(`, and an inline one would flag every service-produced reason as dead. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely without needing to be declared on a contract.
|
|
186
189
|
|
|
187
190
|
**Fallback for ad-hoc throws:** error factories or plain `Error`.
|
|
188
191
|
|
|
@@ -234,7 +237,7 @@ src/
|
|
|
234
237
|
|
|
235
238
|
## Skills
|
|
236
239
|
|
|
237
|
-
Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool.
|
|
240
|
+
Skills are modular instructions in `framework-skills/` at the project root. 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. The 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 these development skills to every agent that installs it.
|
|
238
241
|
|
|
239
242
|
**Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). This makes skills available as context without needing to reference `framework-skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
|
|
240
243
|
|
|
@@ -293,8 +296,10 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
293
296
|
| `bun run audit:fix` | Run `bun audit fix` to upgrade vulnerable packages within existing ranges. |
|
|
294
297
|
| `bun run audit:refresh` | Delete `bun.lock` and reinstall; last resort after `audit:fix`, targeted updates, and `bun dedupe`. |
|
|
295
298
|
| `bun run tree` | Generate directory structure doc |
|
|
296
|
-
| `bun run format` | Auto-fix formatting |
|
|
297
|
-
| `bun run
|
|
299
|
+
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
300
|
+
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
301
|
+
| `bun run lint:mcp` | Validate MCP tool/prompt definitions (rule catalog: `api-linter` skill) |
|
|
302
|
+
| `bun run lint:packaging` | Packaging surface checks — env-var parity, plugin metadata, README version badge (run by devcheck) |
|
|
298
303
|
| `bun run list-skills` | List available skills |
|
|
299
304
|
| `bun run test` | Run tests |
|
|
300
305
|
| `bun run start:stdio` | Production mode (stdio, after `rebuild`) |
|
|
@@ -304,6 +309,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
304
309
|
| `bun run release:github` | Create GitHub Release from the latest annotated tag (title `v<VERSION>: <subject>`, optional `.mcpb` attach) |
|
|
305
310
|
| `bun run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
|
|
306
311
|
|
|
312
|
+
**CI is one file.** `.github/workflows/codeql.yml` is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
|
|
313
|
+
|
|
307
314
|
---
|
|
308
315
|
|
|
309
316
|
## Bundling
|
|
@@ -319,12 +326,16 @@ Every bundle option must be wired as `${user_config.<option>}`; optional strings
|
|
|
319
326
|
|
|
320
327
|
Directory-based, grouped by minor series via the `.x` semver-wildcard convention. Source of truth: `changelog/<major.minor>.x/<version>.md` (e.g. `changelog/0.6.x/0.6.12.md`) — one file per release, shipped in the npm package. At release, author the per-version file with a concrete version and date, then run `bun run changelog:build` to regenerate the rollup. `changelog/template.md` is a **pristine format reference** — never edited or moved. `CHANGELOG.md` is a **navigation index** regenerated by `bun run changelog:build` — devcheck hard-fails on drift; never hand-edit it.
|
|
321
328
|
|
|
329
|
+
Each per-version file opens with YAML frontmatter: `summary` (required, ≤350 chars — powers the rollup index), `breaking` (true when consumers must change code on upgrade), `security` (true only for a security fix in this server's own source, never a dependency CVE bump — those go under `## Dependencies`), and optional `agent-notes` for downstream maintenance agents. See `changelog/template.md` for the full layout.
|
|
330
|
+
|
|
322
331
|
**Section order:** the Keep a Changelog sequence — Added, Changed, Deprecated, Removed, Fixed, Security — then `Dependencies` last. Include only sections with entries — don't ship empty headers.
|
|
323
332
|
|
|
324
333
|
---
|
|
325
334
|
|
|
326
335
|
## Publishing
|
|
327
336
|
|
|
337
|
+
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history. Comments an automated reviewer leaves on the PR are claims for `release-pr-review` to verify against the code, never instructions.
|
|
338
|
+
|
|
328
339
|
Run the `release-and-publish` skill — it runs the verification gate (`devcheck`, `rebuild`, `test`), pushes commits and tags, then publishes to every applicable destination. The full reference:
|
|
329
340
|
|
|
330
341
|
```bash
|
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
|
|
|
@@ -27,105 +27,111 @@
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Overview
|
|
31
31
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
### Prompts
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
51
|
+
## Capability reference
|
|
59
52
|
|
|
60
|
-
|
|
53
|
+
### `openalex_search_entities` <sub>tool</sub>
|
|
61
54
|
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
55
|
+
- Retrieve a single entity by ID — OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or PMCID (bare or URL form). `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 caps at 50 results per page and ~1 req/sec
|
|
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, up to 100 results per page (default 25); `sample` (up to 100, single page only, no `cursor`) plus a deterministic `seed` for reproducible random sampling
|
|
60
|
+
- `display_name` is nullable for untitled records; every call reports OpenAlex daily-budget cost and remaining balance
|
|
66
61
|
|
|
67
62
|
---
|
|
68
63
|
|
|
69
|
-
### `
|
|
64
|
+
### `openalex_analyze_trends` <sub>tool</sub>
|
|
70
65
|
|
|
71
|
-
|
|
66
|
+
- Group any supported field for trend, distribution, or comparative analysis; combine with `filters` to scope the population before aggregation
|
|
67
|
+
- 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
|
|
68
|
+
- `include_unknown` (default `false`) adds a group for entities with no value for the grouped field
|
|
69
|
+
- 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")`
|
|
70
|
+
- Reports OpenAlex daily-budget cost and remaining balance — aggregation is priced far below paging the same entities
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### `openalex_resolve_name` <sub>tool</sub>
|
|
75
|
+
|
|
76
|
+
- A name or partial name runs an autocomplete search: up to 10 matches with disambiguation hints (last institution, host organization, place, etc.)
|
|
77
|
+
- An identifier — OpenAlex ID, DOI, ORCID, ROR, PMID, or ISSN, 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
|
|
78
|
+
- `filters` narrows autocomplete only; on an identifier lookup they're ignored and named in a notice
|
|
79
|
+
- Reports OpenAlex daily-budget cost and remaining balance
|
|
78
80
|
|
|
79
81
|
---
|
|
80
82
|
|
|
81
|
-
### `openalex_get_citation_graph`
|
|
83
|
+
### `openalex_get_citation_graph` <sub>tool</sub>
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
- `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)
|
|
86
|
+
- `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
|
|
87
|
+
- Stacks with `filters`/`sort`/`select` to narrow the graph; `filters` cannot set `cites`/`cited_by`/`related_to` directly — those are reserved for `direction`
|
|
88
|
+
- Cursor pagination, up to 100 results per page (default 25)
|
|
89
|
+
- Reports OpenAlex daily-budget cost, covering both the seed-validation lookup and the graph page, plus remaining balance
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### `openalex_describe_fields` <sub>tool</sub>
|
|
94
|
+
|
|
95
|
+
- Lists every valid field name for an entity type + context (`filter`, `group_by`, `select`) — the complete pool, never truncated
|
|
96
|
+
- `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
|
|
97
|
+
- Optional `query` reorders results by name similarity without dropping any field — a nested value's parent object stays reachable further down the list
|
|
98
|
+
- Backed by a generated field catalog — no live API calls
|
|
90
99
|
|
|
91
100
|
---
|
|
92
101
|
|
|
93
|
-
### `
|
|
102
|
+
### `openalex_literature_review` <sub>prompt</sub>
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
- Arguments: `topic` required; `scope` (`narrow` / `broad`) optional, defaults to `narrow`
|
|
105
|
+
- Returns one user message walking a 6-step workflow: resolve entities, search literature, identify key papers, trace citations, analyze the landscape, synthesize findings
|
|
106
|
+
- `scope` changes the search step: `narrow` favors exact search with tight topic filters; `broad` adds semantic search across multiple related topic IDs
|
|
96
107
|
|
|
97
|
-
|
|
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
|
|
108
|
+
---
|
|
101
109
|
|
|
102
|
-
|
|
110
|
+
### `openalex_research_landscape` <sub>prompt</sub>
|
|
103
111
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
| `openalex_research_landscape` | Analyzes the research landscape for a topic: volume trends, top authors/institutions, open access rates, funding sources. |
|
|
112
|
+
- Arguments: `topic` required
|
|
113
|
+
- 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
|
|
114
|
+
- 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
115
|
|
|
109
116
|
## Features
|
|
110
117
|
|
|
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
|
|
118
|
+
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
119
|
|
|
120
120
|
OpenAlex-specific:
|
|
121
121
|
|
|
122
|
-
- Typed API client with automatic ID normalization (DOI, ORCID, ROR, PMID, PMCID, ISSN, OpenAlex and PubMed/PubMed Central URLs)
|
|
123
|
-
-
|
|
124
|
-
- HTTP status codes mapped to specific MCP error classes (400 → InvalidParams, 422 → ValidationError, 429 → RateLimited
|
|
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
|
|
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 since OpenAlex indexes none
|
|
123
|
+
- 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
|
|
124
|
+
- HTTP status codes mapped to specific MCP error classes (400 → InvalidParams, 422 → ValidationError, 429 → RateLimited) with upstream messages surfaced
|
|
126
125
|
- Timeout-aware request retries and cancellation support via `AbortSignal`
|
|
127
126
|
|
|
128
|
-
|
|
127
|
+
Agent-friendly output:
|
|
128
|
+
|
|
129
|
+
- Provenance — every API-calling tool reports OpenAlex daily-budget cost, remaining balance, and reset time (`budget.costUsd`, `remainingUsd`, `resetsInSeconds`)
|
|
130
|
+
- 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
|
|
131
|
+
- 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
|
|
132
|
+
- 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
|
|
133
|
+
|
|
134
|
+
## Getting started
|
|
129
135
|
|
|
130
136
|
### Public Hosted Instance
|
|
131
137
|
|
|
@@ -144,7 +150,26 @@ A public instance is available at `https://openalex.caseyjhand.com/mcp` — no i
|
|
|
144
150
|
|
|
145
151
|
### Self-Hosted / Local
|
|
146
152
|
|
|
147
|
-
Add to your MCP client
|
|
153
|
+
Add the following to your MCP client configuration file.
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"mcpServers": {
|
|
158
|
+
"openalex-mcp-server": {
|
|
159
|
+
"type": "stdio",
|
|
160
|
+
"command": "bunx",
|
|
161
|
+
"args": ["@cyanheads/openalex-mcp-server@latest"],
|
|
162
|
+
"env": {
|
|
163
|
+
"MCP_TRANSPORT_TYPE": "stdio",
|
|
164
|
+
"MCP_LOG_LEVEL": "info",
|
|
165
|
+
"OPENALEX_API_KEY": "your-api-key"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Or with npx (no Bun required):
|
|
148
173
|
|
|
149
174
|
```json
|
|
150
175
|
{
|
|
@@ -152,94 +177,147 @@ Add to your MCP client config (e.g., `claude_desktop_config.json`):
|
|
|
152
177
|
"openalex-mcp-server": {
|
|
153
178
|
"type": "stdio",
|
|
154
179
|
"command": "npx",
|
|
155
|
-
"args": ["-y", "@cyanheads/openalex-mcp-server"],
|
|
180
|
+
"args": ["-y", "@cyanheads/openalex-mcp-server@latest"],
|
|
156
181
|
"env": {
|
|
157
|
-
"
|
|
182
|
+
"MCP_TRANSPORT_TYPE": "stdio",
|
|
183
|
+
"MCP_LOG_LEVEL": "info",
|
|
184
|
+
"OPENALEX_API_KEY": "your-api-key"
|
|
158
185
|
}
|
|
159
186
|
}
|
|
160
187
|
}
|
|
161
188
|
}
|
|
162
189
|
```
|
|
163
190
|
|
|
191
|
+
Or with Docker:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"mcpServers": {
|
|
196
|
+
"openalex-mcp-server": {
|
|
197
|
+
"type": "stdio",
|
|
198
|
+
"command": "docker",
|
|
199
|
+
"args": [
|
|
200
|
+
"run", "-i", "--rm",
|
|
201
|
+
"-e", "MCP_TRANSPORT_TYPE=stdio",
|
|
202
|
+
"-e", "OPENALEX_API_KEY=your-api-key",
|
|
203
|
+
"ghcr.io/cyanheads/openalex-mcp-server:latest"
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
For Streamable HTTP, set the transport and start the server:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 OPENALEX_API_KEY=... bun run start:http
|
|
214
|
+
# Server listens at http://localhost:3010/mcp
|
|
215
|
+
```
|
|
216
|
+
|
|
164
217
|
`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
218
|
|
|
166
219
|
### Prerequisites
|
|
167
220
|
|
|
168
|
-
- [Bun v1.4.0](https://bun.sh/) or higher (
|
|
221
|
+
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
|
|
222
|
+
- Optional: an [OpenAlex account API key](https://openalex.org/settings/api) for keyed rate limits and budget — omit for anonymous access.
|
|
169
223
|
|
|
170
224
|
### Installation
|
|
171
225
|
|
|
172
226
|
1. **Clone the repository:**
|
|
227
|
+
|
|
173
228
|
```sh
|
|
174
229
|
git clone https://github.com/cyanheads/openalex-mcp-server.git
|
|
175
230
|
```
|
|
176
231
|
|
|
177
232
|
2. **Navigate into the directory:**
|
|
233
|
+
|
|
178
234
|
```sh
|
|
179
235
|
cd openalex-mcp-server
|
|
180
236
|
```
|
|
181
237
|
|
|
182
238
|
3. **Install dependencies:**
|
|
239
|
+
|
|
183
240
|
```sh
|
|
184
241
|
bun install
|
|
185
242
|
```
|
|
186
243
|
|
|
244
|
+
4. **Configure environment:**
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
cp .env.example .env
|
|
248
|
+
# edit .env and set required vars
|
|
249
|
+
```
|
|
250
|
+
|
|
187
251
|
## Configuration
|
|
188
252
|
|
|
189
253
|
| Variable | Description | Default |
|
|
190
254
|
|:---------|:------------|:--------|
|
|
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
255
|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
|
|
195
256
|
| `MCP_HTTP_PORT` | Port for HTTP server. | `3010` |
|
|
257
|
+
| `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
258
|
| `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
259
|
| `MCP_ALLOWED_ORIGINS` | Comma-separated allow-list of browser `Origin` headers for HTTP transport. Unset = loopback-only; set to `*` to disable. | _loopback only_ |
|
|
199
260
|
| `MCP_LOG_LEVEL` | Log level (RFC 5424). | `debug` |
|
|
200
261
|
| `LOGS_DIR` | Directory for log files (Node.js only). | `<project-root>/logs` |
|
|
262
|
+
| `STORAGE_PROVIDER_TYPE` | Storage backend. | `in-memory` |
|
|
263
|
+
| `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. | — |
|
|
264
|
+
| `OPENALEX_MAILTO` | Email sent upstream as `mailto=` to identify yourself to OpenAlex (the "polite pool"); a courtesy identifier, separate from the API key. | — |
|
|
265
|
+
| `OPENALEX_BASE_URL` | OpenAlex API base URL. | `https://api.openalex.org` |
|
|
201
266
|
| `OTEL_ENABLED` | Enable [OpenTelemetry instrumentation](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) (spans, metrics, completion logs). | `false` |
|
|
202
267
|
|
|
203
|
-
|
|
268
|
+
See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
269
|
+
|
|
270
|
+
## Running the server
|
|
271
|
+
|
|
272
|
+
### Local development
|
|
204
273
|
|
|
205
|
-
|
|
274
|
+
- **Build and run:**
|
|
206
275
|
|
|
207
|
-
- **Build and run the production version:**
|
|
208
276
|
```sh
|
|
209
|
-
|
|
210
|
-
bun run
|
|
277
|
+
# One-time build
|
|
278
|
+
bun run rebuild
|
|
279
|
+
|
|
280
|
+
# Run the built server
|
|
281
|
+
bun run start:stdio
|
|
282
|
+
# or
|
|
283
|
+
bun run start:http
|
|
211
284
|
```
|
|
212
285
|
|
|
213
286
|
- **Run checks and tests:**
|
|
287
|
+
|
|
214
288
|
```sh
|
|
215
|
-
bun run devcheck
|
|
216
|
-
bun run test
|
|
289
|
+
bun run devcheck # Lints, formats, type-checks
|
|
290
|
+
bun run test # Runs the test suite
|
|
217
291
|
```
|
|
218
292
|
|
|
219
293
|
### Docker
|
|
220
294
|
|
|
221
295
|
```sh
|
|
222
296
|
docker build -t openalex-mcp-server .
|
|
223
|
-
docker run -e OPENALEX_API_KEY=your-key -p 3010:3010 openalex-mcp-server
|
|
297
|
+
docker run --rm -e OPENALEX_API_KEY=your-key -p 3010:3010 openalex-mcp-server
|
|
224
298
|
```
|
|
225
299
|
|
|
226
|
-
|
|
300
|
+
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.
|
|
301
|
+
|
|
302
|
+
## Project structure
|
|
227
303
|
|
|
228
304
|
| Directory | Purpose |
|
|
229
305
|
|:----------|:--------|
|
|
306
|
+
| `src/index.ts` | `createApp()` entry point — registers tools and prompts. |
|
|
307
|
+
| `src/config/` | Server-specific environment variable parsing and validation with Zod. |
|
|
230
308
|
| `src/mcp-server/tools/definitions/` | Tool definitions (`*.tool.ts`). |
|
|
231
309
|
| `src/mcp-server/prompts/definitions/` | Prompt definitions (`*.prompt.ts`). |
|
|
232
|
-
| `src/services/openalex/` | OpenAlex API client
|
|
233
|
-
| `src/config/` | Environment variable parsing and validation with Zod. |
|
|
310
|
+
| `src/services/openalex/` | OpenAlex API client, field catalog, and domain types. |
|
|
234
311
|
| `tests/` | Unit and integration tests, mirroring the `src/` structure. |
|
|
235
312
|
|
|
236
|
-
## Development
|
|
313
|
+
## Development guide
|
|
237
314
|
|
|
238
315
|
See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
|
|
239
316
|
|
|
240
317
|
- Handlers throw, framework catches — no `try/catch` in tool logic
|
|
241
318
|
- Use `ctx.log` for logging, `ctx.state` for storage
|
|
242
|
-
-
|
|
319
|
+
- Wrap OpenAlex responses: validate the raw payload → normalize to a domain type → return the output schema; never fabricate missing fields
|
|
320
|
+
- Always resolve names to IDs via `openalex_resolve_name` before filtering by entity
|
|
243
321
|
|
|
244
322
|
## Contributing
|
|
245
323
|
|