@uphealth/mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ # Use Uphealth Signal from an AI agent (MCP)
2
+
3
+ The Uphealth MCP server lets any [Model Context Protocol](https://modelcontextprotocol.io/) client — Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, or your own agent — drive a cued health-message stream as four tools. It wraps the [Signal API](https://uphealth.us/docs); the model never sees your raw HTTP layer.
4
+
5
+ **Get a free sandbox key** (no credit card, no PHI, no BAA) at **https://uphealth.us/signup**, then pick an install method below.
6
+
7
+ ## The four tools
8
+
9
+ | Tool | Wraps | Does |
10
+ |---|---|---|
11
+ | `create_patient_stream` | `POST /v1/signal/streams` | Start a stream from a template; returns the opening cue |
12
+ | `get_next_cued_message` | `POST /v1/signal/streams/:id/cue` | Submit the patient's response, get the next message |
13
+ | `read_stream_state` | `GET /v1/signal/streams/:id` | Read state + current cue without advancing |
14
+ | `list_sandbox_topics` | `GET /v1/signal/sandbox-topics` | List the five sandbox topics |
15
+
16
+ **The one rule:** every cue after the first requires feedback on the prior one (the model passes the patient's `response_action`). The tool descriptions teach this, so a capable agent drives the loop correctly.
17
+
18
+ ---
19
+
20
+ ## Option A — Local (stdio via `npx`) · recommended
21
+
22
+ No hosting, no URL. The client launches the server as a subprocess with your key in its environment.
23
+
24
+ ### Claude Desktop
25
+
26
+ `claude_desktop_config.json` (Settings → Developer → Edit Config):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "uphealth": {
32
+ "command": "npx",
33
+ "args": ["-y", "@uphealth/mcp-server"],
34
+ "env": { "UPHEALTH_API_KEY": "up_sandbox_YOUR_KEY" }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### Cursor
41
+
42
+ `~/.cursor/mcp.json` (or a project's `.cursor/mcp.json`):
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "uphealth": {
48
+ "command": "npx",
49
+ "args": ["-y", "@uphealth/mcp-server"],
50
+ "env": { "UPHEALTH_API_KEY": "up_sandbox_YOUR_KEY" }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### VS Code (Copilot agent mode)
57
+
58
+ `.vscode/mcp.json`:
59
+
60
+ ```json
61
+ {
62
+ "servers": {
63
+ "uphealth": {
64
+ "command": "npx",
65
+ "args": ["-y", "@uphealth/mcp-server"],
66
+ "env": { "UPHEALTH_API_KEY": "up_sandbox_YOUR_KEY" }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ ### Claude Code (CLI)
73
+
74
+ ```bash
75
+ claude mcp add uphealth --env UPHEALTH_API_KEY=up_sandbox_YOUR_KEY -- npx -y @uphealth/mcp-server
76
+ ```
77
+
78
+ Any other stdio MCP client uses the same shape: command `npx`, args `["-y", "@uphealth/mcp-server"]`, env `UPHEALTH_API_KEY`.
79
+
80
+ ---
81
+
82
+ ## Option B — Remote (hosted Streamable HTTP)
83
+
84
+ The server is also hosted at `https://mcp.uphealth.us/mcp`. Clients with native remote-MCP support add it directly with an `Authorization: Bearer` header. For clients that only speak stdio, bridge with `mcp-remote`:
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "uphealth": {
90
+ "command": "npx",
91
+ "args": [
92
+ "-y", "mcp-remote", "https://mcp.uphealth.us/mcp",
93
+ "--header", "Authorization: Bearer up_sandbox_YOUR_KEY"
94
+ ]
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Verify
103
+
104
+ Use the MCP Inspector against either transport:
105
+
106
+ ```bash
107
+ # local stdio
108
+ UPHEALTH_API_KEY=up_sandbox_YOUR_KEY npx @modelcontextprotocol/inspector npx -y @uphealth/mcp-server
109
+
110
+ # hosted
111
+ npx @modelcontextprotocol/inspector --transport streamable-http \
112
+ --url https://mcp.uphealth.us/mcp \
113
+ --header "Authorization: Bearer up_sandbox_YOUR_KEY"
114
+ ```
115
+
116
+ Then call `create_patient_stream` with `template_id: "general_wellness_daily"`, and `get_next_cued_message` with a `response_action` (e.g. `did_it`).
117
+
118
+ ## Notes
119
+
120
+ - **Keep your key private.** Treat `UPHEALTH_API_KEY` like any secret; don't commit client configs containing a live key.
121
+ - **Display mode only** in the sandbox — no PHI, no BAA. Deliver mode is BAA-gated at the PMPM tier.
122
+ - **Discovery quota:** 50 cues / calendar month. Over the cap, tools return the `over_quota` result with an upgrade link.
123
+
124
+ ## Links
125
+
126
+ - Docs: https://uphealth.us/docs
127
+ - OpenAPI: https://uphealth.us/openapi/signal-v1.yaml
128
+ - SDKs: [`@uphealth/signal`](../sdks/typescript) (Node/TS) · [`uphealth-signal`](../sdks/python) (Python)
129
+ - Get a key: https://uphealth.us/signup
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anschutz Media, Inc. (Uphealth)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Uphealth MCP Server (Track A A4)
2
+
3
+ Wraps the [`api.uphealth.us` cued-stream API](../docs/spec_track_a_a1_cued_stream_api.md) as agent-callable [Model Context Protocol](https://modelcontextprotocol.io/) tools. Buyers configure their Claude Desktop / Cursor / agentic platform with the MCP server URL + a `up_sandbox_…` API token; the agent then calls 4 MCP tools to drive a cued patient stream.
4
+
5
+ Locks captured in [`docs/spec_track_a_a4_mcp_server.md`](../docs/spec_track_a_a4_mcp_server.md):
6
+
7
+ - **SDK** — TypeScript via `@modelcontextprotocol/sdk` (flipped from Python at 2026-05-22 build-kickoff revalidation)
8
+ - **Tool surface** — 4 tools: `create_patient_stream` / `get_next_cued_message` / `read_stream_state` / `list_sandbox_topics`
9
+ - **Auth** — Bearer-token passthrough (MCP server is stateless thin proxy; Rails validates)
10
+ - **Corpus** — sandbox only at v1
11
+ - **Hostname** — `mcp.uphealth.us` subdomain (Option A locked)
12
+ - **Transport** — Streamable HTTP
13
+
14
+ ## Local dev
15
+
16
+ ```bash
17
+ nvm use # picks Node 22 from .nvmrc
18
+ npm install
19
+ cp .env.example .env
20
+ npm run dev # tsx hot-reload of src/index.ts
21
+ ```
22
+
23
+ The 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>`.
24
+
25
+ ## Build + run (production)
26
+
27
+ ```bash
28
+ npm ci # full install — typescript (the compiler) is a devDependency
29
+ npm run build # tsc → dist/
30
+ node dist/index.js # systemd does this via bin/mcp-server-systemd.sh
31
+ ```
32
+
33
+ ## Tests
34
+
35
+ ```bash
36
+ npm test # vitest run; mocks global fetch
37
+ ```
38
+
39
+ ## Environment
40
+
41
+ See [`.env.example`](.env.example) for the full list. Required: `RAILS_API_BASE_URL`. Optional with defaults: `MCP_SERVER_PORT` (`3001`), `NODE_ENV` (`development`), `LOG_LEVEL` (`info`).
42
+
43
+ ## Deployment (co-located on the `.us` droplet at `mcp.uphealth.us`)
44
+
45
+ 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`](../bin/deploy) cron handles MCP rebuilds when files under `mcp-server/` change.
46
+
47
+ 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.
48
+
49
+ ### One-time provisioning sequence
50
+
51
+ 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.
52
+
53
+ 1. **Install Node 22 LTS** (only step that adds new software; the `.us` droplet currently runs Ruby + Rails only). Install via the **NodeSource apt repo** so `node` + `npm` land at `/usr/bin/` on the system PATH:
54
+ ```bash
55
+ curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
56
+ apt install -y nodejs # Node 22 + npm at /usr/bin/{node,npm}
57
+ node --version # expect v22.x
58
+ which node npm # expect /usr/bin/node /usr/bin/npm
59
+ ```
60
+ > A system-PATH install is required, not nvm: (a) the systemd unit's `ExecStart=/usr/bin/node` is an absolute path, and (b) the `bin/deploy` auto-rebuild hook gates on `command -v npm`, which the cron environment only resolves for a `/usr/bin` install. Plain `apt install nodejs` (without the NodeSource repo) ships an EOL Node on Ubuntu; nvm puts `node` under `~/.nvm` where neither the absolute `ExecStart` nor the cron PATH will find it.
61
+ 2. **Add DNS** — Eric adds A record `mcp.uphealth.us → 164.92.77.128` via DO Networking → Domains → uphealth.us (same IP as the existing `uphealth.us` / `www.uphealth.us` / `api.uphealth.us` records; we're co-locating). Propagation typically minutes.
62
+ 3. **Configure env** — copy [`mcp-server/.env.example`](.env.example) to `/root/uphealth-us/mcp-server/.env.production`; set `RAILS_API_BASE_URL=https://api.uphealth.us` and `NODE_ENV=production`. `chmod 600` since it's root-only.
63
+ 4. **Initial build** — `cd /root/uphealth-us/mcp-server && npm ci && npm run build` (full install — `typescript` is a devDependency the build needs — then `tsc` → `dist/`; this is exactly what `bin/mcp-server-systemd.sh` runs on every subsequent deploy). Verify `dist/index.js` exists. Do NOT use `npm ci --omit=dev` here: it skips the compiler and the build silently fails (see the comment block in `bin/mcp-server-systemd.sh` documenting the `tsc@2.0.4` fall-through).
64
+ 5. **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-server` to verify "active (running)". Logs via `journalctl -u uphealth-mcp-server -f`.
65
+ 6. **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`.
66
+ 7. **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.
67
+ 8. **Auto-deploy cron is already running** on `/root/uphealth-us/bin/deploy` per the existing `.us` setup. The `bin/deploy` extension (PR #424) detects changes under `mcp-server/` and runs `bin/mcp-server-systemd.sh` automatically. No new cron entry needed.
68
+
69
+ 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`).
70
+
71
+ ### Smoke verification (post-deploy)
72
+
73
+ Mirror the A1 PR C 6-step smoke against the MCP server using `@modelcontextprotocol/inspector`:
74
+
75
+ ```bash
76
+ npx @modelcontextprotocol/inspector \
77
+ --transport streamable-http \
78
+ --url https://mcp.uphealth.us/mcp \
79
+ --header "Authorization: Bearer <up_sandbox_…>"
80
+ ```
81
+
82
+ Then in the inspector UI:
83
+
84
+ 1. `create_patient_stream` with `template_id: "general_wellness_daily"` → returns `stream_id` + `first_cue`
85
+ 2. `get_next_cued_message` WITHOUT feedback → returns `isError: true` with the moat-explainer message
86
+ 3. `get_next_cued_message` WITH valid feedback → returns next cue
87
+ 4. `get_next_cued_message` again with same `idempotency_key` + same feedback → returns the cached cue (idempotent)
88
+ 5. `read_stream_state` → returns current state + events_count
89
+ 6. `list_sandbox_topics` → returns 5 topic labels
90
+
91
+ Pass criteria: all 6 steps green; the moat error in step 2 includes the phrase "structural, not a bug".
92
+
93
+ ## Registry submission (at A4 launch)
94
+
95
+ Per [`docs/spec_track_a_a4_mcp_server.md`](../docs/spec_track_a_a4_mcp_server.md) §9 and [`future_roadmap.md`](../docs/future_roadmap.md) §3 A4 strategic context, submit at launch to:
96
+
97
+ - Anthropic MCP registry
98
+ - Claude Desktop MCP directory
99
+ - Third-party catalogs (mcp.so, mcpservers.com if active at launch time)
100
+
101
+ Captures the 5-month MCP-positioning window. Don't defer.
@@ -0,0 +1,10 @@
1
+ // Side-effect module imported FIRST by the stdio entrypoint, before any module
2
+ // that reads config.ts. A local `npx @uphealth/mcp-server` user should not have
3
+ // to set RAILS_API_BASE_URL — it always points at the public API host — so we
4
+ // default it here when unset. The hosted HTTP deployment still sets it
5
+ // explicitly via .env.production, which takes precedence.
6
+ if (!process.env.RAILS_API_BASE_URL) {
7
+ process.env.RAILS_API_BASE_URL = "https://api.uphealth.us";
8
+ }
9
+ export {};
10
+ //# sourceMappingURL=bootstrap-env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-env.js","sourceRoot":"","sources":["../src/bootstrap-env.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,gFAAgF;AAChF,8EAA8E;AAC9E,uEAAuE;AACvE,0DAA0D;AAC1D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC;IACpC,OAAO,CAAC,GAAG,CAAC,kBAAkB,GAAG,yBAAyB,CAAC;AAC7D,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,23 @@
1
+ // Track A A4 PR 2 — Env-driven config for the MCP server.
2
+ // Strict env parsing at boot; throws with a clear message if a required
3
+ // var is missing so systemd boot logs surface the misconfiguration
4
+ // instead of silently defaulting.
5
+ function requireEnv(name) {
6
+ const value = process.env[name];
7
+ if (!value) {
8
+ throw new Error(`Missing required env var: ${name}. See mcp-server/.env.example for the full list.`);
9
+ }
10
+ return value;
11
+ }
12
+ function optionalEnv(name, fallback) {
13
+ return process.env[name] ?? fallback;
14
+ }
15
+ export const config = {
16
+ port: parseInt(optionalEnv("MCP_SERVER_PORT", "3001"), 10),
17
+ railsApiBaseUrl: requireEnv("RAILS_API_BASE_URL"),
18
+ nodeEnv: optionalEnv("NODE_ENV", "development"),
19
+ logLevel: optionalEnv("LOG_LEVEL", "info"),
20
+ serverName: "uphealth-mcp",
21
+ serverVersion: "1.0.0",
22
+ };
23
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,wEAAwE;AACxE,mEAAmE;AACnE,kCAAkC;AAElC,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,6BAA6B,IAAI,kDAAkD,CACpF,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,QAAgB;IACjD,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,QAAQ,CAAC,WAAW,CAAC,iBAAiB,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;IAC1D,eAAe,EAAE,UAAU,CAAC,oBAAoB,CAAC;IACjD,OAAO,EAAE,WAAW,CAAC,UAAU,EAAE,aAAa,CAAC;IAC/C,QAAQ,EAAE,WAAW,CAAC,WAAW,EAAE,MAAM,CAAC;IAC1C,UAAU,EAAE,cAAc;IAC1B,aAAa,EAAE,OAAO;CACd,CAAC"}
@@ -0,0 +1,37 @@
1
+ // Track A A4 PR 2 — Per-request context for Bearer-passthrough auth.
2
+ //
3
+ // MCP tool handlers don't directly receive the inbound HTTP headers
4
+ // (the SDK abstracts the transport). Use AsyncLocalStorage to bind the
5
+ // inbound Authorization header to the request's async context so tool
6
+ // handlers can read it without it leaking across concurrent requests.
7
+ //
8
+ // Spec: docs/spec_track_a_a4_mcp_server.md §4 (statelessness — server
9
+ // holds no per-tenant state; the AsyncLocalStorage scope is per-async-
10
+ // frame, not a global cache, so this preserves the property).
11
+ import { AsyncLocalStorage } from "node:async_hooks";
12
+ const storage = new AsyncLocalStorage();
13
+ // Process-level fallback context. The HTTP transport (index.ts) binds the
14
+ // bearer per request via withRequestContext (AsyncLocalStorage), so each
15
+ // concurrent tenant stays isolated. The stdio transport (stdio.ts) is a
16
+ // single-tenant local process — one user, one key for the process lifetime —
17
+ // where there is no per-request scope to wrap the long-lived connection's
18
+ // later message callbacks in. For that case a process-level fallback is the
19
+ // correct model. The ALS store always wins when present, so the HTTP path is
20
+ // unaffected and multi-tenant isolation is preserved.
21
+ let fallbackContext = null;
22
+ export function setFallbackContext(ctx) {
23
+ fallbackContext = ctx;
24
+ }
25
+ export function withRequestContext(ctx, fn) {
26
+ return storage.run(ctx, fn);
27
+ }
28
+ export function currentContext() {
29
+ const ctx = storage.getStore() ?? fallbackContext;
30
+ if (!ctx) {
31
+ // Tool handler called outside any request context — only happens in tests
32
+ // that forget the harness, or in misconfigured code paths.
33
+ throw new Error("No request context — tool handler called outside withRequestContext scope");
34
+ }
35
+ return ctx;
36
+ }
37
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,EAAE;AACF,oEAAoE;AACpE,uEAAuE;AACvE,sEAAsE;AACtE,sEAAsE;AACtE,EAAE;AACF,sEAAsE;AACtE,uEAAuE;AACvE,8DAA8D;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAMrD,MAAM,OAAO,GAAG,IAAI,iBAAiB,EAAkB,CAAC;AAExD,0EAA0E;AAC1E,yEAAyE;AACzE,wEAAwE;AACxE,6EAA6E;AAC7E,0EAA0E;AAC1E,4EAA4E;AAC5E,6EAA6E;AAC7E,sDAAsD;AACtD,IAAI,eAAe,GAA0B,IAAI,CAAC;AAElD,MAAM,UAAU,kBAAkB,CAAC,GAA0B;IAC3D,eAAe,GAAG,GAAG,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAI,GAAmB,EAAE,EAAoB;IAC7E,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,EAAE,IAAI,eAAe,CAAC;IAClD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,0EAA0E;QAC1E,2DAA2D;QAC3D,MAAM,IAAI,KAAK,CACb,2EAA2E,CAC5E,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,32 @@
1
+ // Track A A4 — Canonical cue `response_action` vocabulary (single TS source).
2
+ //
3
+ // This list MUST stay byte-for-byte in sync with the Rails canonical constant
4
+ // AdaptiveResponse::Archetypes::CUE_ACCEPTED_ACTIONS
5
+ // (= CuedStreams::SandboxSelector::VALID_RESPONSE_ACTIONS)
6
+ // in lib/adaptive_response/archetypes.rb. Per the A4 spec §6.3 + §9.5.6 the MCP
7
+ // tool schema must NOT hand-maintain a second copy that can drift — so every
8
+ // tool that advertises a response_action enum imports THIS constant, and a
9
+ // Rails CI drift spec (spec/lib/adaptive_response/mcp_cue_enum_drift_spec.rb)
10
+ // fails the build if the two ever diverge.
11
+ //
12
+ // History: before 2026-06-13 the get_next_cued_message tool hand-listed the
13
+ // pre-§9.5 set ["did_it", "check_it", "know_it", "skipped"]. That advertised the
14
+ // now-removed `skipped` (Rails 422s it) and omitted six valid actions
15
+ // (already_do / will_try / new_to_me / needed_this / acknowledge / no_response),
16
+ // so the tool's own JSON-Schema rejected valid feedback before it reached Rails.
17
+ //
18
+ // Forward-compat (§9.5.4): the vocabulary GROWS over time; MCP clients MUST
19
+ // tolerate unknown response_action values in responses and SHOULD treat unknown
20
+ // archetypes as `acknowledge`-class. Widening = bump this list + redeploy.
21
+ export const CUE_RESPONSE_ACTIONS = [
22
+ "did_it",
23
+ "already_do",
24
+ "will_try",
25
+ "check_it",
26
+ "new_to_me",
27
+ "needed_this",
28
+ "know_it",
29
+ "acknowledge",
30
+ "no_response",
31
+ ];
32
+ //# sourceMappingURL=cue-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cue-actions.js","sourceRoot":"","sources":["../src/cue-actions.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,8EAA8E;AAC9E,uDAAuD;AACvD,6DAA6D;AAC7D,gFAAgF;AAChF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,2CAA2C;AAC3C,EAAE;AACF,4EAA4E;AAC5E,iFAAiF;AACjF,sEAAsE;AACtE,iFAAiF;AACjF,iFAAiF;AACjF,EAAE;AACF,4EAA4E;AAC5E,gFAAgF;AAChF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,QAAQ;IACR,YAAY;IACZ,UAAU;IACV,UAAU;IACV,WAAW;IACX,aAAa;IACb,SAAS;IACT,aAAa;IACb,aAAa;CACL,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,86 @@
1
+ // Track A A4 PR 2 — MCP server entry point.
2
+ //
3
+ // Stack:
4
+ // Express ←→ StreamableHTTPServerTransport ←→ MCP Server ←→ tool handlers
5
+ //
6
+ // Per-request flow:
7
+ // 1. nginx vhost at mcp.uphealth.us terminates TLS + proxies to localhost:3001
8
+ // 2. Express receives POST /mcp (or GET /mcp for SSE upgrade) with
9
+ // Authorization: Bearer up_sandbox_… header
10
+ // 3. Express middleware extracts the Bearer + wraps the rest of the
11
+ // handler chain in withRequestContext({ bearer }) so tool handlers
12
+ // can read it without it leaking across concurrent requests.
13
+ // 4. StreamableHTTPServerTransport handles the MCP JSON-RPC envelope
14
+ // + dispatches to the registered tool handler.
15
+ // 5. Tool handler calls Rails via the rails-client.ts wrapper, which
16
+ // reads the bearer from currentContext().
17
+ // 6. Rails response (200 or non-2xx) gets translated to an MCP tool
18
+ // result by tool-result.ts and returned to the agent.
19
+ //
20
+ // Statelessness: the MCP server holds no per-tenant state, no token
21
+ // cache, no session store. Auth state lives in Rails. Per the A4 spec
22
+ // §4 "Statelessness" property.
23
+ //
24
+ // Spec: docs/spec_track_a_a4_mcp_server.md §3 + §4 + §6.
25
+ import express from "express";
26
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
27
+ import { config } from "./config.js";
28
+ import { withRequestContext } from "./context.js";
29
+ import { buildServer } from "./server.js";
30
+ // The MCP Server factory lives in server.ts and is shared with the stdio
31
+ // entrypoint (stdio.ts), so tools + annotations are registered in exactly
32
+ // one place. This module is the Streamable HTTP transport only.
33
+ // ---------- Express wrapper ----------
34
+ function extractBearer(req) {
35
+ const header = req.header("authorization") ?? req.header("Authorization");
36
+ if (!header)
37
+ return null;
38
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
39
+ return match ? match[1].trim() : null;
40
+ }
41
+ function startHttpServer() {
42
+ const app = express();
43
+ app.use(express.json({ limit: "1mb" }));
44
+ // Health check (does NOT require auth; useful for nginx/load-balancer probes).
45
+ app.get("/health", (_req, res) => {
46
+ res.status(200).json({ status: "ok", server: config.serverName, version: config.serverVersion });
47
+ });
48
+ // MCP endpoint — POST is request/response; GET is SSE upgrade for
49
+ // server-initiated streams (Streamable HTTP transport spec).
50
+ const mcpHandler = async (req, res, _next) => {
51
+ const bearer = extractBearer(req);
52
+ if (!bearer) {
53
+ res.status(401).json({
54
+ error: "unauthorized",
55
+ message: "Missing or malformed Authorization: Bearer header. Get a free sandbox key at https://uphealth.us/signup, then send it as 'Authorization: Bearer up_sandbox_…'.",
56
+ });
57
+ return;
58
+ }
59
+ try {
60
+ await withRequestContext({ bearer }, async () => {
61
+ const server = buildServer();
62
+ const transport = new StreamableHTTPServerTransport({
63
+ sessionIdGenerator: undefined, // stateless mode per A4 spec §4
64
+ });
65
+ await server.connect(transport);
66
+ await transport.handleRequest(req, res, req.body);
67
+ });
68
+ }
69
+ catch (err) {
70
+ // Catch-all error guard — keep nginx + the Streamable HTTP transport
71
+ // from observing an unhandled exception. Logs server-side.
72
+ console.error("[mcp-server] handler error:", err);
73
+ if (!res.headersSent) {
74
+ res.status(500).json({ error: "internal_error" });
75
+ }
76
+ }
77
+ };
78
+ app.post("/mcp", mcpHandler);
79
+ app.get("/mcp", mcpHandler);
80
+ app.listen(config.port, () => {
81
+ console.log(`[${config.serverName}@${config.serverVersion}] listening on :${config.port} → ${config.railsApiBaseUrl}`);
82
+ });
83
+ }
84
+ // Boot.
85
+ startHttpServer();
86
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ // Track A A4 / A5 (2026-06-14) — MCP `outputSchema` declarations for the 4 tools.
2
+ //
3
+ // Declaring outputSchema (+ returning `structuredContent`, see tool-result.ts)
4
+ // lets MCP hosts validate and parse a tool's result reliably instead of
5
+ // re-parsing the human-readable text blob. The shapes mirror the Rails
6
+ // responses in public/openapi/signal-v1.json (StreamCreated / CueOutcome /
7
+ // StreamRead / SandboxTopics) and the SDK types.
8
+ //
9
+ // Forward-compat by construction: JSON Schema's `additionalProperties`
10
+ // defaults to TRUE when omitted, so a future additive response field never
11
+ // makes a strict host reject a valid result — the same tolerance the response
12
+ // vocabulary itself follows. Only `required` lists fields the API always emits.
13
+ //
14
+ // The SDK's Tool.outputSchema is a ZodObject<{ type: "object", properties?,
15
+ // required? }> — so each tool's TOP-LEVEL schema carries exactly those keys
16
+ // (the top-level `type` MUST be the literal "object"). Descriptions + nested
17
+ // detail live on the property schemas, which the SDK passes through verbatim.
18
+ // A single cued message + its receptivity score and audience-safety verdict.
19
+ const CUE_SCHEMA = {
20
+ type: "object",
21
+ description: "A cued message plus its per-cue receptivity score and audience-safety verdict.",
22
+ properties: {
23
+ cue_event_id: { type: "integer" },
24
+ message_id: { type: "integer" },
25
+ message_class: { type: "string" },
26
+ kind: { type: "string", description: "fact | list | tip" },
27
+ body: { type: "string" },
28
+ source_url: { type: "string" },
29
+ source_quote: { type: "string" },
30
+ receptivity: { type: "number", description: "Advisory receptivity in [0.0, 1.0]; 0.5 = neutral/new patient." },
31
+ safety: { type: "object", additionalProperties: true, description: "PHI-free audience-safety verdict." },
32
+ cued_at: { type: "string", description: "ISO 8601 timestamp." },
33
+ },
34
+ additionalProperties: true,
35
+ };
36
+ // Legal/disclosure block returned on every success (A1 §6.7). Do not strip in your UI.
37
+ const META_SCHEMA = {
38
+ type: "object",
39
+ description: "Legal/disclosure block returned on every 2xx (A1 §6.7).",
40
+ properties: {
41
+ api_version: { type: "string" },
42
+ no_medical_advice: { type: "boolean" },
43
+ not_samd: { type: "boolean" },
44
+ license_attribution: { type: "string" },
45
+ delivery_mode: { type: "string" },
46
+ sequencing_ip_notice: { type: "string" },
47
+ },
48
+ additionalProperties: true,
49
+ };
50
+ export const createPatientStreamOutputSchema = {
51
+ type: "object",
52
+ properties: {
53
+ stream_id: { type: "integer", description: "Persist this — every later cue/read references it." },
54
+ state: { type: "string" },
55
+ first_cue: CUE_SCHEMA,
56
+ next_cue_eligible_at: { type: ["string", "null"] },
57
+ _meta: META_SCHEMA,
58
+ },
59
+ required: ["stream_id", "state", "first_cue", "_meta"],
60
+ };
61
+ export const getNextCuedMessageOutputSchema = {
62
+ type: "object",
63
+ properties: {
64
+ stream_id: { type: "integer" },
65
+ state: { type: "string" },
66
+ // CueResult fields:
67
+ idempotent: { type: "boolean", description: "true when this is the cached result of a repeated idempotency_key." },
68
+ next_cue: CUE_SCHEMA,
69
+ // StreamExhausted fields (end-of-stream marker):
70
+ sandbox_exhausted: { type: "boolean", description: "Present + true when the sandbox subset is exhausted; no next_cue then." },
71
+ message: { type: "string" },
72
+ _meta: META_SCHEMA,
73
+ },
74
+ // next_cue is omitted from `required` because the exhausted-stream result has
75
+ // no next_cue — only the common fields are guaranteed on every cue response.
76
+ required: ["stream_id", "state", "_meta"],
77
+ };
78
+ export const readStreamStateOutputSchema = {
79
+ type: "object",
80
+ properties: {
81
+ stream_id: { type: "integer" },
82
+ template_id: { type: "string" },
83
+ state: { type: "string" },
84
+ current_cue: { type: ["object", "null"], additionalProperties: true },
85
+ next_cue_eligible: { type: "boolean" },
86
+ events_count: { type: "integer" },
87
+ _meta: META_SCHEMA,
88
+ },
89
+ required: ["stream_id", "state", "_meta"],
90
+ };
91
+ export const listSandboxTopicsOutputSchema = {
92
+ type: "object",
93
+ properties: {
94
+ topic_labels: { type: "array", items: { type: "string" } },
95
+ template_id_for_sandbox: { type: "string" },
96
+ _meta: META_SCHEMA,
97
+ },
98
+ required: ["topic_labels", "_meta"],
99
+ };
100
+ //# sourceMappingURL=output-schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-schemas.js","sourceRoot":"","sources":["../src/output-schemas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ // Track A A4 PR 2 — Thin HTTP wrapper around api.uphealth.us A1 endpoints.
2
+ //
3
+ // Bearer-token passthrough per Eric Gate-1 2026-05-18 (preserved at
4
+ // 2026-05-22 build-kickoff revalidation). The MCP server reads the
5
+ // Authorization header on inbound MCP requests and forwards it
6
+ // unmodified to Rails. No token cache, no session store, no auth
7
+ // state on the MCP server side — Rails is the single source of truth.
8
+ //
9
+ // Spec: docs/spec_track_a_a4_mcp_server.md §6.4 (error propagation).
10
+ import { config } from "./config.js";
11
+ /**
12
+ * Perform a Bearer-passthrough HTTP call to the Rails A1 API.
13
+ *
14
+ * `bearer` is the agent's auth token (already in `up_sandbox_…` shape).
15
+ * Returns the full Rails response (status + parsed body) so callers can
16
+ * branch on Rails error codes per spec §6.4.
17
+ */
18
+ export async function callRails(opts) {
19
+ const url = new URL(opts.path, config.railsApiBaseUrl).toString();
20
+ const headers = {
21
+ Authorization: `Bearer ${opts.bearer}`,
22
+ Accept: "application/json",
23
+ };
24
+ const fetchInit = {
25
+ method: opts.method,
26
+ headers,
27
+ };
28
+ if (opts.body !== undefined) {
29
+ headers["Content-Type"] = "application/json";
30
+ fetchInit.body = JSON.stringify(opts.body);
31
+ }
32
+ const response = await fetch(url, fetchInit);
33
+ const responseHeaders = {};
34
+ response.headers.forEach((value, key) => {
35
+ responseHeaders[key] = value;
36
+ });
37
+ let parsed = null;
38
+ const text = await response.text();
39
+ if (text.length > 0) {
40
+ try {
41
+ parsed = JSON.parse(text);
42
+ }
43
+ catch {
44
+ // Rails should always return JSON for API endpoints; if it didn't,
45
+ // the response body is opaque text. Surface it as an error body.
46
+ parsed = { error: "non_json_response", raw: text };
47
+ }
48
+ }
49
+ return { status: response.status, body: parsed, headers: responseHeaders };
50
+ }
51
+ /**
52
+ * Detect Rails error responses (non-2xx). Used by tool handlers to
53
+ * decide whether to return MCP tool results as isError per spec §6.4.
54
+ *
55
+ * Note: create_patient_stream wraps `POST /v1/signal/streams`, which renders
56
+ * 201 Created on success — the `< 200 || >= 300` range (not `!== 200`)
57
+ * deliberately treats 201 as success. tools.test.ts pins this.
58
+ */
59
+ export function isErrorStatus(status) {
60
+ return status < 200 || status >= 300;
61
+ }
62
+ //# sourceMappingURL=rails-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rails-client.js","sourceRoot":"","sources":["../src/rails-client.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,EAAE;AACF,oEAAoE;AACpE,mEAAmE;AACnE,+DAA+D;AAC/D,iEAAiE;AACjE,sEAAsE;AACtE,EAAE;AACF,qEAAqE;AAErE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAgBrC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAc,IAK5C;IACC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC,QAAQ,EAAE,CAAC;IAClE,MAAM,OAAO,GAA2B;QACtC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;QACtC,MAAM,EAAE,kBAAkB;KAC3B,CAAC;IAEF,MAAM,SAAS,GAAgB;QAC7B,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO;KACR,CAAC;IAEF,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAC;QAC7C,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAE7C,MAAM,eAAe,GAA2B,EAAE,CAAC;IACnD,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACtC,eAAe,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,IAAI,MAAM,GAAY,IAAI,CAAC;IAC3B,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;YACnE,iEAAiE;YACjE,MAAM,GAAG,EAAE,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,IAAI,EAA2B,CAAC;QAC9E,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,MAAW,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC;AAClF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,OAAO,MAAM,GAAG,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC;AACvC,CAAC"}
package/dist/server.js ADDED
@@ -0,0 +1,38 @@
1
+ // Shared MCP Server factory — used by BOTH transports:
2
+ // - src/index.ts (Streamable HTTP, hosted at mcp.uphealth.us)
3
+ // - src/stdio.ts (stdio, for local `npx @uphealth/mcp-server` installs)
4
+ //
5
+ // Holds no per-tenant state. The bearer is resolved per call from the request
6
+ // context (context.ts): the HTTP path binds it per request; the stdio path
7
+ // sets a process-level fallback. Tools + tool-result are identical across both.
8
+ //
9
+ // Spec: docs/spec_track_a_a4_mcp_server.md §3 + §4 + §6.
10
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
+ import { config } from "./config.js";
13
+ import { tools, findToolByName } from "./tools/index.js";
14
+ import { errorResult } from "./tool-result.js";
15
+ export function buildServer() {
16
+ const server = new Server({ name: config.serverName, version: config.serverVersion }, { capabilities: { tools: {} } });
17
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
18
+ tools: tools.map((t) => ({
19
+ name: t.name,
20
+ description: t.description,
21
+ inputSchema: t.inputSchema,
22
+ annotations: t.annotations,
23
+ // A5 (2026-06-14) — outputSchema lets hosts validate/parse structuredContent.
24
+ outputSchema: t.outputSchema,
25
+ })),
26
+ }));
27
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
28
+ const toolName = req.params.name;
29
+ const args = (req.params.arguments ?? {});
30
+ const tool = findToolByName(toolName);
31
+ if (!tool) {
32
+ return errorResult(`Unknown tool: ${toolName}`);
33
+ }
34
+ return (await tool.handler(args));
35
+ });
36
+ return server;
37
+ }
38
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,kEAAkE;AAClE,4EAA4E;AAC5E,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,gFAAgF;AAChF,EAAE;AACF,yDAAyD;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GAEvB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,UAAU,WAAW;IACzB,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,aAAa,EAAE,EAC1D,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QAC5D,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACvB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,8EAA8E;YAC9E,YAAY,EAAE,CAAC,CAAC,YAAY;SAC7B,CAAC,CAAC;KACJ,CAAC,CAAC,CAAC;IAEJ,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,GAAG,EAA2B,EAAE;QACrF,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QACjC,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAA4B,CAAC;QACrE,MAAM,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,WAAW,CAAC,iBAAiB,QAAQ,EAAE,CAAmB,CAAC;QACpE,CAAC;QACD,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAmB,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/dist/stdio.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // Stdio entrypoint — makes the Uphealth MCP server runnable as a local
3
+ // subprocess by any MCP client (Claude Desktop, Cursor, VS Code, etc.) via:
4
+ //
5
+ // npx -y @uphealth/mcp-server
6
+ //
7
+ // with the user's key in the environment:
8
+ //
9
+ // UPHEALTH_API_KEY=up_sandbox_…
10
+ //
11
+ // This is the dominant MCP install pattern (local stdio), complementing the
12
+ // hosted Streamable HTTP deployment at mcp.uphealth.us (src/index.ts). The two
13
+ // share the same tools and the same Server factory (server.ts).
14
+ //
15
+ // A stdio process is single-tenant: one user, one key for its lifetime. We set
16
+ // that key as the process-level fallback context, so the same tool handlers
17
+ // that read the per-request bearer over HTTP work unchanged here.
18
+ import "./bootstrap-env.js"; // must run before anything imports config.ts
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { setFallbackContext } from "./context.js";
21
+ import { buildServer } from "./server.js";
22
+ async function main() {
23
+ const apiKey = process.env.UPHEALTH_API_KEY?.trim();
24
+ if (!apiKey) {
25
+ // Write guidance to stderr (stdout is the MCP JSON-RPC channel).
26
+ console.error("Uphealth MCP: missing UPHEALTH_API_KEY. Get a free sandbox key at " +
27
+ "https://uphealth.us/signup, then set UPHEALTH_API_KEY in your MCP client config.");
28
+ process.exit(1);
29
+ }
30
+ setFallbackContext({ bearer: apiKey });
31
+ const server = buildServer();
32
+ const transport = new StdioServerTransport();
33
+ await server.connect(transport);
34
+ // The process now stays alive serving JSON-RPC over stdio until the client
35
+ // closes stdin. No banner on stdout — it is the protocol channel.
36
+ }
37
+ main().catch((err) => {
38
+ console.error("[uphealth-mcp] fatal:", err);
39
+ process.exit(1);
40
+ });
41
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":";AACA,uEAAuE;AACvE,4EAA4E;AAC5E,EAAE;AACF,gCAAgC;AAChC,EAAE;AACF,0CAA0C;AAC1C,EAAE;AACF,kCAAkC;AAClC,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,gEAAgE;AAChE,EAAE;AACF,+EAA+E;AAC/E,4EAA4E;AAC5E,kEAAkE;AAElE,OAAO,oBAAoB,CAAC,CAAC,6CAA6C;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,CAAC;IACpD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,iEAAiE;QACjE,OAAO,CAAC,KAAK,CACX,oEAAoE;YAClE,kFAAkF,CACrF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAEvC,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;IAC7B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,2EAA2E;IAC3E,kEAAkE;AACpE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,90 @@
1
+ // Track A A4 PR 2 — Helpers for building MCP tool results.
2
+ //
3
+ // Spec: docs/spec_track_a_a4_mcp_server.md §6.4 — Rails non-2xx
4
+ // responses propagate as isError with a structured message. Successful
5
+ // responses include the Rails body verbatim (preserving the _meta
6
+ // block per A1 §6.7).
7
+ export function successResult(body) {
8
+ return {
9
+ // The text block stays as the JSON serialization of structuredContent so
10
+ // hosts that don't support structuredContent still get the full payload
11
+ // (MCP backward-compat guidance).
12
+ content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
13
+ structuredContent: body,
14
+ };
15
+ }
16
+ export function errorResult(message) {
17
+ return {
18
+ content: [{ type: "text", text: message }],
19
+ isError: true,
20
+ };
21
+ }
22
+ /**
23
+ * Translate a Rails error response into an MCP tool error result per
24
+ * the spec §6.4 propagation table. Preserves Rails error codes + any
25
+ * structured detail fields so agents can branch on the underlying cause.
26
+ *
27
+ * THE MOAT (A1 §3) is enforced via the 409 feedback_required case —
28
+ * the error message must clearly communicate the constraint is
29
+ * structural, not a transient bug.
30
+ */
31
+ export function railsErrorToToolResult(status, body, context, headers) {
32
+ const errCode = body?.error ?? "unknown_error";
33
+ const ctxPrefix = context ? `${context}: ` : "";
34
+ switch (status) {
35
+ case 401:
36
+ return errorResult(`${ctxPrefix}Authentication failed. Check that your Authorization: Bearer header carries a valid up_sandbox_… token. Get a free sandbox key at https://uphealth.us/signup.`);
37
+ case 403:
38
+ if (errCode === "insufficient_scope") {
39
+ return errorResult(`${ctxPrefix}Insufficient scope. Your token lacks the '${body?.required ?? "required"}' scope. Upgrade tier or request a scope grant.`);
40
+ }
41
+ return errorResult(`${ctxPrefix}Forbidden: ${errCode}`);
42
+ case 404:
43
+ return errorResult(`${ctxPrefix}Resource not found (or belongs to a different tenant).`);
44
+ case 409:
45
+ if (errCode === "feedback_required") {
46
+ return errorResult(`${ctxPrefix}THE MOAT (A1 §3 Design Constraint #1) — cannot advance stream without submitting feedback for cue_event_id ${body?.last_cue_event_id} first. This is structural, not a bug. Provide feedback in your next call.`);
47
+ }
48
+ if (errCode === "idempotency_key_conflict") {
49
+ return errorResult(`${ctxPrefix}Idempotency key already used for a different feedback payload (existing_event_id ${body?.existing_event_id}). Use a fresh UUID or replay the original payload exactly.`);
50
+ }
51
+ if (errCode === "stream_ended") {
52
+ return errorResult(`${ctxPrefix}Stream is ended; cannot advance further.`);
53
+ }
54
+ return errorResult(`${ctxPrefix}Conflict: ${errCode}`);
55
+ case 422:
56
+ if (errCode === "missing_required_audience_tags") {
57
+ return errorResult(`${ctxPrefix}This template requires audience tag(s) ${JSON.stringify(body?.required ?? [])} that were not supplied. Add them to the audience_tags array and retry.`);
58
+ }
59
+ if (errCode === "incompatible_audience_tag") {
60
+ return errorResult(`${ctxPrefix}Incompatible audience tag(s) ${JSON.stringify(body?.incompatible ?? [])} for this template. Remove them from the audience_tags array and retry.`);
61
+ }
62
+ return errorResult(`${ctxPrefix}Invalid parameters: ${JSON.stringify(body?.details ?? body?.message ?? body)}`);
63
+ case 429:
64
+ if (errCode === "over_quota") {
65
+ // GTM P7 Discovery cap — the upgrade moment, NOT a transient failure.
66
+ // Surface the upgrade_url + usage so the agent can route the developer
67
+ // to checkout, plus the Retry-After window (seconds until the calendar-
68
+ // month reset, UTC) from the response header (rails-client populates it).
69
+ const retryAfter = headers?.["retry-after"];
70
+ const usage = body?.cap != null ? ` (${body?.current_count ?? "?"}/${body?.cap} this calendar month)` : "";
71
+ const upgrade = body?.upgrade_url ? ` Upgrade to continue: ${body?.upgrade_url}.` : "";
72
+ const retry = retryAfter
73
+ ? ` The cap resets at the start of the next calendar month (UTC) — Retry-After: ~${retryAfter}s.`
74
+ : "";
75
+ return errorResult(`${ctxPrefix}Discovery monthly cue cap reached${usage}. This is an upgrade signal, not a transient bug — retrying the same call will keep hitting the cap.${upgrade}${retry}`);
76
+ }
77
+ return errorResult(`${ctxPrefix}Too many requests: ${errCode}`);
78
+ case 503:
79
+ if (errCode === "sandbox_corpus_empty") {
80
+ return errorResult(`${ctxPrefix}Sandbox corpus is being seeded; retry in a moment.`);
81
+ }
82
+ if (errCode === "content_unavailable") {
83
+ return errorResult(`${ctxPrefix}The content corpus is temporarily unreachable upstream. This is transient — retry in a moment.`);
84
+ }
85
+ return errorResult(`${ctxPrefix}Service unavailable: ${errCode}`);
86
+ default:
87
+ return errorResult(`${ctxPrefix}Unexpected Rails response (status=${status}): ${errCode}`);
88
+ }
89
+ }
90
+ //# sourceMappingURL=tool-result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-result.js","sourceRoot":"","sources":["../src/tool-result.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ // Track A A4 PR 2 — MCP tool: create_patient_stream
2
+ //
3
+ // Wraps Rails:
4
+ // POST /v1/signal/streams
5
+ // Creates a cued stream for a patient. Returns stream_id + first
6
+ // cued message. Each subsequent cue requires submitting the prior
7
+ // message's feedback (the moat per A1 §3).
8
+ //
9
+ // v1 accepts only `general_wellness_daily` template + `display`
10
+ // delivery_mode per the locked sandbox scope. Deliver-mode unlocks
11
+ // at Per-Episode tier after BAA execution.
12
+ //
13
+ // Scope required: stream:create.
14
+ // Spec: docs/spec_track_a_a4_mcp_server.md §6.3.
15
+ import { callRails, isErrorStatus } from "../rails-client.js";
16
+ import { currentContext } from "../context.js";
17
+ import { successResult, railsErrorToToolResult, } from "../tool-result.js";
18
+ import { createPatientStreamOutputSchema } from "../output-schemas.js";
19
+ export const createPatientStreamTool = {
20
+ name: "create_patient_stream",
21
+ description: "Create a cued stream for a patient. Returns stream_id + first cued message. Each subsequent cue MUST submit the prior message's feedback — the engine refuses to advance without it (the moat mechanic).",
22
+ annotations: {
23
+ title: "Create patient stream",
24
+ readOnlyHint: false,
25
+ destructiveHint: false,
26
+ openWorldHint: true,
27
+ },
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ template_id: {
32
+ type: "string",
33
+ enum: ["general_wellness_daily"],
34
+ description: "v1 sandbox supports one template; future templates land as v1+ ships.",
35
+ },
36
+ attributes: {
37
+ type: "object",
38
+ description: "De-identified patient context. v1 accepts free-form jsonb; future templates will enforce schemas.",
39
+ },
40
+ audience_tags: {
41
+ type: "array",
42
+ items: { type: "string" },
43
+ description: "Subset of the 14-tag B5/B6 schema. See list_sandbox_topics for sample topic labels.",
44
+ },
45
+ delivery_mode: {
46
+ type: "string",
47
+ enum: ["display"],
48
+ default: "display",
49
+ description: "v1 is Display-only; Deliver-mode unlocks at Per-Episode tier after BAA.",
50
+ },
51
+ feedback_timeout_days: {
52
+ type: "integer",
53
+ default: 14,
54
+ },
55
+ },
56
+ required: ["template_id"],
57
+ additionalProperties: true,
58
+ },
59
+ outputSchema: createPatientStreamOutputSchema,
60
+ async handler(args) {
61
+ const { bearer } = currentContext();
62
+ const response = await callRails({
63
+ method: "POST",
64
+ path: "/v1/signal/streams",
65
+ bearer,
66
+ body: args,
67
+ });
68
+ if (isErrorStatus(response.status)) {
69
+ return railsErrorToToolResult(response.status, response.body, "create_patient_stream", response.headers);
70
+ }
71
+ return successResult(response.body);
72
+ },
73
+ };
74
+ //# sourceMappingURL=create-patient-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-patient-stream.js","sourceRoot":"","sources":["../../src/tools/create-patient-stream.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,EAAE;AACF,eAAe;AACf,4BAA4B;AAC5B,iEAAiE;AACjE,kEAAkE;AAClE,2CAA2C;AAC3C,EAAE;AACF,gEAAgE;AAChE,mEAAmE;AACnE,2CAA2C;AAC3C,EAAE;AACF,iCAAiC;AACjC,iDAAiD;AAEjD,OAAO,EAAE,SAAS,EAAE,aAAa,EAAuB,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,sBAAsB,GAEvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,+BAA+B,EAAE,MAAM,sBAAsB,CAAC;AAEvE,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EACT,0MAA0M;IAC5M,WAAW,EAAE;QACX,KAAK,EAAE,uBAAuB;QAC9B,YAAY,EAAE,KAAK;QACnB,eAAe,EAAE,KAAK;QACtB,aAAa,EAAE,IAAI;KACpB;IACD,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,WAAW,EAAE;gBACX,IAAI,EAAE,QAAiB;gBACvB,IAAI,EAAE,CAAC,wBAAwB,CAAC;gBAChC,WAAW,EAAE,uEAAuE;aACrF;YACD,UAAU,EAAE;gBACV,IAAI,EAAE,QAAiB;gBACvB,WAAW,EACT,mGAAmG;aACtG;YACD,aAAa,EAAE;gBACb,IAAI,EAAE,OAAgB;gBACtB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE;gBAClC,WAAW,EAAE,qFAAqF;aACnG;YACD,aAAa,EAAE;gBACb,IAAI,EAAE,QAAiB;gBACvB,IAAI,EAAE,CAAC,SAAS,CAAC;gBACjB,OAAO,EAAE,SAAS;gBAClB,WAAW,EAAE,yEAAyE;aACvF;YACD,qBAAqB,EAAE;gBACrB,IAAI,EAAE,SAAkB;gBACxB,OAAO,EAAE,EAAE;aACZ;SACF;QACD,QAAQ,EAAE,CAAC,aAAa,CAAC;QACzB,oBAAoB,EAAE,IAAI;KAC3B;IACD,YAAY,EAAE,+BAA+B;IAC7C,KAAK,CAAC,OAAO,CAAC,IAA6B;QACzC,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC;YAC/B,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,oBAAoB;YAC1B,MAAM;YACN,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;QACH,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACnC,OAAO,sBAAsB,CAC3B,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,IAA6B,EACtC,uBAAuB,EACvB,QAAQ,CAAC,OAAO,CACjB,CAAC;QACJ,CAAC;QACD,OAAO,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;CACF,CAAC"}
@@ -0,0 +1,78 @@
1
+ // Track A A4 PR 2 — MCP tool: get_next_cued_message
2
+ //
3
+ // Wraps Rails:
4
+ // POST /v1/signal/streams/:id/cue
5
+ // THIS TOOL REFUSES WITHOUT PRIOR-MESSAGE FEEDBACK — the moat per
6
+ // A1 §3 Design Constraint #1. Missing feedback returns 409 from Rails;
7
+ // railsErrorToToolResult maps that to an isError result with a clear
8
+ // message explaining the constraint is structural, not transient.
9
+ //
10
+ // Scope required: stream:cue.
11
+ // Spec: docs/spec_track_a_a4_mcp_server.md §6.3 + §6.4 (moat error).
12
+ import { callRails, isErrorStatus } from "../rails-client.js";
13
+ import { currentContext } from "../context.js";
14
+ import { successResult, railsErrorToToolResult, errorResult, } from "../tool-result.js";
15
+ import { CUE_RESPONSE_ACTIONS } from "../cue-actions.js";
16
+ import { getNextCuedMessageOutputSchema } from "../output-schemas.js";
17
+ export const getNextCuedMessageTool = {
18
+ name: "get_next_cued_message",
19
+ description: "Submit the prior message's feedback and receive the next cued message. THIS TOOL REFUSES WITHOUT PRIOR-MESSAGE FEEDBACK — the moat. Same idempotency_key + same feedback returns the cached cue; conflicting feedback under the same key returns 409.",
20
+ annotations: {
21
+ title: "Submit feedback, get next cued message",
22
+ readOnlyHint: false,
23
+ idempotentHint: true,
24
+ openWorldHint: true,
25
+ },
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ stream_id: {
30
+ type: "integer",
31
+ description: "The stream_id returned by create_patient_stream.",
32
+ },
33
+ feedback: {
34
+ type: "object",
35
+ properties: {
36
+ response_action: {
37
+ type: "string",
38
+ // Canonical cue vocabulary — single-sourced from cue-actions.ts,
39
+ // drift-guarded against AdaptiveResponse::Archetypes::CUE_ACCEPTED_ACTIONS
40
+ // (A4 spec §6.3 + §9.5.6). Do NOT hand-edit this list here.
41
+ enum: [...CUE_RESPONSE_ACTIONS],
42
+ },
43
+ free_text: { type: "string" },
44
+ idempotency_key: {
45
+ type: "string",
46
+ format: "uuid",
47
+ description: "UUID v4 recommended. Same key + same feedback returns the cached cue; conflicting feedback under the same key returns 409.",
48
+ },
49
+ },
50
+ required: ["response_action", "idempotency_key"],
51
+ },
52
+ },
53
+ required: ["stream_id", "feedback"],
54
+ additionalProperties: false,
55
+ },
56
+ outputSchema: getNextCuedMessageOutputSchema,
57
+ async handler(args) {
58
+ if (typeof args.stream_id !== "number" || !Number.isInteger(args.stream_id)) {
59
+ return errorResult("get_next_cued_message: stream_id must be an integer. Received: " +
60
+ JSON.stringify(args.stream_id));
61
+ }
62
+ if (!args.feedback || typeof args.feedback !== "object") {
63
+ return errorResult("get_next_cued_message: feedback object is required (the moat). Provide response_action + idempotency_key at minimum.");
64
+ }
65
+ const { bearer } = currentContext();
66
+ const response = await callRails({
67
+ method: "POST",
68
+ path: `/v1/signal/streams/${args.stream_id}/cue`,
69
+ bearer,
70
+ body: { feedback: args.feedback },
71
+ });
72
+ if (isErrorStatus(response.status)) {
73
+ return railsErrorToToolResult(response.status, response.body, "get_next_cued_message", response.headers);
74
+ }
75
+ return successResult(response.body);
76
+ },
77
+ };
78
+ //# sourceMappingURL=get-next-cued-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-next-cued-message.js","sourceRoot":"","sources":["../../src/tools/get-next-cued-message.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,EAAE;AACF,eAAe;AACf,oCAAoC;AACpC,kEAAkE;AAClE,uEAAuE;AACvE,qEAAqE;AACrE,kEAAkE;AAClE,EAAE;AACF,8BAA8B;AAC9B,qEAAqE;AAErE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAuB,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,WAAW,GAEZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AAEtE,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EACT,uPAAuP;IACzP,WAAW,EAAE;QACX,KAAK,EAAE,wCAAwC;QAC/C,YAAY,EAAE,KAAK;QACnB,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;KACpB;IACD,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,SAAS,EAAE;gBACT,IAAI,EAAE,SAAkB;gBACxB,WAAW,EAAE,kDAAkD;aAChE;YACD,QAAQ,EAAE;gBACR,IAAI,EAAE,QAAiB;gBACvB,UAAU,EAAE;oBACV,eAAe,EAAE;wBACf,IAAI,EAAE,QAAiB;wBACvB,iEAAiE;wBACjE,2EAA2E;wBAC3E,4DAA4D;wBAC5D,IAAI,EAAE,CAAC,GAAG,oBAAoB,CAAC;qBAChC;oBACD,SAAS,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE;oBACtC,eAAe,EAAE;wBACf,IAAI,EAAE,QAAiB;wBACvB,MAAM,EAAE,MAAM;wBACd,WAAW,EACT,4HAA4H;qBAC/H;iBACF;gBACD,QAAQ,EAAE,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;aACjD;SACF;QACD,QAAQ,EAAE,CAAC,WAAW,EAAE,UAAU,CAAC;QACnC,oBAAoB,EAAE,KAAK;KAC5B;IACD,YAAY,EAAE,8BAA8B;IAC5C,KAAK,CAAC,OAAO,CAAC,IAGb;QACC,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5E,OAAO,WAAW,CAChB,iEAAiE;gBAC/D,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CACjC,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACxD,OAAO,WAAW,CAChB,sHAAsH,CACvH,CAAC;QACJ,CAAC;QACD,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC;YAC/B,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,sBAAsB,IAAI,CAAC,SAAS,MAAM;YAChD,MAAM;YACN,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE;SAClC,CAAC,CAAC;QACH,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACnC,OAAO,sBAAsB,CAC3B,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,IAA6B,EACtC,uBAAuB,EACvB,QAAQ,CAAC,OAAO,CACjB,CAAC;QACJ,CAAC;QACD,OAAO,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;CACF,CAAC"}
@@ -0,0 +1,16 @@
1
+ // Track A A4 PR 2 — MCP tool registry.
2
+ // All 4 v1 tools per docs/spec_track_a_a4_mcp_server.md §6.3.
3
+ import { createPatientStreamTool } from "./create-patient-stream.js";
4
+ import { getNextCuedMessageTool } from "./get-next-cued-message.js";
5
+ import { readStreamStateTool } from "./read-stream-state.js";
6
+ import { listSandboxTopicsTool } from "./list-sandbox-topics.js";
7
+ export const tools = [
8
+ createPatientStreamTool,
9
+ getNextCuedMessageTool,
10
+ readStreamStateTool,
11
+ listSandboxTopicsTool,
12
+ ];
13
+ export function findToolByName(name) {
14
+ return tools.find((t) => t.name === name);
15
+ }
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,8DAA8D;AAE9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,uBAAuB;IACvB,sBAAsB;IACtB,mBAAmB;IACnB,qBAAqB;CACb,CAAC;AAIX,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,42 @@
1
+ // Track A A4 PR 2 — MCP tool: list_sandbox_topics
2
+ //
3
+ // Wraps the Rails describe-only meta endpoint
4
+ // GET /v1/signal/sandbox-topics
5
+ // shipped in A4 PR 1 (uphealth#422). Returns the 5-topic-label
6
+ // allowlist that defines the v1 sandbox corpus. Use to preview what
7
+ // content domains exist before creating a stream.
8
+ //
9
+ // Scope required: stream:metadata (in DISCOVERY_SCOPES per A2 §5.1).
10
+ // Spec: docs/spec_track_a_a4_mcp_server.md §6.3.
11
+ import { callRails, isErrorStatus } from "../rails-client.js";
12
+ import { currentContext } from "../context.js";
13
+ import { successResult, railsErrorToToolResult, } from "../tool-result.js";
14
+ import { listSandboxTopicsOutputSchema } from "../output-schemas.js";
15
+ export const listSandboxTopicsTool = {
16
+ name: "list_sandbox_topics",
17
+ description: "Returns the 5-topic-label allowlist that defines the v1 sandbox corpus. Use this to preview what content domains exist before creating a stream. Does NOT enumerate messages — the corpus stays internal.",
18
+ annotations: {
19
+ title: "List sandbox topics",
20
+ readOnlyHint: true,
21
+ openWorldHint: false,
22
+ },
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {},
26
+ additionalProperties: false,
27
+ },
28
+ outputSchema: listSandboxTopicsOutputSchema,
29
+ async handler(_args) {
30
+ const { bearer } = currentContext();
31
+ const response = await callRails({
32
+ method: "GET",
33
+ path: "/v1/signal/sandbox-topics",
34
+ bearer,
35
+ });
36
+ if (isErrorStatus(response.status)) {
37
+ return railsErrorToToolResult(response.status, response.body, "list_sandbox_topics", response.headers);
38
+ }
39
+ return successResult(response.body);
40
+ },
41
+ };
42
+ //# sourceMappingURL=list-sandbox-topics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-sandbox-topics.js","sourceRoot":"","sources":["../../src/tools/list-sandbox-topics.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,EAAE;AACF,8CAA8C;AAC9C,kCAAkC;AAClC,+DAA+D;AAC/D,oEAAoE;AACpE,kDAAkD;AAClD,EAAE;AACF,qEAAqE;AACrE,iDAAiD;AAEjD,OAAO,EAAE,SAAS,EAAE,aAAa,EAAuB,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,sBAAsB,GAEvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAErE,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,qBAAqB;IAC3B,WAAW,EACT,2MAA2M;IAC7M,WAAW,EAAE;QACX,KAAK,EAAE,qBAAqB;QAC5B,YAAY,EAAE,IAAI;QAClB,aAAa,EAAE,KAAK;KACrB;IACD,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE,EAAE;QACd,oBAAoB,EAAE,KAAK;KAC5B;IACD,YAAY,EAAE,6BAA6B;IAC3C,KAAK,CAAC,OAAO,CAAC,KAA8B;QAC1C,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC;YAC/B,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,2BAA2B;YACjC,MAAM;SACP,CAAC,CAAC;QAEH,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACnC,OAAO,sBAAsB,CAC3B,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,IAA6B,EACtC,qBAAqB,EACrB,QAAQ,CAAC,OAAO,CACjB,CAAC;QACJ,CAAC;QACD,OAAO,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;CACF,CAAC"}
@@ -0,0 +1,52 @@
1
+ // Track A A4 PR 2 — MCP tool: read_stream_state
2
+ //
3
+ // Wraps Rails:
4
+ // GET /v1/signal/streams/:id
5
+ // Read-only; does NOT advance the stream. Returns stream_id +
6
+ // template_id + state + current_cue + next_cue_eligible + events_count
7
+ // + _meta block.
8
+ //
9
+ // Scope required: stream:read.
10
+ // Spec: docs/spec_track_a_a4_mcp_server.md §6.3.
11
+ import { callRails, isErrorStatus } from "../rails-client.js";
12
+ import { currentContext } from "../context.js";
13
+ import { successResult, railsErrorToToolResult, errorResult, } from "../tool-result.js";
14
+ import { readStreamStateOutputSchema } from "../output-schemas.js";
15
+ export const readStreamStateTool = {
16
+ name: "read_stream_state",
17
+ description: "Read current stream state, current cued message, and events count. Read-only; does NOT advance the stream.",
18
+ annotations: {
19
+ title: "Read stream state",
20
+ readOnlyHint: true,
21
+ openWorldHint: true,
22
+ },
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {
26
+ stream_id: {
27
+ type: "integer",
28
+ description: "The stream_id returned by create_patient_stream.",
29
+ },
30
+ },
31
+ required: ["stream_id"],
32
+ additionalProperties: false,
33
+ },
34
+ outputSchema: readStreamStateOutputSchema,
35
+ async handler(args) {
36
+ if (typeof args.stream_id !== "number" || !Number.isInteger(args.stream_id)) {
37
+ return errorResult("read_stream_state: stream_id must be an integer. Received: " +
38
+ JSON.stringify(args.stream_id));
39
+ }
40
+ const { bearer } = currentContext();
41
+ const response = await callRails({
42
+ method: "GET",
43
+ path: `/v1/signal/streams/${args.stream_id}`,
44
+ bearer,
45
+ });
46
+ if (isErrorStatus(response.status)) {
47
+ return railsErrorToToolResult(response.status, response.body, "read_stream_state", response.headers);
48
+ }
49
+ return successResult(response.body);
50
+ },
51
+ };
52
+ //# sourceMappingURL=read-stream-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-stream-state.js","sourceRoot":"","sources":["../../src/tools/read-stream-state.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,eAAe;AACf,+BAA+B;AAC/B,8DAA8D;AAC9D,uEAAuE;AACvE,iBAAiB;AACjB,EAAE;AACF,+BAA+B;AAC/B,iDAAiD;AAEjD,OAAO,EAAE,SAAS,EAAE,aAAa,EAAuB,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,WAAW,GAEZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AAEnE,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,IAAI,EAAE,mBAAmB;IACzB,WAAW,EACT,4GAA4G;IAC9G,WAAW,EAAE;QACX,KAAK,EAAE,mBAAmB;QAC1B,YAAY,EAAE,IAAI;QAClB,aAAa,EAAE,IAAI;KACpB;IACD,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,SAAS,EAAE;gBACT,IAAI,EAAE,SAAkB;gBACxB,WAAW,EAAE,kDAAkD;aAChE;SACF;QACD,QAAQ,EAAE,CAAC,WAAW,CAAC;QACvB,oBAAoB,EAAE,KAAK;KAC5B;IACD,YAAY,EAAE,2BAA2B;IACzC,KAAK,CAAC,OAAO,CAAC,IAA6B;QACzC,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5E,OAAO,WAAW,CAChB,6DAA6D;gBAC3D,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CACjC,CAAC;QACJ,CAAC;QACD,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC;YAC/B,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,sBAAsB,IAAI,CAAC,SAAS,EAAE;YAC5C,MAAM;SACP,CAAC,CAAC;QACH,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACnC,OAAO,sBAAsB,CAC3B,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,IAA6B,EACtC,mBAAmB,EACnB,QAAQ,CAAC,OAAO,CACjB,CAAC;QACJ,CAAC;QACD,OAAO,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;CACF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@uphealth/mcp-server",
3
+ "version": "1.0.0",
4
+ "mcpName": "us.uphealth/mcp",
5
+ "description": "Uphealth MCP server — drive cued health-message streams (Uphealth Signal) from any MCP client. Local stdio via npx, or hosted at mcp.uphealth.us.",
6
+ "license": "MIT",
7
+ "author": "Uphealth (Anschutz Media, Inc.)",
8
+ "homepage": "https://uphealth.us/docs",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/ericinsf/uphealth.git",
12
+ "directory": "mcp-server"
13
+ },
14
+ "bugs": {
15
+ "url": "https://uphealth.us/docs"
16
+ },
17
+ "keywords": ["mcp","model-context-protocol","uphealth","signal","health","healthcare","digital-health","agent","ai","llm-tools"],
18
+ "type": "module",
19
+ "bin": {
20
+ "uphealth-mcp": "dist/stdio.js"
21
+ },
22
+ "files": ["dist", "README.md", "DISTRIBUTION.md", "LICENSE"],
23
+ "engines": {
24
+ "node": ">=22.0.0"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "start": "node dist/index.js",
29
+ "start:stdio": "node dist/stdio.js",
30
+ "dev": "tsx src/index.ts",
31
+ "dev:stdio": "tsx src/stdio.ts",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.0.0",
41
+ "express": "^4.21.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/express": "^4.17.21",
45
+ "@types/node": "^22.0.0",
46
+ "tsx": "^4.19.0",
47
+ "typescript": "^5.6.0",
48
+ "vitest": "^4.1.7"
49
+ }
50
+ }