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