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,627 @@
|
|
|
1
|
+
"""MCP tools for BPA bot operations.
|
|
2
|
+
|
|
3
|
+
This module provides tools for listing, retrieving, creating, and updating BPA bots.
|
|
4
|
+
Bots are workflow automation entities that execute actions on form components.
|
|
5
|
+
|
|
6
|
+
Write operations follow the audit-before-write pattern:
|
|
7
|
+
1. Validate parameters (pre-flight, no audit record if validation fails)
|
|
8
|
+
2. Create PENDING audit record
|
|
9
|
+
3. Execute BPA API call
|
|
10
|
+
4. Update audit record to SUCCESS or FAILED
|
|
11
|
+
|
|
12
|
+
API Endpoints used:
|
|
13
|
+
- GET /service/{service_id}/bot - List bots for a service
|
|
14
|
+
- GET /bot/{bot_id} - Get bot by ID
|
|
15
|
+
- POST /service/{service_id}/bot - Create bot within service
|
|
16
|
+
- PUT /bot - Update bot
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
24
|
+
|
|
25
|
+
from mcp_eregistrations_bpa.audit.context import (
|
|
26
|
+
NotAuthenticatedError,
|
|
27
|
+
get_current_user_email,
|
|
28
|
+
)
|
|
29
|
+
from mcp_eregistrations_bpa.audit.logger import AuditLogger
|
|
30
|
+
from mcp_eregistrations_bpa.bpa_client import BPAClient
|
|
31
|
+
from mcp_eregistrations_bpa.bpa_client.errors import (
|
|
32
|
+
BPAClientError,
|
|
33
|
+
BPANotFoundError,
|
|
34
|
+
translate_error,
|
|
35
|
+
)
|
|
36
|
+
from mcp_eregistrations_bpa.tools.large_response import large_response_handler
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"bot_list",
|
|
40
|
+
"bot_get",
|
|
41
|
+
"bot_create",
|
|
42
|
+
"bot_update",
|
|
43
|
+
"bot_delete",
|
|
44
|
+
"register_bot_tools",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _transform_bot_response(data: dict[str, Any]) -> dict[str, Any]:
|
|
49
|
+
"""Transform bot API response from camelCase to snake_case.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
data: Raw API response with camelCase keys.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict: Transformed response with snake_case keys.
|
|
56
|
+
"""
|
|
57
|
+
return {
|
|
58
|
+
"id": data.get("id"),
|
|
59
|
+
"name": data.get("name"),
|
|
60
|
+
"bot_type": data.get("botType"),
|
|
61
|
+
"description": data.get("description"),
|
|
62
|
+
"enabled": data.get("enabled", True),
|
|
63
|
+
"service_id": data.get("serviceId"),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@large_response_handler(
|
|
68
|
+
threshold_bytes=50 * 1024, # 50KB threshold for list tools
|
|
69
|
+
navigation={
|
|
70
|
+
"list_all": "jq '.bots'",
|
|
71
|
+
"find_by_type": "jq '.bots[] | select(.bot_type == \"BotType\")'",
|
|
72
|
+
"find_by_name": "jq '.bots[] | select(.name | contains(\"search\"))'",
|
|
73
|
+
"enabled_only": "jq '.bots[] | select(.enabled == true)'",
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
async def bot_list(service_id: str | int) -> dict[str, Any]:
|
|
77
|
+
"""List bots for a service.
|
|
78
|
+
|
|
79
|
+
Large responses (>50KB) are saved to file with navigation hints.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
service_id: Service ID to list bots for.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
dict with bots, service_id, total.
|
|
86
|
+
"""
|
|
87
|
+
if not service_id:
|
|
88
|
+
raise ToolError(
|
|
89
|
+
"Cannot list bots: 'service_id' is required. "
|
|
90
|
+
"Use 'service_list' to find valid service IDs."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
async with BPAClient() as client:
|
|
95
|
+
try:
|
|
96
|
+
bots_data = await client.get_list(
|
|
97
|
+
"/service/{service_id}/bot",
|
|
98
|
+
path_params={"service_id": service_id},
|
|
99
|
+
resource_type="bot",
|
|
100
|
+
)
|
|
101
|
+
except BPANotFoundError:
|
|
102
|
+
raise ToolError(
|
|
103
|
+
f"Service '{service_id}' not found. "
|
|
104
|
+
"Use 'service_list' to see available services."
|
|
105
|
+
)
|
|
106
|
+
except ToolError:
|
|
107
|
+
raise
|
|
108
|
+
except BPAClientError as e:
|
|
109
|
+
raise translate_error(e, resource_type="bot")
|
|
110
|
+
|
|
111
|
+
# Transform to consistent output format
|
|
112
|
+
bots = [_transform_bot_response(bot) for bot in bots_data]
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"bots": bots,
|
|
116
|
+
"service_id": service_id,
|
|
117
|
+
"total": len(bots),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def bot_get(bot_id: str | int) -> dict[str, Any]:
|
|
122
|
+
"""Get bot details by ID.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
bot_id: Bot ID.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
dict with id, name, bot_type, description, enabled, service_id.
|
|
129
|
+
"""
|
|
130
|
+
if not bot_id:
|
|
131
|
+
raise ToolError(
|
|
132
|
+
"Cannot get bot: 'bot_id' is required. "
|
|
133
|
+
"Use 'bot_list' with service_id to find valid bot IDs."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
async with BPAClient() as client:
|
|
138
|
+
try:
|
|
139
|
+
bot_data = await client.get(
|
|
140
|
+
"/bot/{bot_id}",
|
|
141
|
+
path_params={"bot_id": bot_id},
|
|
142
|
+
resource_type="bot",
|
|
143
|
+
resource_id=bot_id,
|
|
144
|
+
)
|
|
145
|
+
except BPANotFoundError:
|
|
146
|
+
raise ToolError(
|
|
147
|
+
f"Bot '{bot_id}' not found. "
|
|
148
|
+
"Use 'bot_list' with service_id to see available bots."
|
|
149
|
+
)
|
|
150
|
+
except ToolError:
|
|
151
|
+
raise
|
|
152
|
+
except BPAClientError as e:
|
|
153
|
+
raise translate_error(e, resource_type="bot", resource_id=bot_id)
|
|
154
|
+
|
|
155
|
+
return _transform_bot_response(bot_data)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _validate_bot_create_params(
|
|
159
|
+
service_id: str | int,
|
|
160
|
+
bot_type: str,
|
|
161
|
+
name: str,
|
|
162
|
+
description: str | None,
|
|
163
|
+
) -> dict[str, Any]:
|
|
164
|
+
"""Validate bot_create parameters (pre-flight).
|
|
165
|
+
|
|
166
|
+
Returns validated params dict or raises ToolError if invalid.
|
|
167
|
+
No audit record is created for validation failures.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
service_id: Parent service ID (required).
|
|
171
|
+
bot_type: Bot type identifier (required).
|
|
172
|
+
name: Bot name (required).
|
|
173
|
+
description: Bot description (optional).
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
dict: Validated parameters ready for API call.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
ToolError: If validation fails.
|
|
180
|
+
"""
|
|
181
|
+
errors = []
|
|
182
|
+
|
|
183
|
+
if not service_id:
|
|
184
|
+
errors.append("'service_id' is required")
|
|
185
|
+
|
|
186
|
+
if not bot_type or not str(bot_type).strip():
|
|
187
|
+
errors.append("'bot_type' is required")
|
|
188
|
+
|
|
189
|
+
if not name or not name.strip():
|
|
190
|
+
errors.append("'name' is required and cannot be empty")
|
|
191
|
+
|
|
192
|
+
if name and len(name.strip()) > 255:
|
|
193
|
+
errors.append("'name' must be 255 characters or less")
|
|
194
|
+
|
|
195
|
+
if errors:
|
|
196
|
+
error_msg = "; ".join(errors)
|
|
197
|
+
raise ToolError(f"Cannot create bot: {error_msg}. Check required fields.")
|
|
198
|
+
|
|
199
|
+
params: dict[str, Any] = {
|
|
200
|
+
"botType": str(bot_type).strip(),
|
|
201
|
+
"name": name.strip(),
|
|
202
|
+
"enabled": True,
|
|
203
|
+
}
|
|
204
|
+
if description:
|
|
205
|
+
params["description"] = description.strip()
|
|
206
|
+
|
|
207
|
+
return params
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def bot_create(
|
|
211
|
+
service_id: str | int,
|
|
212
|
+
bot_type: str,
|
|
213
|
+
name: str,
|
|
214
|
+
description: str | None = None,
|
|
215
|
+
enabled: bool = True,
|
|
216
|
+
) -> dict[str, Any]:
|
|
217
|
+
"""Create bot in a service. Audited write operation.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
service_id: Parent service ID.
|
|
221
|
+
bot_type: Bot type identifier.
|
|
222
|
+
name: Bot name.
|
|
223
|
+
description: Optional description.
|
|
224
|
+
enabled: Enabled status (default: True).
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
dict with id, name, bot_type, description, enabled, service_id, audit_id.
|
|
228
|
+
"""
|
|
229
|
+
# Pre-flight validation (no audit record for validation failures)
|
|
230
|
+
validated_params = _validate_bot_create_params(
|
|
231
|
+
service_id, bot_type, name, description
|
|
232
|
+
)
|
|
233
|
+
validated_params["enabled"] = enabled
|
|
234
|
+
|
|
235
|
+
# Get authenticated user for audit (before any API calls)
|
|
236
|
+
try:
|
|
237
|
+
user_email = get_current_user_email()
|
|
238
|
+
except NotAuthenticatedError as e:
|
|
239
|
+
raise ToolError(str(e))
|
|
240
|
+
|
|
241
|
+
# Use single BPAClient connection for all operations
|
|
242
|
+
try:
|
|
243
|
+
async with BPAClient() as client:
|
|
244
|
+
# Verify parent service exists before creating audit record
|
|
245
|
+
try:
|
|
246
|
+
await client.get(
|
|
247
|
+
"/service/{id}",
|
|
248
|
+
path_params={"id": service_id},
|
|
249
|
+
resource_type="service",
|
|
250
|
+
resource_id=service_id,
|
|
251
|
+
)
|
|
252
|
+
except BPANotFoundError:
|
|
253
|
+
raise ToolError(
|
|
254
|
+
f"Cannot create bot: Service '{service_id}' not found. "
|
|
255
|
+
"Use 'service_list' to see available services."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Create audit record BEFORE API call (audit-before-write pattern)
|
|
259
|
+
audit_logger = AuditLogger()
|
|
260
|
+
audit_id = await audit_logger.record_pending(
|
|
261
|
+
user_email=user_email,
|
|
262
|
+
operation_type="create",
|
|
263
|
+
object_type="bot",
|
|
264
|
+
params={
|
|
265
|
+
"service_id": str(service_id),
|
|
266
|
+
**validated_params,
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
bot_data = await client.post(
|
|
272
|
+
"/service/{service_id}/bot",
|
|
273
|
+
path_params={"service_id": service_id},
|
|
274
|
+
json=validated_params,
|
|
275
|
+
resource_type="bot",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Save rollback state (for create, save ID to enable deletion)
|
|
279
|
+
created_id = bot_data.get("id")
|
|
280
|
+
await audit_logger.save_rollback_state(
|
|
281
|
+
audit_id=audit_id,
|
|
282
|
+
object_type="bot",
|
|
283
|
+
object_id=str(created_id),
|
|
284
|
+
previous_state={
|
|
285
|
+
"id": created_id,
|
|
286
|
+
"name": bot_data.get("name"),
|
|
287
|
+
"botType": bot_data.get("botType"),
|
|
288
|
+
"description": bot_data.get("description"),
|
|
289
|
+
"enabled": bot_data.get("enabled"),
|
|
290
|
+
"serviceId": str(service_id),
|
|
291
|
+
"_operation": "create", # Marker for rollback to DELETE
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Mark audit as success
|
|
296
|
+
await audit_logger.mark_success(
|
|
297
|
+
audit_id,
|
|
298
|
+
result={
|
|
299
|
+
"bot_id": bot_data.get("id"),
|
|
300
|
+
"name": bot_data.get("name"),
|
|
301
|
+
"service_id": str(service_id),
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
result = _transform_bot_response(bot_data)
|
|
306
|
+
result["service_id"] = service_id # Ensure service_id is always set
|
|
307
|
+
result["audit_id"] = audit_id
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
except BPAClientError as e:
|
|
311
|
+
# Mark audit as failed
|
|
312
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
313
|
+
raise translate_error(e, resource_type="bot")
|
|
314
|
+
|
|
315
|
+
except ToolError:
|
|
316
|
+
raise
|
|
317
|
+
except BPAClientError as e:
|
|
318
|
+
raise translate_error(e, resource_type="service", resource_id=service_id)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _validate_bot_update_params(
|
|
322
|
+
bot_id: str | int,
|
|
323
|
+
name: str | None,
|
|
324
|
+
description: str | None,
|
|
325
|
+
enabled: bool | None,
|
|
326
|
+
) -> dict[str, Any]:
|
|
327
|
+
"""Validate bot_update parameters (pre-flight).
|
|
328
|
+
|
|
329
|
+
Returns validated params dict or raises ToolError if invalid.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
bot_id: ID of bot to update (required).
|
|
333
|
+
name: New name (optional).
|
|
334
|
+
description: New description (optional).
|
|
335
|
+
enabled: New enabled status (optional).
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
dict: Validated parameters ready for API call.
|
|
339
|
+
|
|
340
|
+
Raises:
|
|
341
|
+
ToolError: If validation fails.
|
|
342
|
+
"""
|
|
343
|
+
errors = []
|
|
344
|
+
|
|
345
|
+
if not bot_id:
|
|
346
|
+
errors.append("'bot_id' is required")
|
|
347
|
+
|
|
348
|
+
if name is not None and not name.strip():
|
|
349
|
+
errors.append("'name' cannot be empty when provided")
|
|
350
|
+
|
|
351
|
+
if name and len(name.strip()) > 255:
|
|
352
|
+
errors.append("'name' must be 255 characters or less")
|
|
353
|
+
|
|
354
|
+
# At least one field must be provided for update
|
|
355
|
+
if name is None and description is None and enabled is None:
|
|
356
|
+
errors.append(
|
|
357
|
+
"At least one field (name, description, enabled) must be provided"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if errors:
|
|
361
|
+
error_msg = "; ".join(errors)
|
|
362
|
+
raise ToolError(f"Cannot update bot: {error_msg}. Check required fields.")
|
|
363
|
+
|
|
364
|
+
params: dict[str, Any] = {"id": bot_id}
|
|
365
|
+
if name is not None:
|
|
366
|
+
params["name"] = name.strip()
|
|
367
|
+
if description is not None:
|
|
368
|
+
params["description"] = description.strip()
|
|
369
|
+
if enabled is not None:
|
|
370
|
+
params["enabled"] = enabled
|
|
371
|
+
|
|
372
|
+
return params
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def bot_update(
|
|
376
|
+
bot_id: str | int,
|
|
377
|
+
name: str | None = None,
|
|
378
|
+
description: str | None = None,
|
|
379
|
+
enabled: bool | None = None,
|
|
380
|
+
) -> dict[str, Any]:
|
|
381
|
+
"""Update a bot. Audited write operation.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
bot_id: Bot ID to update.
|
|
385
|
+
name: New name (optional).
|
|
386
|
+
description: New description (optional).
|
|
387
|
+
enabled: New enabled status (optional).
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
dict with id, name, bot_type, description, enabled, previous_state, audit_id.
|
|
391
|
+
"""
|
|
392
|
+
# Pre-flight validation (no audit record for validation failures)
|
|
393
|
+
validated_params = _validate_bot_update_params(bot_id, name, description, enabled)
|
|
394
|
+
|
|
395
|
+
# Get authenticated user for audit
|
|
396
|
+
try:
|
|
397
|
+
user_email = get_current_user_email()
|
|
398
|
+
except NotAuthenticatedError as e:
|
|
399
|
+
raise ToolError(str(e))
|
|
400
|
+
|
|
401
|
+
# Use single BPAClient connection for all operations
|
|
402
|
+
try:
|
|
403
|
+
async with BPAClient() as client:
|
|
404
|
+
# Capture current state for rollback BEFORE making changes
|
|
405
|
+
try:
|
|
406
|
+
previous_state = await client.get(
|
|
407
|
+
"/bot/{bot_id}",
|
|
408
|
+
path_params={"bot_id": bot_id},
|
|
409
|
+
resource_type="bot",
|
|
410
|
+
resource_id=bot_id,
|
|
411
|
+
)
|
|
412
|
+
except BPANotFoundError:
|
|
413
|
+
raise ToolError(
|
|
414
|
+
f"Bot '{bot_id}' not found. "
|
|
415
|
+
"Use 'bot_list' with service_id to see available bots."
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Merge provided changes with current state for full object PUT
|
|
419
|
+
full_params = {
|
|
420
|
+
"id": bot_id,
|
|
421
|
+
"name": validated_params.get("name", previous_state.get("name")),
|
|
422
|
+
"botType": previous_state.get("botType"),
|
|
423
|
+
"description": validated_params.get(
|
|
424
|
+
"description", previous_state.get("description")
|
|
425
|
+
),
|
|
426
|
+
"enabled": validated_params.get(
|
|
427
|
+
"enabled", previous_state.get("enabled", True)
|
|
428
|
+
),
|
|
429
|
+
"serviceId": previous_state.get("serviceId"),
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# Create audit record BEFORE API call (audit-before-write pattern)
|
|
433
|
+
audit_logger = AuditLogger()
|
|
434
|
+
audit_id = await audit_logger.record_pending(
|
|
435
|
+
user_email=user_email,
|
|
436
|
+
operation_type="update",
|
|
437
|
+
object_type="bot",
|
|
438
|
+
object_id=str(bot_id),
|
|
439
|
+
params={
|
|
440
|
+
"changes": validated_params,
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Save rollback state for undo capability
|
|
445
|
+
await audit_logger.save_rollback_state(
|
|
446
|
+
audit_id=audit_id,
|
|
447
|
+
object_type="bot",
|
|
448
|
+
object_id=str(bot_id),
|
|
449
|
+
previous_state={
|
|
450
|
+
"id": previous_state.get("id"),
|
|
451
|
+
"name": previous_state.get("name"),
|
|
452
|
+
"botType": previous_state.get("botType"),
|
|
453
|
+
"description": previous_state.get("description"),
|
|
454
|
+
"enabled": previous_state.get("enabled"),
|
|
455
|
+
"serviceId": previous_state.get("serviceId"),
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
bot_data = await client.put(
|
|
461
|
+
"/bot",
|
|
462
|
+
json=full_params,
|
|
463
|
+
resource_type="bot",
|
|
464
|
+
resource_id=bot_id,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Mark audit as success
|
|
468
|
+
await audit_logger.mark_success(
|
|
469
|
+
audit_id,
|
|
470
|
+
result={
|
|
471
|
+
"bot_id": bot_data.get("id"),
|
|
472
|
+
"name": bot_data.get("name"),
|
|
473
|
+
"changes_applied": {
|
|
474
|
+
k: v for k, v in validated_params.items() if k != "id"
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
result = _transform_bot_response(bot_data)
|
|
480
|
+
result["previous_state"] = {
|
|
481
|
+
"name": previous_state.get("name"),
|
|
482
|
+
"description": previous_state.get("description"),
|
|
483
|
+
"enabled": previous_state.get("enabled"),
|
|
484
|
+
}
|
|
485
|
+
result["audit_id"] = audit_id
|
|
486
|
+
return result
|
|
487
|
+
|
|
488
|
+
except BPAClientError as e:
|
|
489
|
+
# Mark audit as failed
|
|
490
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
491
|
+
raise translate_error(e, resource_type="bot", resource_id=bot_id)
|
|
492
|
+
|
|
493
|
+
except ToolError:
|
|
494
|
+
raise
|
|
495
|
+
except BPAClientError as e:
|
|
496
|
+
raise translate_error(e, resource_type="bot", resource_id=bot_id)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# =============================================================================
|
|
500
|
+
# bot_delete
|
|
501
|
+
# =============================================================================
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _validate_bot_delete_params(bot_id: str | int) -> None:
|
|
505
|
+
"""Validate bot_delete parameters before processing.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
bot_id: ID of the bot to delete.
|
|
509
|
+
|
|
510
|
+
Raises:
|
|
511
|
+
ToolError: If validation fails.
|
|
512
|
+
"""
|
|
513
|
+
if not bot_id or (isinstance(bot_id, str) and not bot_id.strip()):
|
|
514
|
+
raise ToolError(
|
|
515
|
+
"'bot_id' is required. "
|
|
516
|
+
"Use 'bot_list' with service_id to see available bots."
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
async def bot_delete(bot_id: str | int) -> dict[str, Any]:
|
|
521
|
+
"""Delete a bot. Audited write operation.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
bot_id: Bot ID to delete.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
dict with deleted (bool), bot_id, deleted_bot, audit_id.
|
|
528
|
+
"""
|
|
529
|
+
# Pre-flight validation (no audit record for validation failures)
|
|
530
|
+
_validate_bot_delete_params(bot_id)
|
|
531
|
+
|
|
532
|
+
# Get authenticated user for audit
|
|
533
|
+
try:
|
|
534
|
+
user_email = get_current_user_email()
|
|
535
|
+
except NotAuthenticatedError as e:
|
|
536
|
+
raise ToolError(str(e))
|
|
537
|
+
|
|
538
|
+
# Use single BPAClient connection for all operations
|
|
539
|
+
try:
|
|
540
|
+
async with BPAClient() as client:
|
|
541
|
+
# Capture current state for rollback BEFORE making changes
|
|
542
|
+
try:
|
|
543
|
+
previous_state = await client.get(
|
|
544
|
+
"/bot/{bot_id}",
|
|
545
|
+
path_params={"bot_id": bot_id},
|
|
546
|
+
resource_type="bot",
|
|
547
|
+
resource_id=bot_id,
|
|
548
|
+
)
|
|
549
|
+
except BPANotFoundError:
|
|
550
|
+
raise ToolError(
|
|
551
|
+
f"Bot '{bot_id}' not found. "
|
|
552
|
+
"Use 'bot_list' with service_id to see available bots."
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Create audit record BEFORE API call (audit-before-write pattern)
|
|
556
|
+
audit_logger = AuditLogger()
|
|
557
|
+
audit_id = await audit_logger.record_pending(
|
|
558
|
+
user_email=user_email,
|
|
559
|
+
operation_type="delete",
|
|
560
|
+
object_type="bot",
|
|
561
|
+
object_id=str(bot_id),
|
|
562
|
+
params={},
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Save rollback state for undo capability (recreate on rollback)
|
|
566
|
+
await audit_logger.save_rollback_state(
|
|
567
|
+
audit_id=audit_id,
|
|
568
|
+
object_type="bot",
|
|
569
|
+
object_id=str(bot_id),
|
|
570
|
+
previous_state={
|
|
571
|
+
"id": previous_state.get("id"),
|
|
572
|
+
"name": previous_state.get("name"),
|
|
573
|
+
"botType": previous_state.get("botType"),
|
|
574
|
+
"description": previous_state.get("description"),
|
|
575
|
+
"enabled": previous_state.get("enabled"),
|
|
576
|
+
"serviceId": previous_state.get("serviceId"),
|
|
577
|
+
},
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
try:
|
|
581
|
+
await client.delete(
|
|
582
|
+
"/bot/{bot_id}",
|
|
583
|
+
path_params={"bot_id": bot_id},
|
|
584
|
+
resource_type="bot",
|
|
585
|
+
resource_id=bot_id,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Mark audit as success
|
|
589
|
+
await audit_logger.mark_success(
|
|
590
|
+
audit_id,
|
|
591
|
+
result={
|
|
592
|
+
"deleted": True,
|
|
593
|
+
"bot_id": str(bot_id),
|
|
594
|
+
},
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
"deleted": True,
|
|
599
|
+
"bot_id": str(bot_id),
|
|
600
|
+
"deleted_bot": _transform_bot_response(previous_state),
|
|
601
|
+
"audit_id": audit_id,
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
except BPAClientError as e:
|
|
605
|
+
# Mark audit as failed
|
|
606
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
607
|
+
raise translate_error(e, resource_type="bot", resource_id=bot_id)
|
|
608
|
+
|
|
609
|
+
except ToolError:
|
|
610
|
+
raise
|
|
611
|
+
except BPAClientError as e:
|
|
612
|
+
raise translate_error(e, resource_type="bot", resource_id=bot_id)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def register_bot_tools(mcp: Any) -> None:
|
|
616
|
+
"""Register bot tools with the MCP server.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
mcp: The FastMCP server instance.
|
|
620
|
+
"""
|
|
621
|
+
# Read operations
|
|
622
|
+
mcp.tool()(bot_list)
|
|
623
|
+
mcp.tool()(bot_get)
|
|
624
|
+
# Write operations (audit-before-write pattern)
|
|
625
|
+
mcp.tool()(bot_create)
|
|
626
|
+
mcp.tool()(bot_update)
|
|
627
|
+
mcp.tool()(bot_delete)
|