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.
- stinger_python_utils-0.1.8rc4/PKG-INFO +276 -0
- stinger_python_utils-0.1.8rc4/README.md +261 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/pyproject.toml +1 -1
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/mcp/__main__.py +5 -3
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/mcp/plugin.py +1 -1
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/mcp/server.py +63 -0
- stinger_python_utils-0.1.8rc4/uv.lock +868 -0
- stinger_python_utils-0.1.8rc2/PKG-INFO +0 -88
- stinger_python_utils-0.1.8rc2/README.md +0 -73
- stinger_python_utils-0.1.8rc2/uv.lock +0 -1476
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.github/workflows/python-tests.yml +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.github/workflows/python37.yml +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.gitignore +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.python-version +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/.vscode/settings.json +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/LICENSE +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/__init__.py +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/mcp/__init__.py +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/message_creator.py +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/src/stinger_python_utils/return_codes.py +0 -0
- {stinger_python_utils-0.1.8rc2 → stinger_python_utils-0.1.8rc4}/test/__init__.py +0 -0
- {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
|
+
|
|
@@ -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[
|
|
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())
|