# Directory Structure
```
├── .gitignore
├── LICENSE
├── mcp_server.py
├── README_zh.md
├── README.md
├── requirements.txt
└── test_mcp_server.py
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | dist/
13 | downloads/
14 | eggs/
15 | .eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | wheels/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 | MANIFEST
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/version info into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .nox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | osetests.xml
45 | .hypothesis/
46 | .pytest_cache/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 | db.sqlite3
56 | db.sqlite3-journal
57 |
58 | # Flask stuff:
59 | instance/
60 | .webassets-cache
61 |
62 | # Scrapy stuff:
63 | .scrapy
64 |
65 | # Sphinx documentation
66 | docs/_build/
67 |
68 | # PyBuilder
69 | target/
70 |
71 | # Jupyter Notebook
72 | .ipynb_checkpoints
73 |
74 | # IPython
75 | profile_default/
76 | ipython_config.py
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # PEP 582; Pyflow
82 | __pypackages__/
83 |
84 | # Celery stuff
85 | celerybeat-schedule
86 | celerybeat.pid
87 |
88 | # SageMath parsed files
89 | *.sage.py
90 |
91 | # Environments
92 | .env
93 | .venv
94 | env/
95 | venv/
96 | ENV/
97 | env.bak/
98 | venv.bak/
99 |
100 | # Spyder project settings
101 | .spyderproject
102 | .spyproject
103 |
104 | # Rope project settings
105 | .ropeproject
106 |
107 | # mkdocs documentation
108 | /site
109 |
110 | # mypy
111 | .mypy_cache/
112 | .dmypy.json
113 | dmypy.json
114 |
115 | # Pyre type checker
116 | .pyre/
117 |
118 | # pytype
119 | .pytype/
120 |
121 | # IDE / Editor specific
122 | .vscode/
123 | .idea/
124 | *.suo
125 | *.ntvs*.*
126 | *.njsproj
127 | *.sln
128 | *.sublime-project
129 | *.sublime-workspace
130 |
131 | # OS specific
132 | .DS_Store
133 | Thumbs.db
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | English | [中文](README_zh.md)
2 |
3 | # ABAQUS MCP Server for GUI Scripting
4 |
5 | An MCP (Model Context Protocol) server designed to interact with an **already running** Abaqus/CAE Graphical User Interface (GUI). It allows for the execution of Python scripts within the Abaqus environment and retrieval of messages from the Abaqus message log/area, all through MCP tools.
6 |
7 | This server uses GUI automation techniques (`pywinauto`) to control the Abaqus/CAE application.
8 |
9 | ## Features
10 |
11 | - **Script Execution in GUI**: Executes Python scripts by automating the "File -> Run Script..." menu in an active Abaqus/CAE session.
12 | - **Message Log Retrieval**: Attempts to scrape text content from the Abaqus/CAE message/log display area.
13 | - **MCP Interface**: Exposes functionalities as standard MCP tools and prompts for easy integration with LLM agents and other MCP-compatible clients.
14 | - **Operates on Existing GUI**: Does not start or stop Abaqus/CAE; it requires an Abaqus/CAE session to be already open and running.
15 |
16 | ## System Requirements
17 |
18 | - **Operating System**: Windows (due to `pywinauto` and Abaqus GUI interaction).
19 | - **Python**: Python 3.7+.
20 | - **Abaqus/CAE**: A compatible version of Abaqus/CAE must be installed and **already running with its GUI open** when using this server.
21 |
22 | ## Python Dependencies
23 |
24 | Install the required Python packages using pip:
25 |
26 | ```bash
27 | pip install -r requirements.txt
28 | ```
29 |
30 | (See `requirements.txt` for the list of dependencies, primarily `mcp[cli]`, `pywinauto`, `pygetwindow`, `psutil`, `pywin32`.)
31 |
32 | ## Configuration
33 |
34 | 1. **Ensure Abaqus/CAE is Running**: Before starting this MCP server, make sure the Abaqus/CAE application is open on your desktop, with the GUI loaded and responsive.
35 | 2. **Accessibility**: Ensure no other modal dialogs are blocking the Abaqus/CAE interface when the server attempts to interact with it.
36 |
37 | ## Usage
38 |
39 | ### Starting the MCP Server
40 |
41 | Navigate to the server's directory and run:
42 |
43 | ```bash
44 | python mcp_server.py
45 | ```
46 |
47 | The server will start and be ready to accept MCP requests.
48 |
49 | ### MCP Tools Provided
50 |
51 | 1. **`execute_script_in_abaqus_gui`**
52 | * **Description**: Executes a given Python code string within the active Abaqus/CAE GUI session. It automates the 'File -> Run Script...' menu process.
53 | * **Argument**:
54 | * `python_code (str)`: The Python script content (Abaqus Scripting Interface commands) to be executed.
55 | * **Returns**: `str` - A message indicating the outcome of the script *submission* attempt (e.g., "Script submitted for execution...").
56 | * **Important**: This tool **does not** return the direct output or error messages from the script's execution within Abaqus. To see the script's actual outcome, you must use the `get_abaqus_gui_message_log` tool after allowing time for the script to run.
57 |
58 | 2. **`get_abaqus_gui_message_log`**
59 | * **Description**: Attempts to retrieve the text content from the Abaqus/CAE message/log area (usually found at the bottom of the main GUI window).
60 | * **Arguments**: None.
61 | * **Returns**: `str` - The extracted text from the message area, or an error/status message if retrieval fails.
62 | * **Note**: The reliability of this tool depends on accurately identifying the message area UI element. The current implementation uses heuristics. For robust operation in a specific Abaqus environment, you might need to update the server code with specific UI element identifiers (e.g., AutomationId, Name, ClassName) obtained using an inspect tool (see Development/Troubleshooting).
63 |
64 | ### MCP Prompts Provided
65 |
66 | 1. **`abaqus_scripting_strategy`**
67 | * **Description**: This prompt provides comprehensive guidance on how to best use the server's tools (`execute_script_in_abaqus_gui` and `get_abaqus_gui_message_log`) together effectively. It explains the workflow for script execution and result verification, tool assumptions, and troubleshooting tips. It is highly recommended that LLM agents consult this prompt before attempting to use the tools.
68 |
69 | ## Important Considerations & Limitations
70 |
71 | - **Requires Running Abaqus GUI**: This server *only* interacts with an Abaqus/CAE session that is already open and has its GUI active and responsive. It cannot start or manage the Abaqus application itself.
72 | - **GUI Automation Sensitivity**: GUI automation can be sensitive to the Abaqus version, screen resolution, window themes, and minor UI layout changes. While `pywinauto` provides a good level of abstraction, issues can still arise.
73 | - **Focus and Window State**: The Abaqus window should ideally be the active, non-minimized window for the most reliable interaction, although the server attempts to manage focus.
74 | - **Modal Dialogs**: Unexpected modal dialogs in Abaqus (e.g., save reminders, warnings) can block the automation tools.
75 | - **Error Reporting**: Differentiate between errors from the MCP tools (e.g., "Abaqus window not found") and errors reported *within* the Abaqus message log (which originate from your script running inside Abaqus).
76 |
77 | ## Development / Troubleshooting
78 |
79 | - **Inspecting UI Elements**: If the `get_abaqus_gui_message_log` tool fails to retrieve messages accurately, or if `execute_script_in_abaqus_gui` has trouble with the "Run Script" dialog, you may need to use a UI inspection tool (e.g., `pywinauto.inspect` module's `InspectDialog` or `py_inspect.py` script, FlaUInspect for UIA backend) to identify the correct properties (AutomationId, Name, ClassName, ControlType) of the target UI elements in your Abaqus version. These properties can then be used to refine the search logic in `mcp_server.py`.
80 | - **Timeouts**: The server includes some `time.sleep()` calls and uses `pywinauto`'s `Timings.slow()` to better handle Abaqus's potentially slow UI response times. These might need adjustment in some environments.
81 |
82 | ## Project Structure
83 |
84 | ```
85 | abaqus-mcp-gui-server/
86 | ├── mcp_server.py # The main MCP server script
87 | ├── requirements.txt # Python dependencies
88 | └── README.md # This documentation file
89 | ```
90 |
91 | ## License
92 |
93 | This project is intended for learning, research, and specific automation tasks. When using Abaqus software, always adhere to the licensing terms provided by Dassault Systèmes.
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
1 | mcp[cli]
2 | pywinauto
3 | pygetwindow
4 | psutil
5 | pywin32
```
--------------------------------------------------------------------------------
/test_mcp_server.py:
--------------------------------------------------------------------------------
```python
1 | import unittest
2 | from unittest.mock import patch, MagicMock
3 | import os
4 | import sys
5 |
6 | # Add the directory containing mcp_server.py to the Python path
7 | # This is to ensure that the mcp_server module can be imported
8 | # Assuming test_mcp_server.py is in the same directory as mcp_server.py
9 | sys.path.append(os.path.dirname(os.path.abspath(__file__)))
10 |
11 | try:
12 | from mcp_server import (
13 | find_abaqus_window_and_app,
14 | execute_script,
15 | get_abaqus_message_log,
16 | mcp # For FastMCP instance if needed for context, though tools are standalone
17 | )
18 | # Mock a dummy FastMCP instance for the decorators to work if they rely on it at import time
19 | # This might not be strictly necessary if the functions can be tested in isolation
20 | # but good for ensuring the module loads.
21 | if not hasattr(mcp, 'tool'): # If mcp was not fully initialized for some reason
22 | class DummyMCP:
23 | def tool(self, *args, **kwargs):
24 | def decorator(f):
25 | return f
26 | return decorator
27 | def prompt(self, *args, **kwargs):
28 | def decorator(f):
29 | return f
30 | return decorator
31 | mcp = DummyMCP()
32 |
33 | except ImportError as e:
34 | print(f"Failed to import from mcp_server: {e}")
35 | # Define dummy functions if import fails, so tests can at least be defined
36 | def find_abaqus_window_and_app(): pass
37 | def execute_script(python_code: str): pass
38 | def get_abaqus_message_log(): pass
39 |
40 | class TestFindAbaqusWindowAndApp(unittest.TestCase):
41 |
42 | @patch('mcp_server.gw')
43 | @patch('mcp_server.win32process')
44 | @patch('mcp_server.psutil')
45 | @patch('mcp_server.Application')
46 | @patch('mcp_server.abaqus_app_instance_cache', new=None)
47 | @patch('mcp_server.abaqus_main_window_cache', new=None)
48 | def test_find_window_success_no_cache(self, mock_app_class, mock_psutil, mock_win32process, mock_gw):
49 | # Setup mocks for a successful window find and connection
50 | mock_window_ref = MagicMock()
51 | mock_window_ref.title = "Abaqus/CAE ModName - JobName"
52 | mock_window_ref._hWnd = 12345
53 | mock_gw.getWindowsWithTitle.return_value = [mock_window_ref]
54 |
55 | mock_win32process.GetWindowThreadProcessId.return_value = (None, 67890)
56 |
57 | mock_proc = MagicMock()
58 | mock_proc.name.return_value = "abaqus_cae.exe"
59 | mock_psutil.Process.return_value = mock_proc
60 |
61 | mock_pywinauto_app = MagicMock()
62 | mock_main_window_spec = MagicMock()
63 | mock_main_window_spec.exists.return_value = True
64 | mock_main_window_spec.is_visible.return_value = True
65 |
66 | mock_pywinauto_app.window.return_value = mock_main_window_spec
67 | mock_app_class.return_value.connect.return_value = mock_pywinauto_app # connect() returns the app instance
68 | # Simulate that app.window() is called on the instance returned by connect()
69 | # If connect() returns the app instance, then app.window() is called on that.
70 | # The original code does app = Application(backend="uia").connect(...)
71 | # then main_window = app.window(...)
72 | # So, mock_app_class itself is the Application class.
73 | # Its instance (returned by __call__ or connect) should have the window method.
74 |
75 | # We need to ensure that the Application constructor itself returns an object
76 | # that then has the connect method.
77 | # And connect itself returns an object that has the window method.
78 |
79 | # Revised mocking for Application:
80 | mock_app_instance_for_connect = MagicMock()
81 | mock_app_instance_for_connect.window.return_value = mock_main_window_spec
82 |
83 | mock_app_constructor_instance = MagicMock()
84 | mock_app_constructor_instance.connect.return_value = mock_app_instance_for_connect
85 | mock_app_class.return_value = mock_app_constructor_instance
86 |
87 |
88 | app, window = find_abaqus_window_and_app()
89 |
90 | self.assertIsNotNone(app)
91 | self.assertIsNotNone(window)
92 | mock_gw.getWindowsWithTitle.assert_called_with("Abaqus/CAE")
93 | mock_win32process.GetWindowThreadProcessId.assert_called_with(12345)
94 | mock_psutil.Process.assert_called_with(67890)
95 | mock_app_class.assert_called_with(backend="uia")
96 | mock_app_constructor_instance.connect.assert_called_with(handle=12345, timeout=20)
97 | mock_app_instance_for_connect.window.assert_called_with(handle=12345)
98 | self.assertEqual(window, mock_main_window_spec)
99 |
100 | @patch('mcp_server.abaqus_app_instance_cache')
101 | @patch('mcp_server.abaqus_main_window_cache')
102 | def test_find_window_cache_hit(self, mock_window_cache, mock_app_cache):
103 | # Setup cache
104 | mock_app_cache.exists.return_value = True # Assuming app cache itself doesn't need exists()
105 | mock_window_cache.exists.return_value = True
106 | mock_window_cache.is_visible.return_value = True
107 |
108 | # Need to assign to the module's global directly for the test's context
109 | import mcp_server
110 | mcp_server.abaqus_app_instance_cache = mock_app_cache
111 | mcp_server.abaqus_main_window_cache = mock_window_cache
112 |
113 | app, window = find_abaqus_window_and_app()
114 |
115 | self.assertEqual(app, mock_app_cache)
116 | self.assertEqual(window, mock_window_cache)
117 |
118 | # Reset caches for other tests
119 | mcp_server.abaqus_app_instance_cache = None
120 | mcp_server.abaqus_main_window_cache = None
121 |
122 |
123 | @patch('mcp_server.gw')
124 | def test_find_window_not_found(self, mock_gw):
125 | mock_gw.getWindowsWithTitle.return_value = []
126 | app, window = find_abaqus_window_and_app()
127 | self.assertIsNone(app)
128 | self.assertIsNone(window)
129 |
130 | # Add more tests for find_abaqus_window_and_app (e.g., process name mismatch, connection error)
131 |
132 |
133 | class TestExecuteScript(unittest.TestCase):
134 |
135 | @patch('mcp_server.find_abaqus_window_and_app')
136 | @patch('mcp_server.tempfile.NamedTemporaryFile')
137 | @patch('mcp_server.os.remove')
138 | @patch('mcp_server.time.sleep') # Mock sleep to speed up tests
139 | def test_execute_script_success(self, mock_sleep, mock_os_remove, mock_tempfile, mock_find_abaqus):
140 | # Mock successful Abaqus window and app
141 | mock_app = MagicMock()
142 | mock_main_window = MagicMock()
143 | mock_main_window.exists.return_value = True
144 | mock_main_window.is_minimized.return_value = False
145 | mock_find_abaqus.return_value = (mock_app, mock_main_window)
146 |
147 | # Mock temp file creation
148 | mock_tmp_file = MagicMock()
149 | mock_tmp_file.name = "C:\temp\somescript.py" # Use a windows-like path for consistency
150 | mock_tempfile.return_value.__enter__.return_value = mock_tmp_file
151 |
152 | # Mock dialog interaction
153 | mock_run_script_dialog = MagicMock()
154 | mock_run_script_dialog.exists.return_value = True
155 | mock_run_script_dialog.window_text.return_value = "Run Script Dialog" # Match one of the conditions
156 |
157 | mock_app.top_window.return_value = mock_run_script_dialog # Primary attempt
158 | # If app.active() or main_window.children() were used, they'd need mocking too.
159 |
160 | mock_file_name_edit = MagicMock()
161 | mock_file_name_edit.exists.return_value = True
162 | mock_run_script_dialog.child_window.return_value = mock_file_name_edit # Assuming first child_window call gets it
163 | # To be more specific, you could use side_effect if multiple calls are made with different args
164 |
165 | mock_ok_button = MagicMock()
166 | mock_ok_button.exists.return_value = True
167 | # Adjust if the second call to child_window is the one for the button
168 | # For simplicity, let's assume child_window with title_re="OK|Run|Open" finds it.
169 | mock_run_script_dialog.child_window.side_effect = [
170 | MagicMock(exists=MagicMock(return_value=True), wrapper_object=MagicMock(return_value=mock_file_name_edit)), # For File &name:
171 | MagicMock(exists=MagicMock(return_value=True), wrapper_object=MagicMock(return_value=mock_ok_button)) # For OK button
172 | ]
173 |
174 |
175 | python_code = "print('Hello Abaqus')"
176 | result = execute_script(python_code)
177 |
178 | mock_find_abaqus.assert_called_once()
179 | mock_tempfile.assert_called_once_with(mode="w", suffix=".py", delete=False, encoding='utf-8')
180 | mock_tmp_file.write.assert_called_with(python_code)
181 |
182 | mock_main_window.menu_select.assert_called_with("File->Run Script...")
183 |
184 | # Check if dialog interactions happened (more specific assertions can be added)
185 | self.assertTrue("File &name:" in str(mock_run_script_dialog.child_window.call_args_list))
186 | self.assertTrue("OK|Run|Open" in str(mock_run_script_dialog.child_window.call_args_list))
187 |
188 | mock_file_name_edit_wrapper = mock_run_script_dialog.child_window.side_effect[0].wrapper_object()
189 | mock_file_name_edit_wrapper.set_edit_text.assert_called_with("C:\temp\somescript.py") # Path replacement happens
190 |
191 | mock_ok_button_wrapper = mock_run_script_dialog.child_window.side_effect[1].wrapper_object()
192 | mock_ok_button_wrapper.click_input.assert_called_once()
193 |
194 | self.assertIn("Script submitted for execution", result)
195 | mock_os_remove.assert_called_with("C:\temp\somescript.py")
196 |
197 |
198 | @patch('mcp_server.find_abaqus_window_and_app')
199 | def test_execute_script_abaqus_not_found(self, mock_find_abaqus):
200 | mock_find_abaqus.return_value = (None, None)
201 | result = execute_script("print('test')")
202 | self.assertIn("Abaqus/CAE window not found", result)
203 |
204 | # Add more tests for execute_script (e.g., dialog not found, control interaction failures)
205 |
206 |
207 | class TestGetAbaqusMessageLog(unittest.TestCase):
208 |
209 | @patch('mcp_server.find_abaqus_window_and_app')
210 | def test_get_log_success_pane_heuristic(self, mock_find_abaqus):
211 | mock_app = MagicMock()
212 | mock_main_window = MagicMock()
213 | mock_main_window.exists.return_value = True
214 | mock_find_abaqus.return_value = (mock_app, mock_main_window)
215 |
216 | mock_pane = MagicMock()
217 | mock_pane.is_visible.return_value = True
218 | mock_pane.rectangle.return_value = MagicMock(height=MagicMock(return_value=150), width=MagicMock(return_value=300))
219 | mock_pane.class_name.return_value = "SomeFXWindow"
220 | mock_pane.texts.return_value = [["Line 1", "Line 2"], ["", "Line 3"]] # Simulate texts structure
221 | mock_pane.exists.return_value = True # For the if message_area_control.exists() check
222 |
223 | mock_main_window.descendants.return_value = [MagicMock(wrapper_object=MagicMock(return_value=mock_pane))] # descendants returns specs
224 |
225 | result = get_abaqus_message_log()
226 |
227 | mock_find_abaqus.assert_called_once()
228 | mock_main_window.descendants.assert_any_call(control_type="Pane")
229 | self.assertIn("Line 1\nLine 2\nLine 3", result)
230 | self.assertIn("Message Log Content", result)
231 |
232 | @patch('mcp_server.find_abaqus_window_and_app')
233 | def test_get_log_success_edit_heuristic(self, mock_find_abaqus):
234 | mock_app = MagicMock()
235 | mock_main_window = MagicMock()
236 | mock_main_window.exists.return_value = True
237 | mock_find_abaqus.return_value = (mock_app, mock_main_window)
238 |
239 | # Pane heuristic fails (no suitable panes)
240 | mock_no_pane = MagicMock()
241 | mock_no_pane.is_visible.return_value = False # Or doesn't match criteria
242 |
243 | # Edit heuristic succeeds
244 | mock_edit = MagicMock()
245 | mock_edit.is_visible.return_value = True
246 | mock_edit.is_editable.return_value = False
247 | mock_edit.rectangle.return_value = MagicMock(height=MagicMock(return_value=60), width=MagicMock(return_value=300))
248 | mock_edit.texts.return_value = [["Error: 123", "Warning: 456"]]
249 | mock_edit.exists.return_value = True
250 |
251 | mock_main_window.descendants.side_effect = [
252 | [MagicMock(wrapper_object=MagicMock(return_value=mock_no_pane))], # For Pane
253 | [MagicMock(wrapper_object=MagicMock(return_value=mock_edit))] # For Edit
254 | ]
255 |
256 | result = get_abaqus_message_log()
257 | mock_find_abaqus.assert_called_once()
258 | self.assertIn("Error: 123\nWarning: 456", result)
259 | self.assertIn("Message Log Content", result)
260 |
261 | @patch('mcp_server.find_abaqus_window_and_app')
262 | def test_get_log_abaqus_not_found(self, mock_find_abaqus):
263 | mock_find_abaqus.return_value = (None, None)
264 | result = get_abaqus_message_log()
265 | self.assertIn("Abaqus/CAE window not found", result)
266 |
267 | @patch('mcp_server.find_abaqus_window_and_app')
268 | def test_get_log_no_message_area_found(self, mock_find_abaqus):
269 | mock_app = MagicMock()
270 | mock_main_window = MagicMock()
271 | mock_main_window.exists.return_value = True
272 | mock_find_abaqus.return_value = (mock_app, mock_main_window)
273 |
274 | # Both heuristics fail
275 | mock_main_window.descendants.return_value = [] # No elements found
276 |
277 | result = get_abaqus_message_log()
278 | self.assertIn("Message area UI element not found", result)
279 |
280 | # Add more tests for get_abaqus_message_log (e.g., control found but no text)
281 |
282 | if __name__ == '__main__':
283 | unittest.main(argv=['first-arg-is-ignored'], exit=False)
```
--------------------------------------------------------------------------------
/mcp_server.py:
--------------------------------------------------------------------------------
```python
1 | from mcp.server.fastmcp import FastMCP
2 | import tempfile
3 | import os
4 | import pygetwindow as gw
5 | import win32process
6 | import psutil
7 | from pywinauto import Application, timings, base_wrapper
8 | from pywinauto.controls import uia_controls
9 | import time
10 | from typing import Tuple, Optional
11 |
12 | mcp = FastMCP(
13 | "ABAQUS GUI Script Executor",
14 | description="An MCP server to execute Python scripts within an existing Abaqus/CAE GUI session and retrieve message log information. Relies on pywinauto for GUI automation."
15 | )
16 |
17 | # Global cache for Abaqus window and app instance
18 | abaqus_main_window_cache: Optional[uia_controls.WindowSpecification] = None
19 | abaqus_app_instance_cache: Optional[Application] = None
20 |
21 | def find_abaqus_window_and_app() -> Tuple[Optional[Application], Optional[uia_controls.WindowSpecification]]:
22 | """
23 | Finds the main Abaqus/CAE window and connects the pywinauto Application object.
24 |
25 | It first checks a cache. If not found or invalid, it searches for windows
26 | starting with the title "Abaqus/CAE", verifies the process name,
27 | and then attempts to connect a pywinauto.Application instance to it.
28 | The found application and window objects are cached for subsequent calls.
29 |
30 | Relies on `pygetwindow` for initial window discovery and `pywinauto` for connection.
31 |
32 | Returns:
33 | Tuple[Optional[Application], Optional[uia_controls.WindowSpecification]]:
34 | A tuple containing the connected `pywinauto.Application` instance and the
35 | main window specification (`WindowSpecification`). Both can be `None` if
36 | the Abaqus/CAE window is not found or connection fails.
37 | """
38 | global abaqus_main_window_cache, abaqus_app_instance_cache
39 | if abaqus_app_instance_cache and abaqus_main_window_cache and \
40 | abaqus_main_window_cache.exists() and abaqus_main_window_cache.is_visible():
41 | return abaqus_app_instance_cache, abaqus_main_window_cache
42 |
43 | abaqus_app_instance_cache = None
44 | abaqus_main_window_cache = None
45 |
46 | windows = gw.getWindowsWithTitle("Abaqus/CAE")
47 | found_win_obj = None
48 | for win_ref in windows:
49 | if win_ref.title.startswith("Abaqus/CAE"):
50 | try:
51 | _, pid = win32process.GetWindowThreadProcessId(win_ref._hWnd)
52 | proc = psutil.Process(pid)
53 | if "abaqus" in proc.name().lower() and \
54 | ("cae" in proc.name().lower() or "viewer" in proc.name().lower()):
55 | found_win_obj = win_ref
56 | break
57 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
58 | continue
59 |
60 | if found_win_obj:
61 | try:
62 | timings.Timings.slow()
63 | app = Application(backend="uia").connect(handle=found_win_obj._hWnd, timeout=20)
64 | main_window = app.window(handle=found_win_obj._hWnd)
65 | if main_window.exists() and main_window.is_visible():
66 | abaqus_app_instance_cache = app
67 | abaqus_main_window_cache = main_window
68 | return app, main_window
69 | except Exception as e:
70 | print(f"Error connecting to Abaqus window via pywinauto: {e}")
71 |
72 | return None, None
73 |
74 | @mcp.tool(
75 | name="execute_script_in_abaqus_gui",
76 | description="Executes a Python script in an already running Abaqus/CAE GUI session using the 'File -> Run Script...' menu. Assumes Abaqus/CAE is open and responsive."
77 | )
78 | def execute_script(python_code: str) -> str:
79 | """
80 | Executes a given Python code string within an active Abaqus/CAE GUI session.
81 |
82 | The process involves:
83 | 1. Saving the `python_code` to a temporary .py file.
84 | 2. Locating the active Abaqus/CAE main window.
85 | 3. Automating the GUI to select 'File -> Run Script...' from the menu.
86 | 4. Typing the path of the temporary script file into the 'Run Script' dialog.
87 | 5. Clicking 'OK' in the dialog to initiate script execution.
88 | 6. Deleting the temporary script file after submission.
89 |
90 | Args:
91 | python_code (str): The Python script content to be executed, as a string.
92 |
93 | Returns:
94 | str: A message indicating the outcome of the script submission attempt.
95 | This primarily confirms if the script was successfully passed to the Abaqus GUI
96 | via the 'Run Script' dialog. It does *not* return the script's own output
97 | or execution status from within Abaqus. The executed script would need to
98 | handle its own output (e.g., writing to files) if results are needed externally.
99 | """
100 | global abaqus_main_window_cache, abaqus_app_instance_cache
101 | app, main_window = find_abaqus_window_and_app()
102 |
103 | if not main_window or not main_window.exists():
104 | abaqus_app_instance_cache = None
105 | abaqus_main_window_cache = None
106 | return "Abaqus/CAE window not found. Please ensure Abaqus/CAE with GUI is running and not minimized initially."
107 |
108 | script_file_path: Optional[str] = None
109 | run_script_dialog: Optional[uia_controls.WindowSpecification] = None
110 | try:
111 | with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding='utf-8') as tmp_script:
112 | tmp_script.write(python_code)
113 | script_file_path = tmp_script.name
114 |
115 | script_file_path_for_dialog = script_file_path.replace("/", "\\")
116 |
117 | if main_window.is_minimized():
118 | main_window.restore()
119 | main_window.set_focus()
120 | time.sleep(0.5)
121 |
122 | main_window.menu_select("File->Run Script...")
123 | time.sleep(1.5) # Wait for Abaqus to open the dialog
124 |
125 | dialog_found = False
126 | try:
127 | run_script_dialog = app.top_window() # Primary attempt
128 | if run_script_dialog.exists() and \
129 | ("Run Script" in run_script_dialog.window_text() or "Select file" in run_script_dialog.window_text()):
130 | dialog_found = True
131 |
132 | if not dialog_found:
133 | time.sleep(1) # Secondary attempt after a slight delay
134 | run_script_dialog = app.active() # Check currently active window of the app
135 | if run_script_dialog.exists() and \
136 | ("Run Script" in run_script_dialog.window_text() or "Select file" in run_script_dialog.window_text()):
137 | dialog_found = True
138 |
139 | if not dialog_found: # Tertiary attempt: search children of main window
140 | # This is a deeper search if the dialog isn't directly the app's top/active window.
141 | # Useful if the dialog is a child dialog or has an unusual relationship.
142 | possible_dialogs = main_window.children(control_type="Window", top_level_only=False, visible=True)
143 | for diag in possible_dialogs:
144 | if diag.exists() and ("Run Script" in diag.window_text() or "Select file" in diag.window_text()):
145 | run_script_dialog = diag
146 | dialog_found = True
147 | break
148 |
149 | if not dialog_found or not run_script_dialog:
150 | raise Exception("'Run Script' dialog not found or title mismatch after menu click.")
151 | except Exception as e_dialog:
152 | print(f"Error finding or identifying the 'Run Script' dialog: {e_dialog}")
153 | return f"Failed to find or identify the 'Run Script' dialog: {e_dialog}"
154 |
155 | try:
156 | file_name_edit: Optional[uia_controls.EditWrapper] = None
157 | # Attempt to find by specific title and control type (more reliable)
158 | potential_edit = run_script_dialog.child_window(title="File &name:", control_type="Edit")
159 | if potential_edit.exists(timeout=1):
160 | file_name_edit = potential_edit.wrapper_object()
161 | else:
162 | # Fallback: by control type and index (less reliable, but common)
163 | potential_edit_by_index = run_script_dialog.child_window(control_type="Edit", found_index=0)
164 | if potential_edit_by_index.exists(timeout=1):
165 | file_name_edit = potential_edit_by_index.wrapper_object()
166 | else:
167 | # Fallback: generic Edit control (least reliable for complex dialogs)
168 | generic_edit = run_script_dialog.Edit(found_index=0)
169 | if generic_edit.exists(timeout=1):
170 | file_name_edit = generic_edit
171 |
172 | if not file_name_edit:
173 | raise Exception("File name input field (Edit control) not found in 'Run Script' dialog. Searched by title 'File &name:', then by type/index.")
174 |
175 | file_name_edit.set_edit_text(script_file_path_for_dialog)
176 | time.sleep(0.3) # Pause after setting text
177 |
178 | ok_button: Optional[uia_controls.ButtonWrapper] = None
179 | # Attempt to find by common titles (OK, Run, Open) and control type
180 | potential_button = run_script_dialog.child_window(title_re="OK|Run|Open", control_type="Button")
181 | if potential_button.exists(timeout=1):
182 | ok_button = potential_button.wrapper_object()
183 | else:
184 | # Fallback: by control type and index (less reliable)
185 | potential_button_by_index = run_script_dialog.child_window(control_type="Button", found_index=0)
186 | if potential_button_by_index.exists(timeout=1):
187 | ok_button = potential_button_by_index.wrapper_object()
188 | else:
189 | # Fallback: generic Button control
190 | generic_button = run_script_dialog.Button(found_index=0)
191 | if generic_button.exists(timeout=1):
192 | ok_button = generic_button
193 |
194 | if not ok_button:
195 | raise Exception("OK/Run/Open button not found in 'Run Script' dialog. Searched by title regex 'OK|Run|Open', then by type/index.")
196 |
197 | ok_button.click_input()
198 |
199 | return f"Script submitted for execution via File->Run Script: {script_file_path}"
200 |
201 | except Exception as e_interact:
202 | error_detail = str(e_interact)
203 | print(f"Error interacting with 'Run Script' dialog controls: {error_detail}")
204 | if run_script_dialog and run_script_dialog.exists():
205 | try: run_script_dialog.close()
206 | except: pass # Best effort to close
207 | return f"Failed to interact with 'Run Script' dialog (e.g., input script path or click OK): {error_detail}"
208 |
209 | except timings.TimeoutError as e_timeout:
210 | abaqus_app_instance_cache = None
211 | abaqus_main_window_cache = None
212 | return f"Timeout occurred during GUI operation: {str(e_timeout)}"
213 | except Exception as e_main:
214 | abaqus_app_instance_cache = None
215 | abaqus_main_window_cache = None
216 | return f"An error occurred during script execution attempt: {str(e_main)}"
217 | finally:
218 | if script_file_path and os.path.exists(script_file_path):
219 | try: os.remove(script_file_path)
220 | except Exception as e_remove:
221 | print(f"Warning: Failed to delete temporary script file {script_file_path}: {e_remove}")
222 |
223 | @mcp.tool(
224 | name="get_abaqus_gui_message_log",
225 | description="Attempts to retrieve text from the Abaqus/CAE message/log area. Accuracy depends on UI structure and may require inspect tool information for reliable targeting of the message area control."
226 | )
227 | def get_abaqus_message_log() -> str:
228 | """
229 | Attempts to retrieve the text content from the Abaqus/CAE message/log area.
230 |
231 | This tool relies on GUI automation to find the Abaqus window and then heuristically
232 | searches for a UI element that represents the message/log display. The reliability
233 | of finding this element and extracting its full text can vary significantly based
234 | on the Abaqus version, specific UI configuration, and the complexity of the
235 | message area control (e.g., simple text vs. rich-text vs. custom widget).
236 |
237 | For robust operation, providing specific identifiers for the message area UI element
238 | (obtained via an inspect tool like py_inspect or FlaUInspect) is highly recommended.
239 | These identifiers (e.g., AutomationId, Name, ClassName) should be used to replace
240 | the heuristic search logic within this function for your specific Abaqus setup.
241 |
242 | Returns:
243 | str: A string containing the extracted text from the message area if found and readable.
244 | If the message area cannot be found or text cannot be extracted, an informative
245 | error or status message is returned.
246 | """
247 | global abaqus_main_window_cache, abaqus_app_instance_cache
248 | app, main_window = find_abaqus_window_and_app()
249 |
250 | if not main_window or not main_window.exists():
251 | abaqus_app_instance_cache = None
252 | abaqus_main_window_cache = None
253 | return "Abaqus/CAE window not found. Cannot retrieve message log."
254 |
255 | try:
256 | message_area_control: Optional[base_wrapper.BaseWrapper] = None
257 |
258 | # **Highly Recommended: Replace heuristic search below with specific identifiers from an inspect tool.**
259 | # Example using AutomationId (BEST choice if available and static):
260 | # try:
261 | # specific_control = main_window.child_window(automation_id="YourAbaqusMessageAreaAutomationId")
262 | # if specific_control.exists() and specific_control.is_visible():
263 | # message_area_control = specific_control.wrapper_object()
264 | # except Exception as e_specific_find:
265 | # print(f"Failed to find message area by specific AutomationId: {e_specific_find}")
266 |
267 | # Heuristic Search (if specific identifiers are not yet configured):
268 | if not message_area_control:
269 | # Heuristic 1: Look for large, visible Panes (often FXWindow in Abaqus for custom areas)
270 | possible_panes = main_window.descendants(control_type="Pane") # More general than just FXWindow initially
271 | for pane_spec in possible_panes:
272 | pane = pane_spec.wrapper_object() # Get the wrapper to access more properties
273 | if pane.is_visible() and pane.rectangle().height > 100 and pane.rectangle().width() > 200:
274 | # Additional check: class_name if it helps narrow down (e.g., "AfxMDIFrame")
275 | # Or if it's known to be an FXWindow that contains text.
276 | if "FXWindow" in pane.class_name(): # Check if it's an FXWindow as they often host content
277 | texts = pane.texts()
278 | if texts and any(line.strip() for group in texts if group for line in group):
279 | message_area_control = pane
280 | break
281 | if message_area_control: print("Message area found via Pane heuristic.")
282 |
283 | if not message_area_control:
284 | # Heuristic 2: Look for large, visible, read-only Edit controls
285 | possible_edits = main_window.descendants(control_type="Edit")
286 | for edit_spec in possible_edits:
287 | edit = edit_spec.wrapper_object()
288 | if edit.is_visible() and not edit.is_editable() and edit.rectangle().height > 50:
289 | texts = edit.texts()
290 | if texts and any(line.strip() for group in texts if group for line in group):
291 | message_area_control = edit
292 | break
293 | if message_area_control: print("Message area found via Edit heuristic.")
294 |
295 | # Add other specific fallbacks here if more UI details are known, e.g., based on Name or a deeper path.
296 | # Example: specific_control_by_name = main_window.child_window(title="Message Window", control_type="Document")
297 |
298 | if message_area_control and message_area_control.exists():
299 | log_content_lines = []
300 | raw_texts = message_area_control.texts() # .texts() often returns list of lists of strings for UIA
301 | for text_group in raw_texts:
302 | if text_group: # Ensure the group itself is not None or empty
303 | for line in text_group:
304 | if line: # Ensure the line string is not None or empty
305 | log_content_lines.append(line)
306 |
307 | log_content = "\n".join(log_content_lines).strip()
308 |
309 | if not log_content and hasattr(message_area_control, 'window_text'):
310 | log_content = message_area_control.window_text().strip()
311 |
312 | if log_content:
313 | return f"Message Log Content (best effort extraction):\n------------------------\n{log_content}\n------------------------"
314 | else:
315 | return ("Found a potential message area UI element, but could not extract text content. "
316 | "The control might be custom, empty, or text extraction method failed. More specific inspect details are needed.")
317 | else:
318 | return ("Message area UI element not found using current heuristics. "
319 | "For reliable message log retrieval, please use an inspect tool to identify the specific properties "
320 | "(e.g., AutomationId, Name, ClassName) of the Abaqus message area and update this function's search logic.")
321 |
322 | except Exception as e:
323 | abaqus_app_instance_cache = None
324 | abaqus_main_window_cache = None
325 | return f"An error occurred while trying to retrieve the Abaqus message log: {str(e)}"
326 |
327 | @mcp.prompt()
328 | def abaqus_scripting_strategy() -> str:
329 | """
330 | Defines the preferred strategy for interacting with an Abaqus/CAE GUI session
331 | using this MCP server. This prompt guides an LLM agent on how to effectively
332 | use the available tools for scripting and information retrieval.
333 | """
334 | return """When performing tasks in an Abaqus/CAE GUI session via this MCP server:
335 |
336 | 1. **Core Assumption:** This server interacts with an ALREADY RUNNING Abaqus/CAE GUI session. It does not start or stop Abaqus/CAE. Ensure the Abaqus/CAE application is open, responsive, and ideally the primary focused window when initiating tool calls.
337 |
338 | 2. **Executing Python Scripts (`execute_script_in_abaqus_gui` tool):
339 | * **Purpose:** Use this tool to run custom Python scripts within the Abaqus/CAE environment.
340 | * **Input:** Provide the complete Python script as a string to the `python_code` argument. This script should contain valid Abaqus Scripting Interface (ASI) commands. Ensure the script is self-contained or that any required models/files are already loaded or accessible within the Abaqus session as the script would expect.
341 | * **Mechanism:** This tool automates the 'File -> Run Script...' menu selection and dialog interaction in the Abaqus GUI.
342 | * **Return Value:** The tool returns a string message indicating that the script was *submitted* to the Abaqus GUI. It does NOT return the direct output (e.g., print statements from your script) or catch Python exceptions raised *within* the Abaqus script's execution context.
343 | * **Idempotency:** This tool is not idempotent. Calling it multiple times with the same script will execute the script multiple times in Abaqus.
344 | * **Checking Script Outcome:** After submitting a script, it is CRUCIAL to use the `get_abaqus_gui_message_log` tool to check the Abaqus message area. This is where you will find:
345 | * Confirmation of script completion (often specific messages printed by Abaqus itself).
346 | * Any `print()` statements from your Python script.
347 | * Error messages or warnings generated by Abaqus or your script if it failed or encountered issues.
348 |
349 | 3. **Retrieving Abaqus GUI Messages (`get_abaqus_gui_message_log` tool):
350 | * **Purpose:** Use this tool to fetch the text content from the Abaqus/CAE message/log area (often at the bottom of the main window).
351 | * **Primary Use Cases:**
352 | * To verify the outcome of scripts run via `execute_script_in_abaqus_gui`.
353 | * To check for general Abaqus status messages, warnings, or errors that may have occurred during manual or scripted operations.
354 | * **Reliability Note:** This tool attempts to scrape text from a GUI element. Its accuracy can depend on the specific Abaqus version and UI configuration. The current implementation uses heuristics to find the message area. If it fails to retrieve the log accurately or completely, the server's GUI interaction logic for this tool might need adjustment based on specific UI element identifiers (e.g., AutomationId, Name, ClassName) obtained from an inspect tool for your Abaqus environment.
355 |
356 | 4. **Recommended Workflow for Script Execution & Verification:**
357 | a. Ensure the Abaqus/CAE GUI is running, visible, and in a stable state (e.g., no blocking modal dialogs other than those expected by the tools).
358 | b. Formulate the Abaqus Python script (ASI commands) you want to run.
359 | c. Call `execute_script_in_abaqus_gui` with your script string.
360 | d. Note the confirmation message (script submitted).
361 | e. Wait a reasonable amount of time for the script to likely execute within Abaqus. This duration depends heavily on the script's complexity and the operations it performs.
362 | f. Call `get_abaqus_gui_message_log`.
363 | g. Carefully examine the returned string from `get_abaqus_gui_message_log` to understand the actual outcome of your script, including any errors or messages it printed.
364 |
365 | 5. **Troubleshooting GUI Interaction and Best Practices:**
366 | * **Window State:** Ensure the Abaqus/CAE window is not minimized when initiating actions. The tools attempt to restore and focus, but an already active window is best.
367 | * **Modal Dialogs:** Avoid having unexpected modal dialogs open in Abaqus, as they can block the GUI automation tools.
368 | * **Tool Failures:** If `execute_script_in_abaqus_gui` fails (e.g., cannot find dialogs/controls), it might indicate an unexpected Abaqus state, a change in UI structure, or the Abaqus window being unresponsive. The `get_abaqus_gui_message_log` (if it can run) might offer clues. Otherwise, manual inspection of the Abaqus GUI will be necessary.
369 | * **Script Errors vs. Tool Errors:** Differentiate between errors returned by the MCP tools (e.g., "dialog not found") and errors that appear in the Abaqus message log (which are errors from your script's execution within Abaqus).
370 | """
371 |
372 | if __name__ == "__main__":
373 | mcp.run()
```