Skip to content

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_KEY is 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_error helpers. A 403 (a dead or revoked key) logs a dedicated WARNING-level event (s2_key_forbidden for tool calls, s2_keepalive_key_forbidden for the keepalive task), separate from ordinary 429 rate-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, and server.json's env arrays are now generated from ProjectConfig field metadata (#257) rather than hand-maintained, so these artifacts can no longer drift from what the server actually reads.
  • SCHOLAR_MCP_OIDC_ADVERTISED_SCOPES joined 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 main and 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.