Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ jobs:
- name: Build
run: npm run build

- name: Smoke test the built binary
run: node dist/index.js --list

- name: Verify the demo runs
run: npm run demo

Expand Down
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![Node](https://img.shields.io/badge/Node-%3E%3D22.13-brightgreen.svg)](package.json)

Engineer MCP is a Model Context Protocol server for mechanical-engineering calculations.
It gives coding agents verified answers for beams, bolts, springs, shafts, bearings, stress, sections, and units.
It gives coding agents verified answers for beams, bolts, springs, shafts, bearings, stress, fatigue, sections, and units.
Every result shows the formula, the method, and the source.

## What it provides
Expand All @@ -23,6 +23,7 @@ The release covers these domains:
- Shaft torsion and first critical speed.
- Bearing rating life to ISO 281.
- von Mises equivalent stress.
- Fatigue safety factors for cyclic loads.
- Cross-section properties.
- Dimension-safe unit conversion.
- Material property lookup.
Expand Down Expand Up @@ -53,6 +54,7 @@ Warnings surface when a method uses an approximation.
| `shaft_analysis` | Torsion stress, twist, and critical speed. |
| `bearing_life` | ISO 281 rating life in revolutions and hours. |
| `von_mises` | Equivalent stress and yield safety factor. |
| `fatigue_analysis` | Fatigue safety factors for cyclic loads. |
| `unit_convert` | Conversion between compatible units. |
| `material_lookup` | Curated mechanical properties of materials. |

Expand Down Expand Up @@ -154,6 +156,29 @@ Error: Category mismatch: N·m is torque, J is energy.
Use a unit of the same quantity.
```

A call to `fatigue_analysis` for a steel part under 400 MPa mean stress and 200 MPa alternating stress:

```text
Mean stress 400 MPa
Alternating stress 200 MPa
Stress ratio 0.3333
Endurance limit 500 MPa
Soderberg safety factor 1.218
Goodman safety factor 1.364
Gerber safety factor 1.699
ASME-elliptic safety factor 1.722

Soderberg safety factor 1.218
Factor for the Soderberg line from Se to Sy. It is the most conservative criterion.

Method: Fatigue failure criteria for fluctuating stress
Formula: Soderberg: sa/Se + sm/Sy = 1/n. Goodman: sa/Se + sm/Sut = 1/n. Gerber: n.sa/Se + (n.sm/Sut)^2 = 1. ASME-elliptic: (n.sa/Se)^2 + (n.sm/Sy)^2 = 1
References:
- Shigley's Mechanical Engineering Design (McGraw-Hill Education, Tenth edition, 2015)
```

The tool always returns all four safety factors. It warns when a factor falls below 1.

## Development

| Command | Purpose |
Expand All @@ -169,7 +194,7 @@ Use a unit of the same quantity.
The test suite is deterministic and offline.
It covers the engines, the unit layer, the database, and the tools.

- 98 tests across 10 files.
- 115 tests across 11 files.
- All tests pass on Node 22 and Node 24.
- The CI workflow runs typecheck, tests, build, demo, and a package check.

Expand All @@ -183,7 +208,10 @@ Run `npm test` to reproduce the results.
- The bearing factors are typical values for deep-groove ball bearings.
- The critical speed is a first-mode approximation.
- The spring design covers static round-wire springs only.
It does not estimate fatigue life for cyclic loads.
Use the `fatigue_analysis` tool for cyclic loads.
- The fatigue criteria assume a tensile mean stress.
The endurance limit defaults to `0.5 x Sut`.
Apply modifying factors for surface, size, load, temperature, and reliability.
- The built-in SQLite module of Node.js is still experimental.

Check the cited sources for exact values.
Expand All @@ -197,10 +225,11 @@ Each release stays useful on its own.

- Helical compression spring design.
The `spring_design` tool reports the spring rate, the shear stress, and the safety factor.
- Fatigue analysis for cyclic loads.
The `fatigue_analysis` tool returns safety factors for the Soderberg, Goodman, Gerber, and ASME-elliptic criteria.

### Remaining

- Add fatigue analysis for cyclic loads.
- Add press-fit and interference-fit calculators.
- Add more unit categories, including viscosity and thermal conductivity.
- Add HTTP transport.
Expand Down
2 changes: 1 addition & 1 deletion data/references.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"title": "Shigley's Mechanical Engineering Design",
"source": "McGraw-Hill Education",
"edition": "Tenth edition, 2015",
"section": "Chapters 3, 5, 8, 11, and 12",
"section": "Chapters 3, 5, 6, 8, 11, and 12",
"note": "Standard reference for strength, fatigue, fasteners, and bearings."
},
"roark-2011": {
Expand Down
2 changes: 1 addition & 1 deletion docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ Engineer MCP grows in independent releases. Each release stays useful on its own
### Complete

- Helical compression spring design. The `spring_design` tool computes the spring rate, the shear stress, and the safety factor.
- Fatigue analysis for cyclic loads. The `fatigue_analysis` tool applies the Soderberg, Goodman, Gerber, and ASME-elliptic criteria.

### Remaining

- Add fatigue analysis for cyclic loads.
- Add press-fit and interference-fit calculators.
- Add more unit categories, including viscosity and thermal conductivity.
- Add HTTP transport in addition to stdio.
Expand Down
34 changes: 34 additions & 0 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,40 @@ Inputs:
- Cartesian mode uses `sigmaX`, `sigmaY`, `sigmaZ`, `tauXY`, `tauXZ`, `tauYZ`.
- `yieldStrength`: enables the safety factor.

## fatigue_analysis

Compute the fatigue safety factor for a fluctuating stress.

The tool applies the four standard failure criteria. They are Soderberg, modified Goodman, Gerber, and ASME-elliptic. Each criterion returns a safety factor for the same load cycle.

Inputs:

- `meanStress`: steady component of the stress in pascals.
- `alternatingStress`: amplitude of the stress in pascals.
- `ultimateStrength`: ultimate tensile strength Sut in pascals.
- `yieldStrength`: tensile yield strength Sy in pascals.
- `enduranceLimit`: endurance limit Se in pascals. The default is `0.5 x Sut`.
- `criterion`: `soderberg`, `goodman`, `gerber`, `asme`, or `all`. The default is `all`.

The tool always returns all four safety factors as quantities. The headline safety factor follows the selected criterion. For `all`, the headline factor is Soderberg, because it is the most conservative.

The tool estimates the endurance limit when you omit it. It warns that modifying factors still apply. It warns when any safety factor drops below 1.

Example:

```json
{
"meanStress": 400000000,
"alternatingStress": 200000000,
"ultimateStrength": 1200000000,
"yieldStrength": 950000000,
"enduranceLimit": 500000000,
"criterion": "goodman"
}
```

The criteria compare the alternating stress and the mean stress against the endurance limit and the static strengths. This method is for an infinite-life design check.

## unit_convert

Convert a value between two units.
Expand Down
10 changes: 10 additions & 0 deletions examples/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type ToolHandlers = {
shaft_analysis: Handler;
bearing_life: Handler;
von_mises: Handler;
fatigue_analysis: Handler;
unit_convert: Handler;
material_lookup: Handler;
};
Expand All @@ -88,6 +89,7 @@ const tools: NamedHandler[] = [
["shaft_analysis", toolHandlers.shaft_analysis],
["bearing_life", toolHandlers.bearing_life],
["von_mises", toolHandlers.von_mises],
["fatigue_analysis", toolHandlers.fatigue_analysis],
["unit_convert", toolHandlers.unit_convert],
["unit_convert (torque to energy)", toolHandlers.unit_convert],
["material_lookup", toolHandlers.material_lookup],
Expand Down Expand Up @@ -145,6 +147,14 @@ const inputs: Array<Record<string, unknown>> = [
yieldStrength: 355e6,
outputUnits: { vonMisesStress: "MPa", maxShearStress: "MPa" },
},
{
meanStress: 400e6,
alternatingStress: 200e6,
ultimateStrength: 1200e6,
yieldStrength: 950e6,
enduranceLimit: 500e6,
outputUnits: { meanStress: "MPa", alternatingStress: "MPa", enduranceLimit: "MPa" },
},
{
value: 1000,
from: "psi",
Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@engineerkit/engineer-mcp",
"version": "0.2.0",
"version": "0.3.0",
"description": "MCP server for verified mechanical-engineering calculations, references, and material data.",
"type": "module",
"license": "MIT",
Expand Down
173 changes: 173 additions & 0 deletions src/engine/fatigue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import type { Computation, MethodRecord, Quantity } from "../types.js";

export type FatigueCriterion = "soderberg" | "goodman" | "gerber" | "asme";

export type FatigueInput = {
meanStress: number;
alternatingStress: number;
ultimateStrength: number;
yieldStrength: number;
enduranceLimit?: number;
criterion?: FatigueCriterion | "all";
};

export const FATIGUE_METHOD: MethodRecord = {
id: "fatigue-analysis",
name: "Fatigue failure criteria for fluctuating stress",
formula:
"Soderberg: sa/Se + sm/Sy = 1/n. Goodman: sa/Se + sm/Sut = 1/n. Gerber: n.sa/Se + (n.sm/Sut)^2 = 1. ASME-elliptic: (n.sa/Se)^2 + (n.sm/Sy)^2 = 1",
notes:
"Each criterion returns the safety factor for a mean stress and an alternating stress. Soderberg is the most conservative line for ductile materials. Without an endurance limit, the tool estimates it as 0.5 x ultimate strength for steel. Use modifying factors for a precise value.",
referenceIds: ["shigley-2015"],
};

export const ESTIMATED_ENDURANCE_FRACTION = 0.5;

const FACTOR_LABELS: Record<FatigueCriterion, string> = {
soderberg: "Soderberg safety factor",
goodman: "Goodman safety factor",
gerber: "Gerber safety factor",
asme: "ASME-elliptic safety factor",
};

const FACTOR_DESCRIPTIONS: Record<FatigueCriterion, string> = {
soderberg: "Factor for the Soderberg line from Se to Sy. It is the most conservative criterion.",
goodman: "Factor for the modified Goodman line from Se to Sut.",
gerber: "Factor for the Gerber parabola through Se and Sut.",
asme: "Factor for the ASME-elliptic curve through Se and Sy.",
};

export function fatigueSafetyFactor(
criterion: FatigueCriterion,
alternatingStress: number,
meanStress: number,
enduranceLimit: number,
ultimateStrength: number,
yieldStrength: number,
): number {
switch (criterion) {
case "soderberg":
return 1 / (alternatingStress / enduranceLimit + meanStress / yieldStrength);
case "goodman":
return 1 / (alternatingStress / enduranceLimit + meanStress / ultimateStrength);
case "gerber": {
const a = (meanStress / ultimateStrength) ** 2;
const b = alternatingStress / enduranceLimit;
if (a === 0) {
return 1 / b;
}
return (-b + Math.sqrt(b ** 2 + 4 * a)) / (2 * a);
}
case "asme":
return 1 / Math.sqrt((alternatingStress / enduranceLimit) ** 2 + (meanStress / yieldStrength) ** 2);
}
}

const CRITERIA: FatigueCriterion[] = ["soderberg", "goodman", "gerber", "asme"];

function factorQuantity(criterion: FatigueCriterion, value: number): Quantity {
return {
key: `${criterion}Factor`,
label: FACTOR_LABELS[criterion],
value,
unit: "",
description: FACTOR_DESCRIPTIONS[criterion],
};
}

export function analyzeFatigue(input: FatigueInput): Computation {
if (!(input.ultimateStrength > 0)) {
throw new Error("ultimateStrength must be positive.");
}
if (!(input.yieldStrength > 0)) {
throw new Error("yieldStrength must be positive.");
}
if (!(input.meanStress >= 0)) {
throw new Error("meanStress must be zero or positive.");
}
if (!(input.alternatingStress >= 0)) {
throw new Error("alternatingStress must be zero or positive.");
}
if (input.meanStress === 0 && input.alternatingStress === 0) {
throw new Error("Provide a positive meanStress or alternatingStress.");
}
if (input.enduranceLimit !== undefined && !(input.enduranceLimit > 0)) {
throw new Error("enduranceLimit must be positive.");
}

const { meanStress, alternatingStress, ultimateStrength, yieldStrength } = input;
const warnings: string[] = [];

const enduranceLimit = input.enduranceLimit ?? ESTIMATED_ENDURANCE_FRACTION * ultimateStrength;
if (input.enduranceLimit === undefined) {
warnings.push(
"The endurance limit is estimated as 0.5 x ultimate strength. Apply surface, size, load, temperature, and reliability factors for a precise value.",
);
}

const factors: Record<FatigueCriterion, number> = {
soderberg: fatigueSafetyFactor("soderberg", alternatingStress, meanStress, enduranceLimit, ultimateStrength, yieldStrength),
goodman: fatigueSafetyFactor("goodman", alternatingStress, meanStress, enduranceLimit, ultimateStrength, yieldStrength),
gerber: fatigueSafetyFactor("gerber", alternatingStress, meanStress, enduranceLimit, ultimateStrength, yieldStrength),
asme: fatigueSafetyFactor("asme", alternatingStress, meanStress, enduranceLimit, ultimateStrength, yieldStrength),
};

const stressRatio = (meanStress - alternatingStress) / (meanStress + alternatingStress);

const quantities: Quantity[] = [
{
key: "meanStress",
label: "Mean stress",
value: meanStress,
unit: "Pa",
description: "Steady component of the fluctuating stress.",
},
{
key: "alternatingStress",
label: "Alternating stress",
value: alternatingStress,
unit: "Pa",
description: "Amplitude of the fluctuating stress about the mean.",
},
{
key: "stressRatio",
label: "Stress ratio",
value: stressRatio,
unit: "",
description: "Ratio R of the minimum stress to the maximum stress. A fully reversed load gives -1.",
},
{
key: "enduranceLimit",
label: "Endurance limit",
value: enduranceLimit,
unit: "Pa",
description: "Completely reversed stress amplitude for indefinite life at the selected material state.",
},
...CRITERIA.map((criterion) => factorQuantity(criterion, factors[criterion])),
];

const selected = input.criterion ?? "all";
const headline: FatigueCriterion = selected === "all" ? "soderberg" : selected;

const lowest = Math.min(...CRITERIA.map((criterion) => factors[criterion]));
if (lowest < 1) {
warnings.push("The lowest fatigue safety factor is below 1. The design may fail in fatigue.");
}

return {
method: FATIGUE_METHOD,
inputs: {
meanStress,
alternatingStress,
ultimateStrength,
yieldStrength,
enduranceLimit,
criterion: selected,
stressRatio,
},
quantities,
safetyFactor: factorQuantity(headline, factors[headline]),
referenceIds: FATIGUE_METHOD.referenceIds,
warnings,
};
}
Loading
Loading