labmcp-ika 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .DS_Store
10
+ *.jsonl
11
+ !**/fixtures/*.jsonl
12
+ .idea/
13
+ .vscode/
14
+
15
+ CLAUDE.md
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.5
2
+ Name: labmcp-ika
3
+ Version: 0.1.0
4
+ Summary: MCP server for IKA hotplate stirrers and overhead stirrers (NAMUR commands): heat, stir, watchdog, wait for temperature.
5
+ Project-URL: Homepage, https://github.com/K-Dense-AI/lab-instrument-mcps/tree/main/servers/chemistry/ika-stirrer
6
+ Author: K-Dense and LabMCP contributors
7
+ License-Expression: Apache-2.0
8
+ Keywords: hotplate,ika,lab-instrument,magnetic-stirrer,mcp,namur,overhead-stirrer
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: labmcp<0.2,>=0.1
11
+ Description-Content-Type: text/markdown
12
+
13
+ # IKA Hotplate & Overhead Stirrer โ€” MCP Server
14
+
15
+ <!-- mcp-name: io.github.K-Dense-AI/labmcp-ika -->
16
+
17
+ Let an AI agent heat and stir with **IKA hotplate stirrers** (C-MAG HS control, IKA Plate / RCT digital) and drive **IKA overhead stirrers** (EUROSTAR control) through IKA's documented **NAMUR** interface commands: read plate, probe and medium temperatures, speed and torque; set and start heating and stirring; wait for a temperature; and arm the hotplate's hardware watchdog so it switches itself off if the computer stops talking to it.
18
+
19
+ | | |
20
+ |---|---|
21
+ | **Package** | `labmcp-ika` |
22
+ | **Instruments** | Hotplate stirrers: C-MAG HS 7 control, IKA Plate (RCT digital). Overhead stirrers (`--option device=overhead`): EUROSTAR 60 control, EUROSTAR 100 control. Other IKA devices with the same NAMUR set are likely to work but are unverified |
23
+ | **Interfaces** | RS-232 (9-pin, PC 1.1 cable), USB (virtual COM port) |
24
+ | **Protocol** | IKA NAMUR commands, "Interfaces and outputs" section of the operating instructions: [IKA Plate (RCT digital)](https://shop.textalk.se/shop/ws72/72372/art12/156948912-a58526-20000015643_20014388a_A2_IKA_Plate_112018_web.pdf) (11/2018), [C-MAG HS 7 control](https://shop.textalk.se/shop/ws72/72372/art16/156948916-cc53a2-20000015641_20014381a_A2_C-MAG_HS_7_control_112018_web.pdf) (11/2018), [EUROSTAR 60/100 control](https://www.wolflabs.co.uk/documents/IKA_overhead-stirrers_Eurostar-60-100-control_manual.pdf) (20000003965c). Official downloads: [ika.com](https://www.ika.com/en/Products-LabEq/Magnetic-Stirrers-pg188/C-MAG-HS-7-control-20002694/Downloads-cpdl.html) |
25
+ | **Status** | ๐Ÿงช **simulated**: tested against a wire-level NAMUR simulator, not yet verified on hardware. [Report a hardware test](https://github.com/K-Dense-AI/lab-instrument-mcps/issues/new?template=hardware-verification.yml) |
26
+
27
+ ## Try it without hardware
28
+
29
+ ```bash
30
+ uvx labmcp-ika --simulate --check
31
+ uvx labmcp-ika --simulate --option device=overhead --check # EUROSTAR simulator
32
+ ```
33
+
34
+ ## Connect your stirrer
35
+
36
+ 1. **Cable:** USB Aโ€“B (the stirrer appears as a virtual COM port; on Windows install IKA's USB driver, linked in the manual) or the 9-pin RS-232 PC 1.1 cable. No setting on the device is needed. The interface is fixed at **9600 baud, 7 data bits, even parity, 1 stop bit, no flow control**, and every command ends with a blank + CR LF.
37
+ 2. **Find the port:** `uvx labmcp ports`
38
+ 3. **Test the connection:**
39
+ ```bash
40
+ uvx labmcp-ika --address /dev/ttyACM0 --check # Linux, hotplate
41
+ uvx labmcp-ika --address /dev/tty.usbmodem14101 --check # macOS
42
+ uvx labmcp-ika --address COM5 --check # Windows
43
+ uvx labmcp-ika --address COM6 --option device=overhead --check # EUROSTAR
44
+ ```
45
+ `--check` prints the name the device reports (`IN_NAME`).
46
+
47
+ ## Add to your MCP client
48
+
49
+ **Claude Code**
50
+ ```bash
51
+ claude mcp add ika -- uvx labmcp-ika --address /dev/ttyACM0 --limit max_temperature_c=120
52
+ ```
53
+
54
+ **Claude Desktop / Cursor / Windsurf** (`claude_desktop_config.json`, `.cursor/mcp.json`, โ€ฆ)
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "ika": {
59
+ "command": "uvx",
60
+ "args": ["labmcp-ika", "--address", "/dev/ttyACM0", "--limit", "max_temperature_c=120"]
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ Add `--read-only` to allow monitoring only (the stop tools stay available). For other clients: `uvx labmcp config ika-stirrer --address /dev/ttyACM0 --client vscode`.
67
+
68
+ ## Tools
69
+
70
+ <!-- TOOLS:START -->
71
+ | Tool | Kind | Description |
72
+ |---|---|---|
73
+ | `disable_watchdog` | ๐ŸŽ› control | Cancel watchdog mode 2 (OUT_WD2@0) and stop the background refresh. Watchdog mode 1 has no cancel command; this tool reports an error for it. |
74
+ | `enable_watchdog` | ๐ŸŽ› control | Arm the hotplate's communication watchdog (OUT_WD1@m / OUT_WD2@m). The server then re-sends the watchdog command in the background; if the computer, this server or the cable fails, the hotplate switches heating and stirring off (mode 1) or falls back to the given safety values (mode 2) after `timeout_s`. Note: stopping or reconnecting the server also stops the refresh, so the watchdog trips unless you enable it again. Mode 1 cannot be cancelled over the interface. |
75
+ | `get_command_log` | ๐Ÿ‘ read | Return the most recent raw commands sent to / replies received from the instrument (newest last). Useful for debugging and for recording what was done. |
76
+ | `get_connection_info` | ๐Ÿ‘ read | Report which instrument is connected (identity, address, simulated or real), whether the server is read-only, and the active safety limits. Call this first. |
77
+ | `get_status` | ๐Ÿ‘ read | Read temperatures, stirring speed and setpoints. On hotplates: plate and external-probe temperature, temperature setpoint and the device's safety-circuit temperature. On overhead stirrers: PT1000 probe temperature, torque and the speed/torque limits set on the device. |
78
+ | `reconnect` | ๐Ÿ›‘ safety | Close and re-open the connection to the instrument (e.g. after it was power cycled or a cable was re-plugged). |
79
+ | `set_speed` | โš ๏ธ hazard | Set the stirring speed setpoint (OUT_SP_4). Does not start the motor; if it is already running, the speed changes immediately. Refused above `max_speed_rpm` (and, on overhead stirrers, above the speed limit set on the device). |
80
+ | `set_temperature` | โš ๏ธ hazard | Set the hotplate temperature setpoint (OUT_SP_1). With an external probe connected this is the target temperature of the medium. Does not start heating; if heating is already on, the plate starts moving to the new setpoint immediately. Refused above `max_temperature_c` or above the device's own safety-circuit temperature. |
81
+ | `start_heating` | โš ๏ธ hazard | Switch the hotplate heater on (START_1). It heats towards the current setpoint, which is checked against `max_temperature_c` first. Make sure the vessel and its contents can take the setpoint temperature and that nothing flammable is near the plate. |
82
+ | `start_stirring` | โš ๏ธ hazard | Start the stirring motor (START_4) at the current speed setpoint, which is checked against `max_speed_rpm` first. Make sure the stir bar or impeller is in place and the vessel is secured (a decoupled stir bar or an unclamped vessel can splash). |
83
+ | `stop_all` | ๐Ÿ›‘ safety | Emergency stop: switch the heater off (hotplates) and stop the motor. Every stop command is sent even if one of them fails. Also ends a running `wait_for_temperature`. |
84
+ | `stop_heating` | ๐Ÿ›‘ safety | Switch the hotplate heater off (STOP_1). Stirring continues. The plate stays hot for a long time after switching off. |
85
+ | `stop_stirring` | ๐Ÿ›‘ safety | Stop the stirring motor (STOP_4). Heating (if on) continues. |
86
+ | `wait_for_temperature` | ๐Ÿ‘ read | Poll a temperature until it is within `tolerance_c` of the target for `stable_for_s` seconds, or until `timeout_s` passes. Does not change anything on the device (start heating first). Returns whether the target was reached plus a short temperature trace. A stop command ends the wait early. |
87
+ <!-- TOOLS:END -->
88
+
89
+ ## Safety limits
90
+
91
+ | Limit | Default | Meaning |
92
+ |---|---|---|
93
+ | `max_temperature_c` | 150 ยฐC | Highest hotplate setpoint an agent may set (also checked before `start_heating`, and for the watchdog fallback temperature) |
94
+ | `max_speed_rpm` | 1000 rpm | Highest stirring speed an agent may set (also checked before `start_stirring`) |
95
+ | `max_wait_s` | 3600 s | Longest `wait_for_temperature` |
96
+
97
+ Override at launch, e.g. `--limit max_temperature_c=250 --limit max_speed_rpm=1500`. On top of these, the hotplate refuses setpoints above its own safety-circuit temperature (set with the screwdriver dial on the device, read with `IN_SP_3`), and overhead stirrers refuse speeds above the speed limit set in the device menu (`IN_SP_6`); the server checks both before sending.
98
+
99
+ ## Example prompts
100
+
101
+ - "Heat the round-bottom flask to 65 ยฐC using the external probe, stir at 400 rpm, and tell me when it has been stable for a minute."
102
+ - "Arm the watchdog with 60 s, then start heating to 80 ยฐC. I'm leaving it running overnight."
103
+ - "Log the medium temperature every 30 s while it cools down and tell me when it is below 30 ยฐC."
104
+ - "Ramp the EUROSTAR up to 300 rpm and report the torque; the mixture is getting viscous."
105
+ - "Something smells wrong, stop everything now."
106
+
107
+ ## Notes
108
+
109
+ - **Two device types.** The default is `--option device=hotplate`. Overhead stirrers need `--option device=overhead`; they have no heater, report their PT1000 probe on `IN_PV_3` and torque on `IN_PV_5`. Tools that don't apply to the configured type return a clear error and send nothing.
110
+ - **No state query for the heater.** The NAMUR set has no "is the heater on?" command, so `get_status` reports the last start/stop *this server* sent (`heating_commanded`, `stirring_commanded`) and `None` when unknown. The front-panel buttons can change it without the server knowing.
111
+ - **Setpoints are read back** after `set_temperature` / `set_speed` (`IN_SP_1` / `IN_SP_4`), and a `warning` is returned if the device reports a different value.
112
+ - **Watchdog** (hotplates, `OUT_WD1@m` / `OUT_WD2@m`, 20โ€“1500 s): `enable_watchdog` arms it and the server re-sends the command in the background every quarter of the watchdog time. If the server or the computer dies or the cable is pulled, the hotplate switches heating and stirring off (mode 1) or falls back to the safety temperature and speed (mode 2). **Stopping or reconnecting the server also stops the refresh**, so the watchdog trips. Mode 1 has no documented cancel command; mode 2 is cancelled with `OUT_WD2@0`. The manual's watchdog error codes differ between sections (ER 2 vs Err. 1/Err. 2); a trip is visible on the display, not over the interface.
113
+ - **Unverified reply layout.** The IKA manuals document the commands but not the exact reply line. The driver takes the first number on the line (devices are reported to append the channel, e.g. `25.3 2`), so both forms work. The value reported by `IN_PV_1` when no external probe is plugged in is not documented; ignore `external_temperature_c` if you have no probe.
114
+ - The IKA manuals print the terminator as "Blank CR LF (hex 0x20 0x0d 0x20 0x0A)"; the extra 0x20 contradicts the text ("Blank CR LF") and is treated as a typo, so the server sends `0x20 0x0D 0x0A`.
115
+ - Not implemented: `SET_MODE_n` (changes the power-on operating mode A/B/D, a persistent setting) and `RESET`; overhead-stirrer torque/speed-limit writes (`OUT_SP_5/6/8`) and direction change (`OUT_MODE_n`).
116
+
117
+ ## Hardware verification
118
+
119
+ | Model | Firmware | Interface | Verified by | Date |
120
+ |---|---|---|---|---|
121
+ | *none yet: [be the first](https://github.com/K-Dense-AI/lab-instrument-mcps/issues/new?template=hardware-verification.yml)* | | | | |
@@ -0,0 +1,109 @@
1
+ # IKA Hotplate & Overhead Stirrer โ€” MCP Server
2
+
3
+ <!-- mcp-name: io.github.K-Dense-AI/labmcp-ika -->
4
+
5
+ Let an AI agent heat and stir with **IKA hotplate stirrers** (C-MAG HS control, IKA Plate / RCT digital) and drive **IKA overhead stirrers** (EUROSTAR control) through IKA's documented **NAMUR** interface commands: read plate, probe and medium temperatures, speed and torque; set and start heating and stirring; wait for a temperature; and arm the hotplate's hardware watchdog so it switches itself off if the computer stops talking to it.
6
+
7
+ | | |
8
+ |---|---|
9
+ | **Package** | `labmcp-ika` |
10
+ | **Instruments** | Hotplate stirrers: C-MAG HS 7 control, IKA Plate (RCT digital). Overhead stirrers (`--option device=overhead`): EUROSTAR 60 control, EUROSTAR 100 control. Other IKA devices with the same NAMUR set are likely to work but are unverified |
11
+ | **Interfaces** | RS-232 (9-pin, PC 1.1 cable), USB (virtual COM port) |
12
+ | **Protocol** | IKA NAMUR commands, "Interfaces and outputs" section of the operating instructions: [IKA Plate (RCT digital)](https://shop.textalk.se/shop/ws72/72372/art12/156948912-a58526-20000015643_20014388a_A2_IKA_Plate_112018_web.pdf) (11/2018), [C-MAG HS 7 control](https://shop.textalk.se/shop/ws72/72372/art16/156948916-cc53a2-20000015641_20014381a_A2_C-MAG_HS_7_control_112018_web.pdf) (11/2018), [EUROSTAR 60/100 control](https://www.wolflabs.co.uk/documents/IKA_overhead-stirrers_Eurostar-60-100-control_manual.pdf) (20000003965c). Official downloads: [ika.com](https://www.ika.com/en/Products-LabEq/Magnetic-Stirrers-pg188/C-MAG-HS-7-control-20002694/Downloads-cpdl.html) |
13
+ | **Status** | ๐Ÿงช **simulated**: tested against a wire-level NAMUR simulator, not yet verified on hardware. [Report a hardware test](https://github.com/K-Dense-AI/lab-instrument-mcps/issues/new?template=hardware-verification.yml) |
14
+
15
+ ## Try it without hardware
16
+
17
+ ```bash
18
+ uvx labmcp-ika --simulate --check
19
+ uvx labmcp-ika --simulate --option device=overhead --check # EUROSTAR simulator
20
+ ```
21
+
22
+ ## Connect your stirrer
23
+
24
+ 1. **Cable:** USB Aโ€“B (the stirrer appears as a virtual COM port; on Windows install IKA's USB driver, linked in the manual) or the 9-pin RS-232 PC 1.1 cable. No setting on the device is needed. The interface is fixed at **9600 baud, 7 data bits, even parity, 1 stop bit, no flow control**, and every command ends with a blank + CR LF.
25
+ 2. **Find the port:** `uvx labmcp ports`
26
+ 3. **Test the connection:**
27
+ ```bash
28
+ uvx labmcp-ika --address /dev/ttyACM0 --check # Linux, hotplate
29
+ uvx labmcp-ika --address /dev/tty.usbmodem14101 --check # macOS
30
+ uvx labmcp-ika --address COM5 --check # Windows
31
+ uvx labmcp-ika --address COM6 --option device=overhead --check # EUROSTAR
32
+ ```
33
+ `--check` prints the name the device reports (`IN_NAME`).
34
+
35
+ ## Add to your MCP client
36
+
37
+ **Claude Code**
38
+ ```bash
39
+ claude mcp add ika -- uvx labmcp-ika --address /dev/ttyACM0 --limit max_temperature_c=120
40
+ ```
41
+
42
+ **Claude Desktop / Cursor / Windsurf** (`claude_desktop_config.json`, `.cursor/mcp.json`, โ€ฆ)
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "ika": {
47
+ "command": "uvx",
48
+ "args": ["labmcp-ika", "--address", "/dev/ttyACM0", "--limit", "max_temperature_c=120"]
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ Add `--read-only` to allow monitoring only (the stop tools stay available). For other clients: `uvx labmcp config ika-stirrer --address /dev/ttyACM0 --client vscode`.
55
+
56
+ ## Tools
57
+
58
+ <!-- TOOLS:START -->
59
+ | Tool | Kind | Description |
60
+ |---|---|---|
61
+ | `disable_watchdog` | ๐ŸŽ› control | Cancel watchdog mode 2 (OUT_WD2@0) and stop the background refresh. Watchdog mode 1 has no cancel command; this tool reports an error for it. |
62
+ | `enable_watchdog` | ๐ŸŽ› control | Arm the hotplate's communication watchdog (OUT_WD1@m / OUT_WD2@m). The server then re-sends the watchdog command in the background; if the computer, this server or the cable fails, the hotplate switches heating and stirring off (mode 1) or falls back to the given safety values (mode 2) after `timeout_s`. Note: stopping or reconnecting the server also stops the refresh, so the watchdog trips unless you enable it again. Mode 1 cannot be cancelled over the interface. |
63
+ | `get_command_log` | ๐Ÿ‘ read | Return the most recent raw commands sent to / replies received from the instrument (newest last). Useful for debugging and for recording what was done. |
64
+ | `get_connection_info` | ๐Ÿ‘ read | Report which instrument is connected (identity, address, simulated or real), whether the server is read-only, and the active safety limits. Call this first. |
65
+ | `get_status` | ๐Ÿ‘ read | Read temperatures, stirring speed and setpoints. On hotplates: plate and external-probe temperature, temperature setpoint and the device's safety-circuit temperature. On overhead stirrers: PT1000 probe temperature, torque and the speed/torque limits set on the device. |
66
+ | `reconnect` | ๐Ÿ›‘ safety | Close and re-open the connection to the instrument (e.g. after it was power cycled or a cable was re-plugged). |
67
+ | `set_speed` | โš ๏ธ hazard | Set the stirring speed setpoint (OUT_SP_4). Does not start the motor; if it is already running, the speed changes immediately. Refused above `max_speed_rpm` (and, on overhead stirrers, above the speed limit set on the device). |
68
+ | `set_temperature` | โš ๏ธ hazard | Set the hotplate temperature setpoint (OUT_SP_1). With an external probe connected this is the target temperature of the medium. Does not start heating; if heating is already on, the plate starts moving to the new setpoint immediately. Refused above `max_temperature_c` or above the device's own safety-circuit temperature. |
69
+ | `start_heating` | โš ๏ธ hazard | Switch the hotplate heater on (START_1). It heats towards the current setpoint, which is checked against `max_temperature_c` first. Make sure the vessel and its contents can take the setpoint temperature and that nothing flammable is near the plate. |
70
+ | `start_stirring` | โš ๏ธ hazard | Start the stirring motor (START_4) at the current speed setpoint, which is checked against `max_speed_rpm` first. Make sure the stir bar or impeller is in place and the vessel is secured (a decoupled stir bar or an unclamped vessel can splash). |
71
+ | `stop_all` | ๐Ÿ›‘ safety | Emergency stop: switch the heater off (hotplates) and stop the motor. Every stop command is sent even if one of them fails. Also ends a running `wait_for_temperature`. |
72
+ | `stop_heating` | ๐Ÿ›‘ safety | Switch the hotplate heater off (STOP_1). Stirring continues. The plate stays hot for a long time after switching off. |
73
+ | `stop_stirring` | ๐Ÿ›‘ safety | Stop the stirring motor (STOP_4). Heating (if on) continues. |
74
+ | `wait_for_temperature` | ๐Ÿ‘ read | Poll a temperature until it is within `tolerance_c` of the target for `stable_for_s` seconds, or until `timeout_s` passes. Does not change anything on the device (start heating first). Returns whether the target was reached plus a short temperature trace. A stop command ends the wait early. |
75
+ <!-- TOOLS:END -->
76
+
77
+ ## Safety limits
78
+
79
+ | Limit | Default | Meaning |
80
+ |---|---|---|
81
+ | `max_temperature_c` | 150 ยฐC | Highest hotplate setpoint an agent may set (also checked before `start_heating`, and for the watchdog fallback temperature) |
82
+ | `max_speed_rpm` | 1000 rpm | Highest stirring speed an agent may set (also checked before `start_stirring`) |
83
+ | `max_wait_s` | 3600 s | Longest `wait_for_temperature` |
84
+
85
+ Override at launch, e.g. `--limit max_temperature_c=250 --limit max_speed_rpm=1500`. On top of these, the hotplate refuses setpoints above its own safety-circuit temperature (set with the screwdriver dial on the device, read with `IN_SP_3`), and overhead stirrers refuse speeds above the speed limit set in the device menu (`IN_SP_6`); the server checks both before sending.
86
+
87
+ ## Example prompts
88
+
89
+ - "Heat the round-bottom flask to 65 ยฐC using the external probe, stir at 400 rpm, and tell me when it has been stable for a minute."
90
+ - "Arm the watchdog with 60 s, then start heating to 80 ยฐC. I'm leaving it running overnight."
91
+ - "Log the medium temperature every 30 s while it cools down and tell me when it is below 30 ยฐC."
92
+ - "Ramp the EUROSTAR up to 300 rpm and report the torque; the mixture is getting viscous."
93
+ - "Something smells wrong, stop everything now."
94
+
95
+ ## Notes
96
+
97
+ - **Two device types.** The default is `--option device=hotplate`. Overhead stirrers need `--option device=overhead`; they have no heater, report their PT1000 probe on `IN_PV_3` and torque on `IN_PV_5`. Tools that don't apply to the configured type return a clear error and send nothing.
98
+ - **No state query for the heater.** The NAMUR set has no "is the heater on?" command, so `get_status` reports the last start/stop *this server* sent (`heating_commanded`, `stirring_commanded`) and `None` when unknown. The front-panel buttons can change it without the server knowing.
99
+ - **Setpoints are read back** after `set_temperature` / `set_speed` (`IN_SP_1` / `IN_SP_4`), and a `warning` is returned if the device reports a different value.
100
+ - **Watchdog** (hotplates, `OUT_WD1@m` / `OUT_WD2@m`, 20โ€“1500 s): `enable_watchdog` arms it and the server re-sends the command in the background every quarter of the watchdog time. If the server or the computer dies or the cable is pulled, the hotplate switches heating and stirring off (mode 1) or falls back to the safety temperature and speed (mode 2). **Stopping or reconnecting the server also stops the refresh**, so the watchdog trips. Mode 1 has no documented cancel command; mode 2 is cancelled with `OUT_WD2@0`. The manual's watchdog error codes differ between sections (ER 2 vs Err. 1/Err. 2); a trip is visible on the display, not over the interface.
101
+ - **Unverified reply layout.** The IKA manuals document the commands but not the exact reply line. The driver takes the first number on the line (devices are reported to append the channel, e.g. `25.3 2`), so both forms work. The value reported by `IN_PV_1` when no external probe is plugged in is not documented; ignore `external_temperature_c` if you have no probe.
102
+ - The IKA manuals print the terminator as "Blank CR LF (hex 0x20 0x0d 0x20 0x0A)"; the extra 0x20 contradicts the text ("Blank CR LF") and is treated as a typo, so the server sends `0x20 0x0D 0x0A`.
103
+ - Not implemented: `SET_MODE_n` (changes the power-on operating mode A/B/D, a persistent setting) and `RESET`; overhead-stirrer torque/speed-limit writes (`OUT_SP_5/6/8`) and direction change (`OUT_MODE_n`).
104
+
105
+ ## Hardware verification
106
+
107
+ | Model | Firmware | Interface | Verified by | Date |
108
+ |---|---|---|---|---|
109
+ | *none yet: [be the first](https://github.com/K-Dense-AI/lab-instrument-mcps/issues/new?template=hardware-verification.yml)* | | | | |
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "labmcp-ika"
3
+ version = "0.1.0"
4
+ description = "MCP server for IKA hotplate stirrers and overhead stirrers (NAMUR commands): heat, stir, watchdog, wait for temperature."
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.10"
8
+ authors = [{ name = "K-Dense and LabMCP contributors" }]
9
+ keywords = ["mcp", "lab-instrument", "ika", "hotplate", "magnetic-stirrer", "overhead-stirrer", "namur"]
10
+ dependencies = ["labmcp>=0.1,<0.2"]
11
+
12
+ [project.scripts]
13
+ labmcp-ika = "labmcp_ika.server:main"
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/K-Dense-AI/lab-instrument-mcps/tree/main/servers/chemistry/ika-stirrer"
17
+
18
+ [tool.labmcp]
19
+ name = "IKA Hotplate & Overhead Stirrer"
20
+ domain = "chemistry"
21
+ category = "Heating & stirring"
22
+ vendor = "IKA"
23
+ models = ["C-MAG HS 7 control", "IKA Plate (RCT digital)", "EUROSTAR 60 control", "EUROSTAR 100 control"]
24
+ interfaces = ["RS-232", "USB (virtual COM)"]
25
+ protocol = "IKA NAMUR command set"
26
+ summary = "Read plate/probe temperature and speed, set and start heating and stirring, hardware watchdog, wait for temperature, stop all."
27
+ status = "simulated"
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/labmcp_ika"]
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.K-Dense-AI/labmcp-ika",
4
+ "title": "IKA Hotplate & Overhead Stirrer",
5
+ "description": "MCP server for IKA hotplate stirrers and overhead stirrers (NAMUR commands): heat, stir, watchdog, w",
6
+ "version": "0.1.0",
7
+ "repository": {
8
+ "url": "https://github.com/K-Dense-AI/lab-instrument-mcps",
9
+ "source": "github",
10
+ "subfolder": "servers/chemistry/ika-stirrer"
11
+ },
12
+ "websiteUrl": "https://github.com/K-Dense-AI/lab-instrument-mcps/tree/main/servers/chemistry/ika-stirrer",
13
+ "packages": [
14
+ {
15
+ "registryType": "pypi",
16
+ "registryBaseUrl": "https://pypi.org",
17
+ "identifier": "labmcp-ika",
18
+ "version": "0.1.0",
19
+ "transport": {
20
+ "type": "stdio"
21
+ },
22
+ "environmentVariables": [
23
+ {
24
+ "name": "LABMCP_ADDRESS",
25
+ "description": "Instrument address, e.g. serial:///dev/ttyUSB0 or tcp://192.168.1.50:5025",
26
+ "isRequired": false
27
+ },
28
+ {
29
+ "name": "LABMCP_SIMULATE",
30
+ "description": "Set to 1 to use the built-in simulator (no hardware)",
31
+ "isRequired": false
32
+ },
33
+ {
34
+ "name": "LABMCP_READ_ONLY",
35
+ "description": "Set to 1 to disable all state-changing tools",
36
+ "isRequired": false
37
+ },
38
+ {
39
+ "name": "LABMCP_LIMITS",
40
+ "description": "Safety limit overrides, e.g. max_temperature_c=80",
41
+ "isRequired": false
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1 @@
1
+ """LabMCP server for IKA hotplate stirrers and overhead stirrers (NAMUR commands)."""
@@ -0,0 +1,310 @@
1
+ """IKA NAMUR driver for hotplate stirrers and overhead stirrers.
2
+
3
+ Commands, serial settings and terminators were verified in the "Interfaces and outputs"
4
+ section of these IKA operating instructions:
5
+
6
+ * IKA Plate (RCT digital), 11/2018 (print code 20014388),
7
+ https://shop.textalk.se/shop/ws72/72372/art12/156948912-a58526-20000015643_20014388a_A2_IKA_Plate_112018_web.pdf
8
+ * C-MAG HS 7 control, 11/2018 (print code 20014381a),
9
+ https://shop.textalk.se/shop/ws72/72372/art16/156948916-cc53a2-20000015641_20014381a_A2_C-MAG_HS_7_control_112018_web.pdf
10
+ * EUROSTAR 60 control / EUROSTAR 100 control (20000003965c, 03/2018),
11
+ https://www.wolflabs.co.uk/documents/IKA_overhead-stirrers_Eurostar-60-100-control_manual.pdf
12
+
13
+ Wire format (identical in all three manuals): 9600 baud, 7 data bits, even parity, 1 stop
14
+ bit, no flow control. Commands are upper case, command and parameter are separated by a
15
+ space, and every command and every response ends with "blank CR LF" (0x20 0x0D 0x0A).
16
+ The device only transmits when asked: ``IN_*`` commands return one line, ``START_*``,
17
+ ``STOP_*`` and ``OUT_SP_n`` return nothing, and the ``@`` variants (``OUT_SP_12@n``,
18
+ ``OUT_SP_42@n``, ``OUT_WDx@m``) echo the value. The manuals do not document the exact
19
+ layout of the reply line, so numeric replies are parsed as "first number on the line"
20
+ (real devices are reported to append the channel number, e.g. ``25.3 2``).
21
+
22
+ Channel numbers: 1 = external temperature sensor, 2 = hotplate, 3 = PT1000 probe
23
+ (overhead stirrers), 4 = stirring speed, 5 = viscosity trend (hotplate) / torque (overhead).
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ import re
30
+ import threading
31
+
32
+ from labmcp import InstrumentError, InstrumentProtocolError, Transport
33
+
34
+ log = logging.getLogger("labmcp.ika")
35
+
36
+ HOTPLATE = "hotplate"
37
+ OVERHEAD = "overhead"
38
+ DEVICE_TYPES = (HOTPLATE, OVERHEAD)
39
+
40
+ #: Watchdog time range documented for OUT_WD1@m / OUT_WD2@m.
41
+ WATCHDOG_MIN_S = 20
42
+ WATCHDOG_MAX_S = 1500
43
+
44
+ _NUMBER_RE = re.compile(r"^\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+))(?:\s+\S+)*\s*$")
45
+
46
+
47
+ class IKAStirrer:
48
+ """An IKA hotplate stirrer (``device_type="hotplate"``) or overhead stirrer (``"overhead"``)."""
49
+
50
+ def __init__(self, transport: Transport, device_type: str = HOTPLATE) -> None:
51
+ if device_type not in DEVICE_TYPES:
52
+ raise ValueError(f"device_type must be one of {DEVICE_TYPES}, got {device_type!r}")
53
+ self.t = transport
54
+ self.device_type = device_type
55
+ # The NAMUR set has no "is the heater on?" query, so remember what we commanded.
56
+ self.heating_commanded: bool | None = None
57
+ self.stirring_commanded: bool | None = None
58
+ #: Set by stop commands so a running wait_for_temperature loop ends early.
59
+ self.abort = threading.Event()
60
+ self._wd_mode: int | None = None
61
+ self._wd_time_s: int | None = None
62
+ self._wd_stop = threading.Event()
63
+ self._wd_thread: threading.Thread | None = None
64
+ self.watchdog_last_error: str | None = None
65
+
66
+ # ------------------------------------------------------------ low level
67
+
68
+ def query(self, cmd: str, timeout: float | None = None) -> str:
69
+ """Send an ``IN_*`` command and return the reply line (stripped)."""
70
+ with self.t.lock:
71
+ # The device never sends unsolicited data; anything waiting is stale.
72
+ self.t.flush_input()
73
+ reply = self.t.query(cmd, timeout).strip()
74
+ if not reply:
75
+ raise InstrumentProtocolError(f"IKA device sent an empty reply to {cmd!r}.")
76
+ return reply
77
+
78
+ def send(self, cmd: str) -> None:
79
+ """Send a command that has no reply (``OUT_SP_n x``, ``START_n``, ``STOP_n``)."""
80
+ self.t.write(cmd)
81
+
82
+ def query_number(self, cmd: str) -> float:
83
+ reply = self.query(cmd)
84
+ m = _NUMBER_RE.match(reply)
85
+ if not m:
86
+ raise InstrumentProtocolError(
87
+ f"IKA device replied {reply!r} to {cmd!r}; expected a number. Check that the "
88
+ f"server's device type ({self.device_type}) matches the instrument."
89
+ )
90
+ return float(m.group(1))
91
+
92
+ def _echo(self, cmd: str, expected: float) -> float:
93
+ """Send an ``@`` command and check the echoed value."""
94
+ value = self.query_number(cmd)
95
+ if abs(value - expected) > 0.5:
96
+ raise InstrumentProtocolError(
97
+ f"IKA device echoed {value:g} to {cmd!r}; expected {expected:g}. The value may be "
98
+ "outside the range the device accepts."
99
+ )
100
+ return value
101
+
102
+ def _require(self, device_type: str, what: str) -> None:
103
+ if self.device_type != device_type:
104
+ raise InstrumentProtocolError(
105
+ f"{what} is only available on IKA {device_type} devices, but this server is "
106
+ f"configured for a {self.device_type} (start it with --option device={device_type} "
107
+ "if that is wrong)."
108
+ )
109
+
110
+ @staticmethod
111
+ def _fmt(value: float) -> str:
112
+ return f"{round(value, 1):g}"
113
+
114
+ # ------------------------------------------------------------ identity
115
+
116
+ def identify(self) -> dict[str, str]:
117
+ info = {"manufacturer": "IKA", "device_type": self.device_type}
118
+ try:
119
+ info["model"] = self.query("IN_NAME")
120
+ except InstrumentError as exc:
121
+ info["model"] = f"unknown ({exc})"
122
+ return info
123
+
124
+ # ------------------------------------------------------------ stirring (both types)
125
+
126
+ def speed_rpm(self) -> float:
127
+ return self.query_number("IN_PV_4")
128
+
129
+ def speed_setpoint_rpm(self) -> float:
130
+ return self.query_number("IN_SP_4")
131
+
132
+ def set_speed(self, rpm: float) -> None:
133
+ self.send(f"OUT_SP_4 {int(round(rpm))}")
134
+
135
+ def start_motor(self) -> None:
136
+ self.abort.clear()
137
+ self.send("START_4")
138
+ self.stirring_commanded = True
139
+
140
+ def stop_motor(self) -> None:
141
+ self.abort.set()
142
+ self.send("STOP_4")
143
+ self.stirring_commanded = False
144
+
145
+ # ------------------------------------------------------------ hotplate
146
+
147
+ def external_temperature_c(self) -> float:
148
+ """``IN_PV_1``: external temperature sensor (e.g. PT 1000 / ETS-D in the medium)."""
149
+ self._require(HOTPLATE, "The external sensor reading IN_PV_1")
150
+ return self.query_number("IN_PV_1")
151
+
152
+ def hotplate_temperature_c(self) -> float:
153
+ """``IN_PV_2``: hotplate sensor."""
154
+ self._require(HOTPLATE, "Hotplate temperature")
155
+ return self.query_number("IN_PV_2")
156
+
157
+ def viscosity_trend(self) -> float:
158
+ """``IN_PV_5``: viscosity trend value (hotplates; relative, no unit)."""
159
+ self._require(HOTPLATE, "The viscosity trend")
160
+ return self.query_number("IN_PV_5")
161
+
162
+ def temperature_setpoint_c(self) -> float:
163
+ self._require(HOTPLATE, "The temperature setpoint")
164
+ return self.query_number("IN_SP_1")
165
+
166
+ def safety_temperature_c(self) -> float:
167
+ """``IN_SP_3``: the safety-circuit temperature set on the device (screwdriver dial)."""
168
+ self._require(HOTPLATE, "The safety temperature")
169
+ return self.query_number("IN_SP_3")
170
+
171
+ def set_temperature(self, celsius: float) -> None:
172
+ self._require(HOTPLATE, "Setting a temperature")
173
+ self.send(f"OUT_SP_1 {self._fmt(celsius)}")
174
+
175
+ def start_heater(self) -> None:
176
+ self._require(HOTPLATE, "Heating")
177
+ self.abort.clear()
178
+ self.send("START_1")
179
+ self.heating_commanded = True
180
+
181
+ def stop_heater(self) -> None:
182
+ self._require(HOTPLATE, "Heating")
183
+ self.abort.set()
184
+ self.send("STOP_1")
185
+ self.heating_commanded = False
186
+
187
+ # ------------------------------------------------------------ overhead stirrer
188
+
189
+ def probe_temperature_c(self) -> float:
190
+ """``IN_PV_3``: PT1000 probe of an overhead stirrer."""
191
+ self._require(OVERHEAD, "The PT1000 probe reading IN_PV_3")
192
+ return self.query_number("IN_PV_3")
193
+
194
+ def torque(self) -> float:
195
+ """``IN_PV_5``: current torque value (overhead stirrers, Ncm as on the display)."""
196
+ self._require(OVERHEAD, "Torque")
197
+ return self.query_number("IN_PV_5")
198
+
199
+ def torque_limit(self) -> float:
200
+ self._require(OVERHEAD, "The torque limit")
201
+ return self.query_number("IN_SP_5")
202
+
203
+ def speed_limit_rpm(self) -> float:
204
+ self._require(OVERHEAD, "The speed limit")
205
+ return self.query_number("IN_SP_6")
206
+
207
+ def safety_speed_rpm(self) -> float:
208
+ self._require(OVERHEAD, "The safety speed")
209
+ return self.query_number("IN_SP_8")
210
+
211
+ # ------------------------------------------------------------ stop
212
+
213
+ def stop_all(self) -> list[str]:
214
+ """Stop heating (hotplates) and stirring. Sends every stop even if one fails."""
215
+ self.abort.set()
216
+ errors: list[str] = []
217
+ stops = [self.stop_heater, self.stop_motor] if self.device_type == HOTPLATE else [self.stop_motor]
218
+ for stop in stops:
219
+ try:
220
+ stop()
221
+ except InstrumentError as exc:
222
+ errors.append(str(exc))
223
+ return errors
224
+
225
+ # ------------------------------------------------------------ watchdog (hotplates)
226
+
227
+ def set_watchdog_safety_values(self, temperature_c: float, speed_rpm: float) -> None:
228
+ """``OUT_SP_12@n`` / ``OUT_SP_42@n``: values watchdog mode 2 falls back to."""
229
+ self._require(HOTPLATE, "The watchdog")
230
+ self._echo(f"OUT_SP_12@{self._fmt(temperature_c)}", temperature_c)
231
+ self._echo(f"OUT_SP_42@{int(round(speed_rpm))}", round(speed_rpm))
232
+
233
+ def enable_watchdog(self, mode: int, timeout_s: int, keepalive_interval_s: float | None = None) -> None:
234
+ """Arm watchdog mode 1 (heater and motor off) or 2 (fall back to the WD safety values)
235
+ and keep it alive from a background thread by re-sending ``OUT_WDx@m``.
236
+
237
+ If this process dies, the cable is pulled or :meth:`close` is called, the device
238
+ is no longer refreshed and trips after ``timeout_s`` seconds.
239
+ """
240
+ self._require(HOTPLATE, "The watchdog")
241
+ if mode not in (1, 2):
242
+ raise ValueError("watchdog mode must be 1 or 2")
243
+ if not WATCHDOG_MIN_S <= timeout_s <= WATCHDOG_MAX_S:
244
+ raise ValueError(f"watchdog time must be {WATCHDOG_MIN_S}-{WATCHDOG_MAX_S} s")
245
+ if self._wd_mode is not None and self._wd_mode != mode:
246
+ raise InstrumentProtocolError(
247
+ f"Watchdog mode {self._wd_mode} is already active; the manual documents no way to "
248
+ "switch modes without it tripping."
249
+ )
250
+ self._stop_keepalive()
251
+ self._echo(f"OUT_WD{mode}@{timeout_s}", timeout_s)
252
+ self._wd_mode, self._wd_time_s = mode, timeout_s
253
+ self.watchdog_last_error = None
254
+ interval = keepalive_interval_s if keepalive_interval_s is not None else max(1.0, timeout_s / 4)
255
+ self._wd_stop.clear()
256
+ self._wd_thread = threading.Thread(
257
+ target=self._keepalive, args=(interval,), name="ika-watchdog", daemon=True
258
+ )
259
+ self._wd_thread.start()
260
+
261
+ def _keepalive(self, interval: float) -> None:
262
+ while not self._wd_stop.wait(interval):
263
+ self.watchdog_tick()
264
+
265
+ def watchdog_tick(self) -> None:
266
+ """Re-send the watchdog command once (called by the keep-alive thread)."""
267
+ mode, time_s = self._wd_mode, self._wd_time_s
268
+ if mode is None or time_s is None:
269
+ return
270
+ try:
271
+ self._echo(f"OUT_WD{mode}@{time_s}", time_s)
272
+ self.watchdog_last_error = None
273
+ except InstrumentError as exc: # keep trying; the device trips if we never get through
274
+ self.watchdog_last_error = str(exc)
275
+ log.warning("IKA watchdog refresh failed: %s", exc)
276
+
277
+ def disable_watchdog(self) -> None:
278
+ """Cancel watchdog mode 2 with ``OUT_WD2@0``. Mode 1 has no documented cancel command."""
279
+ self._require(HOTPLATE, "The watchdog")
280
+ if self._wd_mode is None:
281
+ return
282
+ if self._wd_mode == 1:
283
+ raise InstrumentProtocolError(
284
+ "Watchdog mode 1 cannot be cancelled over the interface (the IKA manual documents "
285
+ "no command for it). It stays armed while this server runs; stopping the server "
286
+ "makes it trip, switching heating and stirring off."
287
+ )
288
+ self._stop_keepalive()
289
+ self.query_number("OUT_WD2@0")
290
+ self._wd_mode = self._wd_time_s = None
291
+
292
+ def watchdog_state(self) -> dict[str, object]:
293
+ return {
294
+ "mode": self._wd_mode,
295
+ "timeout_s": self._wd_time_s,
296
+ "keepalive_running": bool(self._wd_thread and self._wd_thread.is_alive()),
297
+ "last_error": self.watchdog_last_error,
298
+ }
299
+
300
+ def _stop_keepalive(self) -> None:
301
+ self._wd_stop.set()
302
+ thread, self._wd_thread = self._wd_thread, None
303
+ if thread is not None and thread is not threading.current_thread():
304
+ thread.join(timeout=5)
305
+
306
+ def close(self) -> None:
307
+ # Deliberately does NOT cancel the watchdog: if it is armed, the device switches
308
+ # itself off after the watchdog time, which is the fail-safe we want.
309
+ self._stop_keepalive()
310
+ self.t.close()