Skip to main content
Glama
J-X0

barrowman-associates-timeseries-mcp

by J-X0
README.md
# barrowman-associates-timeseries-mcp

Project Oxbow. Timeseries anomaly analysis for fleet telemetry, delivered as an
MCP server. Changepoint detection runs over every series for free; explaining a
changepoint costs money, and each tenant has a hard monthly spend cap. The cap
decides how many anomalies get explained.

## The constraint drives the design

Operating spend is capped per tenant per month. That single rule shapes the
pipeline:

- **Detection is free.** `detect_changepoints` is pure-Python robust CUSUM. It
  runs over the full series for every tenant and never touches the budget.
- **Explanation is metered.** Asking a model to explain a changepoint goes
  through `ExplanationProvider`, which quotes a per-call price in cents.
- **The budget rations explanation.** The pipeline ranks changepoints
  most-severe first and explains them in order, charging each call against the
  tenant's remaining monthly balance in `SpendLedger`. When the balance can no
  longer cover a call, the rest are recorded as *deferred* -- never explained,
  never charged. The "anomaly budget" is exactly `remaining_cents // price`.
- **Every decision is auditable.** `AnomalyReport.audit` has one row per
  changepoint: escalated or deferred, what it cost, and the remaining balance
  after.

Money is integer cents everywhere. Month buckets are UTC (`YYYY-MM`) so a cap
boundary never moves with server timezone.

## Layout

```
oxbowtimeseries/
  types.py          domain types (Point, Changepoint, Anomaly, AnomalyReport, TenantBudget)
  changepoint.py    robust CUSUM detector
  budget.py         SpendLedger, BudgetExceeded, month_key, refund -- the spend cap
  pipeline.py       AnomalyPipeline: detect -> rank -> ration -> audit
  validation.py     input validation at the tool boundary (ValidationError)
  config.py         JSON config load + validation, tenant caps (ConfigError)
  obs.py            structured JSON logging and timing
  service.py        AnalysisService: validate -> analyse -> log -> degrade
  server.py         FastMCP entry point exposing the analyze_series tool
  providers/
    base.py         ExplanationProvider interface, ExplanationRequest
    stub.py         deterministic offline provider (used by all tests)
    real.py         Anthropic-backed provider (optional 'real' extra)
tests/
config.example.json
```

## Install and test

```
make venv
make install
make test
```

`make test` runs the whole suite offline against `StubProvider`; no API key is
required. Override the interpreter with `make test PY=/path/to/python`.

## Using the core library

Runnable as-is (a step from level 1 to level 40 halfway through the series):

```python
from oxbowtimeseries import SpendLedger, TenantBudget, AnomalyPipeline, Point
from oxbowtimeseries.providers import StubProvider

ledger = SpendLedger()
pipeline = AnomalyPipeline(StubProvider(cost_cents=5), ledger)
# cap covers exactly one explanation at 5 cents/call
budget = TenantBudget("acme-fleet", monthly_cap_cents=5, cost_per_investigation_cents=5)

values = [1.0] * 20 + [40.0] * 20
series = [Point(t=1_772_323_200.0 + i * 60, value=v) for i, v in enumerate(values)]
report = pipeline.analyze(budget, "fuel_l_per_100km", series)

print(report.escalated_count, "explained,", report.deferred_count, "deferred")
for row in report.audit:
    print(row)
```

## Running the server

The server speaks the MCP protocol over stdio and is driven by a JSON config
file. `config.example.json` is a working starting point:

```
make run                      # uses config.example.json
# or
OXBOW_CONFIG=my.json .venv/bin/oxbow-timeseries
.venv/bin/oxbow-timeseries --config my.json
```

Config controls the provider, detector parameters, per-analysis limits, and
every tenant's monthly cap. Caps live in config so they change without a
redeploy. A malformed config fails at startup with a specific message and a
non-zero exit code, not at request time.

The server exposes one tool, `analyze_series(tenant_id, metric, points, month?)`,
where `points` is a list of `[unix_timestamp, value]` pairs sorted by time. It
returns the anomaly report (see `AnomalyReport.to_dict`) including the audit
trail. Logs are single-line JSON on stderr (stdout is reserved for the MCP
channel); set `OXBOW_LOG_LEVEL` to change verbosity.

### Failure handling and degradation

- Unknown tenant, bad points (wrong shape, NaN/inf, unsorted, over the point
  limit) are rejected at the boundary with a clear error.
- Missing or invalid config exits non-zero at startup.
- Provider outage degrades to a detection-only report: the reserved spend is
  refunded, the changepoints are still returned, and the audit rows read
  `provider_error`. The tenant is never billed for a call that produced nothing.
- Concurrent analyses for one tenant cannot overspend: the ledger charges
  atomically and refuses once the cap is reached.

## Configuration reference

The config is a JSON object. Unknown keys are ignored; every key below is
validated at load time.

| Key | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `provider.kind` | `"stub"` \| `"real"` | yes | -- | `real` needs `ANTHROPIC_API_KEY` and the `real` extra |
| `provider.cost_cents` | int ≥ 0 | no | `5` | price charged per explanation |
| `provider.model` | string | no | `claude-3-5-haiku-latest` | used only by the real provider |
| `tenants` | object | yes | -- | must be non-empty; keys are tenant ids |
| `tenants.<id>.monthly_cap_cents` | int ≥ 0 | yes | -- | the hard monthly spend cap |
| `tenants.<id>.cost_per_investigation_cents` | int ≥ 0 | no | `provider.cost_cents` | per-tenant price override |
| `detector.threshold` | float > 0 | no | `5.0` | CUSUM alarm level in robust sigmas |
| `detector.drift` | float ≥ 0 | no | `0.5` | slack; smallest shift worth flagging is ~`2*drift` sigmas |
| `limits.max_points` | int > 0 | no | `100000` | per-analysis point cap |

Environment variables: `OXBOW_CONFIG` (config path, alternative to `--config`),
`OXBOW_LOG_LEVEL` (default `INFO`), `ANTHROPIC_API_KEY` (real provider only).

## Known limitations

- **Ledger is in-process.** Spend state lives in memory and resets on restart.
  A restart mid-month gives every tenant a fresh cap. See ADR-0004 for why this
  was deferred and what replaces it (a shared store keyed by tenant+month).
- **One metric per call.** The tool analyses a single named series; batching
  many metrics is left to the caller.
- **Detection favours sustained level shifts**, not slow trends or seasonality.
  A gradual drift under `2*drift` sigmas will not alarm. This is deliberate
  (ADR-0002); a metric with strong daily seasonality should be deseasonalised
  before analysis.
- **Ranking uses detection magnitude as the severity proxy.** A large but
  operationally benign shift can outrank a small critical one; the budget then
  spends on the wrong anomaly. Severity weighting by metric is future work.
- **No retry on provider failure.** A failed explanation refunds and defers
  rather than retrying, to keep spend bounded and latency predictable.

## Architecture decisions

The contested calls are recorded as ADRs under [`docs/adr/`](docs/adr/):

- [ADR-0001](docs/adr/0001-ration-explanation-not-refuse.md) -- ration the
  expensive stage instead of refusing service at the cap
- [ADR-0002](docs/adr/0002-cusum-with-baseline-reset.md) -- robust CUSUM with a
  re-anchored baseline over PELT / Bayesian methods
- [ADR-0003](docs/adr/0003-reserve-then-refund.md) -- reserve-then-refund
  around provider calls
- [ADR-0004](docs/adr/0004-in-process-ledger.md) -- in-process ledger for the
  first release

## The real provider

`RealProvider` calls the Anthropic API and is never imported by the tests.
Install it with the extra and set a key:

```
pip install -e ".[real]"
export ANTHROPIC_API_KEY=sk-...
```

Then pass `RealProvider(...)` where the example passes `StubProvider()`.

## Status

Detection, budget, pipeline, providers, config, validation, logging, the
application service, and the FastMCP stdio server are implemented and tested.
The ledger is in-process for now; persisting it to SQLite so caps survive a
restart is the remaining known gap (see the `budget.py` module docstring).

---

*Barrowman Associates is an illustrative client; this repository is a self-directed reference implementation built to work end to end.*

TDQS

A3.7/5.0

Scored across 1 tool

Disambiguation5/5

With only a single tool, there is no possibility of confusion between tool purposes. The analyze_series tool has a clear, specific mandate to detect and explain changepoints.

Naming Consistency5/5

The sole tool follows a clear verb_noun convention (analyze_series). No conflicting naming styles exist since there is only one tool.

Tool Count2/5

A single tool feels too few for a server branded as a timeseries MCP. The broader domain typically requires multiple operations, making the tool count significantly under-scoped.

Completeness2/5

The server offers only changepoint analysis and lacks supporting operations such as data retrieval, transformation, or lifecycle management. This creates significant gaps for workflows that require more than the single analysis.

Maintenance

ActivityInactive
ResponsivenessNo issues