Mechanical Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Mechanical Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A Model Context Protocol (MCP) server that connects any MCP-compatible AI client to ANSYS Mechanical via PyMechanical gRPC. Talk to your simulation in plain English.
The repo also ships a second server, `ansys-acp-mcp`, for building composite layups in ANSYS Composite PrepPost (ACP) through PyACP. See ACP server.
https://github.com/user-attachments/assets/309952dc-dce0-4aa6-9b06-e8047f32c2de
Works with any MCP-compatible client, including:
| Category | Tools |
|---|---|
| Connection | connect_to_mechanical, check_mechanical_connection, disconnect_from_mechanical (one session on this machine at a time) |
| Model | get_model_info, list_materials, assign_material, suppress_bodies, set_2d_behavior |
| Mesh | set_mesh_element_size, set_body_mesh_size, generate_mesh, get_mesh_statistics |
| Connections | add_contact, set_contact_type, list_contact_regions |
| Loads and supports | add_fixed_support, add_frictionless_support, add_displacement, add_remote_displacement, add_force, add_remote_force, add_pressure, add_moment, add_standard_gravity, list_boundary_conditions |
| Analysis settings | set_nonlinear_settings |
| Solve | solve_analysis (blocking or background), get_solve_status |
| Results | add_total_deformation, add_total_deformation_all_modes, add_directional_deformation, add_equivalent_stress, add_normal_stress, add_principal_stress, add_stress_intensity, add_stress_tool, add_reaction_force, list_results, get_result_values, get_modal_frequencies |
| Post-processing | export_nodal_results, linearize_path (needs the postproc extra) |
| Point mass | convert_part_to_point_mass, convert_prefix_to_point_mass, list_point_masses |
| Named selections | create_named_selection, list_named_selections, delete_named_selection |
| Reports | generate_report (TXT, or DOCX with the report extra) |
| Scripting | run_mechanical_script - off by default, see Security |
Every tool that changes the model names the analysis it used (analysis: index, name, type). With several analyses in the model, analysis_index or analysis_name must be given (index 0 is often not the one meant); expect_analysis_type refuses a mismatch. Set MECHANICAL_MCP_REQUIRE_EXPLICIT_ANALYSIS=0 to restore the old default of index 0.
ok: true meansEvery tool answers with a JSON object that has an ok key. For tools that create something in the model, ok: true means the object was created, scoped and read back - it does not mean the result is valid. A result added before the analysis is solved comes back with max: null, min: null, solved: false and a note (Mechanical itself would report 0). If anything fails after an object was created, the object is deleted again and the response says rolled_back: true.
lack API members used here (the gravity and reaction-force calls differ between releases) and are untested.
ansys-mechanical-core, mcp[cli]>=1,<2git clone https://github.com/codersag/mechanical-mcp.git
cd mechanical-mcp
pip install -e .Optional extras:
pip install -e ".[report]" # python-docx: generate_report(fmt="docx")
pip install -e ".[postproc]" # numpy + scipy: linearize_pathAdd the following to your MCP client's config file. The config file location varies by client:
| Client | Config file location |
|---|---|
| Claude Desktop (Windows) | %APPDATA%\Claude\claude_desktop_config.json |
| Claude Desktop (Mac) | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Cursor | .cursor/mcp.json in your project, or ~/.cursor/mcp.json globally |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
{
"mcpServers": {
"mechanical-mcp": {
"command": "ansys-mechanical-mcp"
}
}
}Or if running from source:
{
"mcpServers": {
"mechanical-mcp": {
"command": "python",
"args": ["C:/path/to/mechanical-mcp/mechanical_mcp_server.py"]
}
}
}Restart your client after editing the config.
Mechanical exposes a gRPC server whose port you pass to connect_to_mechanical.
In Workbench (Tools → Scripting → Run Script):
port = GetSystem("SYS").GetContainer(ComponentName="Model").StartGrpcServer()
print(port)If you have multiple Mechanical systems open (e.g. Modal + Static Structural), run the script for each system by replacing "SYS" with the system name (e.g. "SYS 1", "SYS 2").
"Connect to Mechanical on port 10000"
Then interact naturally:
"Assign Structural Steel to the body called Plate"
"Generate a mesh with 50mm element size"
"Add a fixed support to the named selection called Base"
"Solve the analysis and get the maximum von Mises stress"
"Convert part Equipment1 to a point mass"
The server holds one connection. Calling connect_to_mechanical with another port replaces it, but only once the new session answers (a dead or wrong port leaves the working connection alone). Only sessions on this machine are supported: the port is probed on 127.0.0.1 first so a dead port fails fast, and a port that accepts connections but never speaks gRPC is detected by a short protocol probe instead of hanging the handshake. The last port is remembered in a per-user folder (%LOCALAPPDATA%\mechanical-mcp, override with MECHANICAL_MCP_STATE_DIR) so a restarted server can reconnect; only the port, a hash of the project folder and the folder's NAME are stored, never a path. A call that fails in transit (timeout, dropped channel) is never re-sent: Mechanical may or may not have run it, so the response says so and you should look at the model before retrying.
Tool arguments come from an AI agent that may have read untrusted text, so the server is restrictive by default.
| Variable | Meaning |
|---|---|
MECHANICAL_MCP_ROOTS | Folders (separated by ; on Windows) that export_nodal_results, linearize_path, generate_report and the ACP server's file tools may read/write. Default: the server's working directory, unless that is a drive root, your home folder or a parent of it, or a Windows/Program Files folder - then nothing is allowed until you set this. |
ACP_MCP_ROOTS | Same for the ACP server; falls back to MECHANICAL_MCP_ROOTS, then to the working directory. |
MECHANICAL_MCP_REDACT | 1 (default) removes your profile path, user name, computer name, licence server / customer / session ids and e-mail addresses from every reply; 0 switches that off. |
MECHANICAL_MCP_REQUIRE_EXPLICIT_ANALYSIS | 1 (default): a tool that changes the model must be told which analysis when there are several. 0 uses index 0. |
MECHANICAL_MCP_ENABLE_SCRIPT | Set to 1 to enable run_mechanical_script, which runs arbitrary IronPython in your Mechanical session and bypasses every limit below. Off by default. |
MECHANICAL_MCP_STATE_DIR | Where the remembered port is stored. |
MECHANICAL_MCP_LOCK_WAIT_S | How long a call waits for Mechanical while another call (a solve) is running, default 60. |
MECHANICAL_MCP_MAX_WAITERS | How many calls may queue for the Mechanical lock before the next gets busy at once, default 4. |
MECHANICAL_MCP_MAX_EXPORT_NODES / MECHANICAL_MCP_EXPORT_TIMEOUT_S | Size and time budget of export_nodal_results (it asks Mechanical for one node at a time inside the lock), defaults 500,000 and 120. |
MECHANICAL_MCP_PROBE_WAIT_S | How long status tools (check_mechanical_connection, get_solve_status, disconnect_from_mechanical) wait before answering "busy", default 1. |
MECHANICAL_MCP_CONNECT_TIMEOUT_S | Bound on the gRPC handshake and first script when connecting, default 20. |
MECHANICAL_MCP_SHUTDOWN_WAIT_S | How long disconnect(shutdown=True) waits for the session's port to close, default 15. |
MECHANICAL_MCP_MAX_NODES / MECHANICAL_MCP_LINEARIZE_TIMEOUT_S | Size and time limits for linearize_path, defaults 1,000,000 and 60. |
Files: network/UNC paths and : (alternate data streams) are refused from the text, only .json (export, linearize) or .docx/.txt (report) are accepted, and an existing file is never replaced unless you pass overwrite=true. export_nodal_results enforces this inside the Mechanical script as well: it writes a uniquely named exclusive temporary file, checks again right before the final rename and refuses (leaving the file) if one appeared meanwhile, and removes only the temporary file it created. Reserved Windows device names (aux.json, COM1.txt, nul, ...) are refused. disconnect_from_mechanical(shutdown=True) force-exits the Mechanical session whoever started it (unsaved work is lost), so it also needs confirm="SHUT DOWN MECHANICAL"; the normal disconnect_from_mechanical() only drops the connection.
Example with an allow-list and scripting enabled:
{
"mcpServers": {
"mechanical-mcp": {
"command": "ansys-mechanical-mcp",
"env": { "MECHANICAL_MCP_ROOTS": "C:\\Work\\fea", "MECHANICAL_MCP_ENABLE_SCRIPT": "0" }
}
}
}The server is meant to be usable on a work machine without telling the AI model, its logs or this repository who you are.
MECHANICAL_MCP_REDACT=0 switches it off): theuser-profile prefix of a path becomes %USERPROFILE%, and your Windows user name, computer name, licence server (port@host), customer / checkout / session / host ids and e-mail addresses are replaced in every tool reply and error. This is pattern-based: a project that has your name in its own file or folder names, or text Mechanical returns in a form the patterns do not know, can still carry it. Use a neutral project folder name if that matters.
the project folder and the folder's name; no path, no user name.
MECHANICAL_MCP_ROOTS / ACP_MCP_ROOTS; errormessages report how many folders are allowed, never which.
127.0.0.1 and makes no networkrequests; it does not log tool arguments to a file. Temporary script and output files are removed after each call and stale ones (older than a day) at start-up.
.gitignore excludes project files (*.mechdb, *.wbpj), caches, logs andlocal configuration; tests/test_privacy.py checks the published files for profile paths, e-mail addresses and licence ids.
Load and support tools (add_fixed_support, add_frictionless_support, add_displacement, add_force, add_pressure) take a named selection, or "" plus either:
entity_ids: comma-separated geometry ids, orbox_mm: "xmin,xmax,ymin,ymax,zmin,zmax" in mm (blank = open side), with entity_type (face, edge, vertex) and optional body_name.Box coordinates are compared in mm whatever unit the geometry uses (read from GeoData.Unit). A missing or empty named selection, ids that do not exist, or a box that matches nothing is an error and nothing is created. entity_ids may name faces, edges or vertices.
Inspecting the model: list_boundary_conditions (loads and supports with scope and key values), list_contact_regions, list_results and get_result_values (Max/Min with unit once solved), list_named_selections, create_named_selection (from body names or prefix, geometry ids or a box; refuses empty results and duplicate names unless replace=true).
Other model and result tools: set_2d_behavior, set_body_mesh_size, set_nonlinear_settings (changes only the settings you pass), set_contact_type (overwrites every contact region unless limited with region_names / only_if_currently; the response lists each region's previous type), add_contact (two or more bodies), add_normal_stress, add_stress_intensity, export_nodal_results and linearize_path.
add_normal_stress (a signed component; linearizing von Mises or stress intensity is refused).solve_analysis (use wait=false + get_solve_status for long solves).export_nodal_results("Normal Stress", "C:/Work/fea/n.json") - writes node ids, xyz_mm(millimetres), values, value_unit, and whether the quantity is signed. Refuses an unsolved model.
linearize_path("C:/Work/fea/n.json", "0,0,0", "0,0,12") - a 3D section line, or two numbers in aplane (then the data must lie in one plane, or give slice_mm). Returns membrane, bending (at the start point; the end point has the opposite sign) and peak = total - (membrane + bending), with section_length_mm.
Not verified against a real solved model: steps 3-4 depend on Result.PlotData, which only exists after a solve, and the server's tests never solve. The export script, units and file format are covered by unit tests with a fake object model; the numerical linearization is tested against analytic profiles (pure membrane, pure bending, linear + peak). Treat the first real run with care.
as given; which way a positive value acts was not checked (it needs a solve and a look at the result), so confirm it in Mechanical before relying on the direction.
set_contact_type snapshots the type AND the friction coefficient of every region it changes andrestores both if any region fails; the response lists the real before/after of both.
solve_analysis marks the solve as possibly still running: the nextsolve request is refused until you check and pass confirm_not_running=true.
poll for the same analysis.
gravity, loads and supports (named selection / ids / box scopes), remote displacement, reaction-force probe creation, results created before a solve, modal/static checks, mesh size, body sizing, nonlinear settings, contact type changes, material and suppression changes, point-mass conversion (it reproduces the previous implementation's centre of gravity and attachment faces, which shows agreement, not that the inertia is right - see the next point), reports (TXT), non-ASCII names including an emoji, list_point_masses in StandardMKS / CGS / NMM / BIN / UMKS, named-selection creation and dependents, the reuse of identical results and contacts, a connection to a silent socket (refused after about 2 s by the protocol probe, the existing connection untouched), a load scoped to a body-type named selection (refused and rolled back; Mechanical leaves such a load without a scope), the analysis-selection checks (explicit analysis, name, expected type) on a model with three analyses, deleting a named selection by object id, the reparse-point link test (System.IO.File.GetAttributes) on a junction, a regular file and a folder, redaction of paths in replies, and recovery after a transport failure (exit code 0 in a subprocess).
counts at once); meshing a fresh model and the failure path were not exercised live.
(max/min, reaction force x/y/z/total, modal frequencies), export_nodal_results on real data, the background solve with a real solver, generate_report(fmt="docx") with real python-docx (tested with a fake docx module), set_2d_behavior on a truly 2D model, and convert_*_to_point_mass on a model whose loads are scoped to the converted bodies (covered by unit tests with the fake model only).
solve_analysis(wait=false) runs the blocking solve on a background thread; other Mechanical tools report "busy" until it ends, and get_solve_status answers Solving without touching Mechanical.
assign_material onlyaccepts names that already exist there.
multiplier 0.1 to 100, friction 0 to 10, forces and moments at most 1e12 in magnitude, pressure 1e15 Pa, displacements 1e9 mm, rotations 36000 deg, samples 2 to 5000.
a body only its three PRINCIPAL moments of inertia (MomentOfInertiaIp1-3; no tensor, no axes, no products of inertia). They are used as Ixx, Iyy, Izz and the response says inertia_basis: "principal" with warnings: always one, one more when several bodies are combined (their products of inertia are dropped because a point mass cannot take them), and one when planar faces are not aligned with the global axes. A body rotated against the global axes gets wrong Ixx/Iyy; check or set the inertia by hand in that case. Mass, centre of gravity and the attachment faces are unaffected.
box_mm picks the faces/edges/vertices whose CENTROID lies inside thebox, not the entities that lie entirely inside; the response counts the selected entities that stick out in warnings, and fully_inside=true requires the whole entity (face and edge bounding boxes) to be inside.
set_contact_type(region_names=...) matches every region of that name and says so; list_contact_regions shows index and duplicate_names. delete_named_selection deletes every selection of that name.
add_contact return an identical existing object(already_exists: true, created: false, and suppressed - a reused object may be suppressed) instead of adding "Equivalent Stress 2"; pass reuse_existing=false to force a duplicate. A contact region counts as the same when it joins exactly the same entities with the same type and (frictional) friction coefficient. If a region joins them but differs, add_contact refuses and lists the differences; update_existing=true changes the type/friction (restored on failure) and reports updated. Behaviour and formulation are not set by the call; they are only reported in existing_properties. Point-mass conversion refuses to run twice for the same EXACT prefix/part (the mass would count twice) unless allow_duplicate=true: the selection it makes is named NearestFace_<start of prefix>_<hash of the whole prefix>, so SYS\\B and SYS\\Backing never collide and are both allowed. Two different prefixes that match the same body are not recognised as duplicates. delete_named_selection refuses while loads, results, contacts, point masses or mesh controls are scoped to the selection (it lists them) unless force=true. Only objects scoped through a Location, GeometryLocation, SourceLocation or TargetLocation are recognised: Force-reaction probes (their scope is a copy of the entities), joints, springs, remote points, coordinate systems and anything else that references a selection some other way are NOT detected.
re-sent and the response says it may or may not have run. The connection object is dropped outside the exception handler and finalized before its channel is closed: doing it inside the handler made the whole server process crash (exit 139) because the failing frames kept the object alive. The handshake with a port that is not Mechanical is bounded (MECHANICAL_MCP_CONNECT_TIMEOUT_S, default 20 s) and does not hold the Mechanical lock; a late connection that arrives after the timeout is dropped the same way.
check_mechanical_connectionanswers connected: true, busy: true, get_solve_status answers status: "Busy" and disconnect_from_mechanical answers busy, each within MECHANICAL_MCP_PROBE_WAIT_S (1 s) plus scheduling. The guarantee is precise: these three tools run on their own small thread pool and never queue for the lock, so other waiting calls cannot starve them. Every other Mechanical tool waits for the lock up to MECHANICAL_MCP_LOCK_WAIT_S (60 s), but at most MECHANICAL_MCP_MAX_WAITERS (4) calls wait at a time; the next ones get busy: true immediately. A connection attempt to a port that never answers takes at most MECHANICAL_MCP_CONNECT_TIMEOUT_S and at most two such attempts may be pending.
The identity is read when you connect: if the project in the session is later saved under another name or a different project is opened in the same session, the remembered identity is stale until you connect again. A silent re-attach is only made while the port still serves that project (otherwise the call fails and tells you to connect deliberately), is reported once as reconnected_to in the tool output, and the port is forgotten when it refuses connections or after three bad answers in a row (tested), not after one; a busy or slow session (handshake timeout) is neither a refusal nor counted. A remembered session with no project identity (an unsaved project) is never re-attached silently: call connect_to_mechanical.
averaged) and triangulates only the nodes near the line (a k-d tree), so 216k nodes take seconds. A line that crosses a hole or notch is a gap error carrying gap_diagnostics (largest span and where it is); allow_gaps=true accepts it and reports interpolated_across_gap. The criterion is local and relative: the simplex a sample falls in is a gap only when its longest edge is more than 3 times the median of the simplices around it AND fewer than 20 % of those neighbours are of similar size. Graded, polar and long-element meshes (10:1, line along the long side or across it) and an abrupt step in element size (a fine region next to a coarse one) are therefore not flagged, but a void smaller than about three element sizes is not detected either. A section line shorter than 0.1 % of the model or than one element is refused. A line end that lies outside the triangulation by less than snap_fraction (default 0.25) of an element - on a curved boundary or a hair outside a flat face - is extrapolated from the nearest simplex and listed in endpoint_adjustments; farther out the line is refused. MECHANICAL_MCP_MAX_NODES (default 1,000,000) and MECHANICAL_MCP_LINEARIZE_TIMEOUT_S (default 60) bound the work; a timed-out run is told to stop (it checks a cancel flag between stages) and only one linearization may run at a time; a second call gets busy until the stopped one has ended. Only the single exported scalar is linearized: the file records its orientation; expect_component makes a mismatch an error (and is reported as component_unverified when the file has none); a line running along another axis than the component is refused unless accept_direction_mismatch=true; components are never combined.
export_nodal_results exports the result at the step it is currentlyset to; an analysis with several load steps needs an explicit load_step, and the file records the step, substep and time (step). Time-history results are exported one step per file.
PlotData does not declare its unit. The export compares its largest value withthe result's own Maximum and records value_unit_verified; when it is false the unit label is unconfirmed. This has only been unit-tested (a solve is needed to run it for real).
entry (the first is 0 at t=0). set_nonlinear_settings on an analysis without those settings returns a clear error.
get_solve_status messages come from Mechanical's global Messages pane (all analyses); the responsesays so.
run_mechanical_script (when enabled) shares Mechanical's persistent globals as before.
ansys-acp-mcp)A second MCP server, acp_mcp_server.py, lets an AI assistant build ANSYS Composite PrepPost (ACP) layups and solid models through PyACP (ansys-acp-core), then convert the exported solid mesh into a clean per-fibre-angle external model for a Mechanical coupled-field analysis (for example thermal-electric lightning studies). Every tool returns JSON with ok and a short, specific error on failure.
Tested with ACP 2026 R1, ansys-acp-core 0.3.2, mcp 1.30, Windows, Python 3.10.
pip install -e ".[acp]"ACP must be installed (the server uses acp_grpcserver.exe from %AWP_ROOT261%\ACP). Set AWP_ROOT261 (or the equivalent for your release) if ANSYS is not in the default location.
{
"mcpServers": {
"ansys-acp": {
"command": "ansys-acp-mcp",
"env": { "AWP_ROOT261": "C:\\Program Files\\ANSYS Inc\\v261" }
}
}
}| Group | Tool | What it does |
|---|---|---|
| Session | acp_launch, acp_status, acp_shutdown | start / inspect / stop the PyACP-managed ACP server |
| Model I/O | acp_import_model, acp_save_model, acp_model_tree | import CDB/H5/DAT/acp:h5, save .acph5, list objects and statuses |
| Definition | acp_create_material, acp_create_fabric, acp_create_rosette, acp_create_oriented_selection_set | materials, fabrics, rosettes, oriented selection sets |
| Layup | acp_create_layup | modeling group with one ply per angle (+ optional bulk ply) |
| Solid | acp_create_solid_model, acp_update, acp_export_solid_model | extrude, recompute, export CDB/H5 |
| CDB (no ACP needed) | acp_inspect_cdb, acp_prepare_external_model | summarise a CDB; convert an ACP solid CDB into a per-angle external model (normalised through MAPDL) |
| Workbench ACP | acp_wb_build_script, acp_wb_commands, acp_wb_ply_split_snippet | generate the script that builds the layup inside Workbench's own ACP, the Workbench commands that run and link it, and the Mechanical snippet that splits plies |
Typical flow: acp_launch → acp_import_model (shell mesh) → acp_create_material → acp_create_fabric → acp_create_rosette → acp_create_oriented_selection_set → acp_create_layup → acp_create_solid_model → acp_update → acp_export_solid_model → acp_prepare_external_model. See examples/build_plate_layup.py.
orientation_point; a shell at z = 0 with orientation_point = (0, 0, -1) puts the plies below the surface.acp_create_layup names plies ply03_-45 etc.; acp_prepare_external_model reads the fibre angle from that name.python run_acp_unit_tests.py # CDB / Workbench-script tests, no ANSYS needed
python examples/stdio_smoke_test.py # start the server over stdio and list toolsIntegration tests need ACP: set ACP_MCP_INTEGRATION=1 and ACP_MCP_SHELL_CDB to a shell CDB, then run pytest tests/test_integration.py.
Apache License 2.0 — see LICENSE
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.