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.