stinger-python-utils 0.1.8rc2__tar.gz → 0.1.8rc4__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.
Files changed (22) hide show
  1. stinger_python_utils-0.1.8rc4/PKG-INFO +276 -0
  2. stinger_python_utils-0.1.8rc4/README.md +261 -0
  3. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/pyproject.toml +1 -1
  4. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/mcp/__main__.py +5 -3
  5. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/mcp/plugin.py +1 -1
  6. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/mcp/server.py +63 -0
  7. stinger_python_utils-0.1.8rc4/uv.lock +868 -0
  8. stinger_python_utils-0.1.8rc2/PKG-INFO +0 -88
  9. stinger_python_utils-0.1.8rc2/README.md +0 -73
  10. stinger_python_utils-0.1.8rc2/uv.lock +0 -1476
  11. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.github/workflows/python-tests.yml +0 -0
  12. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.github/workflows/python37.yml +0 -0
  13. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.gitignore +0 -0
  14. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.python-version +0 -0
  15. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.vscode/settings.json +0 -0
  16. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/LICENSE +0 -0
  17. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/__init__.py +0 -0
  18. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/mcp/__init__.py +0 -0
  19. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/message_creator.py +0 -0
  20. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/return_codes.py +0 -0
  21. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/test/__init__.py +0 -0
  22. {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/test/test_message_creator.py +0 -0
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.4
2
+ Name: stinger-python-utils
3
+ Version: 0.1.8rc4
4
+ Summary: Common utilities for Stinger Python services.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.7
8
+ Requires-Dist: pydantic>=2.5.3
9
+ Requires-Dist: pyqttier>=0.2.0
10
+ Provides-Extra: mcp
11
+ Requires-Dist: mcp>=1.0.0; extra == 'mcp'
12
+ Requires-Dist: stevedore>=5.0; extra == 'mcp'
13
+ Requires-Dist: uvicorn>=0.20.0; extra == 'mcp'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # stinger-python-utils
17
+
18
+ Shared utilities for Stinger Python services, providing convenient message creation for MQTT communication.
19
+
20
+
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ uv add stinger-python-utils
26
+ ```
27
+
28
+ ## MessageCreator
29
+
30
+ `MessageCreator` is a utility class for creating MQTT messages with standardized properties and payloads.
31
+
32
+ ### Basic Usage
33
+
34
+ ```python
35
+ from pydantic import BaseModel
36
+ from stinger_python_utils.message_creator import MessageCreator
37
+
38
+ class MyPayload(BaseModel):
39
+ name: str
40
+ value: int
41
+
42
+ payload = MyPayload(name="test", value=42)
43
+ message = MessageCreator.signal_message("my/topic", payload)
44
+ ```
45
+
46
+ ### Methods
47
+
48
+ | Method | Purpose | Return Code |
49
+ |--------|---------|-------------|
50
+ | `signal_message(topic, payload)` | Send a signal with one-time delivery | QoS 1, no retain |
51
+ | `status_message(topic, payload, expiry_seconds)` | Send status that expires | QoS 1, retained, with expiry |
52
+ | `error_response_message(topic, return_code, correlation_id, debug_info)` | Error response to a request | QoS 1, user properties: `ReturnCode` |
53
+ | `response_message(topic, payload, return_code, correlation_id)` | Successful response to a request | QoS 1, user properties: `ReturnCode` |
54
+ | `property_state_message(topic, payload, state_version)` | Publish property state | QoS 1, retained, JSON content type |
55
+ | `property_update_request_message(topic, payload, version, response_topic, correlation_id)` | Request property update | QoS 1, user property: `PropertyVersion` |
56
+ | `property_response_message(topic, payload, version, return_code, correlation_id, debug_info)` | Respond to property update | QoS 1, user properties: `ReturnCode`, `PropertyVersion` |
57
+ | `request_message(topic, payload, response_topic, correlation_id)` | Send a request (auto-generates UUID if no correlation_id) | QoS 1, auto correlation ID |
58
+
59
+ ### Example: Request/Response Pattern
60
+
61
+ ```python
62
+ from pydantic import BaseModel
63
+ from stinger_python_utils.message_creator import MessageCreator
64
+
65
+ class Request(BaseModel):
66
+ action: str
67
+
68
+ request = Request(action="start")
69
+ msg = MessageCreator.request_message(
70
+ "devices/cmd",
71
+ request,
72
+ response_topic="devices/response"
73
+ )
74
+ # Returns a Message with auto-generated correlation ID
75
+ ```
76
+
77
+ ### Example: Error Response
78
+
79
+ ```python
80
+ msg = MessageCreator.error_response_message(
81
+ "devices/response",
82
+ return_code=500,
83
+ correlation_id="req-123",
84
+ debug_info="Device not found"
85
+ )
86
+ # User properties include: ReturnCode=500, DebugInfo=Device not found
87
+ ```
88
+
89
+ ---
90
+
91
+ ## MCP Server
92
+
93
+ `stinger-python-utils` includes an optional **Model Context Protocol (MCP) server** that exposes stinger-ipc services to AI coding assistants such as GitHub Copilot in VS Code.
94
+
95
+ The server discovers live stinger-ipc service instances over MQTT and dynamically registers them as MCP **resources** (properties and signal mailboxes) and **tools** (methods and writable property setters). Functionality is provided by plugins — third-party packages that register against the `stinger_python_utils.mcp_plugins` stevedore entry-point namespace.
96
+
97
+ ### Installation
98
+
99
+ Install with the `mcp` extra to pull in the MCP SDK, stevedore, and uvicorn:
100
+
101
+ ```bash
102
+ pip install 'stinger-python-utils[mcp]'
103
+ # or with uv:
104
+ uv add 'stinger-python-utils[mcp]'
105
+ ```
106
+
107
+ ### MQTT Connection Configuration
108
+
109
+ The server connects to an MQTT broker using environment variables:
110
+
111
+ | Variable | Default | Description |
112
+ |----------|---------|-------------|
113
+ | `MQTT_HOST` | `localhost` | Broker hostname or IP |
114
+ | `MQTT_PORT` | `1883` | Broker port |
115
+ | `MQTT_TRANSPORT` | `tcp` | `tcp`, `websocket`, or `unix` |
116
+ | `MQTT_CLIENT_ID` | *(random)* | MQTT client identifier |
117
+
118
+ ### Running the Server
119
+
120
+ **stdio transport** (recommended for VS Code / Copilot):
121
+
122
+ ```bash
123
+ stinger-mcp-server --transport stdio
124
+ ```
125
+
126
+ **SSE transport** (for browser-based or remote clients):
127
+
128
+ ```bash
129
+ stinger-mcp-server --transport sse --host 0.0.0.0 --port 8000
130
+ ```
131
+
132
+ ### Adding to VS Code (GitHub Copilot)
133
+
134
+ Add the server to your VS Code user or workspace MCP configuration.
135
+
136
+ **Option 1 — User settings** (`~/.vscode/mcp.json` or via *Settings → MCP*):
137
+
138
+ ```json
139
+ {
140
+ "servers": {
141
+ "stinger": {
142
+ "type": "stdio",
143
+ "command": "stinger-mcp-server",
144
+ "args": ["--transport", "stdio"],
145
+ "env": {
146
+ "MQTT_HOST": "localhost",
147
+ "MQTT_PORT": "1883"
148
+ }
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ **Option 2 — Workspace settings** (`.vscode/mcp.json` in your repo, checked into source control so the whole team shares the same configuration):
155
+
156
+ ```json
157
+ {
158
+ "servers": {
159
+ "stinger": {
160
+ "type": "stdio",
161
+ "command": "stinger-mcp-server",
162
+ "args": ["--transport", "stdio"],
163
+ "env": {
164
+ "MQTT_HOST": "localhost",
165
+ "MQTT_PORT": "1883"
166
+ }
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ Once saved, open the Copilot Chat panel, switch to **Agent** mode, and the stinger service tools and resources will be available.
173
+
174
+ > **Tip:** If `stinger-mcp-server` is installed inside a virtual environment rather than globally, use the full path to the executable, e.g. `"/path/to/.venv/bin/stinger-mcp-server"`.
175
+
176
+ **Option 3 — SSE transport** (connect to a server already running elsewhere, e.g. on a remote device or in a container):
177
+
178
+ First start the server with the SSE transport:
179
+
180
+ ```bash
181
+ MQTT_HOST=192.168.1.100 stinger-mcp-server --transport sse --host 0.0.0.0 --port 8000
182
+ ```
183
+
184
+ Then point VS Code at it in `.vscode/mcp.json`:
185
+
186
+ ```json
187
+ {
188
+ "servers": {
189
+ "stinger": {
190
+ "type": "sse",
191
+ "url": "http://localhost:8000/sse"
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ Replace `localhost` with the hostname or IP of the machine running the server.
198
+
199
+ ### Writing a Plugin
200
+
201
+ Plugins provide the actual service-type knowledge to the MCP server. Create a class that extends `StingerMCPPlugin` and register it as a stevedore entry-point:
202
+
203
+ **`my_service/mcp_plugin.py`**:
204
+
205
+ ```python
206
+ from stinger_python_utils.mcp import (
207
+ StingerMCPPlugin,
208
+ SignalDefinition,
209
+ PropertyDefinition,
210
+ MethodDefinition,
211
+ )
212
+ from .client import MyServiceClient
213
+ from .discoverer import MyServiceDiscoverer
214
+
215
+ class MyServicePlugin(StingerMCPPlugin):
216
+
217
+ def get_plugin_name(self) -> str:
218
+ return "my_service" # used as URI scheme: my_service://instance/...
219
+
220
+ def get_discovery_class(self) -> type:
221
+ return MyServiceDiscoverer
222
+
223
+ def get_client_class(self) -> type:
224
+ return MyServiceClient
225
+
226
+ def get_signals(self) -> list[SignalDefinition]:
227
+ return [
228
+ SignalDefinition(name="status_changed", description="Emitted when device status changes"),
229
+ ]
230
+
231
+ def get_properties(self) -> list[PropertyDefinition]:
232
+ return [
233
+ PropertyDefinition(
234
+ name="brightness",
235
+ schema={
236
+ "type": "object",
237
+ "properties": {"value": {"type": "integer", "minimum": 0, "maximum": 100}},
238
+ "required": ["value"],
239
+ },
240
+ readonly=False,
241
+ description="Brightness level (0–100)",
242
+ ),
243
+ ]
244
+
245
+ def get_methods(self) -> list[MethodDefinition]:
246
+ return [
247
+ MethodDefinition(
248
+ name="restart",
249
+ arguments_schema={"type": "object", "properties": {}},
250
+ description="Restart the device",
251
+ ),
252
+ ]
253
+ ```
254
+
255
+ **`pyproject.toml`** of the plugin package:
256
+
257
+ ```toml
258
+ [project.entry-points."stinger_python_utils.mcp_plugins"]
259
+ my_service = "my_service.mcp_plugin:MyServicePlugin"
260
+ ```
261
+
262
+ Install the plugin package into the same environment as `stinger-python-utils[mcp]` and the server will discover it automatically on next start.
263
+
264
+ ### MCP Resource and Tool Naming
265
+
266
+ For each discovered service instance the server exposes:
267
+
268
+ | MCP primitive | Name / URI pattern | Description |
269
+ |---------------|--------------------|-------------|
270
+ | Resource | `{plugin}://{instance_id}/property/{name}` | Current property value (JSON) |
271
+ | Resource | `{plugin}://{instance_id}/signal/{name}` | Mailbox of the last 10 signals (JSON array with timestamps) |
272
+ | Tool | `{plugin}_{instance_id}_{method_name}` | Calls a method on the instance |
273
+ | Tool | `{plugin}_{instance_id}_set_{property_name}` | Sets a writable property |
274
+
275
+ As instances come and go the tool and resource lists update automatically.
276
+
@@ -0,0 +1,261 @@
1
+ # stinger-python-utils
2
+
3
+ Shared utilities for Stinger Python services, providing convenient message creation for MQTT communication.
4
+
5
+
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ uv add stinger-python-utils
11
+ ```
12
+
13
+ ## MessageCreator
14
+
15
+ `MessageCreator` is a utility class for creating MQTT messages with standardized properties and payloads.
16
+
17
+ ### Basic Usage
18
+
19
+ ```python
20
+ from pydantic import BaseModel
21
+ from stinger_python_utils.message_creator import MessageCreator
22
+
23
+ class MyPayload(BaseModel):
24
+ name: str
25
+ value: int
26
+
27
+ payload = MyPayload(name="test", value=42)
28
+ message = MessageCreator.signal_message("my/topic", payload)
29
+ ```
30
+
31
+ ### Methods
32
+
33
+ | Method | Purpose | Return Code |
34
+ |--------|---------|-------------|
35
+ | `signal_message(topic, payload)` | Send a signal with one-time delivery | QoS 1, no retain |
36
+ | `status_message(topic, payload, expiry_seconds)` | Send status that expires | QoS 1, retained, with expiry |
37
+ | `error_response_message(topic, return_code, correlation_id, debug_info)` | Error response to a request | QoS 1, user properties: `ReturnCode` |
38
+ | `response_message(topic, payload, return_code, correlation_id)` | Successful response to a request | QoS 1, user properties: `ReturnCode` |
39
+ | `property_state_message(topic, payload, state_version)` | Publish property state | QoS 1, retained, JSON content type |
40
+ | `property_update_request_message(topic, payload, version, response_topic, correlation_id)` | Request property update | QoS 1, user property: `PropertyVersion` |
41
+ | `property_response_message(topic, payload, version, return_code, correlation_id, debug_info)` | Respond to property update | QoS 1, user properties: `ReturnCode`, `PropertyVersion` |
42
+ | `request_message(topic, payload, response_topic, correlation_id)` | Send a request (auto-generates UUID if no correlation_id) | QoS 1, auto correlation ID |
43
+
44
+ ### Example: Request/Response Pattern
45
+
46
+ ```python
47
+ from pydantic import BaseModel
48
+ from stinger_python_utils.message_creator import MessageCreator
49
+
50
+ class Request(BaseModel):
51
+ action: str
52
+
53
+ request = Request(action="start")
54
+ msg = MessageCreator.request_message(
55
+ "devices/cmd",
56
+ request,
57
+ response_topic="devices/response"
58
+ )
59
+ # Returns a Message with auto-generated correlation ID
60
+ ```
61
+
62
+ ### Example: Error Response
63
+
64
+ ```python
65
+ msg = MessageCreator.error_response_message(
66
+ "devices/response",
67
+ return_code=500,
68
+ correlation_id="req-123",
69
+ debug_info="Device not found"
70
+ )
71
+ # User properties include: ReturnCode=500, DebugInfo=Device not found
72
+ ```
73
+
74
+ ---
75
+
76
+ ## MCP Server
77
+
78
+ `stinger-python-utils` includes an optional **Model Context Protocol (MCP) server** that exposes stinger-ipc services to AI coding assistants such as GitHub Copilot in VS Code.
79
+
80
+ The server discovers live stinger-ipc service instances over MQTT and dynamically registers them as MCP **resources** (properties and signal mailboxes) and **tools** (methods and writable property setters). Functionality is provided by plugins — third-party packages that register against the `stinger_python_utils.mcp_plugins` stevedore entry-point namespace.
81
+
82
+ ### Installation
83
+
84
+ Install with the `mcp` extra to pull in the MCP SDK, stevedore, and uvicorn:
85
+
86
+ ```bash
87
+ pip install 'stinger-python-utils[mcp]'
88
+ # or with uv:
89
+ uv add 'stinger-python-utils[mcp]'
90
+ ```
91
+
92
+ ### MQTT Connection Configuration
93
+
94
+ The server connects to an MQTT broker using environment variables:
95
+
96
+ | Variable | Default | Description |
97
+ |----------|---------|-------------|
98
+ | `MQTT_HOST` | `localhost` | Broker hostname or IP |
99
+ | `MQTT_PORT` | `1883` | Broker port |
100
+ | `MQTT_TRANSPORT` | `tcp` | `tcp`, `websocket`, or `unix` |
101
+ | `MQTT_CLIENT_ID` | *(random)* | MQTT client identifier |
102
+
103
+ ### Running the Server
104
+
105
+ **stdio transport** (recommended for VS Code / Copilot):
106
+
107
+ ```bash
108
+ stinger-mcp-server --transport stdio
109
+ ```
110
+
111
+ **SSE transport** (for browser-based or remote clients):
112
+
113
+ ```bash
114
+ stinger-mcp-server --transport sse --host 0.0.0.0 --port 8000
115
+ ```
116
+
117
+ ### Adding to VS Code (GitHub Copilot)
118
+
119
+ Add the server to your VS Code user or workspace MCP configuration.
120
+
121
+ **Option 1 — User settings** (`~/.vscode/mcp.json` or via *Settings → MCP*):
122
+
123
+ ```json
124
+ {
125
+ "servers": {
126
+ "stinger": {
127
+ "type": "stdio",
128
+ "command": "stinger-mcp-server",
129
+ "args": ["--transport", "stdio"],
130
+ "env": {
131
+ "MQTT_HOST": "localhost",
132
+ "MQTT_PORT": "1883"
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ **Option 2 — Workspace settings** (`.vscode/mcp.json` in your repo, checked into source control so the whole team shares the same configuration):
140
+
141
+ ```json
142
+ {
143
+ "servers": {
144
+ "stinger": {
145
+ "type": "stdio",
146
+ "command": "stinger-mcp-server",
147
+ "args": ["--transport", "stdio"],
148
+ "env": {
149
+ "MQTT_HOST": "localhost",
150
+ "MQTT_PORT": "1883"
151
+ }
152
+ }
153
+ }
154
+ }
155
+ ```
156
+
157
+ Once saved, open the Copilot Chat panel, switch to **Agent** mode, and the stinger service tools and resources will be available.
158
+
159
+ > **Tip:** If `stinger-mcp-server` is installed inside a virtual environment rather than globally, use the full path to the executable, e.g. `"/path/to/.venv/bin/stinger-mcp-server"`.
160
+
161
+ **Option 3 — SSE transport** (connect to a server already running elsewhere, e.g. on a remote device or in a container):
162
+
163
+ First start the server with the SSE transport:
164
+
165
+ ```bash
166
+ MQTT_HOST=192.168.1.100 stinger-mcp-server --transport sse --host 0.0.0.0 --port 8000
167
+ ```
168
+
169
+ Then point VS Code at it in `.vscode/mcp.json`:
170
+
171
+ ```json
172
+ {
173
+ "servers": {
174
+ "stinger": {
175
+ "type": "sse",
176
+ "url": "http://localhost:8000/sse"
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ Replace `localhost` with the hostname or IP of the machine running the server.
183
+
184
+ ### Writing a Plugin
185
+
186
+ Plugins provide the actual service-type knowledge to the MCP server. Create a class that extends `StingerMCPPlugin` and register it as a stevedore entry-point:
187
+
188
+ **`my_service/mcp_plugin.py`**:
189
+
190
+ ```python
191
+ from stinger_python_utils.mcp import (
192
+ StingerMCPPlugin,
193
+ SignalDefinition,
194
+ PropertyDefinition,
195
+ MethodDefinition,
196
+ )
197
+ from .client import MyServiceClient
198
+ from .discoverer import MyServiceDiscoverer
199
+
200
+ class MyServicePlugin(StingerMCPPlugin):
201
+
202
+ def get_plugin_name(self) -> str:
203
+ return "my_service" # used as URI scheme: my_service://instance/...
204
+
205
+ def get_discovery_class(self) -> type:
206
+ return MyServiceDiscoverer
207
+
208
+ def get_client_class(self) -> type:
209
+ return MyServiceClient
210
+
211
+ def get_signals(self) -> list[SignalDefinition]:
212
+ return [
213
+ SignalDefinition(name="status_changed", description="Emitted when device status changes"),
214
+ ]
215
+
216
+ def get_properties(self) -> list[PropertyDefinition]:
217
+ return [
218
+ PropertyDefinition(
219
+ name="brightness",
220
+ schema={
221
+ "type": "object",
222
+ "properties": {"value": {"type": "integer", "minimum": 0, "maximum": 100}},
223
+ "required": ["value"],
224
+ },
225
+ readonly=False,
226
+ description="Brightness level (0–100)",
227
+ ),
228
+ ]
229
+
230
+ def get_methods(self) -> list[MethodDefinition]:
231
+ return [
232
+ MethodDefinition(
233
+ name="restart",
234
+ arguments_schema={"type": "object", "properties": {}},
235
+ description="Restart the device",
236
+ ),
237
+ ]
238
+ ```
239
+
240
+ **`pyproject.toml`** of the plugin package:
241
+
242
+ ```toml
243
+ [project.entry-points."stinger_python_utils.mcp_plugins"]
244
+ my_service = "my_service.mcp_plugin:MyServicePlugin"
245
+ ```
246
+
247
+ Install the plugin package into the same environment as `stinger-python-utils[mcp]` and the server will discover it automatically on next start.
248
+
249
+ ### MCP Resource and Tool Naming
250
+
251
+ For each discovered service instance the server exposes:
252
+
253
+ | MCP primitive | Name / URI pattern | Description |
254
+ |---------------|--------------------|-------------|
255
+ | Resource | `{plugin}://{instance_id}/property/{name}` | Current property value (JSON) |
256
+ | Resource | `{plugin}://{instance_id}/signal/{name}` | Mailbox of the last 10 signals (JSON array with timestamps) |
257
+ | Tool | `{plugin}_{instance_id}_{method_name}` | Calls a method on the instance |
258
+ | Tool | `{plugin}_{instance_id}_set_{property_name}` | Sets a writable property |
259
+
260
+ As instances come and go the tool and resource lists update automatically.
261
+
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "stinger-python-utils"
7
- version = "0.1.8rc2"
7
+ version = "0.1.8rc4"
8
8
  description = "Common utilities for Stinger Python services."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -14,20 +14,20 @@ def main() -> None:
14
14
  )
15
15
  parser.add_argument(
16
16
  "--transport",
17
- choices=["stdio", "sse"],
17
+ choices=["stdio", "sse", "streamable-http"],
18
18
  default="stdio",
19
19
  help="MCP transport to use (default: stdio)",
20
20
  )
21
21
  parser.add_argument(
22
22
  "--host",
23
23
  default="0.0.0.0",
24
- help="Bind address for SSE transport (default: 0.0.0.0)",
24
+ help="Bind address for SSE/streamable-http transport (default: 0.0.0.0)",
25
25
  )
26
26
  parser.add_argument(
27
27
  "--port",
28
28
  type=int,
29
29
  default=8000,
30
- help="Port for SSE transport (default: 8000)",
30
+ help="Port for SSE/streamable-http transport (default: 8000)",
31
31
  )
32
32
  parser.add_argument(
33
33
  "--log-level",
@@ -50,6 +50,8 @@ def main() -> None:
50
50
  asyncio.run(server.run_stdio())
51
51
  elif args.transport == "sse":
52
52
  asyncio.run(server.run_sse(host=args.host, port=args.port))
53
+ elif args.transport == "streamable-http":
54
+ asyncio.run(server.run_streamable_http(host=args.host, port=args.port))
53
55
 
54
56
 
55
57
  if __name__ == "__main__":
@@ -186,7 +186,7 @@ class StingerMCPPlugin(ABC):
186
186
  Override when the property value is a composite type that must
187
187
  be reconstructed from several arguments.
188
188
  """
189
- setattr(client, prop_name, arguments["value"])
189
+ setattr(client, prop_name, list(arguments.values())[0])
190
190
 
191
191
  def call_method(
192
192
  self, client: Any, method_name: str, arguments: dict[str, Any]
@@ -523,6 +523,8 @@ class StingerMCPServer:
523
523
  try:
524
524
  from mcp.server.sse import SseServerTransport
525
525
  from starlette.applications import Starlette
526
+ from starlette.middleware import Middleware
527
+ from starlette.middleware.cors import CORSMiddleware
526
528
  from starlette.requests import Request
527
529
  from starlette.routing import Mount, Route
528
530
  import uvicorn
@@ -550,8 +552,69 @@ class StingerMCPServer:
550
552
  Route("/sse", endpoint=handle_sse),
551
553
  Mount("/messages/", app=sse.handle_post_message),
552
554
  ],
555
+ middleware=[
556
+ Middleware(
557
+ CORSMiddleware,
558
+ allow_origins=["*"],
559
+ allow_methods=["*"],
560
+ allow_headers=["*"],
561
+ ),
562
+ ],
553
563
  )
554
564
 
555
565
  config = uvicorn.Config(app, host=host, port=port)
556
566
  server = uvicorn.Server(config)
557
567
  await server.serve()
568
+
569
+ async def run_streamable_http(
570
+ self, host: str = "0.0.0.0", port: int = 8000
571
+ ) -> None:
572
+ """Run the MCP server over the *Streamable HTTP* transport."""
573
+ try:
574
+ from mcp.server.streamable_http import (
575
+ StreamableHTTPServerTransport,
576
+ )
577
+ from starlette.applications import Starlette
578
+ from starlette.middleware import Middleware
579
+ from starlette.middleware.cors import CORSMiddleware
580
+ from starlette.routing import Mount
581
+ import uvicorn
582
+ except ImportError as exc:
583
+ raise RuntimeError(
584
+ "Streamable HTTP transport requires additional packages. "
585
+ "Install with: pip install 'stinger-python-utils[mcp]'"
586
+ ) from exc
587
+
588
+ self._loop = asyncio.get_running_loop()
589
+ self._connection = create_mqtt_connection()
590
+ self._load_plugins()
591
+ self._start_discovery()
592
+
593
+ transport = StreamableHTTPServerTransport(
594
+ mcp_session_id=uuid.uuid4().hex,
595
+ )
596
+
597
+ async def run_transport() -> None:
598
+ async with transport.connect() as (read, write):
599
+ await self._server.run(read, write, self._init_options())
600
+
601
+ app = Starlette(
602
+ routes=[
603
+ Mount("/mcp", app=transport.handle_request),
604
+ ],
605
+ middleware=[
606
+ Middleware(
607
+ CORSMiddleware,
608
+ allow_origins=["*"],
609
+ allow_methods=["*"],
610
+ allow_headers=["*"],
611
+ ),
612
+ ],
613
+ )
614
+
615
+ config = uvicorn.Config(app, host=host, port=port)
616
+ server = uvicorn.Server(config)
617
+
618
+ async with asyncio.TaskGroup() as tg:
619
+ tg.create_task(run_transport())
620
+ tg.create_task(server.serve())