1.9¶
Scholar MCP keeps working through the Semantic Scholar API key's 60-day inactivity window instead of silently going dark, and the server now tells you clearly when an API key has actually been rejected rather than mixing that in with ordinary rate-limit retries.
Papers, S2 API key liveness¶
Deployments configured with SCHOLAR_MCP_S2_API_KEY stopped working after
Semantic Scholar rejected the key with 403 Forbidden on every call. The
motivating issue
narrows the cause precisely:
Root cause: Semantic Scholar removes API keys that are inactive for 60 days.
The issue confirms this directly against the S2 API: the deployed key
returns 403, while omitting the key entirely returns the normal 429
rate-limit response, ruling out a scholar-mcp bug or an S2 outage.
#231 closes that gap two ways:
- A background keepalive task. When
SCHOLAR_MCP_S2_API_KEYis set, the server pings Semantic Scholar once at startup and every 7 days after that (comfortably inside the 60-day removal window), so a configured key that would otherwise sit idle stays active. Nothing to configure: it starts and stops with the server lifespan whenever the key is present. - Distinguishable 403 logging. Every Semantic Scholar call site (paper
and citation-graph lookups, recommendations, citation generation,
enrichment, and the keepalive ping itself) now routes through shared
format_s2_error/log_s2_errorhelpers. A403(a dead or revoked key) logs a dedicatedWARNING-level event (s2_key_forbiddenfor tool calls,s2_keepalive_key_forbiddenfor the keepalive task), separate from ordinary429rate-limit retry logging, so an operator grepping logs can tell "the key is gone" from "we're being rate-limited" at a glance. The error returned to the calling LLM stays a generic, non-leaking message; full upstream response detail goes to the server-side log only.
See the Semantic Scholar configuration guide for the
SCHOLAR_MCP_S2_API_KEY setting itself; the keepalive and logging behavior
are documented in README.md.
Two related issues were opened during this work and are intentionally not
part of this release: surfacing S2 key health through get_server_info
(#229, deferred
because no external monitoring currently polls scholar-mcp), and a
pre-existing lifespan-startup-exception resource leak
(#230) found while
reviewing this change but predating it.
Upgrading¶
No breaking changes to the operator surface or the public library interface
between v1.8.2 and v1.9.0: no new env vars, no tool surface changes, and the
S2 keepalive task above activates automatically off the existing
SCHOLAR_MCP_S2_API_KEY setting.
v1.9.1¶
A maintenance release with no user-facing features or fixes: it carries the
project's fastmcp-server-template baseline forward from v2.3.0 to v5.6.2.
Nothing to act on beyond the OIDC and bare-CLI notes below.
Every commit in this release is conventionally typed chore(copier) or
chore; there are no feat/fix/! commits
(#257,
#267,
#269,
#272,
#273,
#275,
#278, tracked under
epic #259), so
CHANGELOG.md's v1.9.1 section is intentionally empty. This page is where
that work is narrated.
The template chain is project build, release, and packaging tooling, not the MCP tool surface. Most of it is invisible day to day, but a few changes reach an operator directly:
- Config surface generation.
.env.example, the config-wizard spec, the README environment-variable tables, andserver.json's env arrays are now generated fromProjectConfigfield metadata (#257) rather than hand-maintained, so these artifacts can no longer drift from what the server actually reads. SCHOLAR_MCP_OIDC_ADVERTISED_SCOPESjoined the environment variable set (#273) for OIDC deployments that need to advertise scopes to MCP clients independently of the scopes required at the token check (SCHOLAR_MCP_OIDC_REQUIRED_SCOPES). It only matters if you run OIDC auth; every other deployment is unaffected.- Repository- and release-process changes: GitHub rulesets protecting
mainand release branches, a PR-title gate, and a public-import-surface snapshot were adopted (#272); the release pipeline moved from python-semantic-release to knope-driven release PRs, with release notes now written as part of the release PR (#273). These are contributor- and maintainer-facing, not something a deployed server's operator needs to act on. - This page (the
docs/releases/release-notes mechanism) was wired up in #272. v1.9.0 above is narrated here retroactively, since it shipped before the page existed.
Three deployment regressions were introduced and fixed within this same
maintenance window, before ever reaching a release:
#257 discovered that
adopting the template's newer CLI defaults would flip the HTTP bind default
from 0.0.0.0 to 127.0.0.1
(#250), and fixed it
in the same change by pinning SCHOLAR_MCP_HOST=0.0.0.0 in the shipped
systemd unit (the Docker image was already unaffected, since its CMD
already passes --host 0.0.0.0 explicitly); it also fixed a config-loading
path that would have printed a raw traceback instead of a clean error
message for an invalid SCHOLAR_MCP_PORT
(#251), and stopped
the generated config surface from overwriting the packaged
packaging/env.example's deliberate SCHOLAR_MCP_CACHE_DIR override
(#252). Because all
three landed inside the same range this page covers, no released version
ever carried the regressions; they are recorded here only because the
fixes are otherwise invisible to CHANGELOG.md.
A fourth issue surfaced from an actual release attempt rather than from
review: publishing v1.9.1-rc.0 to PyPI failed at the Publish to PyPI
step with InvalidDistribution: Invalid distribution metadata: '2.5' is
not a valid metadata version. Root-caused upstream as
fastmcp-server-template#479:
two template-owned pins encoded one invariant that nothing asserted:
pyproject.toml left hatchling unbounded while release.yml pinned
gh-action-pypi-publish at v1.14.0, so once hatchling 1.32 began emitting
Metadata-Version: 2.5, that action's bundled twine 6.1.0 refused it.
Template v5.6.2 fixes both halves
(#277,
#278):
gh-action-pypi-publish moves to v1.14.2 (the first release bundling
twine 7.0.0, which accepts metadata 2.5), and pyproject.toml bounds
hatchling>=1.32,<1.33 (the same bound was applied by hand to
packaging/mcpb/pyproject.toml.in, which copier update never touches).
No PyPI version was consumed by the failure, since twine check runs
before the upload begins. This is release-pipeline reliability, not
something a deployed server's operator or a library consumer needs to
act on.
No breaking changes to the operator surface or the public library interface
against v1.9.0: the public import surface (tests/public_import_surface.txt)
was introduced in this range and starts empty, and the only new environment
variable is the additive SCHOLAR_MCP_OIDC_ADVERTISED_SCOPES above. One
nuance for anyone running the server via a bare CLI invocation (not the
Docker image or the packaged systemd service, both of which pin their bind
address explicitly): the underlying serve --transport http default bind
host changed from 0.0.0.0 to 127.0.0.1 partway through this range. If
you invoke scholar-mcp serve --transport http directly and rely on the
implicit default for external reachability, set SCHOLAR_MCP_HOST=0.0.0.0
(or --host 0.0.0.0) explicitly going forward.