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,447 @@
1
+ """MCP tools for BPA role status operations.
2
+
3
+ Role statuses define workflow transition states for roles (e.g., FILE VALIDATED,
4
+ SEND BACK). They control where applications go next in the workflow.
5
+
6
+ Write operations follow the audit-before-write pattern.
7
+
8
+ API Endpoints used:
9
+ - GET /role_status/{role_status_id} - Get role status by ID
10
+ - POST /role/{role_id}/role_status/user_defined_status - Create user-defined status
11
+ - PUT /role_status/{role_status_id}/user_defined_status - Update user-defined status
12
+ - DELETE /role_status/{role_status_id} - Delete role status
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any
18
+
19
+ from mcp.server.fastmcp.exceptions import ToolError
20
+
21
+ from mcp_eregistrations_bpa.audit.context import (
22
+ NotAuthenticatedError,
23
+ get_current_user_email,
24
+ )
25
+ from mcp_eregistrations_bpa.audit.logger import AuditLogger
26
+ from mcp_eregistrations_bpa.bpa_client import BPAClient
27
+ from mcp_eregistrations_bpa.bpa_client.errors import (
28
+ BPAClientError,
29
+ BPANotFoundError,
30
+ translate_error,
31
+ )
32
+
33
+ __all__ = [
34
+ "rolestatus_get",
35
+ "rolestatus_create",
36
+ "rolestatus_update",
37
+ "rolestatus_delete",
38
+ "register_role_status_tools",
39
+ ]
40
+
41
+
42
+ def _transform_role_status_response(data: dict[str, Any]) -> dict[str, Any]:
43
+ """Transform role status API response from camelCase to snake_case.
44
+
45
+ Args:
46
+ data: Raw API response with camelCase keys.
47
+
48
+ Returns:
49
+ dict: Transformed response with snake_case keys.
50
+ """
51
+ return {
52
+ "id": data.get("id"),
53
+ "name": data.get("name"),
54
+ "type": data.get("type"),
55
+ "role_status_type": data.get("roleStatusType"),
56
+ "destination_id": data.get("destinationId"),
57
+ "role_id": data.get("roleId"),
58
+ # Role status message (for notifications)
59
+ "role_status_message": data.get("roleStatusMessage"),
60
+ # Audit fields
61
+ "created_by": data.get("createdBy"),
62
+ "created_when": data.get("createdWhen"),
63
+ "last_changed_by": data.get("lastChangedBy"),
64
+ "last_changed_when": data.get("lastChangedWhen"),
65
+ }
66
+
67
+
68
+ async def rolestatus_get(role_status_id: str | int) -> dict[str, Any]:
69
+ """Get details of a BPA role status by ID.
70
+
71
+ Role statuses define workflow transitions (e.g., FILE VALIDATED, SEND BACK).
72
+
73
+ Args:
74
+ role_status_id: The unique identifier of the role status.
75
+
76
+ Returns:
77
+ dict with id, name, type, role_status_type, destination_id, role_id.
78
+ """
79
+ if not role_status_id:
80
+ raise ToolError(
81
+ "Cannot get role status: 'role_status_id' is required. "
82
+ "Use 'role_get' to see statuses for a role."
83
+ )
84
+
85
+ try:
86
+ async with BPAClient() as client:
87
+ try:
88
+ status_data = await client.get(
89
+ "/role_status/{role_status_id}",
90
+ path_params={"role_status_id": role_status_id},
91
+ resource_type="role_status",
92
+ resource_id=role_status_id,
93
+ )
94
+ except BPANotFoundError:
95
+ raise ToolError(
96
+ f"Role status '{role_status_id}' not found. "
97
+ "Use 'role_get' with a role_id to see available statuses."
98
+ )
99
+ except ToolError:
100
+ raise
101
+ except BPAClientError as e:
102
+ raise translate_error(
103
+ e, resource_type="role_status", resource_id=role_status_id
104
+ )
105
+
106
+ return _transform_role_status_response(status_data)
107
+
108
+
109
+ async def rolestatus_create(
110
+ role_id: str | int,
111
+ name: str,
112
+ destination_role_id: str | int,
113
+ role_status_type: int = 1,
114
+ message: str | None = None,
115
+ ) -> dict[str, Any]:
116
+ """Create a user-defined role status. Audited write operation.
117
+
118
+ Role statuses control workflow transitions. Types:
119
+ - 1: Forward/approve (goes to next role)
120
+ - 2: Return/revise (goes back to applicant or previous role)
121
+
122
+ Args:
123
+ role_id: Role to add status to.
124
+ name: Status name (e.g., "APPROVED", "SEND BACK").
125
+ destination_role_id: Target role ID for this transition.
126
+ role_status_type: 1=forward/approve, 2=return/revise (default: 1).
127
+ message: Optional notification message for this status.
128
+
129
+ Returns:
130
+ dict with id, name, role_status_type, destination_id, role_id, audit_id.
131
+ """
132
+ # Pre-flight validation
133
+ if not role_id:
134
+ raise ToolError(
135
+ "Cannot create role status: 'role_id' is required. "
136
+ "Use 'role_list' with service_id to find valid role IDs."
137
+ )
138
+ if not name or not name.strip():
139
+ raise ToolError("Role status name is required.")
140
+ if not destination_role_id:
141
+ raise ToolError(
142
+ "Cannot create role status: 'destination_role_id' is required. "
143
+ "This is the target role for the workflow transition."
144
+ )
145
+ if role_status_type not in (1, 2):
146
+ raise ToolError("role_status_type must be 1 (forward) or 2 (return).")
147
+
148
+ # Get authenticated user for audit
149
+ try:
150
+ user_email = get_current_user_email()
151
+ except NotAuthenticatedError as e:
152
+ raise ToolError(str(e))
153
+
154
+ # Build payload
155
+ payload: dict[str, Any] = {
156
+ "name": name.strip(),
157
+ "roleStatusType": role_status_type,
158
+ "destinationId": str(destination_role_id),
159
+ }
160
+ if message:
161
+ payload["roleStatusMessage"] = message
162
+
163
+ try:
164
+ async with BPAClient() as client:
165
+ # Verify role exists
166
+ try:
167
+ await client.get(
168
+ "/role/{role_id}",
169
+ path_params={"role_id": role_id},
170
+ resource_type="role",
171
+ resource_id=role_id,
172
+ )
173
+ except BPANotFoundError:
174
+ raise ToolError(
175
+ f"Role '{role_id}' not found. "
176
+ "Use 'role_list' with service_id to see available roles."
177
+ )
178
+
179
+ # Create audit record BEFORE API call
180
+ audit_logger = AuditLogger()
181
+ audit_id = await audit_logger.record_pending(
182
+ user_email=user_email,
183
+ operation_type="create",
184
+ object_type="role_status",
185
+ params={"role_id": str(role_id), **payload},
186
+ )
187
+
188
+ try:
189
+ status_data = await client.post(
190
+ "/role/{role_id}/role_status/user_defined_status",
191
+ path_params={"role_id": role_id},
192
+ json=payload,
193
+ resource_type="role_status",
194
+ )
195
+
196
+ # Save rollback state
197
+ created_id = status_data.get("id")
198
+ await audit_logger.save_rollback_state(
199
+ audit_id=audit_id,
200
+ object_type="role_status",
201
+ object_id=str(created_id),
202
+ previous_state={
203
+ "id": created_id,
204
+ "role_id": str(role_id),
205
+ "name": status_data.get("name"),
206
+ "_operation": "create",
207
+ },
208
+ )
209
+
210
+ await audit_logger.mark_success(
211
+ audit_id=audit_id,
212
+ result={"id": created_id, "name": status_data.get("name")},
213
+ )
214
+
215
+ except BPAClientError as e:
216
+ await audit_logger.mark_failed(audit_id=audit_id, error_message=str(e))
217
+ raise translate_error(e, resource_type="role_status")
218
+
219
+ except ToolError:
220
+ raise
221
+ except BPAClientError as e:
222
+ raise translate_error(e, resource_type="role", resource_id=role_id)
223
+
224
+ result = _transform_role_status_response(status_data)
225
+ result["audit_id"] = audit_id
226
+ return result
227
+
228
+
229
+ async def rolestatus_update(
230
+ role_status_id: str | int,
231
+ name: str | None = None,
232
+ destination_role_id: str | int | None = None,
233
+ message: str | None = None,
234
+ ) -> dict[str, Any]:
235
+ """Update a user-defined role status. Audited write operation.
236
+
237
+ Args:
238
+ role_status_id: Role status ID to update.
239
+ name: New status name (optional).
240
+ destination_role_id: New target role ID (optional).
241
+ message: New notification message (optional).
242
+
243
+ Returns:
244
+ dict with id, name, role_status_type, destination_id, previous_state, audit_id.
245
+ """
246
+ # Pre-flight validation
247
+ if not role_status_id:
248
+ raise ToolError(
249
+ "Cannot update role status: 'role_status_id' is required. "
250
+ "Use 'role_get' to see statuses for a role."
251
+ )
252
+
253
+ # At least one field must be provided
254
+ if name is None and destination_role_id is None and message is None:
255
+ raise ToolError("At least one field must be provided for update.")
256
+
257
+ # Get authenticated user for audit
258
+ try:
259
+ user_email = get_current_user_email()
260
+ except NotAuthenticatedError as e:
261
+ raise ToolError(str(e))
262
+
263
+ try:
264
+ async with BPAClient() as client:
265
+ # Capture current state for rollback
266
+ try:
267
+ previous_state = await client.get(
268
+ "/role_status/{role_status_id}",
269
+ path_params={"role_status_id": role_status_id},
270
+ resource_type="role_status",
271
+ resource_id=role_status_id,
272
+ )
273
+ except BPANotFoundError:
274
+ raise ToolError(
275
+ f"Role status '{role_status_id}' not found. "
276
+ "Use 'role_get' to see available statuses."
277
+ )
278
+
279
+ # Merge with current state
280
+ payload: dict[str, Any] = {
281
+ "id": role_status_id,
282
+ "name": name.strip() if name else previous_state.get("name"),
283
+ "roleStatusType": previous_state.get("roleStatusType"),
284
+ "destinationId": (
285
+ str(destination_role_id)
286
+ if destination_role_id
287
+ else previous_state.get("destinationId")
288
+ ),
289
+ }
290
+ if message is not None:
291
+ payload["roleStatusMessage"] = message
292
+ elif previous_state.get("roleStatusMessage"):
293
+ payload["roleStatusMessage"] = previous_state.get("roleStatusMessage")
294
+
295
+ # Create audit record
296
+ audit_logger = AuditLogger()
297
+ audit_id = await audit_logger.record_pending(
298
+ user_email=user_email,
299
+ operation_type="update",
300
+ object_type="role_status",
301
+ object_id=str(role_status_id),
302
+ params={"changes": payload},
303
+ )
304
+
305
+ # Save rollback state
306
+ await audit_logger.save_rollback_state(
307
+ audit_id=audit_id,
308
+ object_type="role_status",
309
+ object_id=str(role_status_id),
310
+ previous_state=previous_state,
311
+ )
312
+
313
+ try:
314
+ status_data = await client.put(
315
+ "/role_status/{role_status_id}/user_defined_status",
316
+ path_params={"role_status_id": role_status_id},
317
+ json=payload,
318
+ resource_type="role_status",
319
+ resource_id=role_status_id,
320
+ )
321
+
322
+ await audit_logger.mark_success(
323
+ audit_id=audit_id,
324
+ result={
325
+ "id": status_data.get("id"),
326
+ "name": status_data.get("name"),
327
+ },
328
+ )
329
+
330
+ except BPAClientError as e:
331
+ await audit_logger.mark_failed(audit_id=audit_id, error_message=str(e))
332
+ raise translate_error(
333
+ e, resource_type="role_status", resource_id=role_status_id
334
+ )
335
+
336
+ except ToolError:
337
+ raise
338
+ except BPAClientError as e:
339
+ raise translate_error(
340
+ e, resource_type="role_status", resource_id=role_status_id
341
+ )
342
+
343
+ result = _transform_role_status_response(status_data)
344
+ result["previous_state"] = _transform_role_status_response(previous_state)
345
+ result["audit_id"] = audit_id
346
+ return result
347
+
348
+
349
+ async def rolestatus_delete(role_status_id: str | int) -> dict[str, Any]:
350
+ """Delete a role status. Audited write operation.
351
+
352
+ Args:
353
+ role_status_id: Role status ID to delete.
354
+
355
+ Returns:
356
+ dict with deleted (bool), role_status_id, deleted_status, audit_id.
357
+ """
358
+ if not role_status_id:
359
+ raise ToolError(
360
+ "Cannot delete role status: 'role_status_id' is required. "
361
+ "Use 'role_get' to see statuses for a role."
362
+ )
363
+
364
+ # Get authenticated user for audit
365
+ try:
366
+ user_email = get_current_user_email()
367
+ except NotAuthenticatedError as e:
368
+ raise ToolError(str(e))
369
+
370
+ try:
371
+ async with BPAClient() as client:
372
+ # Capture current state for rollback
373
+ try:
374
+ previous_state = await client.get(
375
+ "/role_status/{role_status_id}",
376
+ path_params={"role_status_id": role_status_id},
377
+ resource_type="role_status",
378
+ resource_id=role_status_id,
379
+ )
380
+ except BPANotFoundError:
381
+ raise ToolError(
382
+ f"Role status '{role_status_id}' not found. "
383
+ "Use 'role_get' to see available statuses."
384
+ )
385
+
386
+ # Create audit record
387
+ audit_logger = AuditLogger()
388
+ audit_id = await audit_logger.record_pending(
389
+ user_email=user_email,
390
+ operation_type="delete",
391
+ object_type="role_status",
392
+ object_id=str(role_status_id),
393
+ params={},
394
+ )
395
+
396
+ # Save rollback state
397
+ await audit_logger.save_rollback_state(
398
+ audit_id=audit_id,
399
+ object_type="role_status",
400
+ object_id=str(role_status_id),
401
+ previous_state=previous_state,
402
+ )
403
+
404
+ try:
405
+ await client.delete(
406
+ "/role_status/{role_status_id}",
407
+ path_params={"role_status_id": role_status_id},
408
+ resource_type="role_status",
409
+ resource_id=role_status_id,
410
+ )
411
+
412
+ await audit_logger.mark_success(
413
+ audit_id=audit_id,
414
+ result={"deleted": True, "role_status_id": str(role_status_id)},
415
+ )
416
+
417
+ except BPAClientError as e:
418
+ await audit_logger.mark_failed(audit_id=audit_id, error_message=str(e))
419
+ raise translate_error(
420
+ e, resource_type="role_status", resource_id=role_status_id
421
+ )
422
+
423
+ except ToolError:
424
+ raise
425
+ except BPAClientError as e:
426
+ raise translate_error(
427
+ e, resource_type="role_status", resource_id=role_status_id
428
+ )
429
+
430
+ return {
431
+ "deleted": True,
432
+ "role_status_id": str(role_status_id),
433
+ "deleted_status": _transform_role_status_response(previous_state),
434
+ "audit_id": audit_id,
435
+ }
436
+
437
+
438
+ def register_role_status_tools(mcp: Any) -> None:
439
+ """Register role status tools with the MCP server.
440
+
441
+ Args:
442
+ mcp: The FastMCP server instance.
443
+ """
444
+ mcp.tool()(rolestatus_get)
445
+ mcp.tool()(rolestatus_create)
446
+ mcp.tool()(rolestatus_update)
447
+ mcp.tool()(rolestatus_delete)