Skip to main content
Glama

Occulytics MCP Server

An MCP server that lets an AI assistant answer portfolio questions for a healthcare-REIT asset-management team (Omega Healthcare Investors), grounded in two public sources: Omega's SEC 10-K filings and the CMS Nursing Home Provider Information file.

The design goal, per the brief: the server must be able to say an answer is complete, uncertain, or unsupported — and why — instead of producing a confident number nothing supports. Every tool returns deterministic data inside an envelope that carries a computed status, caveats, and provenance.

Quickstart

Everything runs offline — the data artifacts are committed.

npm install
npm run build
npm test          # 41 tests: curated-data checksums, domain units, full e2e over MCP

Try it in a UI (MCP Inspector opens in your browser):

npm run inspect

Connect to Claude Code: a project-scoped .mcp.json is included — open this repo in Claude Code after npm run build and the occulytics server is available. Or register it globally:

claude mcp add occulytics -- node /absolute/path/to/occulytics-mcp/dist/src/server/index.js

Connect to Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "occulytics": {
      "command": "node",
      "args": ["/absolute/path/to/occulytics-mcp/dist/src/server/index.js"]
    }
  }
}

Pre-demo check that the compiled server works over real stdio: npm run smoke. To refresh data from the live sources: npm run ingest (see Data pipeline).

Related MCP server: Medical Billing MCP

What you can ask

The five target questions, and what the server actually does:

Question

Answer path

Honest outcome

Top five operators by % of investment, and how many facilities each runs?

operator_concentration + operator_facilities

Partial by design: Omega discontinued the full operator table after its FY2020 10-K. You get the complete FY2020 ranking (with lease/mortgage decomposition — including that Ciena, not Consulate, was actually #1 with mortgages included) and the FY2025 named disclosures (Maplewood ≥10%, CommuniCare 7.2%), each dated, never blended. "Actually runs" = live CMS chain counts.

Share of top operators' facilities below the national staffing average?

operator_metrics (multi-operator)

Computed per operator and pooled server-side against the national mean (3.86 reported nurse HPRD). Unmappable operators are named and excluded, not silently dropped.

Largest operator's average star rating and two-year direction?

operator_metrics

Unsupported for Maplewood (largest by investment): it runs senior-living communities, which are not CMS-certified nursing homes — the server says so and why. For CommuniCare (largest by revenue): avg 3.05 stars, improved 2.26 → 3.04 on a 117-facility constant panel (Jul 2024 → Jul 2026).

Portfolio occupancy?

portfolio_occupancy

A labeled proxy: Omega discloses neither occupancy nor a facility list. Bed-weighted occupancy across mapped operator chains (83.5% vs 80.5% national), with coverage accounting — what share of the portfolio the proxy actually represents and who's excluded (UK operators, Maplewood, low-confidence maps).

One-paragraph exposure briefing on the largest operator?

portfolio_overview + resolve_operator (+ concentration)

The model writes the paragraph; the server supplies only deterministic facts: ≥10% of investments, 6.6%/5.2%/5.4% revenue trend, the $12.5M termination-fee note, and the CMS coverage gap.

Architecture

Three layers, one direction of dependency, no database, no runtime network:

scripts/ingest.ts      CMS download → validate → project → data/processed/*.json  (committed)
data/curated/*.json    Hand-transcribed 10-K facts + operator→CMS map, per-fact citations
        │
src/domain/            Pure, deterministic, unit-tested: store, resolve, metrics
        │
src/server/            MCP wiring: 9 tools + 1 resource → envelope responses (stdio)
  • data/curated/omega-10k.json — FY2025 portfolio summary + concentration note, FY2020 operator investment table. Every block cites its filing/section.

  • data/curated/operator-map.json — the honesty backbone: each Omega operator's CMS mapping with method (chain-exact / legal-name-pattern / curated-alias), confidence (high/medium/low), and caveats; unmappable operators carry the reason.

  • src/domain/metrics.ts — all arithmetic: rankings, occupancy, benchmark comparisons, constant-panel star trends. Nothing numeric is left to the model.

  • src/server/tools.ts — thin: validate input (zod), call domain, wrap in envelope.

The answer envelope

Every tool returns:

{
  "status": "complete" | "partial" | "unsupported",   // brief's complete / uncertain / unsupported
  "data": { /* deterministic numbers & records, never prose */ },
  "caveats": [ /* why partial; staleness; method notes — computed, not decorative */ ],
  "provenance": [ { "source", "asOf", "detail", "url" } ],
  "cost": { "chars", "estTokens", "basis" }   // self-reported payload size, labeled estimate
}

status is computed from the data path, not hardcoded: an unmapped operator yields unsupported with the mapping entry's recorded reason; anything touching the FY2020 table is partial with the staleness caveat; the occupancy proxy is always partial.

Tool surface

Tool

Returns

Raw or resolved?

portfolio_overview

FY2025 totals, mix, geo + named operator concentration

resolved facts, as filed

operator_concentration

Two dated ranking blocks (FY2020 full / FY2025 named)

resolved; % computed from filed dollars

resolve_operator

name → canonical operator + CMS mapping + confidence + 10-K context (FY2020 rank/%, FY2025 disclosed %)

metadata

operator_facilities

paged facility rows + full-population summary

raw rows + resolved summary

operator_metrics

stars (mean + per-star distribution), staffing-vs-national, occupancy, and 2-yr constant-panel trends for all three; pooled block for multi-operator

resolved (all arithmetic server-side)

find_facility

facility drill-down by CCN/name: current metrics, per-snapshot history, reverse Omega-operator affiliation

raw detail + resolved affiliation

portfolio_occupancy

proxy occupancy + 2-yr trend + coverage accounting

resolved, explicitly labeled proxy

national_benchmarks

national staffing/star/occupancy references + methods

resolved

data_coverage

sources, vintages, mappings, known gaps (also coverage://data-sources resource)

metadata

Granularity rationale: tools are question-shaped but composable — deterministic aggregation (where LLM arithmetic over 100+ rows is a correctness risk) is a tool responsibility; narrative synthesis is the model's. Every operator-taking tool accepts free text and resolves internally, so a client never needs a two-step protocol; a failed resolution is an unsupported answer (with candidates and the known universe), not an error.

Cross-source questions (10-K piece ↔ CMS piece) are first-class: the operator identity is the join key, verified round-trip (every name in the 10-K ranking resolves in every CMS-backed tool — e2e-tested), and every resolved operator block embeds its 10-K context (omegaContext: FY2020 rank and % of portfolio, FY2025 disclosed concentration), so "how good is our biggest operator?" style questions resolve without a second call.

Key decisions & trade-offs

1. Two vintages, never blended. The decisive research finding: Omega's 10-Ks after FY2020 do not contain a per-operator investment table — the FY2025 filing names only Maplewood (≥10% of investments) and CommuniCare (7.2%). So a current "top five" is not fully supportable from the named sources, and the server says exactly that: rankings come as two separately-dated blocks, and the status is partial with the reason. Trade-off: less satisfying than one clean list; chosen because a blended list would be numerically incoherent (2020 dollars vs 2025 percentages on different denominators).

2. Hand-transcribed SEC facts, machine-ingested CMS data. The Omega facts are ~30 numbers across two tables in two differently-formatted filings. A generic 10-K parser at this scope has the worst failure mode possible for this brief — silently wrong extraction. Instead: curated JSON with per-fact citations, guarded by checksum tests (every summable column must reproduce the filing's own subtotals and totals — a mistyped digit fails the build). The CMS side (14,693 rows × 3 monthly vintages) is fully automated with validation, because there scale makes automation the safer option. Trade-off: refreshing for a new 10-K is a manual edit; accepted for a annually-filed document.

3. The operator→CMS join is a curated, confidence-tagged artifact. Neither dataset references the other. The join (10-K operator name → CMS chain) is the riskiest inference in the system, so it is data, not code: each mapping records how it was made and how much to trust it, and unmappable operators record why (Maplewood: senior living, outside CMS; Healthcare Homes: UK). Low-confidence mappings (Agemo → Signature) are excluded from pooled aggregates by default and surfaced when included. Trade-off: doesn't scale to hundreds of REITs; correct for one REIT's ~11 named operators, and the mechanism (method/confidence/caveat per mapping) is what would scale.

4. Chain metrics are supersets, and say so. Omega's facility-level portfolio is not public (verified: Schedule III aggregates by state). CMS metrics therefore describe an operator's whole operation, not just Omega's buildings — every affected response carries that caveat, and the occupancy proxy reports what share of the (FY2020) portfolio its coverage represents (~40%). Trade-off: a facility-level reconstruction from the CMS Ownership file was possible but is multi-day fuzzy-matching work; the honest proxy with coverage accounting is the four-hour answer. That reconstruction is the natural next step.

5. Methodology is part of the answer. Star trend = constant panel (facilities rated in both endpoint snapshots), with panel size, exclusions, and the known bias (chain membership is current-only) in the response. Staffing benchmark = reported total nurse HPRD, facility-mean (what the question asks, unadjusted; case-mix-adjusted exists and is noted). Occupancy = avg residents/day ÷ certified beds, which understates operational occupancy (certified > in-service beds). All stated in the payloads, not just here.

6. In-memory JSON, no database, artifacts committed. 15k rows load in milliseconds; a DB adds operational surface for zero query need. Committed artifacts (~6MB) mean install → build → demo works with no network — the live demo cannot be broken by a CMS outage or a changed download URL. Cost: repo carries data; ingestion re-derives it from sources at any time.

7. Bounded outputs. Facility lists are paged (default 25) with an always-complete summary block and total count — a 185-facility chain never floods the client context.

Testing

  • tests/curated.test.ts — transcription checksums against the filings' own totals.

  • tests/metrics.test.ts, tests/resolve.test.ts — domain units on fixtures (exact values).

  • tests/e2e.test.ts — a real MCP client over an in-memory transport against the real data: one test per demo question, including the unsupported paths.

  • npm run smoke — the compiled server over real stdio from a foreign cwd.

Efficiency & token cost

npm run cost measures what an LLM client pays in context per demo question (tool-result text + one-time tool schemas), fully offline. Token figures are estimates (chars ÷ 4; real tokenizers vary ±20%) — the value is relative cost and regression tracking.

Current measurements (committed artifacts):

Question

Calls

Est. tokens

Q1 top-5 + facility counts

2

~4.1k

Q2 staffing below national

1

~3.0k

Q3 largest operator stars + trend

2

~1.9k

Q4 portfolio occupancy

1

~1.0k

Q5 exposure briefing

2

~1.4k

Five-question session

8

~11.4k (+ ~3.2k one-time schemas)

Every response also stamps its own cost block ({chars, estTokens, basis}) so the assistant can cite what an answer cost in context — labeled an estimate, because real tokenization happens client-side and the server never sees it (in Claude Code, /cost and /context remain the ground truth at session level).

Two deliberate optimizations keep this lean (a 31% reduction vs the naive version, measured): the model-facing text mirror is compact JSON (pretty-print whitespace alone was ~26% of payload), and repeated methodology strings live once per response in the envelope caveats rather than on every trend block. Facility lists are paged; summaries are always full-population. The cost stamp itself adds ~21 tokens per response — measured, and worth it for the visibility.

Data pipeline

npm run ingest downloads and rebuilds data/processed/:

  1. Resolves the current Provider Information CSV URL from the CMS PDC metastore API (the file URL changes monthly), downloads it plus two archived snapshots (Jul 2024, Jul 2025) for the trend.

  2. Validates (row counts, required columns with header aliasing across CMS's 2024→2025 column renames, rating ranges, null rates) — fails loudly, never writes partial artifacts.

  3. Projects to three artifacts: per-facility slice, CCN→rating history, national benchmarks (with methods recorded in the file).

Raw downloads cache in data/raw/ (gitignored); --force re-downloads.

Repo layout

data/curated/     hand-verified 10-K facts + operator map (source-cited, checksummed)
data/processed/   generated CMS artifacts (committed; rebuild with npm run ingest)
scripts/          ingest.ts, stdio-smoke.mjs
src/domain/       types, store, resolve, metrics — pure & unit-tested
src/server/       MCP tools + entry (stdio)
tests/            checksums, units, e2e
docs/             PLAN.md (build plan + audit trail), DEMO.md (presentation script)

Known limitations & next steps

  • Omega-owned facilities are not individually identifiable → operator-chain proxy (next: cross-walk the CMS Ownership file's property-company records).

  • Current-year operator ranking is inherently incomplete (disclosure stopped in FY2020); Omega's quarterly supplementals could narrow this but are outside the brief's sources.

  • Trends (stars, staffing, occupancy) use two endpoint snapshots + a midpoint; more monthly snapshots would smooth them.

  • UK facilities (17.7% of real estate) have no CMS-equivalent ingestion (CQC would be the analogous UK source).

Install Server
F
license - not found
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables document search, grounded question answering, summarization, patient timeline extraction, and PHI redaction for healthcare documents using retrieval-augmented generation.

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/siddak1234/occulytics-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server