stinger-python-utils 0.1.7__tar.gz → 0.1.8__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.8/PKG-INFO +276 -0
- stinger_python_utils-0.1.8/README.md +261 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/pyproject.toml +11 -1
- stinger_python_utils-0.1.8/src/stinger_python_utils/mcp/__init__.py +20 -0
- stinger_python_utils-0.1.8/src/stinger_python_utils/mcp/__main__.py +58 -0
- stinger_python_utils-0.1.8/src/stinger_python_utils/mcp/plugin.py +220 -0
- stinger_python_utils-0.1.8/src/stinger_python_utils/mcp/server.py +633 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/src/stinger_python_utils/message_creator.py +17 -0
- stinger_python_utils-0.1.8/uv.lock +868 -0
- stinger_python_utils-0.1.7/PKG-INFO +0 -84
- stinger_python_utils-0.1.7/README.md +0 -73
- stinger_python_utils-0.1.7/uv.lock +0 -1476
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/.github/workflows/python-tests.yml +0 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/.github/workflows/python37.yml +0 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/.gitignore +0 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/.python-version +0 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/.vscode/settings.json +0 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/LICENSE +0 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/src/stinger_python_utils/__init__.py +0 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/src/stinger_python_utils/return_codes.py +0 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/test/__init__.py +0 -0
- {stinger_python_utils-0.1.7 → stinger_python_utils-0.1.8}/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.8
|
|
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.
|
|
7
|
+
version = "0.1.8"
|
|
8
8
|
description = "Common utilities for Stinger Python services."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -14,6 +14,16 @@ dependencies = [
|
|
|
14
14
|
"pyqttier>=0.2.0",
|
|
15
15
|
]
|
|
16
16
|
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
mcp = [
|
|
19
|
+
"mcp>=1.0.0",
|
|
20
|
+
"stevedore>=5.0",
|
|
21
|
+
"uvicorn>=0.20.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
stinger-mcp-server = "stinger_python_utils.mcp.__main__:main"
|
|
26
|
+
|
|
17
27
|
[tool.uv]
|
|
18
28
|
managed = true
|
|
19
29
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Stinger MCP server – plugin-based MCP interface to stinger-ipc services.
|
|
2
|
+
|
|
3
|
+
Public API re-exported here for convenience::
|
|
4
|
+
|
|
5
|
+
from stinger_python_utils.mcp import StingerMCPPlugin, SignalDefinition, ...
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .plugin import (
|
|
9
|
+
MethodDefinition,
|
|
10
|
+
PropertyDefinition,
|
|
11
|
+
SignalDefinition,
|
|
12
|
+
StingerMCPPlugin,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"MethodDefinition",
|
|
17
|
+
"PropertyDefinition",
|
|
18
|
+
"SignalDefinition",
|
|
19
|
+
"StingerMCPPlugin",
|
|
20
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Entry-point for ``python -m stinger_python_utils.mcp``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="stinger-mcp-server",
|
|
13
|
+
description="Stinger MCP Server – expose stinger-ipc services over MCP",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--transport",
|
|
17
|
+
choices=["stdio", "sse", "streamable-http"],
|
|
18
|
+
default="stdio",
|
|
19
|
+
help="MCP transport to use (default: stdio)",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--host",
|
|
23
|
+
default="0.0.0.0",
|
|
24
|
+
help="Bind address for SSE/streamable-http transport (default: 0.0.0.0)",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--port",
|
|
28
|
+
type=int,
|
|
29
|
+
default=8000,
|
|
30
|
+
help="Port for SSE/streamable-http transport (default: 8000)",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--log-level",
|
|
34
|
+
default="INFO",
|
|
35
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
36
|
+
help="Logging level (default: INFO)",
|
|
37
|
+
)
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
|
|
40
|
+
logging.basicConfig(
|
|
41
|
+
level=getattr(logging, args.log_level),
|
|
42
|
+
format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from .server import StingerMCPServer
|
|
46
|
+
|
|
47
|
+
server = StingerMCPServer()
|
|
48
|
+
|
|
49
|
+
if args.transport == "stdio":
|
|
50
|
+
asyncio.run(server.run_stdio())
|
|
51
|
+
elif args.transport == "sse":
|
|
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))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|