mcp-eregistrations-bpa 0.8.5__py3-none-any.whl
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.
Potentially problematic release.
This version of mcp-eregistrations-bpa might be problematic. Click here for more details.
- mcp_eregistrations_bpa/__init__.py +121 -0
- mcp_eregistrations_bpa/__main__.py +6 -0
- mcp_eregistrations_bpa/arazzo/__init__.py +21 -0
- mcp_eregistrations_bpa/arazzo/expression.py +379 -0
- mcp_eregistrations_bpa/audit/__init__.py +56 -0
- mcp_eregistrations_bpa/audit/context.py +66 -0
- mcp_eregistrations_bpa/audit/logger.py +236 -0
- mcp_eregistrations_bpa/audit/models.py +131 -0
- mcp_eregistrations_bpa/auth/__init__.py +64 -0
- mcp_eregistrations_bpa/auth/callback.py +391 -0
- mcp_eregistrations_bpa/auth/cas.py +409 -0
- mcp_eregistrations_bpa/auth/oidc.py +252 -0
- mcp_eregistrations_bpa/auth/permissions.py +162 -0
- mcp_eregistrations_bpa/auth/token_manager.py +348 -0
- mcp_eregistrations_bpa/bpa_client/__init__.py +84 -0
- mcp_eregistrations_bpa/bpa_client/client.py +740 -0
- mcp_eregistrations_bpa/bpa_client/endpoints.py +193 -0
- mcp_eregistrations_bpa/bpa_client/errors.py +276 -0
- mcp_eregistrations_bpa/bpa_client/models.py +203 -0
- mcp_eregistrations_bpa/config.py +349 -0
- mcp_eregistrations_bpa/db/__init__.py +21 -0
- mcp_eregistrations_bpa/db/connection.py +64 -0
- mcp_eregistrations_bpa/db/migrations.py +168 -0
- mcp_eregistrations_bpa/exceptions.py +39 -0
- mcp_eregistrations_bpa/py.typed +0 -0
- mcp_eregistrations_bpa/rollback/__init__.py +19 -0
- mcp_eregistrations_bpa/rollback/manager.py +616 -0
- mcp_eregistrations_bpa/server.py +152 -0
- mcp_eregistrations_bpa/tools/__init__.py +372 -0
- mcp_eregistrations_bpa/tools/actions.py +155 -0
- mcp_eregistrations_bpa/tools/analysis.py +352 -0
- mcp_eregistrations_bpa/tools/audit.py +399 -0
- mcp_eregistrations_bpa/tools/behaviours.py +1042 -0
- mcp_eregistrations_bpa/tools/bots.py +627 -0
- mcp_eregistrations_bpa/tools/classifications.py +575 -0
- mcp_eregistrations_bpa/tools/costs.py +765 -0
- mcp_eregistrations_bpa/tools/debug_strategies.py +351 -0
- mcp_eregistrations_bpa/tools/debugger.py +1230 -0
- mcp_eregistrations_bpa/tools/determinants.py +2235 -0
- mcp_eregistrations_bpa/tools/document_requirements.py +670 -0
- mcp_eregistrations_bpa/tools/export.py +899 -0
- mcp_eregistrations_bpa/tools/fields.py +162 -0
- mcp_eregistrations_bpa/tools/form_errors.py +36 -0
- mcp_eregistrations_bpa/tools/formio_helpers.py +971 -0
- mcp_eregistrations_bpa/tools/forms.py +1269 -0
- mcp_eregistrations_bpa/tools/jsonlogic_builder.py +466 -0
- mcp_eregistrations_bpa/tools/large_response.py +163 -0
- mcp_eregistrations_bpa/tools/messages.py +523 -0
- mcp_eregistrations_bpa/tools/notifications.py +241 -0
- mcp_eregistrations_bpa/tools/registration_institutions.py +680 -0
- mcp_eregistrations_bpa/tools/registrations.py +897 -0
- mcp_eregistrations_bpa/tools/role_status.py +447 -0
- mcp_eregistrations_bpa/tools/role_units.py +400 -0
- mcp_eregistrations_bpa/tools/roles.py +1236 -0
- mcp_eregistrations_bpa/tools/rollback.py +335 -0
- mcp_eregistrations_bpa/tools/services.py +674 -0
- mcp_eregistrations_bpa/tools/workflows.py +2487 -0
- mcp_eregistrations_bpa/tools/yaml_transformer.py +991 -0
- mcp_eregistrations_bpa/workflows/__init__.py +28 -0
- mcp_eregistrations_bpa/workflows/loader.py +440 -0
- mcp_eregistrations_bpa/workflows/models.py +336 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/METADATA +965 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/RECORD +66 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/WHEEL +4 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/entry_points.txt +2 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/licenses/LICENSE +86 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""MCP tools for BPA message operations.
|
|
2
|
+
|
|
3
|
+
Messages are reusable email/SMS/WhatsApp templates. Unlike notifications
|
|
4
|
+
(service-scoped), messages are global and can be linked to roles.
|
|
5
|
+
|
|
6
|
+
Write operations follow the audit-before-write pattern.
|
|
7
|
+
|
|
8
|
+
API Endpoints used:
|
|
9
|
+
- GET /message - List messages (paginated)
|
|
10
|
+
- GET /message/{message_id} - Get message by ID
|
|
11
|
+
- POST /message - Create message
|
|
12
|
+
- PUT /message - Update message
|
|
13
|
+
- DELETE /message/{message_id} - Delete message
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
21
|
+
|
|
22
|
+
from mcp_eregistrations_bpa.audit.context import (
|
|
23
|
+
NotAuthenticatedError,
|
|
24
|
+
get_current_user_email,
|
|
25
|
+
)
|
|
26
|
+
from mcp_eregistrations_bpa.audit.logger import AuditLogger
|
|
27
|
+
from mcp_eregistrations_bpa.bpa_client import BPAClient
|
|
28
|
+
from mcp_eregistrations_bpa.bpa_client.errors import (
|
|
29
|
+
BPAClientError,
|
|
30
|
+
BPANotFoundError,
|
|
31
|
+
translate_error,
|
|
32
|
+
)
|
|
33
|
+
from mcp_eregistrations_bpa.tools.large_response import large_response_handler
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"message_list",
|
|
37
|
+
"message_get",
|
|
38
|
+
"message_create",
|
|
39
|
+
"message_update",
|
|
40
|
+
"message_delete",
|
|
41
|
+
"register_message_tools",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _transform_message_response(data: dict[str, Any]) -> dict[str, Any]:
|
|
46
|
+
"""Transform message API response from camelCase to snake_case.
|
|
47
|
+
|
|
48
|
+
Based on Message model from BPA frontend:
|
|
49
|
+
- id, name, code, subject, content
|
|
50
|
+
- messageType (MESSAGE/ALERT), channel (EMAIL/SMS/WHATSAPP)
|
|
51
|
+
- businessKey, roleRegistrations, messageRoleStatuses, messageBots
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
data: Raw API response with camelCase keys.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
dict: Transformed response with snake_case keys.
|
|
58
|
+
"""
|
|
59
|
+
return {
|
|
60
|
+
"id": data.get("id"),
|
|
61
|
+
"name": data.get("name"),
|
|
62
|
+
"code": data.get("code"),
|
|
63
|
+
"subject": data.get("subject"),
|
|
64
|
+
"content": data.get("content"),
|
|
65
|
+
"message_type": data.get("messageType"),
|
|
66
|
+
"channel": data.get("channel"),
|
|
67
|
+
"business_key": data.get("businessKey"),
|
|
68
|
+
"role_registrations": data.get("roleRegistrations"),
|
|
69
|
+
"message_role_statuses": data.get("messageRoleStatuses"),
|
|
70
|
+
"message_bots": data.get("messageBots"),
|
|
71
|
+
# Audit fields
|
|
72
|
+
"created_by": data.get("createdBy"),
|
|
73
|
+
"created_when": data.get("createdWhen"),
|
|
74
|
+
"last_changed_by": data.get("lastChangedBy"),
|
|
75
|
+
"last_changed_when": data.get("lastChangedWhen"),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@large_response_handler(
|
|
80
|
+
threshold_bytes=50 * 1024, # 50KB threshold for list tools
|
|
81
|
+
navigation={
|
|
82
|
+
"list_all": "jq '.messages'",
|
|
83
|
+
"find_by_channel": "jq '.messages[] | select(.channel == \"EMAIL\")'",
|
|
84
|
+
"find_by_type": "jq '.messages[] | select(.message_type == \"MESSAGE\")'",
|
|
85
|
+
"find_by_name": "jq '.messages[] | select(.name | contains(\"search\"))'",
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
async def message_list(
|
|
89
|
+
page: int = 0,
|
|
90
|
+
size: int = 50,
|
|
91
|
+
channel: str | None = None,
|
|
92
|
+
name_filter: str | None = None,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
"""List BPA messages (global reusable templates).
|
|
95
|
+
|
|
96
|
+
Large responses (>50KB) are saved to file with navigation hints.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
page: Page number, 0-indexed (default: 0).
|
|
100
|
+
size: Page size (default: 50).
|
|
101
|
+
channel: Filter by channel: EMAIL, SMS, WHATSAPP (optional).
|
|
102
|
+
name_filter: Filter by name substring (optional).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
dict with messages, total, page, size, has_more.
|
|
106
|
+
"""
|
|
107
|
+
if page < 0:
|
|
108
|
+
page = 0
|
|
109
|
+
if size <= 0:
|
|
110
|
+
size = 50
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
async with BPAClient() as client:
|
|
114
|
+
# Build query params for pagination
|
|
115
|
+
params: dict[str, Any] = {
|
|
116
|
+
"page": page,
|
|
117
|
+
"size": size,
|
|
118
|
+
}
|
|
119
|
+
if channel:
|
|
120
|
+
params["channel"] = channel.upper()
|
|
121
|
+
if name_filter:
|
|
122
|
+
params["filter"] = name_filter
|
|
123
|
+
|
|
124
|
+
messages_data = await client.get_list(
|
|
125
|
+
"/message",
|
|
126
|
+
params=params,
|
|
127
|
+
resource_type="message",
|
|
128
|
+
)
|
|
129
|
+
except BPAClientError as e:
|
|
130
|
+
raise translate_error(e, resource_type="message")
|
|
131
|
+
|
|
132
|
+
messages = [_transform_message_response(m) for m in messages_data]
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"messages": messages,
|
|
136
|
+
"total": len(messages),
|
|
137
|
+
"page": page,
|
|
138
|
+
"size": size,
|
|
139
|
+
"has_more": len(messages) == size,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def message_get(message_id: str) -> dict[str, Any]:
|
|
144
|
+
"""Get a BPA message by ID.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
message_id: The message ID.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
dict with id, name, code, subject, content, message_type, channel.
|
|
151
|
+
"""
|
|
152
|
+
if not message_id:
|
|
153
|
+
raise ToolError(
|
|
154
|
+
"Cannot get message: 'message_id' is required. "
|
|
155
|
+
"Use 'message_list' to find valid IDs."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
async with BPAClient() as client:
|
|
160
|
+
try:
|
|
161
|
+
message_data = await client.get(
|
|
162
|
+
"/message/{message_id}",
|
|
163
|
+
path_params={"message_id": message_id},
|
|
164
|
+
resource_type="message",
|
|
165
|
+
resource_id=message_id,
|
|
166
|
+
)
|
|
167
|
+
except BPANotFoundError:
|
|
168
|
+
raise ToolError(
|
|
169
|
+
f"Message '{message_id}' not found. "
|
|
170
|
+
"Use 'message_list' to see available messages."
|
|
171
|
+
)
|
|
172
|
+
except ToolError:
|
|
173
|
+
raise
|
|
174
|
+
except BPAClientError as e:
|
|
175
|
+
raise translate_error(e, resource_type="message", resource_id=message_id)
|
|
176
|
+
|
|
177
|
+
return _transform_message_response(message_data)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def message_create(
|
|
181
|
+
name: str,
|
|
182
|
+
channel: str = "EMAIL",
|
|
183
|
+
message_type: str = "MESSAGE",
|
|
184
|
+
subject: str | None = None,
|
|
185
|
+
content: str | None = None,
|
|
186
|
+
code: str | None = None,
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
"""Create a BPA message template. Audited write operation.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
name: Message name (required).
|
|
192
|
+
channel: EMAIL, SMS, or WHATSAPP (default: EMAIL).
|
|
193
|
+
message_type: MESSAGE or ALERT (default: MESSAGE).
|
|
194
|
+
subject: Email subject line (optional).
|
|
195
|
+
content: Message body/template (optional).
|
|
196
|
+
code: Unique code identifier (optional).
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
dict with id, name, channel, message_type, audit_id.
|
|
200
|
+
"""
|
|
201
|
+
# Pre-flight validation
|
|
202
|
+
if not name or not name.strip():
|
|
203
|
+
raise ToolError("Message name is required.")
|
|
204
|
+
|
|
205
|
+
channel = (channel or "EMAIL").upper()
|
|
206
|
+
if channel not in ("EMAIL", "SMS", "WHATSAPP"):
|
|
207
|
+
raise ToolError("Channel must be EMAIL, SMS, or WHATSAPP.")
|
|
208
|
+
|
|
209
|
+
message_type = (message_type or "MESSAGE").upper()
|
|
210
|
+
if message_type not in ("MESSAGE", "ALERT"):
|
|
211
|
+
raise ToolError("Message type must be MESSAGE or ALERT.")
|
|
212
|
+
|
|
213
|
+
# Get authenticated user for audit
|
|
214
|
+
try:
|
|
215
|
+
user_email = get_current_user_email()
|
|
216
|
+
except NotAuthenticatedError as e:
|
|
217
|
+
raise ToolError(str(e))
|
|
218
|
+
|
|
219
|
+
# Build payload matching BPA Message model
|
|
220
|
+
payload: dict[str, Any] = {
|
|
221
|
+
"name": name.strip(),
|
|
222
|
+
"channel": channel,
|
|
223
|
+
"messageType": message_type,
|
|
224
|
+
}
|
|
225
|
+
if subject:
|
|
226
|
+
payload["subject"] = subject
|
|
227
|
+
if content:
|
|
228
|
+
payload["content"] = content
|
|
229
|
+
if code:
|
|
230
|
+
payload["code"] = code
|
|
231
|
+
|
|
232
|
+
audit_logger = AuditLogger()
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
async with BPAClient() as client:
|
|
236
|
+
# Create PENDING audit record
|
|
237
|
+
audit_id = await audit_logger.record_pending(
|
|
238
|
+
user_email=user_email,
|
|
239
|
+
operation_type="create",
|
|
240
|
+
object_type="message",
|
|
241
|
+
params=payload,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
message_data = await client.post(
|
|
246
|
+
"/message",
|
|
247
|
+
json=payload,
|
|
248
|
+
resource_type="message",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Save rollback state
|
|
252
|
+
created_id = message_data.get("id")
|
|
253
|
+
await audit_logger.save_rollback_state(
|
|
254
|
+
audit_id=audit_id,
|
|
255
|
+
object_type="message",
|
|
256
|
+
object_id=str(created_id),
|
|
257
|
+
previous_state={
|
|
258
|
+
"id": created_id,
|
|
259
|
+
"name": message_data.get("name"),
|
|
260
|
+
"_operation": "create",
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
await audit_logger.mark_success(
|
|
265
|
+
audit_id=audit_id,
|
|
266
|
+
result={"id": created_id, "name": message_data.get("name")},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
except BPAClientError as e:
|
|
270
|
+
await audit_logger.mark_failed(audit_id=audit_id, error_message=str(e))
|
|
271
|
+
raise translate_error(e, resource_type="message")
|
|
272
|
+
|
|
273
|
+
except ToolError:
|
|
274
|
+
raise
|
|
275
|
+
except BPAClientError as e:
|
|
276
|
+
raise translate_error(e, resource_type="message")
|
|
277
|
+
|
|
278
|
+
result = _transform_message_response(message_data)
|
|
279
|
+
result["audit_id"] = audit_id
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
async def message_update(
|
|
284
|
+
message_id: str,
|
|
285
|
+
name: str | None = None,
|
|
286
|
+
subject: str | None = None,
|
|
287
|
+
content: str | None = None,
|
|
288
|
+
channel: str | None = None,
|
|
289
|
+
message_type: str | None = None,
|
|
290
|
+
code: str | None = None,
|
|
291
|
+
) -> dict[str, Any]:
|
|
292
|
+
"""Update a BPA message. Audited write operation.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
message_id: Message ID to update (required).
|
|
296
|
+
name: New message name (optional).
|
|
297
|
+
subject: New subject line (optional).
|
|
298
|
+
content: New message body (optional).
|
|
299
|
+
channel: New channel: EMAIL, SMS, WHATSAPP (optional).
|
|
300
|
+
message_type: New type: MESSAGE, ALERT (optional).
|
|
301
|
+
code: New code identifier (optional).
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
dict with id, name, channel, previous_state, audit_id.
|
|
305
|
+
"""
|
|
306
|
+
if not message_id:
|
|
307
|
+
raise ToolError(
|
|
308
|
+
"Cannot update message: 'message_id' is required. "
|
|
309
|
+
"Use 'message_list' to find valid IDs."
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# At least one field must be provided
|
|
313
|
+
if all(v is None for v in [name, subject, content, channel, message_type, code]):
|
|
314
|
+
raise ToolError("At least one field must be provided for update.")
|
|
315
|
+
|
|
316
|
+
# Validate channel if provided
|
|
317
|
+
if channel:
|
|
318
|
+
channel = channel.upper()
|
|
319
|
+
if channel not in ("EMAIL", "SMS", "WHATSAPP"):
|
|
320
|
+
raise ToolError("Channel must be EMAIL, SMS, or WHATSAPP.")
|
|
321
|
+
|
|
322
|
+
# Validate message_type if provided
|
|
323
|
+
if message_type:
|
|
324
|
+
message_type = message_type.upper()
|
|
325
|
+
if message_type not in ("MESSAGE", "ALERT"):
|
|
326
|
+
raise ToolError("Message type must be MESSAGE or ALERT.")
|
|
327
|
+
|
|
328
|
+
# Get authenticated user for audit
|
|
329
|
+
try:
|
|
330
|
+
user_email = get_current_user_email()
|
|
331
|
+
except NotAuthenticatedError as e:
|
|
332
|
+
raise ToolError(str(e))
|
|
333
|
+
|
|
334
|
+
audit_logger = AuditLogger()
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
async with BPAClient() as client:
|
|
338
|
+
# Capture current state for rollback
|
|
339
|
+
try:
|
|
340
|
+
previous_state = await client.get(
|
|
341
|
+
"/message/{message_id}",
|
|
342
|
+
path_params={"message_id": message_id},
|
|
343
|
+
resource_type="message",
|
|
344
|
+
resource_id=message_id,
|
|
345
|
+
)
|
|
346
|
+
except BPANotFoundError:
|
|
347
|
+
raise ToolError(
|
|
348
|
+
f"Message '{message_id}' not found. "
|
|
349
|
+
"Use 'message_list' to see available messages."
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Merge with current state - PUT /message requires full object
|
|
353
|
+
prev = previous_state
|
|
354
|
+
payload: dict[str, Any] = {
|
|
355
|
+
"id": message_id,
|
|
356
|
+
"name": name.strip() if name else prev.get("name"),
|
|
357
|
+
"subject": subject if subject is not None else prev.get("subject"),
|
|
358
|
+
"content": content if content is not None else prev.get("content"),
|
|
359
|
+
"channel": channel if channel else prev.get("channel"),
|
|
360
|
+
"messageType": (
|
|
361
|
+
message_type if message_type else prev.get("messageType")
|
|
362
|
+
),
|
|
363
|
+
"code": code if code is not None else prev.get("code"),
|
|
364
|
+
}
|
|
365
|
+
# Preserve linked data
|
|
366
|
+
if previous_state.get("roleRegistrations"):
|
|
367
|
+
payload["roleRegistrations"] = previous_state["roleRegistrations"]
|
|
368
|
+
if previous_state.get("messageRoleStatuses"):
|
|
369
|
+
payload["messageRoleStatuses"] = previous_state["messageRoleStatuses"]
|
|
370
|
+
if previous_state.get("messageBots"):
|
|
371
|
+
payload["messageBots"] = previous_state["messageBots"]
|
|
372
|
+
if previous_state.get("businessKey"):
|
|
373
|
+
payload["businessKey"] = previous_state["businessKey"]
|
|
374
|
+
|
|
375
|
+
# Create PENDING audit record
|
|
376
|
+
audit_id = await audit_logger.record_pending(
|
|
377
|
+
user_email=user_email,
|
|
378
|
+
operation_type="update",
|
|
379
|
+
object_type="message",
|
|
380
|
+
object_id=message_id,
|
|
381
|
+
params={"changes": payload},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Save rollback state
|
|
385
|
+
await audit_logger.save_rollback_state(
|
|
386
|
+
audit_id=audit_id,
|
|
387
|
+
object_type="message",
|
|
388
|
+
object_id=message_id,
|
|
389
|
+
previous_state=previous_state,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
message_data = await client.put(
|
|
394
|
+
"/message",
|
|
395
|
+
json=payload,
|
|
396
|
+
resource_type="message",
|
|
397
|
+
resource_id=message_id,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
await audit_logger.mark_success(
|
|
401
|
+
audit_id=audit_id,
|
|
402
|
+
result={
|
|
403
|
+
"id": message_data.get("id"),
|
|
404
|
+
"name": message_data.get("name"),
|
|
405
|
+
},
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
except BPAClientError as e:
|
|
409
|
+
await audit_logger.mark_failed(audit_id=audit_id, error_message=str(e))
|
|
410
|
+
raise translate_error(
|
|
411
|
+
e, resource_type="message", resource_id=message_id
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
except ToolError:
|
|
415
|
+
raise
|
|
416
|
+
except BPAClientError as e:
|
|
417
|
+
raise translate_error(e, resource_type="message", resource_id=message_id)
|
|
418
|
+
|
|
419
|
+
result = _transform_message_response(message_data)
|
|
420
|
+
result["previous_state"] = _transform_message_response(previous_state)
|
|
421
|
+
result["audit_id"] = audit_id
|
|
422
|
+
return result
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
async def message_delete(message_id: str) -> dict[str, Any]:
|
|
426
|
+
"""Delete a BPA message. Audited write operation.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
message_id: Message ID to delete.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
dict with deleted (bool), message_id, deleted_message, audit_id.
|
|
433
|
+
"""
|
|
434
|
+
if not message_id:
|
|
435
|
+
raise ToolError(
|
|
436
|
+
"Cannot delete message: 'message_id' is required. "
|
|
437
|
+
"Use 'message_list' to find valid IDs."
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Get authenticated user for audit
|
|
441
|
+
try:
|
|
442
|
+
user_email = get_current_user_email()
|
|
443
|
+
except NotAuthenticatedError as e:
|
|
444
|
+
raise ToolError(str(e))
|
|
445
|
+
|
|
446
|
+
audit_logger = AuditLogger()
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
async with BPAClient() as client:
|
|
450
|
+
# Capture current state for rollback
|
|
451
|
+
try:
|
|
452
|
+
previous_state = await client.get(
|
|
453
|
+
"/message/{message_id}",
|
|
454
|
+
path_params={"message_id": message_id},
|
|
455
|
+
resource_type="message",
|
|
456
|
+
resource_id=message_id,
|
|
457
|
+
)
|
|
458
|
+
except BPANotFoundError:
|
|
459
|
+
raise ToolError(
|
|
460
|
+
f"Message '{message_id}' not found. "
|
|
461
|
+
"Use 'message_list' to see available messages."
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Create PENDING audit record
|
|
465
|
+
audit_id = await audit_logger.record_pending(
|
|
466
|
+
user_email=user_email,
|
|
467
|
+
operation_type="delete",
|
|
468
|
+
object_type="message",
|
|
469
|
+
object_id=message_id,
|
|
470
|
+
params={},
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Save rollback state
|
|
474
|
+
await audit_logger.save_rollback_state(
|
|
475
|
+
audit_id=audit_id,
|
|
476
|
+
object_type="message",
|
|
477
|
+
object_id=message_id,
|
|
478
|
+
previous_state=previous_state,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
await client.delete(
|
|
483
|
+
"/message/{message_id}",
|
|
484
|
+
path_params={"message_id": message_id},
|
|
485
|
+
resource_type="message",
|
|
486
|
+
resource_id=message_id,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
await audit_logger.mark_success(
|
|
490
|
+
audit_id=audit_id,
|
|
491
|
+
result={"deleted": True, "message_id": message_id},
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
except BPAClientError as e:
|
|
495
|
+
await audit_logger.mark_failed(audit_id=audit_id, error_message=str(e))
|
|
496
|
+
raise translate_error(
|
|
497
|
+
e, resource_type="message", resource_id=message_id
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
except ToolError:
|
|
501
|
+
raise
|
|
502
|
+
except BPAClientError as e:
|
|
503
|
+
raise translate_error(e, resource_type="message", resource_id=message_id)
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
"deleted": True,
|
|
507
|
+
"message_id": message_id,
|
|
508
|
+
"deleted_message": _transform_message_response(previous_state),
|
|
509
|
+
"audit_id": audit_id,
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def register_message_tools(mcp: Any) -> None:
|
|
514
|
+
"""Register message tools with the MCP server.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
mcp: The FastMCP server instance.
|
|
518
|
+
"""
|
|
519
|
+
mcp.tool()(message_list)
|
|
520
|
+
mcp.tool()(message_get)
|
|
521
|
+
mcp.tool()(message_create)
|
|
522
|
+
mcp.tool()(message_update)
|
|
523
|
+
mcp.tool()(message_delete)
|