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.

Files changed (66) hide show
  1. mcp_eregistrations_bpa/__init__.py +121 -0
  2. mcp_eregistrations_bpa/__main__.py +6 -0
  3. mcp_eregistrations_bpa/arazzo/__init__.py +21 -0
  4. mcp_eregistrations_bpa/arazzo/expression.py +379 -0
  5. mcp_eregistrations_bpa/audit/__init__.py +56 -0
  6. mcp_eregistrations_bpa/audit/context.py +66 -0
  7. mcp_eregistrations_bpa/audit/logger.py +236 -0
  8. mcp_eregistrations_bpa/audit/models.py +131 -0
  9. mcp_eregistrations_bpa/auth/__init__.py +64 -0
  10. mcp_eregistrations_bpa/auth/callback.py +391 -0
  11. mcp_eregistrations_bpa/auth/cas.py +409 -0
  12. mcp_eregistrations_bpa/auth/oidc.py +252 -0
  13. mcp_eregistrations_bpa/auth/permissions.py +162 -0
  14. mcp_eregistrations_bpa/auth/token_manager.py +348 -0
  15. mcp_eregistrations_bpa/bpa_client/__init__.py +84 -0
  16. mcp_eregistrations_bpa/bpa_client/client.py +740 -0
  17. mcp_eregistrations_bpa/bpa_client/endpoints.py +193 -0
  18. mcp_eregistrations_bpa/bpa_client/errors.py +276 -0
  19. mcp_eregistrations_bpa/bpa_client/models.py +203 -0
  20. mcp_eregistrations_bpa/config.py +349 -0
  21. mcp_eregistrations_bpa/db/__init__.py +21 -0
  22. mcp_eregistrations_bpa/db/connection.py +64 -0
  23. mcp_eregistrations_bpa/db/migrations.py +168 -0
  24. mcp_eregistrations_bpa/exceptions.py +39 -0
  25. mcp_eregistrations_bpa/py.typed +0 -0
  26. mcp_eregistrations_bpa/rollback/__init__.py +19 -0
  27. mcp_eregistrations_bpa/rollback/manager.py +616 -0
  28. mcp_eregistrations_bpa/server.py +152 -0
  29. mcp_eregistrations_bpa/tools/__init__.py +372 -0
  30. mcp_eregistrations_bpa/tools/actions.py +155 -0
  31. mcp_eregistrations_bpa/tools/analysis.py +352 -0
  32. mcp_eregistrations_bpa/tools/audit.py +399 -0
  33. mcp_eregistrations_bpa/tools/behaviours.py +1042 -0
  34. mcp_eregistrations_bpa/tools/bots.py +627 -0
  35. mcp_eregistrations_bpa/tools/classifications.py +575 -0
  36. mcp_eregistrations_bpa/tools/costs.py +765 -0
  37. mcp_eregistrations_bpa/tools/debug_strategies.py +351 -0
  38. mcp_eregistrations_bpa/tools/debugger.py +1230 -0
  39. mcp_eregistrations_bpa/tools/determinants.py +2235 -0
  40. mcp_eregistrations_bpa/tools/document_requirements.py +670 -0
  41. mcp_eregistrations_bpa/tools/export.py +899 -0
  42. mcp_eregistrations_bpa/tools/fields.py +162 -0
  43. mcp_eregistrations_bpa/tools/form_errors.py +36 -0
  44. mcp_eregistrations_bpa/tools/formio_helpers.py +971 -0
  45. mcp_eregistrations_bpa/tools/forms.py +1269 -0
  46. mcp_eregistrations_bpa/tools/jsonlogic_builder.py +466 -0
  47. mcp_eregistrations_bpa/tools/large_response.py +163 -0
  48. mcp_eregistrations_bpa/tools/messages.py +523 -0
  49. mcp_eregistrations_bpa/tools/notifications.py +241 -0
  50. mcp_eregistrations_bpa/tools/registration_institutions.py +680 -0
  51. mcp_eregistrations_bpa/tools/registrations.py +897 -0
  52. mcp_eregistrations_bpa/tools/role_status.py +447 -0
  53. mcp_eregistrations_bpa/tools/role_units.py +400 -0
  54. mcp_eregistrations_bpa/tools/roles.py +1236 -0
  55. mcp_eregistrations_bpa/tools/rollback.py +335 -0
  56. mcp_eregistrations_bpa/tools/services.py +674 -0
  57. mcp_eregistrations_bpa/tools/workflows.py +2487 -0
  58. mcp_eregistrations_bpa/tools/yaml_transformer.py +991 -0
  59. mcp_eregistrations_bpa/workflows/__init__.py +28 -0
  60. mcp_eregistrations_bpa/workflows/loader.py +440 -0
  61. mcp_eregistrations_bpa/workflows/models.py +336 -0
  62. mcp_eregistrations_bpa-0.8.5.dist-info/METADATA +965 -0
  63. mcp_eregistrations_bpa-0.8.5.dist-info/RECORD +66 -0
  64. mcp_eregistrations_bpa-0.8.5.dist-info/WHEEL +4 -0
  65. mcp_eregistrations_bpa-0.8.5.dist-info/entry_points.txt +2 -0
  66. 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)