@cyanheads/openalex-mcp-server 0.7.16 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +48 -33
- package/CLAUDE.md +48 -33
- package/Dockerfile +98 -33
- package/README.md +23 -16
- package/changelog/0.8.x/0.8.0.md +38 -0
- package/changelog/0.9.x/0.9.0.md +48 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/prompts/definitions/literature-review.prompt.js +1 -1
- package/dist/mcp-server/prompts/definitions/research-landscape.prompt.js +1 -1
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts +28 -3
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.js +57 -10
- package/dist/mcp-server/tools/definitions/analyze-trends.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts +62 -5
- package/dist/mcp-server/tools/definitions/citation-graph.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/citation-graph.tool.js +100 -52
- package/dist/mcp-server/tools/definitions/citation-graph.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/describe-fields.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/describe-fields.tool.js +34 -4
- package/dist/mcp-server/tools/definitions/describe-fields.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts +38 -2
- package/dist/mcp-server/tools/definitions/resolve-name.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/resolve-name.tool.js +176 -41
- package/dist/mcp-server/tools/definitions/resolve-name.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts +75 -4
- package/dist/mcp-server/tools/definitions/search-entities.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-entities.tool.js +169 -73
- package/dist/mcp-server/tools/definitions/search-entities.tool.js.map +1 -1
- package/dist/mcp-server/tools/render-budget.d.ts +4 -2
- package/dist/mcp-server/tools/render-budget.d.ts.map +1 -1
- package/dist/mcp-server/tools/render-budget.js +4 -2
- package/dist/mcp-server/tools/render-budget.js.map +1 -1
- package/dist/mcp-server/tools/render-entity-record.d.ts +12 -1
- package/dist/mcp-server/tools/render-entity-record.d.ts.map +1 -1
- package/dist/mcp-server/tools/render-entity-record.js +23 -8
- package/dist/mcp-server/tools/render-entity-record.js.map +1 -1
- package/dist/mcp-server/tools/response-budget.d.ts +160 -0
- package/dist/mcp-server/tools/response-budget.d.ts.map +1 -0
- package/dist/mcp-server/tools/response-budget.js +489 -0
- package/dist/mcp-server/tools/response-budget.js.map +1 -0
- package/dist/services/openalex/budget.d.ts +3 -1
- package/dist/services/openalex/budget.d.ts.map +1 -1
- package/dist/services/openalex/budget.js +3 -1
- package/dist/services/openalex/budget.js.map +1 -1
- package/dist/services/openalex/field-catalog.json +50 -1
- package/dist/services/openalex/filter-values.d.ts +46 -0
- package/dist/services/openalex/filter-values.d.ts.map +1 -0
- package/dist/services/openalex/filter-values.js +294 -0
- package/dist/services/openalex/filter-values.js.map +1 -0
- package/dist/services/openalex/iso-codes.json +1565 -0
- package/dist/services/openalex/openalex-service.d.ts +181 -16
- package/dist/services/openalex/openalex-service.d.ts.map +1 -1
- package/dist/services/openalex/openalex-service.js +593 -141
- package/dist/services/openalex/openalex-service.js.map +1 -1
- package/dist/services/openalex/provider-text.d.ts +13 -5
- package/dist/services/openalex/provider-text.d.ts.map +1 -1
- package/dist/services/openalex/provider-text.js +799 -51
- package/dist/services/openalex/provider-text.js.map +1 -1
- package/dist/services/openalex/types.d.ts +1 -1
- package/dist/services/openalex/types.js +1 -1
- package/package.json +16 -13
- package/server.json +10 -24
package/Dockerfile
CHANGED
|
@@ -3,8 +3,18 @@
|
|
|
3
3
|
#
|
|
4
4
|
# This stage installs all dependencies (including dev), builds the TypeScript
|
|
5
5
|
# source code into JavaScript, and prepares the production assets.
|
|
6
|
+
#
|
|
7
|
+
# Pinned to $BUILDPLATFORM rather than the target platform: `bun run build` emits
|
|
8
|
+
# JavaScript, and only `dist/` crosses into the production stage. Built for the
|
|
9
|
+
# target instead, the non-native leg of a `--platform linux/amd64,linux/arm64`
|
|
10
|
+
# build runs under QEMU, where bun >= 1.4 aborts with a JavaScriptCore allocator
|
|
11
|
+
# assertion and fails the multi-arch push.
|
|
12
|
+
#
|
|
13
|
+
# The constraint this assumes: the build stage produces platform-independent
|
|
14
|
+
# output. A stage that compiles a native addon needs the target-arch toolchain
|
|
15
|
+
# and cannot cross-compile this way — drop the flag there.
|
|
6
16
|
# ==============================================================================
|
|
7
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
17
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.3 AS build
|
|
8
18
|
|
|
9
19
|
WORKDIR /usr/src/app
|
|
10
20
|
|
|
@@ -23,53 +33,106 @@ COPY . .
|
|
|
23
33
|
RUN bun run build
|
|
24
34
|
|
|
25
35
|
|
|
36
|
+
# ==============================================================================
|
|
37
|
+
# Production Dependencies Stage
|
|
38
|
+
#
|
|
39
|
+
# Installs the production dependency tree for the target platform. Every step
|
|
40
|
+
# here can run JavaScript — bunfig.toml's security scanner runs as a Bun
|
|
41
|
+
# program, and so do the OTel and musl-prune scripts — so the stage runs on
|
|
42
|
+
# $BUILDPLATFORM and cross-installs with `--os`/`--cpu`, which pick each
|
|
43
|
+
# platform-specific optional dependency (native bindings such as DuckDB's) for
|
|
44
|
+
# the target. Only `node_modules` leaves this stage.
|
|
45
|
+
#
|
|
46
|
+
# A clean image rather than `FROM build`: the build stage's node_modules holds
|
|
47
|
+
# devDependencies.
|
|
48
|
+
# ==============================================================================
|
|
49
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.3 AS deps
|
|
50
|
+
|
|
51
|
+
WORKDIR /usr/src/app
|
|
52
|
+
|
|
53
|
+
# Copy dependency manifests. `bunfig.toml` rides along so every install below
|
|
54
|
+
# passes its release-age gate and security scanner, as a local install does.
|
|
55
|
+
COPY package.json bun.lock bunfig.toml ./
|
|
56
|
+
|
|
57
|
+
# The scanner bunfig.toml names is a devDependency, and Bun installs a missing
|
|
58
|
+
# scanner through the same production-filtered install, which omits it and
|
|
59
|
+
# aborts. Seed it from the build stage's full install instead. Remove this line,
|
|
60
|
+
# and the `rm` at the end of this stage, if bunfig.toml stops naming a scanner.
|
|
61
|
+
COPY --from=build /usr/src/app/node_modules/@socketsecurity/bun-security-scanner ./node_modules/@socketsecurity/bun-security-scanner
|
|
62
|
+
|
|
63
|
+
# Docker names the target architecture `amd64`/`arm64`; Bun's `--cpu` takes
|
|
64
|
+
# `x64`/`arm64`. Mapped once here, read by both installs below. `oven/bun`
|
|
65
|
+
# publishes only these two architectures, so any other target fails here.
|
|
66
|
+
ARG TARGETOS
|
|
67
|
+
ARG TARGETARCH
|
|
68
|
+
RUN case "$TARGETARCH" in \
|
|
69
|
+
amd64) echo x64 ;; \
|
|
70
|
+
arm64) echo arm64 ;; \
|
|
71
|
+
*) echo "Unsupported TARGETARCH '$TARGETARCH': expected amd64 or arm64" >&2; exit 1 ;; \
|
|
72
|
+
esac > .bun-cpu
|
|
73
|
+
|
|
74
|
+
# Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
|
|
75
|
+
# that are not needed in the final production image.
|
|
76
|
+
# `--omit=peer` drops the framework's optional peer tiers (test runner, service
|
|
77
|
+
# SDKs, parsers) that Bun would otherwise auto-install. Anything this server
|
|
78
|
+
# actually imports belongs in its own `dependencies`, so nothing needed at
|
|
79
|
+
# runtime is lost.
|
|
80
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
81
|
+
bun install --production --omit=peer --frozen-lockfile --ignore-scripts \
|
|
82
|
+
--os="$TARGETOS" --cpu="$(cat .bun-cpu)"
|
|
83
|
+
|
|
84
|
+
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
85
|
+
# Installed by default. Omit them for a leaner image at build time
|
|
86
|
+
# with: docker build --build-arg OTEL_ENABLED=false
|
|
87
|
+
# The script reads the list and each range from the installed framework's
|
|
88
|
+
# `peerDependencies` and passes the target flags on to its `bun install`.
|
|
89
|
+
COPY scripts/install-otel.ts ./scripts/
|
|
90
|
+
ARG OTEL_ENABLED=true
|
|
91
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
92
|
+
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
93
|
+
bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# `--os`/`--cpu` have no libc counterpart, so a native dependency published in
|
|
97
|
+
# glibc and musl variants (DuckDB's bindings, for one) installs both. The
|
|
98
|
+
# runtime image is Debian (glibc) and never loads the musl copy; the script
|
|
99
|
+
# deletes every package whose own `libc` admits only musl. It follows every
|
|
100
|
+
# install, since a later `bun install` restores what it removes. A server on a
|
|
101
|
+
# musl (Alpine) runtime image drops these two lines.
|
|
102
|
+
COPY scripts/prune-musl-packages.ts ./scripts/
|
|
103
|
+
RUN bun scripts/prune-musl-packages.ts
|
|
104
|
+
|
|
105
|
+
# The seeded scanner served only the installs above; keep it out of the image.
|
|
106
|
+
RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
107
|
+
|
|
108
|
+
|
|
26
109
|
# ==============================================================================
|
|
27
110
|
# Production Stage
|
|
28
111
|
#
|
|
29
112
|
# This stage creates a minimal, optimized, and secure image for running the
|
|
30
113
|
# application. It uses a slim base image and only includes production
|
|
31
|
-
# dependencies and build artifacts.
|
|
114
|
+
# dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
|
|
115
|
+
# and CMD, which run on the real target at container start.
|
|
32
116
|
# ==============================================================================
|
|
33
|
-
FROM oven/bun:1.4.
|
|
117
|
+
FROM oven/bun:1.4.3-slim AS production
|
|
34
118
|
|
|
35
119
|
WORKDIR /usr/src/app
|
|
36
120
|
|
|
37
|
-
# Set the environment to production for performance
|
|
38
|
-
# production dependencies are installed.
|
|
121
|
+
# Set the environment to production for performance.
|
|
39
122
|
ENV NODE_ENV=production
|
|
40
123
|
|
|
41
124
|
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
125
|
+
ARG APP_VERSION
|
|
42
126
|
LABEL org.opencontainers.image.title="openalex-mcp-server"
|
|
43
|
-
LABEL org.opencontainers.image.description="Access the OpenAlex academic research catalog
|
|
127
|
+
LABEL org.opencontainers.image.description="Access the OpenAlex academic research catalog — 270M+ publications — via MCP. STDIO or Streamable HTTP."
|
|
44
128
|
LABEL org.opencontainers.image.source="https://github.com/cyanheads/openalex-mcp-server"
|
|
45
129
|
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
130
|
+
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
46
131
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# that are not needed in the final production image. `--omit=peer` keeps the
|
|
52
|
-
# framework's optional peer tiers out; imported peers belong in dependencies.
|
|
53
|
-
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
54
|
-
bun install --production --omit=peer --frozen-lockfile --ignore-scripts
|
|
55
|
-
|
|
56
|
-
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
57
|
-
# These are not bundled by default to keep the base image lean. Enable at build time
|
|
58
|
-
# with: docker build --build-arg OTEL_ENABLED=true
|
|
59
|
-
ARG OTEL_ENABLED=true
|
|
60
|
-
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
61
|
-
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
62
|
-
bun add --omit=dev --omit=peer --ignore-scripts @hono/otel \
|
|
63
|
-
@opentelemetry/instrumentation-http \
|
|
64
|
-
@opentelemetry/exporter-metrics-otlp-http \
|
|
65
|
-
@opentelemetry/exporter-trace-otlp-http \
|
|
66
|
-
@opentelemetry/instrumentation-pino \
|
|
67
|
-
@opentelemetry/resources \
|
|
68
|
-
@opentelemetry/sdk-metrics \
|
|
69
|
-
@opentelemetry/sdk-node \
|
|
70
|
-
@opentelemetry/sdk-trace-node \
|
|
71
|
-
@opentelemetry/semantic-conventions; \
|
|
72
|
-
fi
|
|
132
|
+
# The manifest comes from the build context: the deps stage's copy was rewritten
|
|
133
|
+
# by the OTel install, and the runtime reads only its name, version, and type.
|
|
134
|
+
COPY package.json ./
|
|
135
|
+
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
73
136
|
|
|
74
137
|
# Copy the compiled application code from the build stage
|
|
75
138
|
COPY --from=build /usr/src/app/dist ./dist
|
|
@@ -95,10 +158,12 @@ ENV MCP_TRANSPORT_TYPE="http"
|
|
|
95
158
|
ENV MCP_SESSION_MODE="stateless"
|
|
96
159
|
ENV MCP_LOG_LEVEL="info"
|
|
97
160
|
ENV LOGS_DIR="/var/log/openalex-mcp-server"
|
|
98
|
-
ENV MCP_FORCE_CONSOLE_LOGGING="true"
|
|
99
161
|
|
|
100
162
|
# Expose the port the server listens on
|
|
101
163
|
EXPOSE ${MCP_HTTP_PORT}
|
|
102
164
|
|
|
165
|
+
# Health check using a bun-native fetch (slim image ships no curl/wget)
|
|
166
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD bun -e "fetch('http://localhost:'+(process.env.MCP_HTTP_PORT??'3010')+'/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
167
|
+
|
|
103
168
|
# The command to start the server
|
|
104
169
|
CMD ["bun", "run", "dist/index.js"]
|
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>@cyanheads/openalex-mcp-server</h1>
|
|
3
|
-
<p><b>Access the OpenAlex academic research catalog
|
|
3
|
+
<p><b>Access the OpenAlex academic research catalog — 270M+ publications — via MCP. STDIO or Streamable HTTP.</b>
|
|
4
4
|
<div>5 Tools • 2 Prompts</div>
|
|
5
5
|
</p>
|
|
6
6
|
</div>
|
|
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
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
## Overview
|
|
31
31
|
|
|
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.
|
|
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.
|
|
33
33
|
|
|
34
34
|
### Tools
|
|
35
35
|
|
|
@@ -52,12 +52,15 @@ Scholarly catalog data from [OpenAlex](https://openalex.org) — 270M+ works, 90
|
|
|
52
52
|
|
|
53
53
|
### `openalex_search_entities` <sub>tool</sub>
|
|
54
54
|
|
|
55
|
-
- Retrieve a single entity by ID — OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or PMCID (bare or URL form), or a keyword by its slug or keyword URL. `id` takes precedence: search parameters passed alongside it are dropped, and the response names which ones. A PMCID resolves nothing (OpenAlex indexes none) — use the work's PMID or DOI instead
|
|
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
|
-
- Rich filter syntax: AND across fields, OR within a field (`|`), NOT (`!`), ranges, comparisons
|
|
58
|
-
- `
|
|
57
|
+
- 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
|
+
- 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
|
+
- `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 `["*"]`
|
|
59
60
|
- Cursor pagination for keyword and exact search, up to 100 results per page (default 25); semantic search walks its candidates with `page` (1-based) instead, and mixing either knob with the wrong `search_mode` is rejected before the upstream call
|
|
60
61
|
- `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
|
+
- 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 — the most OpenAlex returns in a list response — is flagged as possibly capped, with the unbilled `id` + `slice` call that fetches the rest of that array
|
|
61
64
|
- `display_name` is nullable for untitled records; every call reports OpenAlex daily-budget cost and remaining balance
|
|
62
65
|
|
|
63
66
|
---
|
|
@@ -67,7 +70,8 @@ Scholarly catalog data from [OpenAlex](https://openalex.org) — 270M+ works, 90
|
|
|
67
70
|
- Group any supported field for trend, distribution, or comparative analysis; combine with `filters` to scope the population before aggregation
|
|
68
71
|
- Up to 200 groups per page (default). `order: "count"` (default) returns the top-N by count with no further pages; `order: "key"` enumerates all distinct values key-ascending with cursor pagination
|
|
69
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`
|
|
70
|
-
- Not every field is groupable — raw date fields, `.search` operators, `from_*`/`to_*` range modifiers, decimal scores such as `fwci`, `display_name`,
|
|
73
|
+
- 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
|
+
- 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
|
|
71
75
|
- Reports OpenAlex daily-budget cost and remaining balance — aggregation is priced far below paging the same entities
|
|
72
76
|
|
|
73
77
|
---
|
|
@@ -75,8 +79,9 @@ Scholarly catalog data from [OpenAlex](https://openalex.org) — 270M+ works, 90
|
|
|
75
79
|
### `openalex_resolve_name` <sub>tool</sub>
|
|
76
80
|
|
|
77
81
|
- A name or partial name runs an autocomplete search: up to 10 matches with disambiguation hints (last institution, host organization, place, etc.)
|
|
78
|
-
- An identifier — OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or keyword URL, bare or in URL form — resolves directly to the
|
|
79
|
-
- `
|
|
82
|
+
- An identifier — OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or keyword URL, bare or in URL form — resolves directly to the record it addresses, as the entity type it belongs to. A ROR can name an organization's institution, funder, and publisher records at once: with no `entity_type` (or any other type) all three are looked up in parallel as unbilled singleton fetches and every record found is returned, institution first; `entity_type` `institutions`, `funders`, or `publishers` narrows it to that one. A Crossref Funder Registry DOI (`10.13039/…`) resolves to the funder unless `entity_type` is `works`. A PMCID is recognized but resolves nothing — OpenAlex indexes none
|
|
83
|
+
- Short names share the bare OpenAlex ID shape (keyword `p53` reads as publisher `P53`), so a bare OpenAlex ID that resolves nothing is searched as a name next, under `entity_type` when given, and the notice names both outcomes; a real ID still resolves with its one request. Under `entity_type: "keywords"` the shape is searched as a keyword name from the start
|
|
84
|
+
- `filters` narrows autocomplete only and requires `entity_type` — a name query passing `filters` without it is rejected before any request (`filters_without_entity_type`). On an identifier lookup they're ignored and named in a notice; a name search that matches nothing under `filters` echoes them and suggests loosening them first
|
|
80
85
|
- With `entity_type` set, OpenAlex autocomplete fails on a name over 1,000 characters; that failure is reported once as `query_too_long` with advice to shorten the name, not retried as an outage
|
|
81
86
|
- Reports OpenAlex daily-budget cost and remaining balance
|
|
82
87
|
|
|
@@ -85,9 +90,10 @@ Scholarly catalog data from [OpenAlex](https://openalex.org) — 270M+ works, 90
|
|
|
85
90
|
### `openalex_get_citation_graph` <sub>tool</sub>
|
|
86
91
|
|
|
87
92
|
- `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)
|
|
88
|
-
- `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
|
|
89
|
-
- Stacks with `filters`/`sort`/`select` to narrow the graph; `filters` cannot set `cites`/`cited_by`/`related_to`, nor an alias of one such as `cited_works` — those keys are reserved for `direction`
|
|
93
|
+
- `seed_id` accepts an OpenAlex work ID (bare, or its OpenAlex record-page or API URL), 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, and the ID of another entity type fails as `id_type_mismatch` before any upstream call
|
|
94
|
+
- Stacks with `filters`/`sort`/`select` (`author_list` included) to narrow the graph; `filters` cannot set `cites`/`cited_by`/`related_to`, nor an alias of one such as `cited_works` — those keys are reserved for `direction`
|
|
90
95
|
- Cursor pagination, up to 100 results per page (default 25)
|
|
96
|
+
- The same 64,000-byte response budget and 100-authorship cap disclosure as `openalex_search_entities`; works cut from a page are named in `omitted` with the `openalex_search_entities` call that returns them, in OpenAlex's order
|
|
91
97
|
- Reports OpenAlex daily-budget cost, covering both the seed-validation lookup and the graph page, plus remaining balance
|
|
92
98
|
|
|
93
99
|
---
|
|
@@ -96,6 +102,7 @@ Scholarly catalog data from [OpenAlex](https://openalex.org) — 270M+ works, 90
|
|
|
96
102
|
|
|
97
103
|
- Lists every valid field name for an entity type + context (`filter`, `group_by`, `select`) — the complete pool, never truncated
|
|
98
104
|
- `group_by` is the filter set minus what OpenAlex rejects as an aggregation key: raw date fields, `.search`/`.search.exact` operators, `from_*`/`to_*` range modifiers, and a per-entity-type set found by grouping every listed field against the live API — decimal scores such as `fwci`, several source year fields, `display_name`, and external-ID fields among them
|
|
105
|
+
- `created_date`, `updated_date`, and their `from_`/`to_` forms are listed filters that OpenAlex serves only on a paid plan; without one, a filter on them fails on its first request as `upstream_plan_required`, with no retry
|
|
99
106
|
- Optional `query` reorders results by name similarity without dropping any field — a nested value's parent object stays reachable further down the list
|
|
100
107
|
- Backed by a generated field catalog — no live API calls
|
|
101
108
|
|
|
@@ -113,7 +120,7 @@ Scholarly catalog data from [OpenAlex](https://openalex.org) — 270M+ works, 90
|
|
|
113
120
|
|
|
114
121
|
- Arguments: `topic` required
|
|
115
122
|
- 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
|
|
116
|
-
- The funding step groups by `
|
|
123
|
+
- The funding step groups by `funders.id`, which counts every work that lists a funder and labels each group with the funder's name; `awards.*` fields stay the route for grant-level questions such as award numbers
|
|
117
124
|
|
|
118
125
|
## Features
|
|
119
126
|
|
|
@@ -121,9 +128,9 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
|
|
|
121
128
|
|
|
122
129
|
OpenAlex-specific:
|
|
123
130
|
|
|
124
|
-
- 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
|
|
131
|
+
- Typed API client with automatic ID normalization (DOI, ORCID, ROR, PMID, PMCID, ISSN, OpenAlex record-page and API URLs over `http`, `https`, or no scheme, PubMed/PubMed Central URLs); a PMCID normalizes but resolves nothing since OpenAlex indexes none
|
|
125
132
|
- 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
|
|
126
|
-
- HTTP status codes mapped to specific MCP error classes (400 → InvalidParams, 422 → ValidationError
|
|
133
|
+
- HTTP status codes mapped to specific MCP error classes (400 → InvalidParams, or ServiceUnavailable when a semantic search's query embedding failed upstream; 422 → ValidationError; 429 → RateLimited, or Forbidden when the 429 refuses a paid-plan filter) with upstream messages surfaced
|
|
127
134
|
- Timeout-aware request retries and cancellation support via `AbortSignal`
|
|
128
135
|
|
|
129
136
|
Agent-friendly output:
|
|
@@ -132,13 +139,13 @@ Agent-friendly output:
|
|
|
132
139
|
- 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
|
|
133
140
|
- 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
|
|
134
141
|
- 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
|
|
135
|
-
- Clean provider text — OpenAlex passes titles, abstracts, and names through unsanitized; HTML entities are decoded against the full WHATWG table, comments and HTML/JATS/MathML formatting tags are removed, and literal text such as `A < B` is kept. The text output escapes that text for Markdown, so a stray `*`, `<tag>`, or `[x](y)` renders as written; IDs, and URLs that render as links, stay byte-identical
|
|
142
|
+
- Clean provider text — OpenAlex passes titles, abstracts, and names through unsanitized; HTML entities are decoded against the full WHATWG table, comments and HTML/JATS/MathML formatting tags are removed, CDATA wrappers and IOP's `<?CDATA …?>` math markers are unwrapped with their TeX kept, MathType equation blobs (`% MathType!MTEF!…!44A1!`) are removed while the TeX that restates them stays, and a double-escaped line break (a literal `\n`) becomes a space; literal text such as `A < B`, `95% CI`, and TeX such as `\nu` is kept. Identifier and URL fields (`id`, `doi`, `ids`, `orcid`, `ror`, `issn`, `*_url`, lineages) are returned exactly as OpenAlex stores them, so each one resolves when passed back. The text output escapes that text for Markdown, so a stray `*`, `<tag>`, or `[x](y)` renders as written; IDs, and URLs that render as links, stay byte-identical
|
|
136
143
|
|
|
137
144
|
## Getting started
|
|
138
145
|
|
|
139
146
|
### Public Hosted Instance
|
|
140
147
|
|
|
141
|
-
A public instance is available at `https://openalex.caseyjhand.com/mcp` — no installation required. Point
|
|
148
|
+
A public instance is available at `https://openalex.caseyjhand.com/mcp` — no installation required. Point an MCP client that supports remote servers at it via Streamable HTTP (Claude Desktop adds remote servers under Customize > Connectors > Add custom connector; its `claude_desktop_config.json` holds local servers only):
|
|
142
149
|
|
|
143
150
|
```json
|
|
144
151
|
{
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Search and citation-graph responses hold to a 64,000-byte budget, with continuations for cut records and a new slice input for windowed arrays, alongside provider-text and identifier fixes."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.8.0 — 2026-09-30
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`slice: {field, offset}`** on `openalex_search_entities` `id` lookups — pages one top-level array of the record from `offset`, as many elements as fit the budget (at least one), with the window and its `next` call in `windows`; it replaces `select`. `slice_without_id` and `slice_field_not_array` reject a `slice` with no `id`, or one naming `*` or a field that is not an array. ([#72](https://github.com/cyanheads/openalex-mcp-server/issues/72))
|
|
12
|
+
|
|
13
|
+
## Changed
|
|
14
|
+
|
|
15
|
+
- **Response budget on `openalex_search_entities` and `openalex_get_citation_graph`** — `structuredContent` and `content[]` each hold to 64,000 bytes. An oversized page returns fewer records and names the rest in a new `omitted` field, whose `next` is the `openalex_search_entities` call that returns them (an `openalex` ID filter, `id` for keywords); `next_cursor` is unchanged. ([#72](https://github.com/cyanheads/openalex-mcp-server/issues/72))
|
|
16
|
+
- **Array windows** — a record too large on its own keeps its non-array fields and returns its largest arrays in part, each listed in a new `windows` field with an `id` + `slice` `next` call; `over_budget: true` marks a response whose smallest form still exceeds the budget. ([#72](https://github.com/cyanheads/openalex-mcp-server/issues/72))
|
|
17
|
+
- **`openalex_literature_review`** step 3 names the `slice` call that pages a windowed `authorships` array. ([#72](https://github.com/cyanheads/openalex-mcp-server/issues/72))
|
|
18
|
+
- **Error envelopes** carry `data.requestId`, and a tool error's `content[]` closes with the request ID (mcp-ts-core 0.13.10).
|
|
19
|
+
- **Docker image** — production dependencies and the OTel peers install in a `$BUILDPLATFORM` `deps` stage that cross-targets the image platform; the image gains a `HEALTHCHECK` on `/healthz` and an `org.opencontainers.image.version` label, and no longer sets `MCP_FORCE_CONSOLE_LOGGING`.
|
|
20
|
+
- **Packaging** — the npm package no longer ships `dist/*.mcpb`, the Docker build context excludes the MCP Registry token files and `.env.bak-*` backups, and the `.mcpb` bundle excludes `docs/idea.md` and `logs/`.
|
|
21
|
+
|
|
22
|
+
## Fixed
|
|
23
|
+
|
|
24
|
+
- **Provider-text delimiters** — CDATA sections, including unterminated ones and ones closed by a bare `]]`, and IOP's `<?CDATA …?>` are unwrapped with their TeX kept, the `<?MML …?>` restating them is removed, an unclosed `<!--inline-formula>` opener is removed as a tag, and a literal `\n` becomes a space; TeX such as `\nu` is kept. ([#89](https://github.com/cyanheads/openalex-mcp-server/issues/89))
|
|
25
|
+
- **Identifier and URL fields** (such as `id`, `doi`, `ids`, `orcid`, `ror`, `issn_l`, and every `*_url`) are returned exactly as OpenAlex stores them rather than entity-decoded, so a returned DOI resolves when passed back. ([#94](https://github.com/cyanheads/openalex-mcp-server/issues/94))
|
|
26
|
+
- **Possibly capped authorships** — a search or citation-graph work carrying exactly 100 `authorships`, the most OpenAlex returns on a list record, gets a `windows` entry with `possibly_capped: true` whose `next` is the `id` + `slice` call for the rest. ([#73](https://github.com/cyanheads/openalex-mcp-server/issues/73))
|
|
27
|
+
- **`openalex_analyze_trends` on `authorships.countries`** — a works group_by on it notes that OpenAlex counts only each work's first 100 authorships and names `authorships.institutions.country_code`, which counts every one. ([#91](https://github.com/cyanheads/openalex-mcp-server/issues/91))
|
|
28
|
+
- **`upstream_budget_exhausted` recovery** on all four tools says only that the server's daily budget refills at midnight UTC; the `OPENALEX_API_KEY` advice is added only when the server sends no key. ([#90](https://github.com/cyanheads/openalex-mcp-server/issues/90))
|
|
29
|
+
- **`Object.prototype` names** — `select` fields and works `filter` keys such as `constructor` or `__proto__` pass through unchanged instead of resolving to the inherited member, `openalex_resolve_name` sends a `constructor:`-prefixed query to autocomplete, and a record key named `__proto__` survives normalization. ([#93](https://github.com/cyanheads/openalex-mcp-server/issues/93))
|
|
30
|
+
|
|
31
|
+
## Dependencies
|
|
32
|
+
|
|
33
|
+
- `@cyanheads/mcp-ts-core` ^0.13.6 → ^0.13.10 (framework skills and scripts resynced)
|
|
34
|
+
- `@socketsecurity/bun-security-scanner` ^1.1.2 → ^1.1.3 (dev)
|
|
35
|
+
- `@types/node` ^26.6.2 → ^26.6.3 (dev)
|
|
36
|
+
- `ignore` ^7.0.9 → ^7.0.10 (dev)
|
|
37
|
+
- `vitest` ^5.0.1 → ^5.0.2 (dev)
|
|
38
|
+
- `packageManager` and Docker base images bun 1.4.0 → 1.4.2
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Filter values reach OpenAlex as it reads them (AND with +, country and language codes, quoted names), mixed OR/AND values are rejected up front, identifiers route to the record they name, and provider-text cleanup converges in one pass."
|
|
3
|
+
breaking: true
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.9.0 — 2026-10-18
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`author_list`** works `select` field on `openalex_search_entities` and `openalex_get_citation_graph` — each author in authorship order as `{id, display_name, orcid}`, about a tenth the size of `authorships`; never part of the default or `["*"]`. ([#102](https://github.com/cyanheads/openalex-mcp-server/issues/102))
|
|
12
|
+
- **`publisher`** sources `select` alias for `host_organization_name`. ([#97](https://github.com/cyanheads/openalex-mcp-server/issues/97))
|
|
13
|
+
- **Identifier forms** — OpenAlex record-page and API URLs (`https://openalex.org/works/W…`, `https://api.openalex.org/works/W…`, `http`, or no scheme) resolve as IDs; a ROR resolves to its institution, funder, and publisher records, and a Crossref Funder Registry DOI (`10.13039/…`) to its funder. ([#98](https://github.com/cyanheads/openalex-mcp-server/issues/98), [#99](https://github.com/cyanheads/openalex-mcp-server/issues/99))
|
|
14
|
+
- **`generate:iso-codes`** script, which builds `iso-codes.json`, and a **`start`** script.
|
|
15
|
+
|
|
16
|
+
## Changed
|
|
17
|
+
|
|
18
|
+
- A filter value that mixes `|` (OR) with an AND — a `+`, or a space on a field where words mean AND — now fails before any request as `mixed_and_or_in_filter_value` on all four filter-taking tools. OpenAlex drops every `|` alternative that ANDs more than one value, so on a multi-valued field such a value returned a partial set (`authorships.institutions.country_code: DE|FR+US` returned only the German works); on a single-valued field such as `type`, `article+review|book` happened to return the right set, and that call is now rejected too. The recovery puts the AND in a second key. ([#107](https://github.com/cyanheads/openalex-mcp-server/issues/107))
|
|
19
|
+
- **`upstream_unavailable`** replaces `upstream_invalid_params_other` when a semantic search's query embedding fails upstream, with a keyword-search fallback in the recovery. ([#96](https://github.com/cyanheads/openalex-mcp-server/issues/96))
|
|
20
|
+
- **`upstream_plan_required`** replaces `rate_limited` for a filter OpenAlex serves only on a paid plan (`created_date`, `updated_date`, and their `from_`/`to_` forms); it fails on the first request instead of retrying. ([#114](https://github.com/cyanheads/openalex-mcp-server/issues/114))
|
|
21
|
+
- **`openalex_research_landscape`** funding step groups by `funders.id`, which counts every work that lists a funder. ([#100](https://github.com/cyanheads/openalex-mcp-server/issues/100))
|
|
22
|
+
- **`openalex_describe_fields`** field catalog regenerated against OpenAlex's current field lists. ([#104](https://github.com/cyanheads/openalex-mcp-server/issues/104))
|
|
23
|
+
- **`openalex_resolve_name`** no-match notice names the `filters` that excluded the match. ([#103](https://github.com/cyanheads/openalex-mcp-server/issues/103))
|
|
24
|
+
- **Docker image** prunes musl-only native packages from the production dependencies; `server.json` npm entries launch the package bin through `npx`, and the HTTP entry sets `MCP_TRANSPORT_TYPE`.
|
|
25
|
+
|
|
26
|
+
## Fixed
|
|
27
|
+
|
|
28
|
+
- **`+` (AND within a field)** now ANDs; it went out percent-encoded and matched nothing. ([#107](https://github.com/cyanheads/openalex-mcp-server/issues/107))
|
|
29
|
+
- **Country and language filter values** are converted to the codes OpenAlex stores: on `country_code`, `country_codes`, and `countries` keys an alpha-3 code, numeric code, or English name becomes its alpha-2 code (`UK` becomes `GB`), and a `language` ISO 639-2 code or English name becomes its ISO 639-1 code. A country value that cannot be a code (`Germanyy`, `999`, `unknown`, `DE US`) fails as `invalid_country_code`; it used to match nothing, and its negation matched everything. ([#101](https://github.com/cyanheads/openalex-mcp-server/issues/101))
|
|
30
|
+
- **Multi-word values on exact-match keys** (`awards.funder_display_name`, `geo.city`, `raw_affiliation_strings`, and the like) are sent as one quoted phrase instead of matching nothing; a comma in them, or in a `.search.exact` value, is quoted rather than rejected. A `"` inside the outer pair of any value the server quotes, or of a caller-quoted value outside a free-text search filter, fails as `quote_in_filter_value`. ([#113](https://github.com/cyanheads/openalex-mcp-server/issues/113), [#115](https://github.com/cyanheads/openalex-mcp-server/issues/115))
|
|
31
|
+
- **By-ID lookups stay on their entity type's path**: a `.` or `..` segment in `id`, `seed_id`, or an `openalex_resolve_name` identifier, plain or percent-encoded, fails as `id_dot_segment` before any request, and a `?`, `#`, `\`, tab, or line break inside an identifier is sent percent-encoded as part of it. ([#109](https://github.com/cyanheads/openalex-mcp-server/issues/109))
|
|
32
|
+
- **An OpenAlex ID of another entity type** than the lookup's on `id` or `seed_id` (or a concept ID) fails as `id_type_mismatch` before any request. ([#99](https://github.com/cyanheads/openalex-mcp-server/issues/99))
|
|
33
|
+
- **A name `query` with `filters` but no `entity_type`** on `openalex_resolve_name` fails as `filters_without_entity_type` before any request; every such call failed upstream. ([#105](https://github.com/cyanheads/openalex-mcp-server/issues/105))
|
|
34
|
+
- **Short names shaped like OpenAlex IDs** (keyword `p53`, `F1`, `S1`) fall through to the name search when the ID lookup misses. ([#106](https://github.com/cyanheads/openalex-mcp-server/issues/106))
|
|
35
|
+
- **Provider text** — MathType MTEF equation blobs and End lines are removed, keeping the TeX that restates each equation; a second cleanup pass no longer changes a result: an instruction target ended by a no-break space, an `<?MML` span measured before trimming, and a removal that joins text into a construct all converge in one pass. ([#92](https://github.com/cyanheads/openalex-mcp-server/issues/92), [#110](https://github.com/cyanheads/openalex-mcp-server/issues/110), [#111](https://github.com/cyanheads/openalex-mcp-server/issues/111), [#112](https://github.com/cyanheads/openalex-mcp-server/issues/112))
|
|
36
|
+
|
|
37
|
+
## Dependencies
|
|
38
|
+
|
|
39
|
+
- `@cyanheads/mcp-ts-core` ^0.13.10 → ^0.13.15 (framework skills and scripts resynced)
|
|
40
|
+
- `pino-pretty` ^13.1.3 → ^13.2.0
|
|
41
|
+
- `zod` ^4.6.5 → ^4.6.7
|
|
42
|
+
- `@biomejs/biome` ^2.5.14 → ^2.5.15 (dev)
|
|
43
|
+
- `@types/node` ^26.6.3 → ^26.6.5 (dev)
|
|
44
|
+
- `ignore` ^7.0.10 → ^7.0.12 (dev)
|
|
45
|
+
- `mdast-util-from-markdown` ^2.0.3 → ^2.1.0 (dev)
|
|
46
|
+
- `tsc-alias` ^1.9.5 → ^1.9.7 (dev)
|
|
47
|
+
- `vitest` ^5.0.2 → ^5.0.4 (dev)
|
|
48
|
+
- `packageManager` and Docker base images bun 1.4.2 → 1.4.3
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ await createApp({
|
|
|
25
25
|
describeFieldsTool,
|
|
26
26
|
],
|
|
27
27
|
prompts: [literatureReviewPrompt, researchLandscapePrompt],
|
|
28
|
-
instructions:
|
|
28
|
+
instructions: "Use the openalex_* tools to query the OpenAlex scholarly catalog (works, authors, sources, institutions, topics, keywords, publishers, funders): resolve names to IDs, search/filter/sort or fetch by ID, and group_by for trends. Names are ambiguous and IDs are not — call openalex_resolve_name before filtering by entity. Record responses hold to 64,000 bytes per surface: a cut page names its `omitted` IDs and the call that returns them (in OpenAlex's order), and an array too long to fit comes back as a window that an `id` lookup pages with `slice`.",
|
|
29
29
|
landing: {
|
|
30
30
|
tagline: 'Search the OpenAlex catalog — 270M+ works, 90M+ authors, 100K+ sources.',
|
|
31
31
|
requireAuth: false,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,8DAA8D,CAAC;AACtG,OAAO,EAAE,uBAAuB,EAAE,MAAM,+DAA+D,CAAC;AACxG,OAAO,EAAE,iBAAiB,EAAE,MAAM,uDAAuD,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,uDAAuD,CAAC;AAC7F,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAC5F,OAAO,EAAE,eAAe,EAAE,MAAM,qDAAqD,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAC5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAE9E,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,qBAAqB;IAC3B,KAAK,EAAE,qBAAqB;IAC5B,qFAAqF;IACrF,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE;QACL,eAAe;QACf,kBAAkB;QAClB,iBAAiB;QACjB,oBAAoB;QACpB,kBAAkB;KACnB;IACD,OAAO,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IAC1D,YAAY,EACV,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,8DAA8D,CAAC;AACtG,OAAO,EAAE,uBAAuB,EAAE,MAAM,+DAA+D,CAAC;AACxG,OAAO,EAAE,iBAAiB,EAAE,MAAM,uDAAuD,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,uDAAuD,CAAC;AAC7F,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAC5F,OAAO,EAAE,eAAe,EAAE,MAAM,qDAAqD,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAC5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAE9E,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,qBAAqB;IAC3B,KAAK,EAAE,qBAAqB;IAC5B,qFAAqF;IACrF,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE;QACL,eAAe;QACf,kBAAkB;QAClB,iBAAiB;QACjB,oBAAoB;QACpB,kBAAkB;KACnB;IACD,OAAO,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IAC1D,YAAY,EACV,yiBAAyiB;IAC3iB,OAAO,EAAE;QACP,OAAO,EAAE,yEAAyE;QAClF,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,kDAAkD;QAC5D,UAAU,EAAE;YACV,gBAAgB,EAAE,uBAAuB;SAC1C;QACD,KAAK,EAAE;YACL,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,sBAAsB,EAAE;YACnD,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,iCAAiC,EAAE;SAC/D;KACF;IACD,KAAK;QACH,mBAAmB,EAAE,CAAC;IACxB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -41,7 +41,7 @@ Follow this workflow using the OpenAlex tools:
|
|
|
41
41
|
- Search with multiple related topic IDs to capture adjacent areas`}
|
|
42
42
|
Use select to keep payloads manageable.
|
|
43
43
|
|
|
44
|
-
3. **Identify key papers** — Re-run the step 2 search with sort: "-cited_by_count" to surface landmark works, keeping the topical filters in place: an explicit sort replaces relevance ranking, so without them you get the most-cited works in the catalog rather than in "${args.topic}". Then call openalex_search_entities with id and a select naming the fields you need (e.g. ["abstract", "
|
|
44
|
+
3. **Identify key papers** — Re-run the step 2 search with sort: "-cited_by_count" to surface landmark works, keeping the topical filters in place: an explicit sort replaces relevance ranking, so without them you get the most-cited works in the catalog rather than in "${args.topic}". Then call openalex_search_entities with id and a select naming the fields you need (e.g. ["abstract", "author_list", "primary_location"]) for the most important ones — an id lookup on its own returns the curated default projection. author_list gives each author's name, ID, and ORCID; select authorships instead when you need affiliations, at several times the size. A long array such as author_list on a large collaboration comes back as a window listed in the response's windows; page the rest with slice: {"field": "author_list", "offset": <the next offset it names>} on the same id.
|
|
45
45
|
|
|
46
46
|
4. **Trace citations** — For each key paper, use openalex_get_citation_graph with direction "cites" to find subsequent work that cites it, and direction "cited_by" to find foundational work it references.
|
|
47
47
|
|
|
@@ -29,7 +29,7 @@ Use the OpenAlex tools to build a quantitative profile:
|
|
|
29
29
|
|
|
30
30
|
4. **Open access** — Group by oa_status. What fraction is freely available?
|
|
31
31
|
|
|
32
|
-
5. **Funding** — Group by
|
|
32
|
+
5. **Funding** — Group by funders.id to identify major funders. It counts every work that lists a funder and labels each group with the funder's name. For grant-level questions such as award numbers, use the awards.* fields.
|
|
33
33
|
|
|
34
34
|
6. **Impact** — Search for the most-cited works (sort by -cited_by_count). Get details on the top 5.
|
|
35
35
|
|
|
@@ -48,7 +48,14 @@ export declare const analyzeTrendsTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
48
48
|
readonly code: JsonRpcErrorCode.RateLimited;
|
|
49
49
|
readonly when: "The OpenAlex daily usage budget is spent (HTTP 429).";
|
|
50
50
|
readonly retryable: false;
|
|
51
|
-
readonly recovery: "
|
|
51
|
+
readonly recovery: "This server's daily OpenAlex budget is spent and refills at midnight UTC — retrying before then will not succeed.";
|
|
52
|
+
readonly thrownBy: "service";
|
|
53
|
+
}, {
|
|
54
|
+
readonly reason: "upstream_plan_required";
|
|
55
|
+
readonly code: JsonRpcErrorCode.Forbidden;
|
|
56
|
+
readonly when: "A filter is one OpenAlex serves only on a paid plan — created_date, updated_date, or a from_/to_ form of either (HTTP 429 \"Plan upgrade required\").";
|
|
57
|
+
readonly retryable: false;
|
|
58
|
+
readonly recovery: "Drop the plan-gated filter and rerun; retrying it unchanged will not succeed. To bound works by date, filter on publication_date, from_publication_date, or to_publication_date, which need no paid plan.";
|
|
52
59
|
readonly thrownBy: "service";
|
|
53
60
|
}, {
|
|
54
61
|
readonly reason: "upstream_timeout";
|
|
@@ -79,8 +86,26 @@ export declare const analyzeTrendsTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
79
86
|
}, {
|
|
80
87
|
readonly reason: "comma_in_filter_value";
|
|
81
88
|
readonly code: JsonRpcErrorCode.InvalidParams;
|
|
82
|
-
readonly when: "A filter value contains a comma, which collides with the OpenAlex filter separator.";
|
|
83
|
-
readonly recovery: "Use `|` for OR within a filter value (e.g. \"2020|2021\"), or
|
|
89
|
+
readonly when: "A filter value contains a comma, which collides with the OpenAlex filter separator, on a field that takes no phrase. Free-text search filters (any `.search` key, such as title.search or abstract.search.exact) and fields matched as whole text (display_name, awards.funder_display_name, awards.funder_award_id, geo.city, geo.region, raw_affiliation_strings, block_key, parsed_longest_name, biblio fields) are exempt: a comma there is sent quoted, as part of the phrase.";
|
|
90
|
+
readonly recovery: "Use `|` for OR within a filter value (e.g. \"2020|2021\"), or move a phrase that contains commas into a free-text search filter such as title.search or raw_affiliation_strings.search.";
|
|
91
|
+
readonly thrownBy: "service";
|
|
92
|
+
}, {
|
|
93
|
+
readonly reason: "invalid_country_code";
|
|
94
|
+
readonly code: JsonRpcErrorCode.ValidationError;
|
|
95
|
+
readonly when: "A country filter (a key ending in country_code, country_codes, or countries) holds a segment that cannot be a country code, such as a misspelled name, an unassigned number, \"unknown\", or two codes joined by a space (\"DE US\").";
|
|
96
|
+
readonly recovery: "Pass the country as an ISO 3166-1 alpha-2 code such as DE, US, or GB (an alpha-3 code, numeric code, or English name is converted), or null for records with no country.";
|
|
97
|
+
readonly thrownBy: "service";
|
|
98
|
+
}, {
|
|
99
|
+
readonly reason: "mixed_and_or_in_filter_value";
|
|
100
|
+
readonly code: JsonRpcErrorCode.ValidationError;
|
|
101
|
+
readonly when: "A filter value puts \"|\" (OR) beside an AND operator — a \"+\", or a space on a field where words mean AND (on a country field a space is always part of a country name, and inside a language name it is part of the name). OpenAlex splits the value on \"|\" first and drops every alternative that ANDs more than one value, so no single value can carry both.";
|
|
102
|
+
readonly recovery: "Keep \"|\" and the AND operator in separate filter keys: OR the alternatives in this key and put the AND in a second key that reaches the same values — on works, {\"authorships.institutions.country_code\": \"DE|FR\", \"institutions.country_code\": \"US\"} matches (DE or FR) and US. The filters object holds each key once, so where no second key fits, run one call per alternative instead.";
|
|
103
|
+
readonly thrownBy: "service";
|
|
104
|
+
}, {
|
|
105
|
+
readonly reason: "quote_in_filter_value";
|
|
106
|
+
readonly code: JsonRpcErrorCode.ValidationError;
|
|
107
|
+
readonly when: "A filter value sent to OpenAlex double-quoted holds a double quote inside its outer pair — a value the server quotes (a multi-word value on a field matched as whole text, or a value holding a comma on such a field or on a free-text search filter), or one the caller wrapped in double quotes on any filter but a free-text search.";
|
|
108
|
+
readonly recovery: "Remove the double quote from the value; a phrase is matched without quoting its own text.";
|
|
84
109
|
readonly thrownBy: "service";
|
|
85
110
|
}, {
|
|
86
111
|
readonly reason: "upstream_invalid_params";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze-trends.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/analyze-trends.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"analyze-trends.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/analyze-trends.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAWjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2Y5B,CAAC"}
|