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,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)
|