Releasesv0.1.0

MCP server reference

Transports, credentials, the read-only tools and their arguments.

Open Health Sync speaks the Model Context Protocol, so an AI agent can read health data directly. The server is read only: no tool writes, deletes or connects anything.

Two ways in:

  • Remote (Streamable HTTP) at /mcp on the same listener as the REST API, for agents that can call a URL with a bearer header.
  • Local (stdio) with ohs mcp, for desktop agents that launch a command. ohs mcp holds no data and needs no database: it forwards every message to the /mcp endpoint of an instance.

Credentials

Every request carries Authorization: Bearer <secret>, one of:

SecretSeesuser_id
Tenant API key (ohs_sk_...)every user of the tenantrequired on every per-user tool; list_users finds it
User token with scope read (ohs_ut_...)that one useromit it (or pass the token's own user); any other value is refused

A user token without the read scope gets 403 on /mcp. A user of another tenant, or a user being deleted, reads as user not found, with no data.

A personal token

For your own agent, mint a user token whose only scope is read. Such a token may live up to 30 days (ttl_seconds up to 2592000); tokens with upload or connect keep the one hour default and a 24 hour maximum.

curl -s -X POST "$OHS_PUBLIC_URL/v1/users/$USER_ID/tokens" \
  -H "Authorization: Bearer $TENANT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scopes": ["read"], "ttl_seconds": 2592000}'
# {"token": "ohs_ut_...", "expires_at": "...", "scopes": ["read"]}

The token is shown once. It stops working at expires_at, or as soon as the user is deleted; revoking the tenant key that minted it does not end it. Mint a new one before it expires.

Desktop agent (stdio)

Most desktop agents take an mcpServers entry that names a command. Point it at the ohs binary:

{
  "mcpServers": {
    "open-health-sync": {
      "command": "/usr/local/bin/ohs",
      "args": ["mcp"],
      "env": {
        "OHS_MCP_URL": "https://ohs.example.org",
        "OHS_MCP_KEY": "ohs_ut_..."
      }
    }
  }
}
VariableDefaultMeaning
OHS_MCP_URLOHS_PUBLIC_URL, then http://localhost:8080the instance; /mcp is appended unless the URL already ends with it; https is required except for a loopback host; redirects are not followed, so give the final URL
OHS_MCP_KEYnone, requireda tenant API key or a user token with scope read

ohs mcp connects at start: a wrong URL or secret makes it exit with the reason on stderr (never the secret). A 401 means the key is wrong or the token expired, a 403 a user token without the read scope, a 404 or 405 a URL that is not the instance's base URL. Its stdout carries only protocol messages.

The ohs binary of each GitHub release runs on Linux and macOS. Without it, the image does the same (-i keeps stdin open; inside the container localhost is the container, so give the instance's real address):

"command": "docker",
"args": ["run", "--rm", "-i", "-e", "OHS_MCP_URL", "-e", "OHS_MCP_KEY",
         "ghcr.io/soma-intelligence/open-health-sync:0.1.0", "mcp"]

Claude Desktop reads this mcpServers block from claude_desktop_config.json (Settings, Developer, Edit Config) and picks up a change after a restart. Claude Code adds the same server from the command line:

claude mcp add open-health-sync -e OHS_MCP_URL=https://ohs.example.org \
  -e OHS_MCP_KEY=ohs_ut_... -- ohs mcp

Or the remote endpoint directly, without the binary:

claude mcp add --transport http open-health-sync https://ohs.example.org/mcp \
  --header "Authorization: Bearer ohs_ut_..."

Remote agent (URL and bearer)

Agents that speak Streamable HTTP connect straight to the instance:

{
  "mcpServers": {
    "open-health-sync": {
      "type": "http",
      "url": "https://ohs.example.org/mcp",
      "headers": { "Authorization": "Bearer ohs_ut_..." }
    }
  }
}

The endpoint is stateless (no Mcp-Session-Id) and answers in plain JSON, so it works behind any reverse proxy and any number of api instances. Serve it over HTTPS: the bearer secret is the only credential. Cross-origin browser requests (Sec-Fetch-Site: cross-site, or an Origin that does not match the host) are refused with 403.

Tools

Arguments mirror the REST query parameters. Times take RFC 3339 or YYYY-MM-DD (midnight UTC); start is inclusive and end exclusive.

ToolArgumentsReturns
list_userscursor, limitthe tenant's users (tenant key only)
list_quantity_typesnonethe quantity type catalog: type, storage unit, ucum_unit, loinc_code
get_samplesuser_id, type (required), start, end, view, cursor, limitquantity samples, as GET /v1/users/{id}/samples
get_category_samplesuser_id, type, start, end, view, cursor, limitcategory samples
get_sleepuser_id, session_id, start, end, view, cursor, limitsleep sessions; with session_id, that session's phases
get_workoutsuser_id, type (activity), start, end, view, cursor, limitworkouts
get_workout_routeuser_id, workout_id (required), start, end, cursor, limitthe workout's GPS points
get_nutritionuser_id, start, end, cursor, limitnutrition logs
get_dailyuser_id, start, end, view, cursor, limitdaily summaries per local day
get_rr_intervalsuser_id, series_id, start, end, cursor, limitbeat-to-beat (RR) intervals, as GET /v1/users/{id}/rr-intervals
get_hrvuser_id, series_id, start, end, cursor, limitthe HRV derived from each heartbeat series, as GET /v1/users/{id}/hrv (see hrv.md)
get_menstrual_cyclesuser_id, start, end, cursor, limitmenstrual cycles derived from the recorded flow, as GET /v1/users/{id}/menstrual-cycles; start and end select cycles by their start day (see menstrual-cycles.md)
get_sample_statsuser_id, type, start, end, interval, tz, provider, viewcount, sum, mean, min and max per hour or local day, one series per provider and source
get_coverageuser_id, provider, start, endrecords per provider per day, plus pending_jobs, parked_jobs and rejected_records (lines not stored)
get_tagsuser_id, kind, start, end, cursor, limitthe user's time tags that overlap the window, ongoing ones included, as GET /v1/users/{id}/tags
list_connectionsuser_idprovider connections, status, last sync time and health
list_webhooksdeliveriesthe tenant's webhook endpoints and their health; with deliveries, each one's 10 latest deliveries (tenant key only; endpoints are managed through the REST API)

Each result is one JSON text content:

{"records": [...], "next_cursor": "MjAyNi0w...", "truncated": true}

view is merged (the default: when several sources recorded the same thing, only the best ranked one, so nothing counts twice) or all (every source); see api.md.

limit defaults to 100 and stops at 1000 (a larger value is a tool error). A sample is half a kilobyte of JSON or more, so a full page of 1000 is 500 KB or more of agent context: the server instructions steer agents to get_daily for trends and to narrow windows for raw samples. When truncated is true, call the same tool again with cursor set to next_cursor. get_coverage is capped rather than paged: when it is truncated, narrow start and end. Without start, it reports the 30 days before end (or before now): years of history are thousands of days.

Records carry strings supplied by devices and providers (source and device names, metadata, meal labels), and time tags carry text a person wrote (label, note): an agent must treat them as data, never as instructions. Tags are recorded through the REST API; get_tags only reads them.

Refusals (missing user_id, another user, a bad cursor or time, an unknown workout) come back as tool errors with a short message, so the agent can correct its call.

Prompts

The server also lists prompts (prompts/list): ready-made questions about a person's own training, sleep and recovery. An agent that supports MCP prompts shows them by name, asks for the blanks, and sends the filled question as the user's message. Every prompt takes an optional user_id (needed with a tenant API key, as for the tools), and every prompt it returns ends with the same rules: read the data first and show the numbers, compare the person with their own history rather than population norms, keep to one source per figure, say when a figure is missing instead of filling it in, check the time tags, and stay on wellness (no diagnosis; point to a clinician when something keeps looking off).

PromptArgumentsAsks
morning_checknonethis morning's sleep, resting heart rate and HRV against the 28 days before
why_flatnonethe last 7 days against the 4 weeks before, biggest changes first
weekly_training_reviewnonelast week's training against the average week of the last 8, and sleep after the hardest days
load_and_recoverynone8 weeks of training minutes, sleep, resting heart rate and HRV, with the weeks load rose while HRV fell
sleep_habitsbedtime (default 23:00)30 nights of sleep, split by bedtime
taper_checkevent_datewhether load came down before the event and sleep and heart figures followed
race_week_sleepnoneeach of the last 7 nights against the 30 day average
after_eventevent_datehow many days each figure took to come back to the usual range
after_breakbreak_start, break_endheart figures week by week since training started again, against before the break
new_habitchange, since3 weeks before and after a change, with night to night variation for scale
what_datanoneeach source and kind of data, first and last date, and gaps over 3 days
compare_sourcesmetric (default sleep)two sources of one figure side by side for 30 days

The questions and the rules live in internal/prompts, the one source for these prompts, the prompt library page and the agent skill in skills/open-health-sync, which teaches an agent the same rules and which tool answers what.

Edit on GitHub

On this page