Command Palette

Search for a command to run...

UnylyUnyly
Browse all

Otchealth

FreeNot checked

Remote MCP server for OTCHealth stack (Customer.io first-class). Built per ADR-001 Option C Hybrid.

GitHubEmbed

About

Remote MCP server for OTCHealth stack (Customer.io first-class). Built per ADR-001 Option C Hybrid.

README

Status: Phase 1 built and tested locally against live Customer.io workspace 193366. Pending Railway deploy + Perplexity hookup. Owner: Matt Moore (President, OTCHealth Inc.) Architecture: ADR-001 (locked) — Option C hybrid, Node.js + n8n.

A remote MCP server exposing OTCHealth's operational stack (Customer.io first, then Shopify, Intercom, n8n, Notion in later phases) to Perplexity custom remote connectors and any other MCP client.


TL;DR

npm install
npm run build
cp .env.example .env       # fill in 6 values
npm start                  # listens on $PORT (default 8080)

Endpoints:

  • GET /health — public, returns operational flags
  • GET /version — public, returns service version/build metadata
  • POST /mcp — Streamable HTTP, JSON-RPC 2.0, bearer-auth required
  • POST /admin/revoke — kill-switch (separate admin token)
  • GET /admin/revoke, POST /admin/clear-revoke — inspect / clear revocation

Phase 1 tool catalog (13 tools)

Tool Type Routing Notes
cio_list_newsletters read direct limit + cursor pagination, created_after/before filters
cio_get_newsletter read direct optional include_contents
cio_get_newsletter_metrics read direct optional include_links for link-level click data
cio_get_newsletter_schedule read direct scheduled_at, original_scheduled_at, send_at, recurring
cio_get_segment read direct optional include_count
cio_list_segment_people read direct paginated
cio_get_customer read direct by email / cio_id / id; optional segments
cio_get_template_or_content read direct best-effort; returns unsupported_via_api if no path works
cio_get_broadcast_history_for_segment read direct join newsletters→segment, optional metrics
cio_track_event write_simple direct (Track API) event name allowlisted; dry-run default
cio_update_customer_attributes write_simple direct (Track API) protected attrs rejected; dry-run default
cio_update_newsletter_variant write_orchestrated n8n webhook field allowlist; HMAC-signed payload
cio_duplicate_newsletter write_orchestrated n8n webhook best-effort; falls back to unsupported_via_api

Every tool returns a structuredContent JSON envelope:

{
  "result": { /* tool-specific payload */ },
  "compliance_warning": null,
  "correlation_id": "uuid-v4",
  "dry_run": false
}

Every tool annotates: readOnlyHint, destructiveHint, idempotentHint, openWorldHint (ADR-001 §4d).


Architecture

        Perplexity (custom remote connector)
              │
              │  Authorization: Bearer <PERPLEXITY_CONNECTOR_TOKEN>
              │  POST https://<railway-url>/mcp
              ▼
   ┌─────────────────────────────────────────────────┐
   │ Fastify (Node 20+ / Streamable HTTP, stateless) │
   │   • bearer auth + revocation check              │
   │   • Zod input validation                        │
   │   • Pino structured audit log (correlation_id)  │
   │   • compliance guardrail scan on output         │
   │   • write-tool feature flags                    │
   └─────┬───────────────────────────────────┬───────┘
         │                                   │
         │ direct (HTTPS, undici)            │ HMAC-SHA256 signed POST
         │                                   │
         ▼                                   ▼
   Customer.io App API                  n8n webhooks
   https://api.customer.io/v1/...       https://otchealth.app.n8n.cloud/webhook/...
   Customer.io Track API                (cio_update_newsletter_variant)
   https://track.customer.io/api/v1/... (cio_duplicate_newsletter)

See ADR-001 §3 for the locked tech stack and §4 for the read-vs-orchestrated routing decisions.


Setup

Prerequisites

  • Node.js 20 LTS or newer (this build was validated on Node 24.15.0 — see "Node version note" below)
  • npm 10+
  • Git
  • For deployment: Railway account, GitHub PAT (read at deploy time from Matt's Notion Token Vault)

Node version note: ADR-001 §3 specifies "Node.js 20 LTS". By May 2026, Node 20 reached end-of-life and the current LTS line is Node 22+. This build runs on Node 22, 24, or any later LTS. The engines.node field is set to >=20 to allow either. If Node 20 is required for compliance reasons, use nvm install 20 && nvm use 20 — there are no v22/v24-only APIs in this code.

Environment variables

Copy .env.example to .env and populate. The full list:

Variable Source Notes
CIO_SITE_ID Customer.io UI → Settings → API Credentials Track API Basic-auth username
CIO_TRACK_KEY Customer.io UI → Settings → API Credentials Track API Basic-auth password
CIO_APP_API_BEARER Customer.io UI → Settings → API Credentials App API bearer token
PERPLEXITY_CONNECTOR_TOKEN Generated at first build (32-byte hex) Perplexity bearer to MCP
ADMIN_REVOKE_TOKEN Generated at first build (32-byte hex) Kill-switch bearer; keep separate from connector token
N8N_BASE_URL n8n cloud URL Default: https://otchealth.app.n8n.cloud
N8N_API_KEY n8n → Settings → API Pulled at orchestrated-write registration time; optional for read-only first deploy
N8N_WEBHOOK_SECRET Generated at first build (32-byte hex) HMAC shared secret with n8n
READ_ONLY_MODE feature flag true on first deploy (hard gate; see §7 ADR)
ENABLE_WRITE_TOOLS feature flag false first deploy
ENABLE_HIGH_RISK_TOOLS feature flag false first deploy; gates n8n-routed writes
DRY_RUN_DEFAULT feature flag true first deploy
PORT server Default 8080; Railway sets this automatically
NODE_ENV server production in deploys, development locally for pretty logs
LOG_LEVEL server info default; debug for tool-call detail

The three generated tokens are stored in Matt's Notion Token Vault (see "Operator playbook → Rotate secrets" below).

Local development

npm install
cp .env.example .env       # fill in 6 values (3 CIO + 3 generated)
npm run dev                # tsx watch on src/server/index.ts
# OR
npm run build && npm start # production mode

Smoke test:

# health (no auth)
curl http://localhost:8080/health

# tools/list (requires bearer)
curl -X POST http://localhost:8080/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H "authorization: Bearer $PERPLEXITY_CONNECTOR_TOKEN" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

# live read against workspace 193366
curl -X POST http://localhost:8080/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H "authorization: Bearer $PERPLEXITY_CONNECTOR_TOKEN" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"cio_list_newsletters","arguments":{"limit":5}},"id":2}'

Security

Per Perplexity Spec §6 and ADR §6:

  1. Secrets — env vars only. Never logged (Pino redacts authorization headers, all *_token / *_secret / *_key paths). Never in repo (.env in .gitignore). Never in error objects.
  2. Auth — bearer token on every /mcp request. Constant-time comparison. SHA256 hash of caller token is logged for traceability; raw token never is.
  3. Authorization classes — three feature flags gate three risk tiers (READ_ONLY_MODE, ENABLE_WRITE_TOOLS, ENABLE_HIGH_RISK_TOOLS). Write tools rejected with write_disabled error code unless all relevant flags allow.
  4. Audit logging — every tool call writes tool_call_start and tool_call_end JSON log lines with: correlation_id, tool name, caller_hash (SHA256), sanitized inputs, dry_run flag, read_only_mode flag, outcome (success / error / rejected), latency_ms, error_code, error_message. Writes include before and after payloads in the end log.
  5. Schema validation — every tool input goes through a Zod schema before the handler runs. Strong types, sensible bounds (limit ≤ 200, etc.).
  6. Data minimization — Pino redact masks emails and phone numbers in log payloads via maskPii. Errors include next_step guidance, not stack traces.
  7. Compliance guardrail — every tool output is scanned for INND ticker mentions, patent-claim language, 510(k) overclaims, A-grade HearAdvisor claims, pre-shipment availability claims, and TReO-as-hearing-aid mentions (ADR §10). Triggers attach a compliance_warning and require acknowledge_warning=true on the caller side before the data is rendered.

Known limitations

  • Strict-mode input rejection is silent. The MCP SDK's input-schema validator silently strips unknown fields before the tool handler runs. Unknown fields are ignored, not loudly rejected. Security intent (no pass-through to upstream APIs) is preserved. Fix path: pre-validate the raw JSON-RPC params before delegating to the SDK; deferred.
  • cio_get_template_or_content is best-effort. Customer.io does not consistently expose newsletter HTML through any public API path; the tool tries several documented endpoints and returns source: "unsupported_via_api" when none succeed.
  • cio_duplicate_newsletter may not actually duplicate. Customer.io's public docs are inconsistent on whether POST /newsletters/{id}/duplicate exists. The n8n workflow returns unsupported_via_api cleanly when 404'd.
  • Compliance guardrail uses substring patterns, not NLP. False positives are possible (a legitimate use of "INND" in an IR-approved context). Pass acknowledge_warning=true to render. False negatives are possible if content is obfuscated (e.g., "I.N.N.D.").
  • Single-instance kill-switch. POST /admin/revoke writes to in-memory state. Process restart clears the revocation by design. For permanent lockout: rotate PERPLEXITY_CONNECTOR_TOKEN in Railway, redeploy.

Deployment (Railway)

  1. Push main to github.com/GBGolfMatt/otchealth-mcp-server (private).
  2. In Railway: New Project → Deploy from GitHub repo.
  3. Set all env vars in Railway → Settings → Variables. Match .env.example keys.
  4. First deploy MUST ship with READ_ONLY_MODE=true. Hard gate from ADR §7.
  5. Confirm GET /health over HTTPS returns 200 with the flags you set.
  6. Confirm POST /mcp with tools/list returns 13 tools.
  7. Then add the connector in Perplexity (see next section).

Dockerfile is multi-stage (build → runtime, runs as non-root app user, Alpine-based). Railway auto-detects it.

Rollback

# Railway dashboard: Deployments → click previous successful deploy → Redeploy
# Or git revert and push
git revert <bad-sha> && git push

Connecting from Perplexity

  1. Perplexity → Settings → Connectors → "Add custom connector" → "Remote".
  2. URL: https://<your-railway-app>.up.railway.app/mcp
  3. Auth method: Bearer token.
  4. Token value: <PERPLEXITY_CONNECTOR_TOKEN> from .env (or Matt's Notion vault).
  5. Save.
  6. Test with a natural-language query: "Using the OTCHealth MCP connector, list the names of the most recent 5 Customer.io newsletters."

If you get an unauthorized response, the token has either been mis-pasted or revoked. See operator playbook.


Operator playbook

Rotate the connector bearer token

When (suspected leak, quarterly cadence, end of a contractor's access window):

# 1. Generate a new 32-byte hex token
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

# 2. Update Railway env var PERPLEXITY_CONNECTOR_TOKEN
# 3. Restart the Railway service (auto on env-var change)
# 4. Update Matt's Notion vault (https://www.notion.so/35d20e2667bc81c2b055cfc612a7e9b5)
#    with the new value and Last rotated date.
# 5. Update Perplexity → Connectors → OTCHealth → bearer token.

Same procedure for ADMIN_REVOKE_TOKEN and N8N_WEBHOOK_SECRET (note: rotating the n8n webhook secret requires re-syncing it on the n8n side too).

Revoke without restarting (kill-switch)

curl -X POST https://<railway-url>/admin/revoke \
  -H "authorization: Bearer $ADMIN_REVOKE_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"reason":"suspected leak via Perplexity workspace"}'

Response:

{"status":"revoked","revoked_at":"2026-05-11T22:11:23.000Z","revoked_token_hash":"<sha256>","reason":"..."}

The connector token starts returning 401 immediately. /health will report connector_token_revoked: true. To clear (after rotation):

curl -X POST https://<railway-url>/admin/clear-revoke \
  -H "authorization: Bearer $ADMIN_REVOKE_TOKEN"

Disable write tools

# In Railway → Settings → Variables:
READ_ONLY_MODE=true
ENABLE_WRITE_TOOLS=false
ENABLE_HIGH_RISK_TOOLS=false
# Then redeploy. Write tools immediately start returning write_disabled.

Inspect logs

Railway → Deployments → Logs. Filter on:

type:tool_call_start    → all incoming tool calls
type:tool_call_end      → outcomes + latencies
type:auth_rejected      → bad bearer attempts
type:admin_revoke_*     → kill-switch activity
type:cio_app_api_*      → Customer.io upstream errors

Each log line includes a correlation_id (UUIDv4). The same id appears in the response structuredContent.correlation_id field returned to Perplexity, so you can trace a single call end-to-end.

Audit a specific tool call

  1. Get the correlation_id from Perplexity's tool-call output.
  2. Railway logs → search for that id.
  3. The tool_call_start line has sanitized inputs; the tool_call_end line has outcome + before/after diffs for writes.

Phase 2 roadmap (scaffolded, not implemented)

Per PERPLEXITY_SPEC §5:

  • Shopify (8 tools): products, analytics, abandoned checkouts, draft orders, product copy edits
  • Intercom (6 tools): help center, conversations, articles, internal notes
  • n8n (6 tools): workflow listing, executions, triggers, recent failures
  • Notion (4 tools): pages, databases, ops-database writes
  • ElevenLabs / Twilio (optional): agent prompts, phone routing

Implementation pattern: add src/tools/<provider>/<action>.ts files, register in src/tools/index.ts, scope each integration's API client into its own src/<provider>/ directory.


Repo layout

.
├── ADR-001.md                       # locked architecture decision (read first)
├── PERPLEXITY_SPEC.md               # original mega-prompt
├── Dockerfile                       # multi-stage, non-root
├── package.json
├── tsconfig.json
├── .env.example                     # template
├── .env                             # gitignored
├── evals/
│   └── phase1.xml                   # 10 read-only eval questions (ADR §8)
├── n8n-workflows/
│   ├── cio-update-newsletter-variant.json
│   └── cio-duplicate-newsletter.json
└── src/
    ├── config/env.ts                # Zod env validation
    ├── audit/logger.ts              # Pino + redaction + correlation IDs
    ├── auth/
    │   ├── bearer.ts                # connector + admin bearer middleware
    │   └── revocation-store.ts      # in-memory kill-switch
    ├── compliance/guardrail.ts      # regulated_data_guardrail (ADR §10)
    ├── customerio/
    │   ├── app-api-client.ts        # App API (bearer)
    │   └── track-api-client.ts      # Track API (basic)
    ├── n8n/webhook-client.ts        # HMAC-signed n8n bridge
    ├── tools/
    │   ├── registry.ts              # central tool wrapper (validation + audit + guardrail + gates)
    │   ├── index.ts                 # registers all 13 tools
    │   └── cio/                     # 13 tool files
    └── server/
        ├── index.ts                 # Fastify entrypoint
        ├── mcp.ts                   # POST /mcp (Streamable HTTP, stateless)
        ├── admin.ts                 # POST /admin/revoke
        ├── health.ts                # GET /health
        ├── version.ts               # GET /version
        └── request-context.ts       # AsyncLocalStorage for caller_hash

Owner

Matt Moore, President, OTCHealth Inc. — Granite Bay, CA.

from github.com/InnerScopeHearing/otchealth-mcp-server

Installing Otchealth

This server has no published package — it is built from source. Open the repository and follow its README.

▸ github.com/InnerScopeHearing/otchealth-mcp-server

FAQ

Is Otchealth MCP free?

Yes, Otchealth MCP is free — one-click install via Unyly at no cost.

Does Otchealth need an API key?

No, Otchealth runs without API keys or environment variables.

Is Otchealth hosted or self-hosted?

Self-hosted: the server runs locally on your machine via the install command above.

How do I install Otchealth in Claude Desktop, Claude Code or Cursor?

Open Otchealth on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.

Related MCPs

Compare Otchealth with

Not sure what to pick?

Find your stack in 60 seconds

Author?

Embed badge for your README

Browse similar

All development MCPs