Metadata-Version: 2.5
Name: variancelabs-mcp
Version: 0.3.1
Summary: VarianceLabs MCP server — mineral estimation & reporting tools (QAQC + 3D viewer + property modelling: NEXUS, LVA-DKNN + geometry + EDA)
Author: VarianceLabs
License: Copyright (c) 2026 VarianceLabs. All rights reserved.
        
        This software is proprietary. No license is granted to copy, modify, distribute,
        sublicense or use this software except under a separate written agreement with
        VarianceLabs.
License-File: LICENSE
Classifier: License :: Other/Proprietary License
Requires-Python: >=3.11
Requires-Dist: mcp<2,>=1.2.0
Requires-Dist: numpy>=1.26
Requires-Dist: pandas>=2.0
Requires-Dist: pydantic>=2.6
Requires-Dist: scikit-image>=0.22
Requires-Dist: scipy>=1.11
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: modelling
Requires-Dist: torch>=2.5; extra == 'modelling'
Description-Content-Type: text/markdown

# VarianceLabs MCP

MCP server exposing VarianceLabs mineral-estimation & reporting tools for agentic
workflows: **QAQC of assays and drillhole data**, a **3D viewer**, **property modelling**
(block-model estimation: NEXUS non-stationary geostatistics by default, LVA-DKNN neural),
**geometry** (implicit wireframes, block models) and **EDA** (exploratory data analysis of
samples and estimates).

- `src/variancelabs_mcp/` — MCP server and tool layers: `qaqc/`, `project/`, `processing/`,
  `modelling/`, `geometry/`, `eda/`, `app/` (the localhost app server and its committed
  bundle)
- `src/project/` — projects: objects (drillholes, QC data, composites, wireframes, block
  models, estimates) with ids, recipes and links between them, rebuilds, the files the
  viewer and Data page read; no MCP dependency
- `src/data_processing/` — drillhole desurvey + compositing engine; the single source of
  hole geometry (the viewer uses it too); no MCP dependency
- `src/deep_modelling/` — property-modelling engine, ported from the `deep-modelling`
  research repo; no MCP dependency, so the web app can call it too
- `src/nexus/` — NEXUS estimation engine (torch, CUDA when present; no scipy); runs inside
  the estimate worker
- `src/geometry/` — spatial geometry engine: wireframes (RBF + marching cubes) and block
  models (rotated, sub-blocked, wireframe-constrained); no MCP dependency
- `src/eda/` — EDA engine: statistics and plot data from arrays (univariate, bivariate,
  outliers, section, grade–tonnage, swath); `src/project/eda.py` feeds it project objects
- `src/qaqc/` — QAQC engine: CRM control charts (Nelson rules), blanks, duplicates, grade,
  drillhole-geometry and spatial checks, flags and settings, from arrays and tables;
  `src/project/qaqc.py` feeds it project objects; no MCP dependency
- `src/web-app/` — browser app (Data, QAQC and EDA pages, 3D viewer)

## Develop

```bash
uv sync --extra dev --extra modelling      # modelling = PyTorch (CUDA build on Win/Linux)
uv run python -m variancelabs_mcp.server   # stdio MCP server
uv run pytest                              # default test suite (~3 min)
uv run pytest -m integration               # slow end-to-end: real worker, demo data
uv run python scripts/benchmark_nexus.py   # NEXUS on the synthetic truths of scripts/generate_synthetic_3d.py
```

## Install as an MCP server (one-time)

Installed **editable** as a uv tool, then registered at **user scope** so the
tools are available in every project — not just this repo.

```bash
uv tool install --python 3.11 --editable ".[modelling]"  # repo root; drop [modelling] to skip torch
claude mcp add --scope user variancelabs ~/.local/bin/variancelabs-mcp
```

On Windows the executable is `%USERPROFILE%\.local\bin\variancelabs-mcp.exe`;
register that absolute path. `uv tool run variancelabs-mcp` does **not** work —
the package is not on any index, so the registration must point at the exe.

Verify with `claude mcp list` (expect `variancelabs: … - ✓ Connected`).

## Updating an installed MCP

Because the install is editable, its `.pth` points straight at this repo's
`src/`. **The server runs whatever is in your working tree** — including after
a `git checkout` of another branch.

| You changed | What to do |
| --- | --- |
| Python source (`src/variancelabs_mcp/`) | Restart the Claude Code session, or `/mcp` → `variancelabs` → reconnect |
| Deps or `[project.scripts]` in `pyproject.toml` | `uv tool install --python 3.11 --editable ".[modelling]" --force`, then restart |
| Web app (`src/web-app/`) | `npm run build` (writes the committed bundle at `src/variancelabs_mcp/app/static/`), then restart |

The tool venv is separate from `.venv`, which is why dependency changes need
the `--force` reinstall — a plain restart will not pick them up. On Windows, stop
every running server first (close the Claude sessions using it): `--force` deletes
the venv's `Lib/` and then fails on the locked `Scripts/`, leaving it broken. To
repair or update in place while a server runs:
`uv pip install --python "%APPDATA%\uv\tools\variancelabs-mcp\Scripts\python.exe" -e ".[modelling]"`. The version
recorded in the tool's metadata is baked at install time and can lag
`pyproject.toml`; that is cosmetic, the code served is always the working tree.

## QAQC tools (`qaqc_*`)

QC files (CRM results with certified values, duplicates, blanks) are loaded as a QC data
object nested under raw drillholes; the checks of grades, drillhole geometry and spatial
outliers need only the drillholes. Every check emits flags that can be reviewed.

- `project_load_qaqc(drillholes, standards?, reference?, duplicates?, blanks?, mapping?,
  name?, project?)` — copy the QC files into the project; columns are detected and reported.
- `qaqc_overview(drillholes?)` — start here: panels with data, verdicts, open flags,
  settings in force, suggested next tools.
- `qaqc_standards(element?, standard?, ...)` — CRM bias and Nelson-rule events by batch.
- `qaqc_blanks(element?, ...)` — contamination against the detection-limit threshold.
- `qaqc_duplicates(element?, set?, ...)` — HARD precision and bias per pair set (duplicates,
  repeats, re-assay columns).
- `qaqc_checks(family, element?, ...)` — `grades`, `geometry` or `spatial` checks.
- `qaqc_issues(family?, check?, severity?, status?, element?, hole?, limit?)` — the flag
  register.
- `qaqc_review(status, flag_ids? | check?, element?, hole?, comment?, only_open?)` — mark
  flags open, resolved or dismissed; reviews and settings are stored in `<project>/qaqc.json`.
- `qaqc_settings(values?)` — detection limits, blank factor, HARD thresholds, minimum
  recovery, spike and spatial ratios (`"auto"` restores one).

Like EDA, each tool returns the numbers and a `url` opening the QAQC page (`/qaqc/`, shared
chart core and UI in `src/web-app/src/analysis/`) on the same view; the page computes on
demand through `GET /api/qaqc/<panel>`. Design:
`docs/superpowers/specs/2026-10-06-qaqc-module-design.md`.

## Data processing tools (`processing_*`)

- `processing_composite(drillholes, options?, name?, project?)` — fixed-length
  composites with X/Y/Z at midpoints, added to the project (ready for
  `modelling_start_run`). `drillholes` is a drillholes/composites object id or
  `{collar, intervals, survey?, mapping?, desurvey?}` CSV paths.
- `processing_desurvey(collar, table, survey?, at?, options?)` — X/Y/Z for intervals
  (midpoint or both ends) or depth points; a standalone export, not a project object.

Every option is optional: unset ones (composite length, value columns, coordinate
system, dip sign) are chosen from the data, and the returned summary says what was
used and why; desurvey `method` (minimum curvature by default, balanced tangent,
tangent) and `azimuth_correction` (degrees) are fixed defaults. `processing_composite` writes into the project folder
(`objects/<object_id>/v<n>/`); `processing_desurvey` writes to
`./variancelabs-output/processing/<run-id>/` (`*.csv` + `report.json`). Design:
`docs/superpowers/specs/2026-09-27-data-processing-design.md`.

## Property modelling tools (`modelling_*`)

A run estimates one grade at every block of a block-model object, optionally domain by
domain (hard boundaries); the result is an `estimate` object nested under its block model.
Two methods (`method`, fixed per estimate):

- `nexus` (default) — non-stationary geostatistics (`NEXUS.md`): stationarity audit,
  multiscale robust trend, local scale / nugget / anisotropy fields, kriging with a
  Paciorek–Schervish covariance, cross-validated settings; ordinary kriging when the data
  are stationary. Needs a hole id and at least 5 holes per domain.
- `lva_dknn` — the neural estimator of `src/deep_modelling/`, conditioned on local data.

Runs execute in a detached worker (`python -m project.estimate run <run_dir>`), so the MCP
server never loads torch; the worker commits the result into the project. The run folder
`runs/<object_id>/` keeps `status.json` and `worker.log`.

- `modelling_start_run(block_model, samples, value, method?, domains?, columns?, options?, name?, project?)`
- `modelling_run_status(object_id, project?)` — progress; metrics and output paths when done.
- `modelling_cancel_run(object_id, project?)`.

NEXUS outputs (`objects/<object_id>/v<n>/`): `estimate.csv` (`<value>_est` block mean,
`_trend`, `_p10/_p50/_p90`, `_sd`, `_rel_unc`, `_p_gt_<cutoff>`, `n_samples`, `n_holes`,
`mean_dist`, `flag`), `metrics.json` (audit, every parameter with its reason, held-out skill
against ordinary kriging and inverse distance, calibration, trend reproduction),
`validation.csv`, `fields.npz`. The Data page creates (**+ Add estimate** on a block model)
and edits NEXUS estimates. An upstream rebuild marks the estimate out of date
(`project_update(object_id, {})` re-runs it). Designs:
`docs/superpowers/specs/2026-10-10-nexus-estimator-design.md`,
`docs/superpowers/specs/2026-09-27-property-modelling-design.md`,
`docs/superpowers/specs/2026-10-01-block-model-estimates-design.md`.

## Geometry tools (`geometry_*`)

Closed wireframes from drillholes: a biharmonic RBF (greedy-fitted, dual form) of the
signed distance to inside/outside contacts, contoured by marching cubes. Every build is
added to the project as an object.

- `geometry_domain_wireframe(drillholes, column, inside, options?, name?, project?)` —
  solid around intervals whose code is in `inside` (e.g. `LITH`, `["SAP"]`).
- `geometry_grade_shell(drillholes, column, cutoff, options?, name?, project?)` — shell
  around composites ≥ cutoff (indicator by default; `transform: log|none|auto`
  interpolates the grade).

`drillholes` is a drillholes/composites object id or `{collar, intervals, survey?,
mapping?, desurvey?}` CSV paths. Every option is optional (resolution, extent, trend —
auto from the contacts —, kernel, drift, accuracy, max_centres, max_distance…); the
summary says what was used and why. Outputs land in the project folder
(`objects/<object_id>/v<n>/`): `wireframe.obj` in world coordinates, `points.csv`,
`report.json`. Design: `docs/superpowers/specs/2026-09-28-geometry-wireframes-design.md`.

- `geometry_block_model(wireframes?, drillholes?, options?, name?, project?)` —
  block-model geometry, no values yet: parent blocks (optionally rotated), regular
  sub-blocks where wireframe boundaries cut them, a `DOMAIN` code per block (first listed
  wireframe wins). Outputs `block_model.csv` (world centroids, sizes, IJK, sub-block
  indices, domain), `block_model.json` (definition) and `report.json` in the project
  folder; estimates attach to it (see Property modelling). Design:
  `docs/superpowers/specs/2026-09-28-geometry-block-model-design.md`.

## EDA tools (`eda_*`)

Read-only analyses of the project's samples (drillholes / composites) and estimates. Each
returns the numbers (statistics, tables, the choices made and why) and a `url` opening the
EDA page on the same analysis (`open=true` opens it; or `project_open(open="eda")`).

- `eda_overview(project?)` — what can be analysed, per-column summaries, suggested order.
- `eda_univariate(source?, property?, group_by?, weight?, log?, bins?)` — statistics per
  group, histogram (length-weighted by default).
- `eda_bivariate(source?, x?, y?, group_by?, log?, method?)` — Pearson / log / Spearman,
  regression, correlation matrix.
- `eda_outliers(source?, property?, group_by?, weight?)` — top-cut diagnostics: decile
  analysis, cap candidates with metal removed, suggested cap, flagged samples.
- `eda_section(source?, start?, end?, width?, property?)` — vertical section along A–A′
  with a corridor (auto: the collars' long axis).
- `eda_grade_tonnage(estimate, density?, cutoffs?, domain?)` — grade–tonnage curve
  (volume without a density).
- `eda_swath(estimate, axis?, width?, domain?)` — estimate against its samples along X, Y, Z.

The page (`/eda/`) computes the same analyses on demand through `GET /api/eda/catalog` and
`GET /api/eda/<analysis>` and draws them with the shared chart core of
`src/web-app/src/analysis/` (Canvas; zoom, pan, hover; no chart dependency), also used by
the QAQC page. Design: `docs/superpowers/specs/2026-10-01-eda-module-design.md`.

## Extending

- **New vertical:** add a subpackage with `register(mcp)` and call it in
  `server.build_server()`.

## Projects (`project_*` tools)

Every loaded or built object (drillholes, composites, wireframes, block models,
estimates) lives in
a project under `variancelabs-output/projects/<project-id>/`. With a single project in
the working folder, tools use it by default (creating and naming one after the folder
the first time); with several, the tools ask which one. `processing_*` and `geometry_*`
tools write their outputs into the project and return `object_id`.

- `project_open(project?, open?)` — resolve (or default-create) a project; `open:
  'data' | 'qaqc' | 'eda' | 'viewer'` opens that page in the browser. Returns the object list,
  summaries and the page URLs.
- `project_create(name, open?)` — a new, empty project; same `open` as `project_open`.
- `project_load_drillholes(collar?, intervals?, survey?, desurveyed?, load_as?, mapping?,
  desurvey?, name?, project?)` — load raw tables (editable later) or an already
  desurveyed/composited table (`load_as: 'drillholes' | 'composites'`, not editable).
- `project_load_qaqc(drillholes, standards?, ...)` — QC files for raw drillholes (see QAQC
  tools).
- `project_load_wireframe(path, name?, project?)` — import an OBJ wireframe.
- `project_update(object_id, params, project?)` — change an object's parameters (as on
  the Data page's Edit panel) and rebuild it and everything built from it.
- `project_remove(object_ids, cascade?, project?)` — remove objects; dependents must be
  confirmed with `cascade=true`.
- `project_set_main_commodity(column, project?)` — the column (e.g. `AU1`) every page and
  eda/qaqc tool shows first where an object has it; `null` clears it. No rebuild.

Opening a page starts a localhost app server (one per MCP process, one token, `?token=`
in the URL) serving the built web app. Positions in the 3D viewer are staged relative to
a local origin (float64 on the Python side)
so UTM-scale coordinates render without float32 jitter; the grid labels show
real-world coordinates. Numeric interval columns become elements; text columns
with ≤ 64 distinct values (e.g. `LITH`, `DOMAIN`) are staged as categories.

**Display** colours holes by element: *Continuous* (linear or log scale,
editable range, Auto = 2nd–98th percentile), *Classed* (grade shells by equal
interval, quantile or custom breaks such as `0.3, 1, 3`) or *Categorical*
(text columns, or numeric codes with ≤ 64 values). Palettes are grouped as
grade/sequential, topography, diverging and categorical; each can be reversed,
and legends show metres per class or category.

**Wireframes** appear under their own tree node and are selectable in 3D like
holes (holes win when both are under the cursor). The Display tab styles the
selected wireframes: Solid · Solid + edges · Edges · Points, colour, edge
colour and opacity; the Object tab gives volume / parts / extent cuts.

**Block models** are coloured by domain, block size or one of their estimates (palette and
range; blocks without a value grey); the Object tab lists each estimate's held-out R². They
are selectable in 3D like wireframes. Display › Rendering › **Ray-traced** draws the whole
model from GPU 3D textures instead of its visible shell: Voxels or Translucent, opacity (blocks
are otherwise opaque: unsorted instances cannot blend in depth order), and
a cutoff (Show ≥ / ≤) on the estimate shown. It streams a multi-resolution chunk tree
(sub-blocks → parents → coarser), finer where blocks cover more than a pixel, within a
1.5 GB GPU budget (`src/web-app/src/viewer/volume/`, staged by `src/project/volume.py`;
`/files/` answers HTTP range requests). A model with more than 2 M visible blocks is
ray-traced only.

Viewer controls (the Project tree on the left; Properties pinned bottom-left
with **Display** styling and **Object** facts about the selection):

| Gesture | Effect |
| --- | --- |
| Drag / right-drag / wheel | orbit / pan / zoom |
| Double-click a hole, wireframe or block model (3D or tree) | select only that one; on empty space, clear |
| Ctrl+click a hole or wireframe (3D or tree) | add / remove it |
| Click, Shift+click, Ctrl+Shift+click (tree) | select one / a range / add a range |
| ↑ ↓, Shift+↑ ↓ (tree) | move / extend the selection |
| Double-click `Drillholes` | select all its holes |
| Ctrl+A | select all of the selected type; again for everything |
| F | frame the selection (everything if none) |
| Esc | clear the selection |

## Web app (`src/web-app/`)

The browser side is a multi-page app: a left rail with the VarianceLabs mark
(`assets/icons/`) above the module tiles (`src/shell/`), one page per module
at `/<module>/`, each fully defined by its URL (`?project=&token=`) so an MCP
tool can open it directly. Four modules: `data/` (the Data page, `src/data/`),
`qaqc/` (the QAQC page, `src/qaqc/`), `eda/` (the EDA page, `src/eda/`) and
`viewer/` (the 3D viewer, `src/viewer/`); `/` redirects to `viewer/`. To add
a module: a `src/<module>/main.ts` that calls `mountShell("<module>")`, a
`<module>/index.html`, an entry in `src/shell/modules.ts` and one in the
Vite `input` map.

- `uv run python scripts/dev_app_server.py` creates (or reuses) a demo
  project with a synthetic dataset (add `--large` for the 10k-hole fluidity
  dataset, or `--qaqc` for `data/test-dataset-QAQC` with its QC files) and prints the
  page URLs.
- `npm run dev` (in `src/web-app/`) hot-reloads the app, proxying `/api` and
  `/files` to that server (default `http://127.0.0.1:8765`, override with
  `VL_PROXY`).
- `npm run test` (vitest) and `npm run build` (type-gate + bundle).
- The bundle is **committed** at `src/variancelabs_mcp/app/static/` so the
  MCP needs no Node at runtime: rebuild + commit after engine changes.
