Skip to content
Merged
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies = [
"starlette>=1.0.0,<2.0.0",
"pydantic>=2,<3",
"pywin32>=311; platform_system == 'Windows'",
"openstaadpy @ git+https://github.com/BentleySystems/openstaadpy.git@306b77ba6a4bed68fd91f21df19c9fd5fc9fb2e1",
"openstaadpy @ https://github.com/BentleySystems/openstaadpy/releases/download/v26.0.0.62/openstaadpy-26.0.0.62-py3-none-any.whl",
"openpyxl>=3.1,<4",
"defusedxml>=0.7",
"chardet>=5,<8",
Expand Down
20 changes: 19 additions & 1 deletion src/openstaad_mcp/staad_skills/staad-analysis/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: staad-analysis
description: 'Use when running structural analysis, solving the model, or executing the STAAD.Pro solver. Covers: PerformAnalysis (adds PERFORM ANALYSIS command — call once only), AnalyzeModel (linear static solver — requires SaveModel first), AnalyzeEx (analysis + design in one call — use for design workflows), P-Delta analysis (PerformPDeltaAnalysisEx), buckling analysis (PerformBucklingAnalysis/Ex), cable analysis, direct analysis (AISC), nonlinear analysis (PerformNonlinearAnalysisEx), print options, DeleteAllAnalysisCommands, CreateSteelDesignCommand. Two steps required for static analysis. Requires staad-core.'
description: 'Use when running structural analysis, solving the model, or executing the STAAD.Pro solver. Covers: PerformAnalysis (adds PERFORM ANALYSIS command — call once only), AnalyzeModel (linear static solver — requires SaveModel first), AnalyzeEx (analysis + design in one call — use for design workflows), GetAnalysisErrorMessages / GetAnalysisWarningMessages (STAAD.Pro v26+), GetAnalysisStatus, P-Delta analysis (PerformPDeltaAnalysisEx), buckling analysis (PerformBucklingAnalysis/Ex), cable analysis, direct analysis (AISC), nonlinear analysis (PerformNonlinearAnalysisEx), print options, DeleteAllAnalysisCommands, CreateSteelDesignCommand. Two steps required for static analysis. Requires staad-core.'
---

# STAAD.Pro Analysis
Expand All @@ -26,6 +26,24 @@ staad.SetSilentMode(False)
# status: 2=OK, 3=warnings, 4=errors, -1=terminated
```

### Analysis Messages *(Requires STAAD.Pro v26+)*

After a run, retrieve the solver's error/warning text. These COM functions exist
only on **STAAD.Pro v26+** — confirm the connected instance's version from
`list_instances` / `get_status` before calling (see staad-core → Version
Compatibility). On older STAAD they raise an "update STAAD.Pro" error.

These methods live on the root `staad` object.

```python
errors = staad.GetAnalysisErrorMessages() # solver error messages
warnings = staad.GetAnalysisWarningMessages() # solver warning messages
```

`GetAnalysisStatus()` raises an exception when the run returned an error
(negative) status code — `execute_code` reports it; no `try/except` needed unless
you want to continue after a failure.

### Print Options

| Value | Output |
Expand Down
28 changes: 28 additions & 0 deletions src/openstaad_mcp/staad_skills/staad-core/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,34 @@ Never guess or invent function names — only use names from the skill documenta
- Call `get_status(instance)` to verify a specific instance is reachable
- Pass `instance` (alias like `staadPro1`) to `execute_code` when multiple instances are running

### Version Compatibility

The MCP is built against a single bundled **openstaadpy** wrapper, identical for
every connected instance — so the wrapper is **never** something to gate on. What
*does* vary is the **STAAD.Pro version** of each running instance. Two consequences:

- **Wrapper behavior is uniform.** Several `Assign*` methods return `bool` and
**raise on failure** instead of returning a negative `int` code (see
staad-steel-design, staad-properties, staad-supports). Use `try/except`; do not
check `if result < 0` for those methods.
- **Some COM functions require STAAD.Pro v26+** (e.g. `GetAnalysisErrorMessages`,
`GetAnalysisWarningMessages`). On an older connected STAAD they do not exist and
raise a clear "update STAAD.Pro" error.

**Gate STAAD-v26-only functions using the version you already have** — do NOT add a
check inside the script:

1. Call `list_instances` → each row includes a `version` field; or
`get_status(instance)` → returns `staad_version`. This is the **STAAD.Pro**
version, not the wrapper version.
2. If the instance is **STAAD.Pro v26+**, compose the `execute_code` call using the
v26-only function.
3. If it is **older**, use the legacy path or tell the user the feature needs
STAAD.Pro v26.

Functions marked *(Requires STAAD.Pro v26+)* in the skills need a connected STAAD of
that version or newer.

### Units & Axis

- Before any modeling operation, query units via `execute_code`:
Expand Down
71 changes: 54 additions & 17 deletions src/openstaad_mcp/staad_skills/staad-errors/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
---
name: staad-errors
description: 'Use when handling errors from OpenSTAAD operations, interpreting negative return codes, or writing robust error-handling patterns. Covers: common error code groups (general, file, node, beam, plate, solid, property, group, load, results), try/except patterns for COM exceptions, checking return values. NOTE: In the MCP sandbox import is blocked — catch generic Exception, not typed oserrors classes.'
description: 'Use when handling errors from OpenSTAAD operations, interpreting negative return codes, or writing robust error-handling patterns. Covers: execute_code reports uncaught exceptions automatically (no try/except needed just to report), common error code groups (general, file, node, beam, plate, solid, property, group, load, results), when to use try/except for control flow (loop-skip, fallback), checking getter return values. NOTE: In the MCP sandbox import is blocked — catch generic Exception, not typed oserrors classes.'
---

# STAAD.Pro Error Handling

## Errors Are Reported Automatically
`execute_code` wraps your whole script in a top-level handler: any uncaught
exception is captured and returned as `success: false` with the (sanitized) error
message plus whatever you printed before it. **You do NOT need `try/except` just to
report a failure** — let it propagate and read the error from the tool result.

Use `try/except` inside a script only when it changes **control flow**, e.g.:
- skipping a bad item inside a bulk loop so the rest keep processing
- providing a fallback value (e.g. version-gated features) and continuing

## Sandbox Limitation
In the MCP sandbox, `import` is blocked. You cannot import typed exception classes from `openstaadpy.os_analytical.oserrors`. Instead, catch generic `Exception` and inspect the message or code.

## Basic Pattern
```python
geo = staad.Geometry
## Clear COM Errors
The openstaadpy wrapper wraps COM objects in a proxy layer that turns cryptic COM
failures into actionable messages. If a COM method is missing or fails, the error
typically tells you to **update STAAD.Pro to the latest version** — this usually
means the *connected* STAAD instance is older than the function requires (see
staad-core → Version Compatibility), not a coding mistake.

## Return Contracts

try:
beam_no = geo.AddBeam(start_node, end_node)
print(f"Added beam {beam_no}")
except Exception as e:
print(f"Error adding beam: {e}")
**Many methods raise on failure** — call them directly and let `execute_code`
surface any error. This applies to the `Assign*` methods and the support
create/assign/query/delete methods, which return `True` on success and raise on
failure:
```python
prop.AssignBeamProperty(beam_ids, prop_id) # True on success; raises on failure
```

## Checking Return Values
Many methods return negative integers on error instead of raising exceptions:
Some **query/getter** methods still return negative integers on error instead of
raising — for those, check the sign:
```python
result = prop.AssignBeamProperty([99], prop_id)
if isinstance(result, int) and result < 0:
print(f"Failed with error code: {result}")
shape = prop.GetShapeCode(country, name)
if isinstance(shape, int) and shape < 0:
print(f"Section not found (code {shape})")
```

## Robust Model Building Pattern
Catch per-item inside a loop so one bad entity doesn't abort the whole batch:
```python
geo = staad.Geometry

Expand All @@ -49,25 +66,45 @@ for s, e in [(1, 2), (2, 3), (3, 99)]:
| Range | Category | Examples |
|-------|----------|----------|
| `-1` | General error | Generic failure |
| `-2` | Invalid model path | `OsInvalidModelPath` |
| `-100` to `-125` | Argument errors | Invalid argument, out of range |
| `-101` | Model not opened | `OsModelNotOpened` |
| `-114` | OLE exception | `OsOleException` |
| `-115` | License not supported | OpenSTAAD Professional functions unavailable |
| `-1003` | File error | File not found / access denied |
| `-2001` to `-2006` | Node errors | Node not found, duplicate node |
| `-3001` to `-3005` | Beam errors | Beam not found, invalid incidence |
| `-3006` | Invalid member number | `OsInvalidMemberNo` |
| `-4001` to `-4009` | Plate errors | Plate not found, invalid node count |
| `-5001` to `-5005` | Solid errors | Solid not found |
| `-6001` to `-6045` | Property errors | Profile not found, invalid property |
| `-7001` | Group error | Group not found |
| `-8001` to `-8041` | Load errors | Load case not found, create failed |
| `-9004`, `-9911` | Results errors | Results not available |

### Named Error Classes

These surface as raised exceptions (the message names the condition). In the
sandbox, catch generic `Exception` and read the message:

| Code | Class | Meaning |
|------|-------|---------|
| `-2` | `OsInvalidModelPath` | Invalid model path |
| `-101` | `OsModelNotOpened` | STAAD model is not opened |
| `-114` | `OsOleException` | OLE exception occurred |
| `-115` | `OsLicenseNotSupported` | License lacks OpenSTAAD Professional functions |
| `-3006` | `OsInvalidMemberNo` | Invalid member number ID(s) |

## Tips
- Always check `out.AreResultsAvailable()` before querying results
- Always check `AssignDesignCommand` return value (0 = success)
- Wrap `AnalyzeEx` / `AnalyzeModel` calls in try/except — analysis can fail
- Let failures propagate — `execute_code` returns the error; only add `try/except` for loop-skip or fallback control flow
- Print intermediate values (node IDs, beam IDs, property IDs) to diagnose failures
- If a function returns -999, the operation was not performed (e.g., member not designed)

## Gotchas
- In standalone Python, you can `from openstaadpy.os_analytical.oserrors import OsBeamNotFound` — but NOT in the MCP sandbox
- `execute_code` already catches uncaught exceptions — do NOT wrap a single call in `try/except` just to `print` the error
- Some methods silently return 0 even when something went wrong (e.g., `UpdateStructure` on read-only paths)
- Negative return codes are integers, not exceptions — always check `if result < 0`
- The `Assign*` methods (`AssignBeamProperty`, `AssignDesignCommand`, `AssignDesignParameter`, `AssignDesignGroup`) and support create/assign/query/delete methods **raise on failure** and return `True` on success — do NOT check `if result < 0` for these
- Negative return codes still apply to many **getter** methods — always check `if result < 0` for those
- A "update STAAD.Pro" error usually means the connected instance is older than the called function requires (see staad-core → Version Compatibility)
3 changes: 2 additions & 1 deletion src/openstaad_mcp/staad_skills/staad-properties/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ description: "Use when assigning section profiles to beams, plate thickness, mat

```python
prop_id = prop.CreateBeamPropertyFromTable(countryCode, sectionName, typeSpec, v1, v2)
prop.AssignBeamProperty(beam_ids, prop_id)
prop.AssignBeamProperty(beam_ids, prop_id) # returns True on success, raises on failure
```

**Country codes** (full table in the Reference section — PROPERTY_CODES.md):
Expand Down Expand Up @@ -158,6 +158,7 @@ inact_id = prop.CreateMemberInactiveSpec()
## Gotchas

- `CreatePlateThicknessProperty` takes a **list of 4 floats**, one value per corner — not a single scalar
- `AssignBeamProperty` returns `True` on success and **raises on failure** — call it directly; do NOT check `if result < 0` (`execute_code` reports any uncaught error)
- Always retrieve actual IDs via `GetBeamList()` / `GetPlateList()` before assigning — never assume IDs start at 1
- `GetMemberDesignSectionName(bid)` raises an error when results are unavailable — use `GetSectionPropertyName` for pre-analysis lookup
- Built-in material names: `"STEEL"`, `"CONCRETE"`, `"ALUMINUM"` — case-sensitive
7 changes: 7 additions & 0 deletions src/openstaad_mcp/staad_skills/staad-results/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ out.GetOutputUnitForStress() # e.g. "KSI"
out.GetOutputUnitForDimension()
out.GetOutputUnitForRotation()
```
The `GetOutputUnitFor*` methods raise on error (e.g. if the model has no output
units established yet) — `execute_code` reports any such error.

### Analysis Messages

For the solver's error/warning text after a run (`GetAnalysisErrorMessages` /
`GetAnalysisWarningMessages`), see the staad-analysis skill.

### Node Results

Expand Down
10 changes: 7 additions & 3 deletions src/openstaad_mcp/staad_skills/staad-steel-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ brief_ref = design.CreateDesignBrief(1067) # AISC 360-16

**Step 2 — Assign design commands to members**
```python
result = design.AssignDesignCommand(brief_ref, 'CHECK CODE', '', beam_list)
# Returns 0 on success — always print and check
design.AssignDesignCommand(brief_ref, 'CHECK CODE', '', beam_list)
# Returns True on success and raises on failure (execute_code reports any error)
```

| Command | Description |
Expand Down Expand Up @@ -73,6 +73,7 @@ min_r = out.GetMemberSteelDesignMinFailureRatio()
Assign parameters before running analysis:
```python
design.AssignDesignParameter(brief_ref, paramName, paramValue, member_ids)
# Returns True on success and raises on failure
```

| Parameter | Description | Example |
Expand All @@ -94,6 +95,7 @@ design.AssignDesignParameter(brief_ref, paramName, paramValue, member_ids)
Group members to use the same section during optimization:
```python
design.AssignDesignGroup(brief_ref, 'scSteelGroup', 'ColumnGroup', sameAsMember=1, member_ids=[1,2,3])
# Returns True on success and raises on failure
```

### Querying Design Parameters
Expand All @@ -117,7 +119,9 @@ See [aisc360-design.py](./scripts/aisc360-design.py) for a complete working exam
## Gotchas
- Use `AnalyzeEx(1, 0, 1)` not AnalyzeModel — only `AnalyzeEx` triggers design
- `GetSteelDesignParameterBlockCount()` returns `0` until `AnalyzeEx` completes
- `AssignDesignCommand` returns non-zero on failure — always check the return value
- `AssignDesignCommand`, `AssignDesignParameter`, and `AssignDesignGroup` return `True` on success and **raise on failure** — call them directly (do NOT check for a non-zero return code); `execute_code` reports any uncaught error
- `CreateDesignBrief` validates the design code and raises on an invalid code
- `GetMemberDesignParameters` validates its arguments and raises on error
- `GetMemberSteelDesignResults` raises an error for members not assigned `CHECK CODE`
- Design section in results may differ from table section if the optimizer re-selected
- Parameter values are passed as **strings** to `AssignDesignParameter`
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
brief_ref = design.CreateDesignBrief(1067)
print(f'Design brief ref: {brief_ref}')

# Step 2: Assign CHECK CODE to all members — returns 0 on success
result = design.AssignDesignCommand(brief_ref, 'CHECK CODE', '', beam_list)
print(f'AssignDesignCommand result: {result}') # non-zero = failure
# Step 2: Assign CHECK CODE to all members — returns True on success, raises on failure
design.AssignDesignCommand(brief_ref, 'CHECK CODE', '', beam_list)
print('AssignDesignCommand: OK')

# Step 3: Save to persist design commands
staad.SaveModel(True)
Expand Down
1 change: 1 addition & 0 deletions src/openstaad_mcp/staad_skills/staad-supports/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ See [assign-fixed-supports.py](./scripts/assign-fixed-supports.py) for a complet
## Gotchas

- `AssignSupportToNode` takes a SINGLE node ID — it does NOT accept a list; iterate with a loop
- Support methods (`CreateSupportFixed`, `CreateSupportPinned`, `CreateSupportFixedBut`, `AssignSupportToNode`, `GetSupportNodes`, `GetSupportType`, `GetSupportInformation`, `DeleteSupport`) **raise on failure** instead of returning a negative code — call them directly; `execute_code` reports any uncaught error
- When nodes were added in-memory in the same script, call `SaveModel(True)` before assigning supports — do NOT use `UpdateStructure()` (it discards unsaved geometry)
- For `CreateSupportFixedBut`: use `-1` for spring DOFs (not `1`); `1` = released, `0` = fixed, `-1` = spring
- **Compression-only supports (`springType=1`) are only compatible with plain linear static analysis** — using them with P-Delta, Nonlinear, Buckling, or Cable analysis causes an engine error. The engine uses spring deactivation iterations that cannot coexist with those solver modes.