diff --git a/pyproject.toml b/pyproject.toml index cc57344..4d5a0f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/openstaad_mcp/staad_skills/staad-analysis/SKILL.md b/src/openstaad_mcp/staad_skills/staad-analysis/SKILL.md index 17c0d85..dc81c36 100644 --- a/src/openstaad_mcp/staad_skills/staad-analysis/SKILL.md +++ b/src/openstaad_mcp/staad_skills/staad-analysis/SKILL.md @@ -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 @@ -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 | diff --git a/src/openstaad_mcp/staad_skills/staad-core/SKILL.md b/src/openstaad_mcp/staad_skills/staad-core/SKILL.md index 85aad27..a377abb 100644 --- a/src/openstaad_mcp/staad_skills/staad-core/SKILL.md +++ b/src/openstaad_mcp/staad_skills/staad-core/SKILL.md @@ -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`: diff --git a/src/openstaad_mcp/staad_skills/staad-errors/SKILL.md b/src/openstaad_mcp/staad_skills/staad-errors/SKILL.md index 6321958..7e0c25b 100644 --- a/src/openstaad_mcp/staad_skills/staad-errors/SKILL.md +++ b/src/openstaad_mcp/staad_skills/staad-errors/SKILL.md @@ -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 @@ -49,10 +66,15 @@ 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 | @@ -60,14 +82,29 @@ for s, e in [(1, 2), (2, 3), (3, 99)]: | `-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) diff --git a/src/openstaad_mcp/staad_skills/staad-properties/SKILL.md b/src/openstaad_mcp/staad_skills/staad-properties/SKILL.md index 5b24de0..57207ee 100644 --- a/src/openstaad_mcp/staad_skills/staad-properties/SKILL.md +++ b/src/openstaad_mcp/staad_skills/staad-properties/SKILL.md @@ -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): @@ -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 diff --git a/src/openstaad_mcp/staad_skills/staad-results/SKILL.md b/src/openstaad_mcp/staad_skills/staad-results/SKILL.md index 167c18c..b0f5191 100644 --- a/src/openstaad_mcp/staad_skills/staad-results/SKILL.md +++ b/src/openstaad_mcp/staad_skills/staad-results/SKILL.md @@ -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 diff --git a/src/openstaad_mcp/staad_skills/staad-steel-design/SKILL.md b/src/openstaad_mcp/staad_skills/staad-steel-design/SKILL.md index 5ced15e..3e116b7 100644 --- a/src/openstaad_mcp/staad_skills/staad-steel-design/SKILL.md +++ b/src/openstaad_mcp/staad_skills/staad-steel-design/SKILL.md @@ -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 | @@ -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 | @@ -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 @@ -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` diff --git a/src/openstaad_mcp/staad_skills/staad-steel-design/scripts/aisc360-design.py b/src/openstaad_mcp/staad_skills/staad-steel-design/scripts/aisc360-design.py index 67a3d8d..3643179 100644 --- a/src/openstaad_mcp/staad_skills/staad-steel-design/scripts/aisc360-design.py +++ b/src/openstaad_mcp/staad_skills/staad-steel-design/scripts/aisc360-design.py @@ -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) diff --git a/src/openstaad_mcp/staad_skills/staad-supports/SKILL.md b/src/openstaad_mcp/staad_skills/staad-supports/SKILL.md index 4160e1a..64956a8 100644 --- a/src/openstaad_mcp/staad_skills/staad-supports/SKILL.md +++ b/src/openstaad_mcp/staad_skills/staad-supports/SKILL.md @@ -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.