Deterministic · standards-traceable · stdlib-only core
LLM agents are excellent at driving an engineering workflow in natural language — and terrible at arithmetic. ccpp-tools-mcp splits the work: any MCP client (Claude Desktop, mobile, Claude Code, Cursor) holds the conversation, while every number comes from a pure Python function that returns the governing standard, the equation it evaluated, the assumptions you must own, and the warnings you must not ignore.
Interactive · real server responses
A diesel transfer system — storage tank to elevated day tank — verified through eight tool calls. Every JSON below is an actual response from the server, not a mock. Step through with the buttons or Space. The full Claude conversation is in the repo transcript.
Design principle
Every tool returns the same contract: result with unit-suffixed fields,
method naming the standard and the exact equation, assumptions
the engineer must own, and warnings that degrade validity.
Warnings are load-bearing. An undersized line answers with the number and a velocity flag. A deep-turndown case flags the laminar–turbulent transition. A near-saturation valve sizing reports choked flow. The orchestrating agent is expected to surface every warning — the case transcript shows that happening.
Refusal over extrapolation. Out-of-range states (IF-97 region 3/5) raise a typed error instead of returning a plausible-looking number. Half of a tool's trustworthiness is what it declines to do.
{
"result": { "npsha_m": 12.99, "margin_m": 8.99 },
"method": {
"standard": "Hydraulic Institute (HI 9.6.1)",
"equation": "NPSHa = (P_abs - Pv)/(rho g)
+ z_static - h_f,suction"
},
"assumptions": [
"atmospheric pressure 101.325 kPa(a)",
"vapor pressure 0.5 kPa(a) at pumping temp"
],
"warnings": [],
"validity": "ok"
}
Why trust the numbers
The runtime core is stdlib-only. fluids and iapws are
test-only dependencies — so agreement with them is a consensus of two independent
implementations, never a circular check.
| Domain | Golden source | Cross oracle | Points | Tolerance |
|---|---|---|---|---|
| IF-97 regions 1/2 (v·h·s·cp) | R7-97 Tables 5, 15 | iapws.IAPWS97 | ~540 grid | rel 1e-8 |
| IF-97 saturation line | R7-97 Tables 35, 36 | _PSat_T / _TSat_P | ~590 | rel 1e-8 |
| Water viscosity | R12-08 Table 4 | _Viscosity | 324 grid | rel 1e-8 |
| Colebrook friction | implicit-equation residual = 0 | fluids.Colebrook | 264 | rel 1e-6 |
| ASME B36.10M table | spot values | nearest_pipe (every row) | 89 rows | ±0.03 mm |
| Crane K structure | TP-410 C·fT form | K_*_Crane ratio | exact | rel 1e-9 |
| ISA 75.01 Cv | definition identity (1 gpm / 1 psi / water = 1) | — | analytic | rel 2e-3 |
Kept as evidence, on purpose
Both incidents below are preserved in docs/verification.md rather than silently fixed — a verification system you can trust is one that has demonstrably fired.
The R12-08 viscosity verification table was first transcribed from memory: 5 of 11 expectations failed. The official release PDF was downloaded and Table 4 re-transcribed — the implementation matched all 11 points unchanged. The memory was wrong, not the code. Rule enforced since: goldens come from documents, never from recall.
The embedded B36.10M table carried NPS 18 SCH 80 as 23.88 mm. The all-rows sweep against
fluids flagged it on its first run; the standard value is 23.83 mm.
One wrong digit in 89 rows, caught without human eyes.
Technical highlight
Regions 1 (compressed liquid), 2 (superheated steam), 4 (saturation), the B23 boundary, and the R12-08 industrial viscosity formulation — implemented in pure stdlib Python directly from the IAPWS releases.
Whether ~130 transcribed coefficients are all correct is decided by the releases' own
computer-program verification tables: a single wrong digit breaks 8–9 significant-figure
agreement. A ~1,100-point sweep against the independent iapws package seals it.
// refusal over extrapolation water_state(pressure_mpa=30, temperature_k=650) IF97RangeError: state falls in IF-97 region 3 (near-critical), which is not implemented // verification anchors (release values) T=300 K, p=3 MPa → v = 0.100215168e-2 m³/kg T=700 K, p=30 MPa → h = 2631.49474 kJ/kg psat(600 K) → 12.3443146 MPa μ(298.15 K, 998) → 889.735100 μPa·s
v1 surface
| Tool | Computes | Standard basis |
|---|---|---|
| fluid_props_liquid | ρ · ν/μ · Pv · SG (preset or 2-point fit) | API gravity · ASTM D341 |
| fluid_props_steam | water/steam v·h·s·cp·μ, saturation states | IAPWS-IF97 · R12-08 (own) |
| tank_capacity | working/nominal volume, residence time | volume balance (ahead of API 650) |
| pipe_size_select | smallest NPS/SCH meeting a velocity limit | ASME B36.10M + continuity |
| pipe_wall_thickness | pressure-design wall vs selected schedule | ASME B31.1 §104.1.2 |
| dp_segment_liquid | friction + fitting losses, Re/velocity flags | Crane TP-410 · Colebrook |
| list_crane_fittings | accepted fitting keys and K basis | Crane TP-410 |
| pump_tdh · pump_npsha · pump_power | head balance · NPSH margin · power | Hydraulic Institute |
| valve_cv_liquid | required Cv/Kv + choked-flow check | ISA 75.01 / IEC 60534-2-1 |
Five minutes to a running chain
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"ccpp-tools": {
"command": "uvx",
"args": ["ccpp-tools-mcp"]
}
}
}
Claude Code
claude mcp add ccpp-tools -- uvx ccpp-tools-mcp
Claude mobile / web (HTTP transport)
uv run ccpp-tools-mcp \
--transport streamable-http --port 8899
cloudflared tunnel --url http://localhost:8899
# add https://<tunnel>/mcp as a custom connector
Links