ls-dyna-mcp
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| LSDYNA_MCP_ROOTS | No | Folders the file-side tools may use (default: the working directory) | |
| LSDYNA_MCP_REDACT | No | Set to 0 to switch off redaction of host, user and licence details in replies | |
| LSDYNA_MCP_MAX_JOBS | No | Standalone jobs run at once (default 1) | |
| LSDYNA_MCP_MAX_NCPU | No | Upper bound on ncpu (default 512) | |
| LSDYNA_MCP_STATE_DIR | No | Where the job table and the remembered Mechanical port are kept | |
| LSDYNA_MCP_ENABLE_SCRIPT | No | Set to 1 to enable run_mechanical_script (arbitrary IronPython) | |
| LSDYNA_MCP_WORKBENCH_ONLY | No | Set to 1 to make submit_lsdyna_job refuse, so runs are driven from Workbench only | |
| LSDYNA_MCP_ENABLE_LSPREPOST | No | Set to 1 to enable the experimental LS-PrePost tools |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| connect_to_mechanicalB | Connect to the Mechanical session that holds your LS-DYNA system. Args: port: gRPC port of the Mechanical session (changes every session) |
| disconnect_from_mechanicalA | Disconnect, leaving Mechanical running. Args: shutdown (True sends Mechanical a forced exit request whoever started it, so unsaved work is lost; a session Workbench owns may restart or linger, so normally close it from Workbench instead), confirm (shutdown=True is refused unless confirm is exactly "SHUT DOWN MECHANICAL") |
| list_lsdyna_analysesA | List every analysis in the Mechanical model, flagging which are LS-DYNA (solver target LSDYNA@LSDYNA, or RestartLSDYNA@LSDYNA for an LS-DYNA Restart system). |
| inspect_objectA | List the public properties of an object in the LS-DYNA analysis tree. Use it to discover real property names before calling set_analysis_settings. Args: path: dotted path below the analysis, e.g. "" (the analysis itself), "AnalysisSettings", "Children[0]"; analysis_index; max_items |
| get_analysis_settingsB | Read the Analysis Settings. Each row has the property name ("Step Controls/Endtime"), the Details-panel caption ("End Time"), the raw value, its type, and whether it is read-only/visible. Enum settings (solver precision, processing type...) come back as integers; -1 is the Program Controlled default. Values are in the model's solver units (End Time here is seconds). Args: analysis_index; group (name prefix, e.g. "Output Controls"); visible_only |
| set_analysis_settingsA | Set Analysis Settings; each value is read back and compared. Each key is attempted independently and reported. A key matches the full property name ("Step Controls/Endtime"), the caption ("End Time") or the last name segment, ignoring case and punctuation; if a caption is ambiguous (e.g. "Time Step Size" exists under Step Controls and EM Step Controls) the reply lists the full names to use instead. Values are set raw, in the model's solver units (End Time is seconds); numeric text such as "5e-4" is parsed (text with a unit in brackets is refused, never silently converted), true/false become 1/0, and plain integer settings take their integer value (read the current one with get_analysis_settings; -1 is Program Controlled). Non-finite values and values outside a sensible range (End Time in (0, 1e6) s, cycle counts > 0, time-step factors in (0, 1]) are rejected before anything is sent. Each value is read back and compared (NaN and Infinity never compare equal): a setting that keeps its old value is reported as 'not applied'. Selection (drop-down) settings such as Hourglass Type, Output Format, Stress File Type or Automatic Mass Scaling were seen to accept only -1 through this interface; set those with keyword snippets (add_keyword_commands, set_dynain_output) or in the GUI. Args: settings_json: JSON object, e.g. {"End Time": 0.0005, "Output Controls/Stress": 1}; analysis_index |
| add_keyword_commandsA | Add a Keyword Snippet (Commands object) holding raw LS-DYNA keyword text, for cards Mechanical has no native object for (e.g. *CONTROL_ENERGY, *DEFINE_CURVE). Leading/trailing blank lines are stripped: trailing blanks are read as empty mandatory cards and break the solve. INCLUDE cards in the text may only name files inside the allowed folders (the solver itself would otherwise open them, e.g. a network path). Args: keywords (must start with a '' card such as *CONTROL_ENERGY; do NOT add *KEYWORD, the snippet goes inside the deck Mechanical writes); analysis_index; scope_path: where to insert it, as a dotted path from the Model, e.g. "Geometry.Children[0].Children[0]" for a body (*MAT, *EOS, *HOURGLASS) or "Connections" (contact types the system lacks). Empty = under the analysis (any keyword). |
| describe_keywordA | List the cards and fields of an LS-DYNA keyword (field name, type, column, width,
default), read from the installed PyDYNA package (pip install ansys-dyna-core; it holds
Ansys-generated definitions of about 3,100 keywords). The name is matched exactly, hyphens
included (MAT_SAMP-1 and MAT_SAMP_1 are different keywords). |
| build_keywordA | Write one keyword card as LS-DYNA text, laid out by PyDYNA's own definition of the keyword
(nothing here is typed from memory). Unknown field names and wrong value types are refused.
Fields you leave out are written with the manual default (PyDYNA writes it explicitly, for
example SPALL 2.0): the reply's |
| write_solver_inputA | Write the LS-DYNA keyword deck Mechanical would solve to output_path, without solving (Analysis.WriteInputFile). If this release lacks it, falls back to a generate-input-only solve and copies input.k. With Split Input File = Yes, the deck includes control.k, mesh.k etc. that stay in the solver working directory. output_path must be a .k/.key/.dyn file inside an allowed folder and must not exist unless overwrite=True. Args: output_path, analysis_index, overwrite |
| solve_analysisA | Solve the LS-DYNA analysis. Mechanical's local solve only runs blocking (it refuses to run in the background), so with wait=False (the default) this server runs the blocking solve on a background thread and returns at once with the solver working_dir: watch the run with check_run_health / tail_solver_file on that folder, and get_solve_status says 'Solving' until it ends. While a solve runs, other Mechanical tools report 'busy'; the file and job tools stay responsive. With wait=True the call itself blocks until the solve ends. Args: analysis_index, wait |
| get_solve_statusA | Solution status, Mechanical messages and the solver working directory. While a solve started by solve_analysis is running, answers 'Solving' without touching Mechanical (which is busy), then returns that solve's result once. Args: analysis_index |
| run_mechanical_scriptA | DISABLED unless the server is started with LSDYNA_MCP_ENABLE_SCRIPT=1. Run a custom IronPython script inside Mechanical (full ACT API) with stock IronPython semantics. Args: script |
| create_lsdyna_analysisA | Add an LS-DYNA analysis (restart=True: an LS-DYNA Restart analysis) to the Mechanical model via Model.AddLSDynaAnalysis / AddLSDynaRestartAnalysis. The Workbench system itself has template ids LSDYNA and RestartLSDYNA (see create_lsdyna_system_snippet). The new analysis is detected as LS-DYNA. It cannot be deleted through the scripting API (null-reference error), so remove it in the GUI if unwanted. Args: restart |
| create_lsdyna_system_snippetB | Workbench journal lines that create an LS-DYNA (or LS-DYNA Restart) system, for execute_workbench_script in the Workbench MCP. Template ids come from the installed LSDYNA ACT extension. Args: restart |
| add_load_or_conditionA | Add a load / boundary / initial condition to the LS-DYNA analysis and scope it. kind (case and spaces ignored): Velocity, InitialVelocity, Acceleration, EarthGravity, FixedSupport, Displacement, RemoteDisplacement, Force, RemoteForce, Moment, Pressure, HydrostaticPressure, LinePressure, RotationalVelocity, RotationalAcceleration, DetonationPoint, ImpedanceBoundary, BoltPretension, ElementBirthAndDeath, NodalForce, NodalDisplacement, FrictionlessSupport, SimplySupported, FixedRotation. properties_json sets properties by name, e.g. {"DefineBy": "Components", "XComponent": "5000 [mm s^-1]"}; each is reported, and unknown names say so (use inspect_object on the new object's path to see what exists). Properties are applied in the order given (put "DefineBy" first). If a property or the scope fails the new object is removed again, so a retry cannot create duplicates (keep_partial=true keeps it). Without a named_selection the object is created unscoped. A Velocity/Displacement overrides Fixed Support on the same body (Users Guide 8). Args: kind, named_selection, properties_json, analysis_index, keep_partial |
| add_resultA | Add a result object under Solution. kind: TotalDeformation, DirectionalDeformation, EquivalentStress, EquivalentPlasticStrain, MaximumPrincipalStress, MinimumPrincipalStress, ShearStress, NormalStress, TotalVelocity, DirectionalVelocity, TotalAcceleration, ForceReaction, Position, UserDefinedResult, EnergyProbe, DeformationProbe, VelocityProbe, ReactionProbe, StressProbe, StrainProbe. For d3max/d3part see set_analysis_settings (Capture Maximum Stress Over Time, Selective Output). Properties are applied in order; a failed property or scope removes the new object again unless keep_partial=true. Args: kind, named_selection, properties_json, analysis_index, keep_partial |
| add_binout_trackerA | Add a Binout Tracker (SolutionInformation.AddLSDYNAGeneralTracker). branch is a binout branch: abstat, bndout, dbfsi, defgeo, deforc, elout, glstat, jntforc, matsum, nodout, rbdout, rcforc, rwforc, secforc, sleout, spcforc, trhist, randles, isoPot. Add trackers BEFORE solving. Branch, then sub-branch, then component are set in that order; a failure removes the tracker again unless keep_partial=true (list_binout_tracker_options shows the valid names). Args: branch, component (e.g. kinetic_energy), subbranch, named_selection, analysis_index, keep_partial |
| list_binout_tracker_optionsA | Which binout branches and components a Binout Tracker accepts in this release (queried from the solver extension, not hard-coded). With no branch: the branches (e.g. glstat, matsum, rcforc, spcforc...). With a branch: that branch's components. A throwaway tracker is created and deleted. Args: branch, analysis_index |
| get_solution_informationC | Members of the Solution Information object (solver output, time step / energy histories) to find what this release exposes. Args: analysis_index |
| list_solver_filesA | List LS-DYNA output files (d3plot, d3hsp, glstat, binout...) in a solver directory. Args: directory (the working_dir from get_solve_status) |
| tail_solver_fileA | Last lines of a solver text file plus termination status. The default name (empty, or the
bare d3hsp / messag) picks the NEWEST d3hsp[nn] / messag[nn], i.e. the file the latest run or
restart is writing; the reply's |
| check_run_healthA | Judge a run from glstat (or binout) and d3hsp. Checks termination, energy balance,
hourglass energy and negative sliding energy at their worst point in the run and, for
quasi-static runs, kinetic energy. The reply has a |
| read_glstatB | Energy history from glstat, thinned to at most max_states evenly spaced states. Args: directory, max_states |
| scan_keyword_fileA | Summarise a .k/.key file: keyword counts and missing essentials (termination, d3plot output, timestep control). Follows *INCLUDE, so a Split Input File deck (input.k pulling in database.k, control.k, mesh.k, component.k, contact.k) is scanned as a whole. Args: path, follow_includes |
| audit_deckA | Read-only audit of an LS-DYNA keyword deck before it is solved (plain text parsing, nothing
is executed). Reports the cards present by family, duplicate ids (MID, PID, SECID, EOSID, HGID,
LCID/TBID), references to ids that do not exist (PART to SECID/MID/EOSID/HGID, curves), first-card
format problems, whether MAT/PART cards look Workbench-written or snippet-written, a guess of the
unit system, the end time against the output intervals, and a table of known Workbench defaults
(for example *MAT_JOHNSON_COOK SPALL 0, which LS-DYNA treats as 2) each with the place in the
LS-DYNA keyword manual it was read at, or marked unverified. Follows *INCLUDE only inside the
allowed folders. Returns |
| compare_decksA | Read-only field-by-field comparison of two keyword decks, for example a standalone reference
against the deck Workbench wrote. Cards are matched by keyword and id (MID, PID...), not order;
card name case, comma versus fixed format, number formatting (1.0e3 equals 1000) and *PARAMETER
values are normalised. Fields of the common cards carry the manual's field names (MAT_JOHNSON_COOK,
MAT_ELASTIC, MAT_PLASTIC_KINEMATIC, EOS_GRUNEISEN, SECTION_SOLID/SPH/SHELL, CONTROL_TERMINATION/
SPH/HOURGLASS/TIMESTEP/CONTACT/ENERGY, DATABASE_BINARY_D3PLOT, INITIAL_VELOCITY, HOURGLASS); other
cards use "field N". Status per row: equal, differs, only-in-reference, only-in-candidate,
default-vs-explicit (one side blank and the other the manual default); |
| scan_solver_messagesA | Count errors, warnings and implicit step retries in d3hsp, messag or mes0* files:
'*** Error nnnnn', 'FATAL ERROR', 'Nonlinear solver failed to find equilibrium',
'automatic time step size DECREASE, RETRY step'. The default name (empty, or the bare d3hsp /
messag) picks the newest d3hsp[nn] / messag[nn]; |
| send_sense_switchA | Steer a running LS-DYNA job by writing a sense-switch file (d3kil) in its run directory. sw1 stops and writes d3dump (continue later with a simple restart); sw3 writes restart files and keeps running; sw4 writes a plot state; swb/swc/swd write dynain (swd also terminates); sw2 prints status; swa flushes buffers. For implicit runs: conv (force the step to converge, troubleshooting only), ttrm (retry the step with a smaller increment), rtrm (stop at the end of the step), lpri / nlpr / iter (toggle solver output). LS-DYNA reads and deletes d3kil while running; a switch sent to a run that already finished is refused. A reply of ok means the file was written, not that the solver has acted on it yet. Args: directory (the solver working_dir), switch |
| check_restart_setupA | Validate a Simple / Small / Full restart before solving: the documented rules (e.g. a
small restart is impossible for a serial single-precision solution; a small restart needs a
later end time) and, if |
| list_restart_filesB | Restart files a run left in its folder: d3dump* (simple/small restart), d3full* (full restart) and dynain / dynain.lsda (initial state for a follow-on analysis), with sizes and times. Args: directory |
| list_solver_versionsA | List the LS-DYNA executables Workbench knows about (lsdyna_solvers.xml: version, SMP/MPP, MPI type, precision, path). Default location is %APPDATA%/Ansys//ACTLSDYNA/lsdyna_solvers.xml. An xml_path must be inside an allowed folder. Args: xml_path (optional) |
| read_binoutA | Browse and read binout (LSDA) results, where Workbench keeps glstat, matsum, rcforc, spcforc, sleout, nodout... Call with no branch to list branches, then branch to list sub-branches/variables, then branch+subbranch+variable for the data (thinned to max_points). Needs: pip install "ls-dyna-mcp[binout]". Args: directory, branch, subbranch, variable, max_points |
| read_history_fileA | Read an ASCII time-history file (matsum, rcforc, sleout, spcforc, rwforc, nodout). Returns
the record count, a min/max/last summary per part/interface/node/wall from ALL records, and
|
| read_d3plotA | Field results from the d3plot: where and when a quantity peaks. With no field it lists the
fields stored. With a field (node_displacement, node_velocity, element_solid_stress,
element_solid_effective_plastic_strain, element_shell_stress...) it returns, for up to
max_states evenly spread states (or the single |
| read_implicit_progressA | Implicit (statics / dynamics) solver progress from messag: per time step the iterations to
converge, stiffness reformations, the last residual norms and whether the step converged, plus
totals, the count of recoverable DIVERGENCE warnings and any lines about non-convergence or
step-size cuts. Use it to judge a Workbench Transient/Static LS-DYNA run that is slow or stuck.
Args: directory, name (default: the newest messag[nn], else mes0000; or the captured screen log
lsrun.stdout.txt), max_steps (the last N steps are returned in |
| set_dynain_outputA | Make the run write a dynain (final deformed shape and stress state) in the format you can use, by adding a *INTERFACE_SPRINGBACK_LSDYNA keyword snippet. Checked on the real solver: FTYPE 0 writes an ASCII dynain (which prepare_dynain can turn into an initial-state deck), 1 writes dynain.bin, 2 both, 3 dynain.lsda (the Workbench default, not convertible here). The Workbench setting 'Stress File for flexible parts' cannot choose ASCII through scripting, so leave it off (0) or the deck gets two springback cards. Args: psid (the part set to write; must exist unless part_ids_json is given), fmt (ascii | binary | both | lsda), part_ids_json (JSON list of part ids: a *SET_PART_LIST with this psid is added), analysis_index |
| search_lsdyna_helpA | Search the LS-DYNA user guide (HTML help) that Ansys installs, for example to find the options of a setting such as Hourglass Type. Only that installed help is read, nothing is written. A page matches when its text contains every word of the query (case-insensitive); hits are ranked by the number of matches and by words in the page title. Each hit has the file name, the page title and a short excerpt. The help gives option names and descriptions, not the integer values behind a setting; after setting a value, call get_analysis_settings to see what is stored. The help is an optional Ansys install component. Args: query, max_results (1-20), help_dir (optional folder with the .html pages; must be inside an allowed folder; default: the help of the installed release, or LSDYNA_MCP_HELP_DIR) |
| list_scoping_targetsB | What loads and results can be scoped to: the named selections (name, kind, entity count), the parts and bodies of the geometry (name and geometry id, to use with the location options of add_load_or_condition), and the objects already in the analysis. Args: analysis_index, max_items |
| list_addable_objectsA | The Add* methods the analysis (loads, supports, conditions), its Solution (results, probes)
or its Solution Information (Binout trackers: AddLSDYNAGeneralTracker...) offers in this
release, so a kind can be found without guessing. host "solution" also lists the Solution
Information methods (in |
| delete_objectA | Delete one object this server (or the user) added to the analysis: a load, support, result, tracker or keyword snippet. Only these paths are accepted: "Children[n]" (loads, supports, snippets), "Solution.Children[n]" (results) and "Solution.SolutionInformation.Children[n]" (trackers). The object's current name must equal expected_name, so a stale index (indexes shift when objects are added or removed) deletes nothing. Analyses, Analysis Settings, Solution, Solution Information, Initial Conditions and anything outside the analysis (named selections, mesh, contacts) are refused. Look first with list_scoping_targets or inspect_object. Args: path, expected_name, analysis_index |
| submit_lsdyna_jobA | Run LS-DYNA on a .k file outside Workbench (what LS-Run does). Only the solvers listed in Workbench's lsdyna_solvers.xml can be run, and the input must be inside an allowed folder. Jobs queue (max concurrent: env LSDYNA_MCP_MAX_JOBS, default 1; one job per folder); output goes to lsrun.stdout.txt in the input's folder. SMP: lsdyna_*.exe i=... ncpu=...; a NEGATIVE ncpu gives reproducible results at up to 15% more CPU time. MPP: mpiexec -np N (IntelMPI uses -localonly). Needs the Ansys LS-DYNA license (LSTC_LICENSE=ANSYS) and, beyond one core, HPC licenses. Args: input_file; ncpu (non-zero, at most 512 in magnitude); process (SMP|MPP); precision (single|double); mpi (IntelMPI|MSMPI); memory (words, e.g. 200m; empty = solver default); depends_on (job id to wait on for normal termination, -1 = last submitted); version; extra_args_json (JSON list of key=value strings; only case, memory2, jobid are accepted, e.g. ["case=1"]); dry_run (build the command, don't run); restart_file (name of a restart file such as d3dump01 or d3full01 in the input's folder: the run is started with r=; check_restart_setup says whether a restart is possible and what the input must contain) |
| list_lsdyna_jobsA | All jobs submitted in this server session with status (pending, running, done, failed, stopped, cancelled) and termination (normal/error/unknown from d3hsp). |
| get_lsdyna_jobC | One job plus the tail of its lsrun.stdout.txt. Args: job_id, tail_lines |
| stop_lsdyna_jobA | Cancel a pending job. For a running one, send sw1 (it stops and writes d3dump, so it can be restarted) or, with force=True, terminate the process. Args: job_id, force |
| run_lsprepostA | DISABLED unless the server is started with LSDYNA_MCP_ENABLE_LSPREPOST=1. Run an LS-PrePost command file (post-processing automation) in working_dir (must be an allowed folder). headless=True uses 'runc=' (no graphics: only non-graphical commands work); False uses 'c= -nographics'. The call runs on a worker thread with a hard timeout (at most 900 s) that kills the whole process tree. Files created are listed. Args: commands (cfile text, at most 20000 characters), working_dir, headless, timeout_s |
| summarize_d3plotA | EXPERIMENTAL and DISABLED unless LSDYNA_MCP_ENABLE_LSPREPOST=1. Have LS-PrePost load a d3plot and write its lspost.msg summary (s=). In testing LS-PrePost 4.13 crashed (0xC0000005) loading a d3plot without a GUI, and the reply then says so. Prefer read_binout / check_run_health. Args: d3plot_path, timeout_s |
| describe_workbench_materialA | Which *MAT/*EOS cards Workbench can write, and how each field maps to Engineering Data (from the installed LSDYNASolverRepresentation.xml). query is matched against the card name, e.g. "JOHNSON_COOK" or "*MAT_ELASTIC". An xml_path must be inside an allowed folder. Args: query, xml_path (optional) |
| prepare_dynainA | Explicit-to-implicit sequential solution, step 2: copy the solver's ASCII |
| write_and_audit_deckA | Write the deck Workbench would solve (the same Analysis.WriteInputFile path as
write_solver_input, no solve) into an allowed folder, audit it (audit_deck) and, with
reference_path, compare it with the reference deck (compare_decks). |
| get_run_progressA | Progress of a running or finished solve, read-only (never touches or kills a process). From
messag / mes0nnnn (MPP), restart messag01 / mes010000, d3hsp and lsrun.stdout.txt: latest time,
cycle and time step, percent of the end time, the solver's own estimate lines when it printed
them (estimated total / to-complete cpu and clock time, percent complete), our own rate and ETA
from successive calls (observations are kept in the per-user state folder, not in the run
folder, so call it again a minute or more later), hang hints (no solver process, newest output
file older than stale_minutes, d3plot not advancing, simulation time unchanged between calls)
and the solver's error block (Error nnnnn, termination due to energy, forrtl) from the tail.
|
| compare_early_resultsA | Early-time gate: compare a scalar history of a short run with a reference CSV (time,value) before committing to the long run. quantity: 'glstat:kinetic_energy' (glstat, or the binout glstat) or '::' for matsum, rcforc, sleout, spcforc, rwforc, nodout (for example 'matsum:1:kinen', 'nodout:5:x_vel'). The reference is interpolated at the simulation's output times inside [t_min, t_max] and multiplied by reference_scale (unit conversion; times must already be in the model's unit). Returns points compared, max and RMS relative error (%), the worst point, the first time the error exceeds tolerance_pct and a verdict within_tolerance / exceeds. A mismatch means diagnose the model (audit_deck, compare_decks): never tune parameters to match an experiment. No solver is run. Args: working_dir, reference_csv, quantity, t_min, t_max, tolerance_pct, reference_scale |
| presolve_checkA | Check before a solve, read-only (nothing is deleted or started). Reports free RAM and free
disk, a memory estimate (the solver's own 'Memory required' report in a previous d3hsp of the
folder when there is one; otherwise a rough heuristic from the node and element counts of the
deck, uncertain by a factor of about 3 either way), the LS-DYNA result files in the folder that
a new run would overwrite (names only), ncpu against the physical and logical cores, and the
unit-system hint and error findings of audit_deck. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 53 tools
Tool descriptions are unusually detailed and draw real boundaries, but several clusters overlap heavily: get_solve_status / get_run_progress / check_run_health / read_implicit_progress all answer 'how is my run doing', and read_glstat / read_binout / read_history_file / read_d3plot all read results. Likewise deck tools (scan_keyword_file, audit_deck, compare_decks, write_solver_input, write_and_audit_deck) require careful reading to pick correctly. An agent can usually choose well, but only by parsing long descriptions rather than from names alone.
The dominant pattern is verb_noun in snake_case (list_lsdyna_analyses, get_analysis_settings, add_result, delete_object, submit_lsdyna_job), applied consistently to prefixes like list_/get_/set_/add_/read_. A handful deviate into noun-ish or adjective forms (audit_deck, presolve_check, build_keyword, describe_keyword, tail_solver_file), but these remain readable and unambiguous in style.
53 tools is well above the 'heavy' threshold, even for a broad domain spanning mechanical connection, settings, loads, results, keyword authoring, deck auditing, solving, job control and post-processing. Several tools could be merged (multiple run-status/progress readers, two deck-writing entry points, four separate result-file readers), so the surface feels accumulated rather than scoped.
The lifecycle is covered end to end: connect/disconnect, create analyses and systems, inspect and set analysis settings, add loads/results/trackers, author and audit keyword decks, write and submit decks, watch and steer running jobs, restart, and read back every major result format. Even edge paths (restart validation, dynain conversion, sense switches, version discovery) have dedicated tools, with disabled features documented rather than silently absent.