Caller authentication and write authorization are owned by the GeneFoundry router at the
trust boundary. Production PubTator-Link additionally requires a router-owned service bearer
token on /mcp; /health remains unauthenticated for container probes. The backend must be
reachable only through the router/proxy, never published directly to a LAN or the internet.
readonly(default) — exposes all read tools and strips the canonical write inventory.lean— read tools plus a subset of review-index write tools. It requires service auth unless a direct development process explicitly enables the loopback-only exception.full— enables the complete write surface, including audit-bundle file export. Runfullonly behind the authenticated router/proxy.
The production /mcp endpoint serves read-only public PubTator3 literature data
(PUBTATOR_LINK_MCP_PROFILE=readonly, unauthenticated writes disabled) and is deliberately
left open: a direct unauthenticated /mcp request returns 200, and /health remains
available. Because only public read-only data is exposed, the transport is not bearer-gated by
default.
PUBTATOR_LINK_MCP_SERVICE_TOKEN is an optional transport credential. When set, it installs
MCPServiceAuthMiddleware, which bearer-gates the entire /mcp path (a request without the
token then returns 401). Use it for private deployments, to restrict the transport to a
trusted caller, or when running a write-capable profile. Generate a dedicated token with
openssl rand -hex 32. It is a service credential, not a caller credential; do not reuse or
forward caller OAuth tokens and do not place the value in Compose YAML, logs, issue comments, or
source control. The token protects the MCP transport only; REST routes remain dependent on edge
authentication, so the reverse proxy must not publish this backend as a general REST origin.
When a token is configured, set the same value as GF_PUBTATOR_TOKEN on the router. Rotation of
the single configured token then requires a coordinated router/backend deployment window; verify
router read calls after rotation and revoke the old value immediately.
Direct host development may set PUBTATOR_LINK_ALLOW_UNAUTHENTICATED_WRITES=true only while
binding to 127.0.0.1, ::1, or localhost. Docker binds inside its container to 0.0.0.0, so
the base Compose stack stays read-only by default. To exercise writes in Compose, set a random
service token and explicitly select PUBTATOR_LINK_MCP_PROFILE=lean or full.
oauth mode lets PubTator-Link be published directly and still serve one /mcp to two
audiences at once: standalone users (claude.ai connectors, Claude Code, scripts) who log in via
Keycloak OAuth, and the router, which presents its own static service token (never the
caller's token — no passthrough). FastMCP MultiAuth accepts a valid Keycloak JWT or the
service token; both reach the configured (full) tool surface. /health stays public.
- Audience binding is enforced. The JWT verifier requires
audience == PUBLIC_BASE_URL + /mcp, andPUBLIC_BASE_URLmust be a bare origin (no path) — this is validated at startup so the advertised protected-resource URI is never doubled to/mcp/mcp, and tokens minted for other backends are rejected. - REST bypass is closed. In
oauthmode the mutating REST review routes (/api/reviews/*) are not registered — they live outside the MCP mount and would otherwise be an unauthenticated write path to the same database. The writable surface is MCP-only. - Client-store persistence. OAuthProxy stores DCR clients + upstream tokens under
FASTMCP_HOME(/home/app/.fastmcp); mount a persistent volume there or interactive clients break on restart.
- In the router's realm, create a confidential client for PubTator (its own id + secret).
- Set
PUBTATOR_LINK_JWT_AUDIENCEto PubTator's resource URI (https://pubtator-link.genefoundry.org/mcp) and add a Keycloak audience mapper emitting it. - Register the OAuthProxy callback
PUBLIC_BASE_URL/auth/callbackas the client's redirect URI in Keycloak. (claude.ai / loopback redirects are the downstream clients — list them inPUBTATOR_LINK_OAUTH_ALLOWED_CLIENT_REDIRECT_URIS, not in Keycloak.) - Add a Keycloak scope/claim mapper that emits
pubtator:read/pubtator:writeinto the tokenscope/scpclaim (the JWT verifier reads those, notrealm_access.roles). - Populate the
oauth/jwtenv vars and setAUTH_MODE=oauth,MCP_PROFILE=full.
By default PUBTATOR_LINK_REQUIRE_WRITE_SCOPE=false — every authenticated caller can write.
On a shared review database with no per-subject ownership this means any authenticated user can
modify or exhaust another user's reviews. This is accepted for a trusted user group. To reserve
writes for a Keycloak-granted role, set REQUIRE_WRITE_SCOPE=true (gates the authoritative
WRITE_TOOLS on pubtator:write). Follow-up: bind reviews to AccessToken.subject + add
quotas for a genuinely multi-tenant public deployment.
PUBTATOR_LINK_REVIEW_EXPORT_BASE_DIR— base directory for server-generatedexport_review_audit_bundlefiles. Callers requestsave_to_file=truebut never select a path. Files use generated names, exclusive no-follow creation, and mode0600. Unset disables file export (inline/compact responses still work). Set it to a dedicated, mounted export volume.index_review_evidencecapspmidsandcurated_urlsat 200 entries each.PUBTATOR_LINK_TRUST_PROXY_HEADERS— settrueonly when a known reverse proxy sits in front; the inbound rate limiter then keys on the rightmostX-Forwarded-Forentry. Leavefalse(default) when directly reachable — the leftmost XFF value is client-spoofable.- The default
docker/docker-compose.ymlpublishes the app port to127.0.0.1only;docker-compose.prod.ymldrops published ports entirely (expose-only behind the proxy).
See PUBTATOR_LINK_TRUST_PROXY_HEADERS above. When enabled, the rate limiter keys on the
rightmost entry in X-Forwarded-For. When disabled (default), the socket peer IP is used,
preventing header-spoofing attacks.
See PUBTATOR_LINK_REVIEW_EXPORT_BASE_DIR above. The mcp_profile full is required to expose
export_review_audit_bundle; the server creates the leaf beneath the configured directory.
Docker networking is not an egress firewall. Hospital deployments require a host or network egress policy in addition to the inbound router boundary.
GitHub secret scanning and push protection are repository settings, not something a workflow or source change can enable. They are the last line of defence against a credential (service token, DB password, API key) being committed to history — push protection blocks the push before the secret lands. This repository already runs CodeQL; secret scanning must be enabled as an operator action.
Enable both (operator, with a token that has admin on the repo):
gh api -X PATCH repos/berntpopp/pubtator-link \
-f 'security_and_analysis[secret_scanning][status]=enabled' \
-f 'security_and_analysis[secret_scanning_push_protection][status]=enabled'Verify:
gh api repos/berntpopp/pubtator-link --jq '.security_and_analysis'Both secret_scanning.status and secret_scanning_push_protection.status must read enabled.
If a committed secret is ever detected, treat it as exposed: rotate it immediately (see the
service-token and DB-password rotation notes above) and purge it from history.
Research use only. Not clinical decision support.