The portable FFA skill now covers collection, justification, preparation and comparison of historical, regional and causal information. Official Bulletin 17C is its primary data-collection/entry resource for both Bayesian and B17C analyses.
| Contract | Purpose |
|---|---|
GET /api/inputdata/{id}/chronology / MCP get_inputdata_chronology |
Before fitting, returns schemaVersion:1, inputData, exact/uncertain/interval observations and inclusive threshold windows; bounds/counts come from the model |
GET /api/inputdata/{id}/source / MCP get_inputdata_source |
Returns detached original creation request, capturedUtc, optional USGS rawText, and SHA-256 of its UTF-8 text. Raw dates/qualifiers remain available for review |
Univariate useJeffreysRuleForScale |
Nullable request/MCP option exposing the existing switch; omission preserves its model default |
Univariate/B17C resource configuration |
Effective parent distribution, AEP ordinates, priors/penalties, exposed sampler settings and relevant switches; save before and after running |
These are additive contracts. Univariate requests now reject a quantile-prior count
the model cannot apply: one with useSingleQuantile=true, otherwise one per parent
distribution parameter. Priors, estimators, formulas and defaults are unchanged.
thresholdData.numberAbove means additional aggregate exceedances not already
entered as explicit exact/uncertain/interval observations. Responses contain
processed counts; the source endpoint preserves submitted counts. An explicit
1882 event inside 1870–1922 with aggregate above count zero leaves 52 censored years.
Do not copy processed responses back as original scientific evidence. Date-only
exact/uncertain observations use calendar year in the API; clients must supply
explicit indexes for a declared water-year convention. Raw USGS preservation does
not change the downloader's classification or interpret peak qualifiers.
See study workflow and chronology contract. The source/chronology endpoints never run an estimator. Their resources share the existing in-memory lifetime; save artifacts before stopping the host. Original requests are retained for manual, USGS, block-maxima and POT inputs; raw downloads are retained here for direct USGS peaks. Older programmatically built resources without a stored request return only the available provenance.
src/RMC.BestFit.Api hosts a headless REST API and an MCP (Model Context Protocol) server over
the RMC-BestFit model library (RMC.BestFit.dll), enabling programmatic and agentic AI
flood-frequency workflows: download USGS data, build input data, fit distributions with Bayesian
MCMC / Bulletin 17C / rating curves, and retrieve results as JSON.
dotnet run --project src/RMC.BestFit.Api # http://localhost:5210 (Development)- OpenAPI document (Development):
GET /openapi/v1.json - Health:
GET /health,GET /health/detailed; service info:GET /api/info - Configuration (
appsettings.json, sectionApi):MaxResources(default 500),MaxConcurrentRuns(default 2),MaxIterations(default 500,000)
All routes, including REST, /mcp, health and documentation endpoints, enforce
browser-origin validation in Development and Production. Requests without an
Origin header remain allowed, so same-session Python REST and native MCP clients
need no browser configuration. An Origin-bearing request is accepted only for an
explicitly configured origin or a literal localhost/loopback origin matching the
request scheme, host and effective port. Matching an arbitrary DNS Host does not
grant implicit trust. Invalid or untrusted origins receive HTTP 403 before the
endpoint runs.
Configure trusted browser clients with Cors:AllowedOrigins in appsettings.json
or indexed environment variables such as
Cors__AllowedOrigins__0=https://client.example. Each entry must be a single
HTTP(S) origin containing only scheme, host and optional port. Wildcards, null,
credentials, trailing slashes/paths, queries, fragments and origin lists are
invalid configuration. Matching normalizes case and default ports; it does not
permit subdomains or different ports. The shipped configuration retains
https://localhost. The same allowlist controls CORS for REST and browser MCP.
This is not authentication: a native client can omit Origin. Bind the
session-local workflow to
loopback. Publishing a skills-only plugin does not deploy or secure a public API.
For a terminal-capable Claude or Codex workflow with matplotlib exports and chat display, use the portable frequency-curve skill.
- Stateful resource store. Creation endpoints store resources in memory keyed by GUID; later calls reference the ids. Resources are immutable after creation; analyses clone their inputs at creation, so deleting or replacing upstream resources never corrupts an existing fit. State is lost on restart (by design for the proof of concept).
- Response contract. Every response carries
success,errorMessage,validationErrors,validationWarnings,computationTimeMs,timestamp,nonFiniteFindings. JSON is camelCase with enums as camelCase strings; NaN serializes as the named literal"NaN"(missing data); ±Infinity is rejected server-side before serialization. - Status codes. 200/201 success; 400 validation or argument errors; 403 rejected origin; 404 unknown id or no USGS
data; 409 store at capacity or analysis already running; 499 client cancelled; 502/503 USGS
upstream failures; 500 run failures. Exception: workflow endpoints report step failures in-body
(
success=false,failedStep) with HTTP 200, preserving the ids of already-created resources. - Probability convention. All probability ordinates are annual exceedance probabilities (AEP), strictly between 0 and 1 (0.01 = 100-year event). Client-supplied ordinates are de-duplicated and sorted ascending automatically.
- Distribution specs and engineering judgment. Wherever a request supplies a distribution as
an input — uncertain-observation measurement errors,
parameterPriors,quantilePriors— it is a{ type, parameters }spec with parameters in the type's canonical order.GET api/metadata/distributionslists the parameter names/order per type (also the names that priors and Bulletin 17C penalties are matched against — the trailing short-form marker like "(γ)" may be omitted);GET api/metadata/enumslists the accepted spec types (priorDistributions). Supplying any parameter prior switches that analysis off its default flat priors; unnamed parameters keep flat priors. Bulletin 17C uses Gaussian penalties ({ parameterName | aep, mean, mse, useLog/useLog10 }) instead of priors, and reports a warning (never silently) when its input data contains uncertain observations, which its Expected Moments Algorithm ignores. - Runs are synchronous.
POST .../runreturns when the estimation finishes (seconds to about a minute for MCMC). Client disconnect cancels the run; the analysis stays rerunnable.
| Endpoint | Purpose |
|---|---|
POST api/timeseries/usgs |
Download from USGS water services: dailyDischarge, dailyStage, instantaneousDischarge, instantaneousStage, peakDischarge, peakStage, measuredDischarge, measuredStage |
POST api/timeseries/manual |
Build from explicit {dateTime, value} points (NaN = missing) |
GET api/timeseries / GET {id}?includePoints&offset&limit / DELETE {id} |
List / detail with paged points / delete |
| Endpoint | Purpose |
|---|---|
POST api/inputdata/manual |
Explicit observations: exact (systematic record), uncertain (per-observation measurement-error distributions, e.g. paleoflood estimates), interval-censored, perception thresholds; plotting parameter, low-outlier threshold, lambda |
POST api/inputdata/block-max |
Block maxima from a stored time series (timeBlock=waterYear default; smoothing for n-day flows) |
POST api/inputdata/peaks-over-threshold |
Independent POT events from a stored series (threshold, minStepsBetweenPeaks, optional lambda override) |
POST api/inputdata/usgs-peaks |
USGS annual peak-flow file directly (no intermediate time series) |
GET api/inputdata / GET {id}?includeData / GET {id}/summary-statistics / DELETE {id} |
List / detail with observations and plotting positions / sample statistics / delete |
Analyses — api/analyses/{univariate|bulletin17c|ratingcurve|mixture|pointprocess|competingrisks|composite|distributionfitting|bivariate|coincidentfrequency|timeseries}
All kinds share the verb set: POST (create), POST {id}/run, GET (list), GET {id},
GET {id}/results, GET {id}/validate, DELETE {id}. Lookups are kind-guarded (a univariate id
404s on the Bulletin 17C routes).
GET api/analyses/{analysisId}/plot-source?includeSamples=false exports one completed run of
any kind for external plotting. The matching MCP tool is get_analysis_plot_source. The version 1
response identifies the analysis and last run, and includes the existing kind-specific results
payload, portable analysisXml settings, available modelXml, frequency dataFrameXml, dated
rating/time-series observations and ARIMAX covariates in series, bivariate marginal observation
frames and model XML, and stored parameter histogram/density/autocorrelation arrays. Histogram bins
include exact lower and upper edges and raw frequencies, allowing clients to reproduce the app's
density normalization. Typed observations and point-process amsObservations avoid requiring a
.NET runtime to decode frequency input. Distribution fitting snapshots carry fittingCurves and
fittingHistogram; bivariate snapshots carry the seeded scatter and desktop contour grids in
bivariatePlot; rating and time-series snapshots carry residualPlot, with an exact dated result
grid in resultDates for time-series forecasts. Parameter diagnostics include their configured
prior density, and influenceDiagnostics keeps leverage, fit, variance, and LOO measures distinct.
Nonstationary univariate snapshots also include a detached chronology
grid with mean and interval arrays. Composite and seasonal point-process snapshots include
componentCurves at each component's configured probabilities. The XML is produced by the
existing model serializers, while results is a detached copy of the established results DTO.
includeSamples=true additionally returns saved parameter draws and per-chain samples when that
analysis has them. For Bulletin 17C, these draws are the GMM-derived uncertainty ensemble under
its configured method, not Bayesian MCMC. An analysis with no completed current run returns 404;
an analysis or live component running concurrently returns 409. Export does not run an analysis or
alter its settings. The response is a same-run snapshot; clients should not combine it with a
separate results request when they need strict run identity.
The API's input-data resources do not retain the desktop project's free-text unit label;
plotting clients should supply one explicitly when needed, or mark that label unavailable.
Live component references (composite, bivariate, coincidentfrequency). Analyses that consume OTHER analyses hold LIVE references to them — the one deliberate exception to the clone-at-create invariant, because components may not be estimated yet when the consumer is created. This applies to a composite's components, a bivariate's two marginal analyses, and a coincident frequency analysis's bivariate (plus, transitively, its marginals). Consequences: every referenced analysis must be RUN before the consumer runs (validate reports which still require estimation); while the consumer runs it holds each referenced analysis's run lock, so any of them mid-run makes the consumer run report 409 (and vice versa); re-running a referenced analysis intentionally refreshes the consumer's next run; deleting one from the store leaves the consumer functional (the live reference keeps the fitted object alive) — the recorded ids are provenance only.
| Kind | Create request highlights | Results |
|---|---|---|
univariate |
inputDataId, distribution (15 supported; default logPearsonTypeIII), probabilityOrdinates?, bayesianOptions? (sampler, iterations, chains, seed, CI width, point estimator), parameterPriors?, quantilePriors? + useSingleQuantile? |
Frequency curve (mode/mean/CI), posterior parameter summaries with R-hat/ESS, AIC/BIC/DIC/WAIC/LOOIC/RMSE, convergence warnings |
bulletin17c |
inputDataId, distribution (6 supported), uncertaintyMethod? (default model linkedMultivariateNormal), probabilityOrdinates?, parameterPenalties? (regional skew), quantilePenalties? |
Frequency curve with confidence intervals, sampled parameter summaries, AIC/BIC/DIC/RMSE/ERL, GMM/uncertainty timing |
ratingcurve |
stageTimeSeriesId, dischargeTimeSeriesId (date-aligned; ≥10 common dates), numberOfSegments 1-3, optional stage grid (minStage/maxStage/stageBins), bayesianOptions?, parameterPriors? |
Stage-discharge curve over the stage grid (mode/mean/CI), fitted parameters, posterior summaries |
mixture |
inputDataId, distributions (1-3 components, all 15 supported), isZeroInflated?, probabilityOrdinates?, bayesianOptions?, parameterPriors?, quantilePriors? |
Frequency curve (mode/mean/CI), posterior summaries for component parameters and weights, AIC/BIC/DIC/WAIC/LOOIC/RMSE |
pointprocess |
inputDataId (POT resource; its threshold seeds the model), isSeasonal? + timeBlock?/startMonth?, threshold?/totalYears? overrides, probabilityOrdinates?, bayesianOptions?, parameterPriors?, quantilePriors? — GEV-only by construction (no distribution field) |
Annual-exceedance frequency curve (mode/mean/CI), posterior summaries, AIC/BIC/DIC/WAIC/LOOIC/RMSE |
competingrisks |
inputDataId, distributions (1-3 components, one per competing process), probabilityOrdinates?, bayesianOptions?, parameterPriors?, quantilePriors? |
Frequency curve (mode/mean/CI), posterior summaries, AIC/BIC/DIC/WAIC/LOOIC/RMSE |
composite |
components ([{analysisId, weight?}], live references; kinds univariate/bulletin17c/mixture/pointprocess/competingrisks), compositeType (competingRisks default | mixture | modelAverage), averageMethod? (DIC/WAIC/LOOIC need MCMC components), dependency?, isMaximum?, probabilityOrdinates?, credibleIntervalWidth?, pointEstimator? — no MCMC of its own |
Frequency curve (mode/mean/CI), composite block with per-component weights (model-average weights are a first-class output), AIC/BIC/DIC/RMSE |
distributionfitting |
inputDataId, distributions? (candidate subset; default all 15) — parallel MLE screen, seconds, no MCMC |
Ranked fits (AIC ascending, failures last): parameters, AIC/BIC/RMSE, fitSucceeded, errorMessage |
bivariate |
marginalXAnalysisId, marginalYAnalysisId (live references; kinds univariate/bulletin17c/mixture/pointprocess; data pair by shared time index, ≥10 overlapping non-outlier exacts), copulaType (7 families; default normal), estimationMethod? (inferenceFromMargins default | pseudoLikelihood), xyOrdinates ([{x, y}] joint-exceedance grid), bayesianOptions?, parameterPriors? (copula params) |
Joint exceedance P(X>x AND Y>y) per grid point (mode/mean/CI), copula posterior summaries with R-hat/ESS, AIC/BIC/DIC/WAIC/LOOIC/RMSE |
coincidentfrequency |
bivariateAnalysisId (live reference; must be run first), xValues/yValues (strictly ascending), bivariateResponse (2-D surface Z[x][y], strictly increasing both axes), numberOfBins 5-1000 (default 50), credibleIntervalWidth?, pointEstimator? — no MCMC of its own; univariate-marginal posterior chains are pulled in automatically when available (marginalX/YChainUsed flags) |
Response frequency curve: AEP per response-magnitude bin (mode/mean/CI) over zValues |
timeseries |
timeSeriesId, modelType (ar | ma | arima | arimax), family-specific orders (order for ar/ma; pOrder/dOrder/qOrder for arima/arimax; xOrder + covariateTimeSeriesIds + trendType + includeSeasonality + covariateExtension for arimax — fields for other families are 400s, never silently ignored), includeIntercept?, transformType? and transformLambda? (the Box-Cox/Yeo-Johnson exponent is fitted on the training window unless transformLambda supplies it, which requires transformType boxCox or yeoJohnson; the MCP tool always fits it), trainingTimeSteps?, forecastingTimeSteps? 0-100, bayesianOptions?, parameterPriors? |
Fitted-plus-forecast curve aligned by time index (mode/mean/CI), parameter posteriors with R-hat/ESS, AIC/BIC/DIC/WAIC/LOOIC/RMSE |
| Endpoint | Steps |
|---|---|
POST api/workflows/usgs-peak-frequency |
USGS peaks → input data → univariate MCMC → results |
POST api/workflows/usgs-daily-block-max-frequency |
USGS daily → time series → block maxima → univariate MCMC → results |
POST api/workflows/usgs-bulletin17c |
USGS peaks → input data → Bulletin 17C → results |
POST api/workflows/usgs-rating-curve |
USGS measured stage + discharge → rating curve MCMC → results |
Responses include every created resource id (timeSeriesId, inputDataId, analysisId, ...)
plus the full results. On a step failure: success=false, failedStep, and the ids of completed
steps so the workflow can be resumed manually through the granular endpoints.
GET api/metadata/distributions (which distributions each analysis kind supports),
GET api/metadata/enums (every accepted enum string), GET api/metadata/defaults (default AEP
ordinates and server limits), GET api/resources (cross-cutting id overview).
Manual input creation, USGS-peak input creation, and the USGS B17C workflow accept
useMultipleGrubbsBeckTest (default false, preserving existing requests).
For example, POST /api/workflows/usgs-bulletin17c:
{"siteNumber":"01646500","useMultipleGrubbsBeckTest":true}Or POST /api/inputdata/manual with all supplied observations and the same flag,
then create a B17C analysis linked to its returned id. Screening delegates to
DataFrame.SetLowOutliersFromMGBT() after all series are populated and before an
analysis clones the data. It requires at least ten exact observations. It flags
low outliers, sets the threshold, and refreshes plotting positions; it does not
delete the observations. The model's existing strict threshold comparison is
preserved. When true, a manual lowOutlierThreshold (including zero) or any
isLowOutlier:true observation is rejected to avoid overwriting the caller's
screening choice. Omitted/false retains existing manual behavior.
Manual input creation also accepts a manual lowOutlierThreshold without MGBT.
It is applied with DataFrame.SetLowOutliersFromThreshold() after the exact series is populated:
every exact observation strictly below the threshold is flagged a low outlier
(regardless of any isLowOutlier supplied on it), and the count is returned as
lowOutlierCount. The data frame requires at least ten exact observations and
rejects a threshold that would censor more than 50% of the record (above the
sorted upper-middle value) with a 400 carrying its own validation message;
nothing is stored. Supplying isLowOutlier:true on an observation whose value is
at or above lowOutlierThreshold is rejected as contradictory (the threshold
would unflag it) before anything is built; a preflagged observation already
below the threshold agrees and is accepted. Omitting lowOutlierThreshold while
any exact observation is preflagged isLowOutlier:true is rejected with a 400,
because the flags would otherwise be stored with no censoring threshold to
define them (NumberOfLowOutliers at zero) — supply lowOutlierThreshold, or
use useMultipleGrubbsBeckTest to derive the flags instead. Omitting
lowOutlierThreshold when no exact observation is preflagged is unaffected.
The MCP tools create_inputdata_manual, create_inputdata_usgs_peaks, and
run_usgs_bulletin17c_workflow expose the same optional boolean. For example:
run_usgs_bulletin17c_workflow(siteNumber="01646500", useMultipleGrubbsBeckTest=true).
Input summaries return lowOutlierThreshold and lowOutlierCount; GET input data
with includeData=true to save flags and the model's computed plotting positions.
MGBT failures store no new input resource; workflow failures identify
failedStep:"createInputData" before analysis creation.
Uncertain observations now include response-only lowerBound/upperBound from
the model's display properties. Frequency results add quantileAnnotations, an
array of {aep,value,lowerBound,upperBound} for enabled univariate priors or B17C
quantile penalties, in physical units. These additions preserve existing fields.
Plotters should use these values directly, including input plottingPosition,
instead of recomputing them from ranks or distribution parameters. The default
matplotlib renderer is skills/bestfit-frequency/scripts/plot_frequency.py.
The same host serves MCP over the streamable HTTP transport at /mcp (stateless mode — all
state lives in the app-singleton resource store, so ids remain valid across MCP sessions and the
REST/MCP boundary). Tools call the same service layer as the controllers and return the same JSON
DTOs.
Connect from an MCP client:
claude mcp add --transport http bestfit http://localhost:5210/mcpTools (32): get_metadata, list_resources, delete_resource, usgs_download_timeseries,
create_manual_timeseries, get_timeseries, create_inputdata_usgs_peaks,
create_inputdata_block_max, create_inputdata_pot, create_inputdata_manual, get_inputdata,
get_inputdata_chronology, get_inputdata_source,
create_univariate_analysis, create_bulletin17c_analysis, create_ratingcurve_analysis,
create_mixture_analysis, create_pointprocess_analysis, create_competingrisks_analysis,
create_composite_analysis, create_distributionfitting_analysis,
create_bivariate_analysis, create_coincidentfrequency_analysis, create_timeseries_analysis,
run_analysis, get_analysis_results, get_analysis_plot_source, validate_analysis, plus the four
run_usgs_*_workflow one-shots.
Typical agent chains:
get_metadata → create_inputdata_usgs_peaks(site) → create_bulletin17c_analysis(inputDataId) → run_analysis(analysisId)
usgs_download_timeseries(site, dailyDischarge) → create_inputdata_block_max(timeSeriesId) → create_univariate_analysis(...) → run_analysis(...)
usgs_download_timeseries(site, measuredStage) + (site, measuredDischarge) → create_ratingcurve_analysis(...) → run_analysis(...)
run_usgs_bulletin17c_workflow(site) # everything in one call
Every analysis endpoint from the original Phase-4 scope is implemented: mixture, point process, competing risks, composite, bivariate, coincident frequency, time series, and distribution fitting, plus the Bayesian input features (uncertain data, parameter priors, quantile priors, Bulletin 17C penalties).
Deferred features: nonstationary trend models + chronology results, an async job pattern (jobId + polling) if synchronous runs outgrow client timeouts, authentication.