Privacy

Privacy Policy

Last updated 2026-09-30

This policy covers the OpenNIH website at opennih.org and the public MCP endpoint at https://mcp.opennih.org/mcp. The service is anonymous: there are no accounts, no registration, no authentication, and no login. To keep an unauthenticated public service healthy and observable, the server records usage telemetry, and this page states exactly what that telemetry contains.

Anonymous is not the same as PII-free

OpenNIH does not ask who you are and cannot look you up. But the telemetry above is not guaranteed to be free of personal information, because the arguments you pass to a tool and the values of the allowlisted headers your client sends are recorded as bounded text. The service also records a self-declared analytics lane only for ccfi-realuser or ccz-realuser; any other non-blank label becomes the fixed value unknown, so caller text is not retained. If you type a person's name, an email address, or anything else personal into a query, it is recorded. Treat tool arguments the way you would a search-engine query log: do not put personal or sensitive data in a query.

What is collected, and why

Telemetry is recorded per HTTP request and per tool call. Recording is best-effort and never blocks a live call: a record is placed on a bounded in-memory queue and written by a background thread. If the queue is full the line is dropped and counted; if the disk write then fails, that line is lost silently. So “recorded” below means a record is enqueued for the call, subject to these rare losses.

Tool arguments

The arguments you pass to a tool (for example a search_grants query, a text_search, or a pi_name) are recorded after MCP protocol validation: each validated non-null value is converted to its Python text form (str()) and truncated to its first 200 characters, while arguments you left unset or passed as null are omitted entirely. This is a faithful record of what you searched for, but it is not your raw JSON and not lexically exact — a JSON true is stored as True, a list as ['a', 'b'], and anything past 200 characters is cut.

Why: Know which tools are used and with what shape of input, so broken or missing capabilities are visible.

Argument names, as you sent them

Separately from the values above, the names of the arguments you sent are recorded before validation — including names the tool does not accept, and including calls that are rejected. Names are truncated to 64 characters and capped at 40 per call. A request with more than 40 argument names is rejected before validation with a bounded error. No value is recorded with them, and the reason a call was rejected is stored as the error type and the offending argument name only, never the input you supplied.

Why: An argument a tool does not declare used to be discarded before anything could record it, so a caller whose filter was being ignored was invisible to us and got an answer to a different question than the one they asked. Recording the names — and nothing else — makes that visible and fixable.

Salted hash of your IP address

Client IP addresses are never stored raw. Each address is salted-SHA256-hashed to a 16-character digest before it reaches disk. The raw IP exists only inside the in-memory rate limiter and is never persisted.

Why: Distinguish one caller from another for abuse control and usage counts, without holding an identifier that points back to you.

Country

Whatever the CF-IPCountry request header carries, bounded to 200 characters. In production Cloudflare stamps this header at the edge with a two-letter country code; the server stores the header value as received and does not itself geolocate a stored IP address.

Why: Understand where the service is used so capacity and coverage decisions are informed.

User agent

The User-Agent header you send, bounded to its first 400 characters.

Why: Identify which clients and versions call the service, and spot automated abuse.

MCP session id and origin

The per-connection MCP session id and the Origin header, each bounded to 200 characters. The server has no accounts and assigns you no identifier of its own, but it does persist the session id your client sends.

Why: Group the calls of one connection together when diagnosing a failed session.

Allowlisted request headers

Only request headers whose lowercase name appears in a fixed allowlist are recorded — accept, accept-encoding, accept-language, cache-control, cf-ipcountry, connection, content-length, content-type, host, mcp-protocol-version, mcp-session-id, origin, te, transfer-encoding, user-agent, via, x-forwarded-proto — each value bounded to 200 characters. The separate X-OpenNIH-Lane header is not copied into this snapshot: its value is mapped to the fixed analytics label described below. Every other header is dropped whole: neither its name nor its value is written. A header dropped for not being on the allowlist increments a single other_header_names_count tally; a header dropped earlier by the credential/client-IP denylist is discarded without being counted at all.

Why: Diagnose protocol and transport problems without letting caller-controlled text into the log through a header name.

Emulated-user lane label

The optional X-OpenNIH-Lane header is used only to separate OpenNIH's own emulated-user probes from external traffic in aggregate analytics. The server stores ccfi-realuser or ccz-realuser when supplied; any other non-blank value is stored as unknown, and a blank or absent header is stored as no lane. The label is caller-forgeable and is never used for authentication, rate limiting, or access control.

Why: Keep our own tests from inflating adoption metrics while retaining their defect signal.

Request and result metadata

The HTTP method; the request path and query string, each bounded to 200 characters; scheme; HTTP version; status code; response byte count; latency in milliseconds; a success/failure flag; the error class name and a bounded error message; the tool name, bounded to 64 characters — normally one of this server's own tool names, but if you call a tool that does not exist the name you sent is recorded as you sent it, truncated to that limit; result-shape counts; a bounded result-metadata summary; a cache-hit flag; and your MCP client's reported name, version, protocol version, and capabilities.

Why: Measure health, latency, error rates, and rate-limit rejections on an endpoint with no other observability.

Server-generated record stamps

Every stored line also carries a UTC timestamp, the OpenNIH version that served the request, a per-process boot id that groups lines by container generation, and a flag marking the line as a request record or a tool-call record. These are generated by the server, not taken from you.

Why: Order events, attribute a problem to a specific deploy, and make server restarts visible.

What is never collected

Retention

Indefinite. Usage history is kept as a permanent product asset; telemetry files are never deleted on a schedule and the service implements no automatic expiry. Records are written to daily files on the server that runs the endpoint.

Sharing and sale

Telemetry is not sold, rented, or shared with third parties. It is used only to operate and improve OpenNIH. Two operational services receive limited data from the server when they are configured, as they are in production: Sentry receives reports of unexpected server errors — the exception, its stack trace (which can include the values of the failing call's arguments), the tool name, and the OpenNIH version — with Sentry's default personal-data collection turned off, and expected input-validation rejections are not reported; Statsig receives feature-flag checks made under one fixed anonymous id, with nothing about you. Traffic reaches the service through Cloudflare, which processes requests as part of delivering and protecting the endpoint.

When telemetry runs

Telemetry is off by default and is collected only when the deployment sets the OPENNIH_MCP_TELEMETRY_DIR environment variable. The production deployment sets it, so assume telemetry is on for the public endpoint.

Data source

All grant data served by OpenNIH is public U.S. Government data from NIH ExPORTER and the NIH RePORTER API. It contains no private data of yours and is not derived from your use of the service.

Your controls

Children

OpenNIH is a research-data tool intended for a general professional audience. It is not directed at children, it has no registration, and it asks no user of any age for their name or age. It does record the pseudonymous and client-supplied values listed above — a salted IP hash, user agent, origin, MCP session id, and whatever text is passed in a query — for every caller alike, and the correction form has an optional contact field that stores whatever address is typed into it.

Changes to this policy

If what the service collects changes, this page changes with it and the “last updated” date above moves. There is no mailing list to notify, because there are no accounts; check this page for the current statement.

Contact

Questions about this policy, or about a specific figure OpenNIH displays, go through the public channels listed on the support page. Data corrections are reported through the correction intake form, which needs no account.