@uphealth/mcp-server
v1.1.4
Published
Uphealth Signal — the adaptive health-messaging engine for apps and agents. Drive federally-sourced, audience-safe cued health-message streams from any MCP client — local stdio via npx, or the hosted mcp.uphealth.us endpoint. Not medical advice.
Maintainers
Readme
Uphealth MCP Server (Track A A4)
The adaptive health-messaging engine for apps and agents — federally-sourced, audience-safe. Not medical advice.
Wraps the api.uphealth.us cued-stream API as agent-callable Model Context Protocol tools. The agent can call lookup_health_fact with zero setup — no key to get one already-public federal health fact, then configure a free up_sandbox_… key to drive an adaptive cued patient stream with the 4 keyed tools.
Locks captured in docs/spec_track_a_a4_mcp_server.md:
- SDK — TypeScript via
@modelcontextprotocol/sdk(flipped from Python at 2026-05-22 build-kickoff revalidation) - Tool surface — 5 tools: a no-key
lookup_health_fact(one already-public federal fact per question, zero setup) + 4 keyed tools (create_patient_stream/get_next_cued_message/read_stream_state/list_sandbox_topics) - Auth — Bearer-token passthrough (MCP server is stateless thin proxy; Rails validates).
lookup_health_factis the sole exception — it needs no key; the per-tool policy still 401s every keyed tool without one. - Corpus — sandbox only at v1
- Hostname —
mcp.uphealth.ussubdomain (Option A locked) - Transport — Streamable HTTP
Local dev
nvm use # picks Node 22 from .nvmrc
npm install
cp .env.example .env
npm run dev # tsx hot-reload of src/index.tsThe dev server listens on :3001. Point an MCP client at http://localhost:3001/mcp with Authorization: Bearer <a valid up_sandbox_… token from api.uphealth.us>.
Build + run (production)
npm ci # full install — typescript (the compiler) is a devDependency
npm run build # tsc → dist/
node dist/index.js # systemd does this via bin/mcp-server-systemd.shTests
npm test # vitest run; mocks global fetchEnvironment
See .env.example for the full list. Required: RAILS_API_BASE_URL. Optional with defaults: MCP_SERVER_PORT (3001), NODE_ENV (development), LOG_LEVEL (info).
Deployment (co-located on the .us droplet at mcp.uphealth.us)
Per spec §6.1 lock 2026-05-22, the MCP server runs as a Node process co-located with Rails on the existing .us droplet (164.92.77.128, $14/mo s-1vcpu-2gb-amd). No separate droplet — the MCP server is a thin Node proxy (< 50 MB RAM, no DB connection, no state) so resource pressure on Rails is negligible. The existing bin/deploy cron handles MCP rebuilds when files under mcp-server/ change.
A separate companion droplet remains a v2 option if MCP traffic exceeds Rails on the shared droplet OR an enterprise buyer requires isolation. See spec §6.1 for the migration path.
One-time provisioning sequence
The provisioning steps live on the existing .us droplet (164.92.77.128). Eric does the DO panel step (#2 DNS); the rest run via SSH.
- Install Node 22 LTS (only step that adds new software; the
.usdroplet currently runs Ruby + Rails only). Install via the NodeSource apt repo sonode+npmland at/usr/bin/on the system PATH:curl -fsSL https://deb.nodesource.com/setup_22.x | bash - apt install -y nodejs # Node 22 + npm at /usr/bin/{node,npm} node --version # expect v22.x which node npm # expect /usr/bin/node /usr/bin/npmA system-PATH install is required, not nvm: (a) the systemd unit's
ExecStart=/usr/bin/nodeis an absolute path, and (b) thebin/deployauto-rebuild hook gates oncommand -v npm, which the cron environment only resolves for a/usr/bininstall. Plainapt install nodejs(without the NodeSource repo) ships an EOL Node on Ubuntu; nvm putsnodeunder~/.nvmwhere neither the absoluteExecStartnor the cron PATH will find it. - Add DNS — Eric adds A record
mcp.uphealth.us → 164.92.77.128via DO Networking → Domains → uphealth.us (same IP as the existinguphealth.us/www.uphealth.us/api.uphealth.usrecords; we're co-locating). Propagation typically minutes. - Configure env — copy
mcp-server/.env.exampleto/root/uphealth-us/mcp-server/.env.production; setRAILS_API_BASE_URL=https://api.uphealth.usandNODE_ENV=production.chmod 600since it's root-only. - Initial build —
cd /root/uphealth-us/mcp-server && npm ci && npm run build(full install —typescriptis a devDependency the build needs — thentsc→dist/; this is exactly whatbin/mcp-server-systemd.shruns on every subsequent deploy). Verifydist/index.jsexists. Do NOT usenpm ci --omit=devhere: it skips the compiler and the build silently fails (see the comment block inbin/mcp-server-systemd.shdocumenting the[email protected]fall-through). - Install systemd unit —
cp /root/uphealth-us/config/systemd/uphealth-mcp-server.service /etc/systemd/system/→systemctl daemon-reload→systemctl enable --now uphealth-mcp-server→systemctl status uphealth-mcp-serverto verify "active (running)". Logs viajournalctl -u uphealth-mcp-server -f. - nginx vhost —
cp /root/uphealth-us/mcp-server/ops/nginx-vhost.conf /etc/nginx/sites-available/uphealth-us-mcp→ln -s /etc/nginx/sites-available/uphealth-us-mcp /etc/nginx/sites-enabled/→nginx -t(must pass) →systemctl reload nginx. - Cert SAN extension —
certbot --nginx --expand -d uphealth.us -d www.uphealth.us -d api.uphealth.us -d mcp.uphealth.us. Existing cert at/etc/letsencrypt/live/uphealth.us/gains the new SAN; auto-renewal via the certbot timer continues. - Auto-deploy cron is already running on
/root/uphealth-us/bin/deployper the existing.ussetup. Thebin/deployextension (PR #424) detects changes undermcp-server/and runsbin/mcp-server-systemd.shautomatically. No new cron entry needed.
After step 8, subsequent pushes that touch mcp-server/ trigger npm ci && npm run build && systemctl restart uphealth-mcp-server automatically (via bin/mcp-server-systemd.sh).
Smoke verification (post-deploy)
Mirror the A1 PR C 6-step smoke against the MCP server using @modelcontextprotocol/inspector:
npx @modelcontextprotocol/inspector \
--transport streamable-http \
--url https://mcp.uphealth.us/mcp \
--header "Authorization: Bearer <up_sandbox_…>"Then in the inspector UI:
create_patient_streamwithtemplate_id: "general_wellness_daily"→ returnsstream_id+first_cueget_next_cued_messageWITHOUT feedback → returnsisError: truewith the moat-explainer messageget_next_cued_messageWITH valid feedback → returns next cueget_next_cued_messageagain with sameidempotency_key+ same feedback → returns the cached cue (idempotent)read_stream_state→ returns current state + events_countlist_sandbox_topics→ returns 5 topic labels
Pass criteria: all 6 steps green; the moat error in step 2 includes the phrase "structural, not a bug".
Registry submission (at A4 launch)
Per docs/spec_track_a_a4_mcp_server.md §9 and future_roadmap.md §3 A4 strategic context, submit at launch to:
- Anthropic MCP registry
- Claude Desktop MCP directory
- Third-party catalogs (mcp.so, mcpservers.com if active at launch time)
Captures the 5-month MCP-positioning window. Don't defer.
