Otchealth
БесплатноНе проверенRemote MCP server for OTCHealth stack (Customer.io first-class). Built per ADR-001 Option C Hybrid.
Описание
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 flagsGET /version— public, returns service version/build metadataPOST /mcp— Streamable HTTP, JSON-RPC 2.0, bearer-auth requiredPOST /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.nodefield is set to>=20to allow either. If Node 20 is required for compliance reasons, usenvm 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:
- Secrets — env vars only. Never logged (Pino redacts authorization headers, all
*_token/*_secret/*_keypaths). Never in repo (.env in .gitignore). Never in error objects. - Auth — bearer token on every
/mcprequest. Constant-time comparison. SHA256 hash of caller token is logged for traceability; raw token never is. - Authorization classes — three feature flags gate three risk tiers (
READ_ONLY_MODE,ENABLE_WRITE_TOOLS,ENABLE_HIGH_RISK_TOOLS). Write tools rejected withwrite_disablederror code unless all relevant flags allow. - Audit logging — every tool call writes
tool_call_startandtool_call_endJSON 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 includebeforeandafterpayloads in the end log. - Schema validation — every tool input goes through a Zod schema before the handler runs. Strong types, sensible bounds (
limit ≤ 200, etc.). - Data minimization — Pino
redactmasks emails and phone numbers in log payloads viamaskPii. Errors includenext_stepguidance, not stack traces. - 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_warningand requireacknowledge_warning=trueon 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_contentis best-effort. Customer.io does not consistently expose newsletter HTML through any public API path; the tool tries several documented endpoints and returnssource: "unsupported_via_api"when none succeed.cio_duplicate_newslettermay not actually duplicate. Customer.io's public docs are inconsistent on whetherPOST /newsletters/{id}/duplicateexists. The n8n workflow returnsunsupported_via_apicleanly 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=trueto render. False negatives are possible if content is obfuscated (e.g., "I.N.N.D."). - Single-instance kill-switch.
POST /admin/revokewrites to in-memory state. Process restart clears the revocation by design. For permanent lockout: rotatePERPLEXITY_CONNECTOR_TOKENin Railway, redeploy.
Deployment (Railway)
- Push
maintogithub.com/GBGolfMatt/otchealth-mcp-server(private). - In Railway: New Project → Deploy from GitHub repo.
- Set all env vars in Railway → Settings → Variables. Match
.env.examplekeys. - First deploy MUST ship with
READ_ONLY_MODE=true. Hard gate from ADR §7. - Confirm
GET /healthover HTTPS returns 200 with the flags you set. - Confirm
POST /mcpwithtools/listreturns 13 tools. - 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
- Perplexity → Settings → Connectors → "Add custom connector" → "Remote".
- URL:
https://<your-railway-app>.up.railway.app/mcp - Auth method: Bearer token.
- Token value:
<PERPLEXITY_CONNECTOR_TOKEN>from.env(or Matt's Notion vault). - Save.
- 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
- Get the correlation_id from Perplexity's tool-call output.
- Railway logs → search for that id.
- The
tool_call_startline has sanitized inputs; thetool_call_endline 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.
Установка Otchealth
У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.
▸ github.com/InnerScopeHearing/otchealth-mcp-serverFAQ
Otchealth MCP бесплатный?
Да, Otchealth MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Otchealth?
Нет, Otchealth работает без API-ключей и переменных окружения.
Otchealth — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Otchealth в Claude Desktop, Claude Code или Cursor?
Открой Otchealth на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
GitHub
PRs, issues, code search, CI status
автор: GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
автор: mcpdotdirectCompare Otchealth with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
