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,400 @@
1
+ """MCP tools for BPA role unit (involved unit) operations.
2
+
3
+ Role units define organizational units assigned to workflow roles. Each role
4
+ can have one or more units that handle applications at that step.
5
+
6
+ Write operations follow the audit-before-write pattern.
7
+
8
+ API Endpoints used:
9
+ - GET /role/{role_id}/role_unit - List units assigned to role
10
+ - POST /role/{role_id}/role_unit - Assign unit to role
11
+ - GET /role_unit/{role_unit_id} - Get specific unit assignment
12
+ - DELETE /role_unit/{role_unit_id} - Remove unit from role
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
+ from mcp_eregistrations_bpa.tools.large_response import large_response_handler
33
+
34
+ __all__ = [
35
+ "roleunit_list",
36
+ "roleunit_get",
37
+ "roleunit_create",
38
+ "roleunit_delete",
39
+ "register_role_unit_tools",
40
+ ]
41
+
42
+
43
+ def _transform_role_unit_response(data: dict[str, Any]) -> dict[str, Any]:
44
+ """Transform role unit API response from camelCase to snake_case.
45
+
46
+ Based on RoleInstitution model from BPA frontend:
47
+ - id, roleId, institutionId, unitId
48
+ - institutionName, unitName
49
+ - units (nested), jsonDeterminants
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
+ "role_id": data.get("roleId"),
60
+ "institution_id": data.get("institutionId"),
61
+ "unit_id": data.get("unitId"),
62
+ "institution_name": data.get("institutionName"),
63
+ "unit_name": data.get("unitName"),
64
+ "units": data.get("units"),
65
+ "json_determinants": data.get("jsonDeterminants"),
66
+ # Audit fields
67
+ "created_by": data.get("createdBy"),
68
+ "created_when": data.get("createdWhen"),
69
+ "last_changed_by": data.get("lastChangedBy"),
70
+ "last_changed_when": data.get("lastChangedWhen"),
71
+ }
72
+
73
+
74
+ @large_response_handler(
75
+ threshold_bytes=50 * 1024, # 50KB threshold for list tools
76
+ navigation={
77
+ "list_all": "jq '.units'",
78
+ "by_institution": "jq '.units[] | select(.institution_id==\"x\")'",
79
+ "by_name": "jq '.units[] | select(.unit_name | contains(\"x\"))'",
80
+ },
81
+ )
82
+ async def roleunit_list(role_id: str | int) -> dict[str, Any]:
83
+ """List units assigned to a role.
84
+
85
+ Large responses (>50KB) are saved to file with navigation hints.
86
+
87
+ Args:
88
+ role_id: Role ID to list units for.
89
+
90
+ Returns:
91
+ dict with units (list), role_id, total.
92
+ """
93
+ if not role_id:
94
+ raise ToolError(
95
+ "Cannot list role units: 'role_id' is required. "
96
+ "Use 'role_list' with service_id to find valid role IDs."
97
+ )
98
+
99
+ try:
100
+ async with BPAClient() as client:
101
+ try:
102
+ units_data = await client.get_list(
103
+ "/role/{role_id}/role_unit",
104
+ path_params={"role_id": role_id},
105
+ resource_type="role_unit",
106
+ )
107
+ except BPANotFoundError:
108
+ raise ToolError(
109
+ f"Role '{role_id}' not found. "
110
+ "Use 'role_list' with service_id to see available roles."
111
+ )
112
+ except ToolError:
113
+ raise
114
+ except BPAClientError as e:
115
+ raise translate_error(e, resource_type="role_unit")
116
+
117
+ units = [_transform_role_unit_response(unit) for unit in units_data]
118
+
119
+ return {
120
+ "units": units,
121
+ "role_id": str(role_id),
122
+ "total": len(units),
123
+ }
124
+
125
+
126
+ async def roleunit_get(role_unit_id: str | int) -> dict[str, Any]:
127
+ """Get a specific role unit assignment by ID.
128
+
129
+ Args:
130
+ role_unit_id: Role unit assignment ID.
131
+
132
+ Returns:
133
+ dict with id, role_id, unit_id, unit_name.
134
+ """
135
+ if not role_unit_id:
136
+ raise ToolError(
137
+ "Cannot get role unit: 'role_unit_id' is required. "
138
+ "Use 'roleunit_list' with role_id to find valid IDs."
139
+ )
140
+
141
+ try:
142
+ async with BPAClient() as client:
143
+ try:
144
+ unit_data = await client.get(
145
+ "/role_unit/{role_unit_id}",
146
+ path_params={"role_unit_id": role_unit_id},
147
+ resource_type="role_unit",
148
+ resource_id=role_unit_id,
149
+ )
150
+ except BPANotFoundError:
151
+ raise ToolError(
152
+ f"Role unit '{role_unit_id}' not found. "
153
+ "Use 'roleunit_list' to see available assignments."
154
+ )
155
+ except ToolError:
156
+ raise
157
+ except BPAClientError as e:
158
+ raise translate_error(e, resource_type="role_unit", resource_id=role_unit_id)
159
+
160
+ return _transform_role_unit_response(unit_data)
161
+
162
+
163
+ async def roleunit_create(
164
+ role_id: str | int,
165
+ institution_id: str,
166
+ unit_id: str,
167
+ institution_name: str | None = None,
168
+ unit_name: str | None = None,
169
+ ) -> dict[str, Any]:
170
+ """Assign a unit to a role. Audited write operation.
171
+
172
+ Role units link organizational units (within institutions) to workflow roles.
173
+
174
+ Args:
175
+ role_id: Role to assign unit to.
176
+ institution_id: Institution the unit belongs to.
177
+ unit_id: Unit ID to assign.
178
+ institution_name: Optional institution name for display.
179
+ unit_name: Optional unit name for display.
180
+
181
+ Returns:
182
+ dict with id, role_id, institution_id, unit_id, audit_id.
183
+ """
184
+ # Pre-flight validation
185
+ if not role_id:
186
+ raise ToolError(
187
+ "Cannot create role unit: 'role_id' is required. "
188
+ "Use 'role_list' with service_id to find valid role IDs."
189
+ )
190
+ if not institution_id:
191
+ raise ToolError(
192
+ "Cannot create role unit: 'institution_id' is required. "
193
+ "Use 'roleinstitution_create' first to link an institution to the role."
194
+ )
195
+ if not unit_id:
196
+ raise ToolError(
197
+ "Cannot create role unit: 'unit_id' is required. "
198
+ "Units are organizational departments within institutions."
199
+ )
200
+
201
+ # Get authenticated user for audit
202
+ try:
203
+ user_email = get_current_user_email()
204
+ except NotAuthenticatedError as e:
205
+ raise ToolError(str(e))
206
+
207
+ # Build payload matching RoleInstitution model
208
+ payload: dict[str, Any] = {
209
+ "institutionId": institution_id,
210
+ "unitId": unit_id,
211
+ }
212
+ if institution_name:
213
+ payload["institutionName"] = institution_name
214
+ if unit_name:
215
+ payload["unitName"] = unit_name
216
+
217
+ audit_logger = AuditLogger()
218
+
219
+ try:
220
+ async with BPAClient() as client:
221
+ # Verify role exists
222
+ try:
223
+ await client.get(
224
+ "/role/{role_id}",
225
+ path_params={"role_id": role_id},
226
+ resource_type="role",
227
+ resource_id=role_id,
228
+ )
229
+ except BPANotFoundError:
230
+ raise ToolError(
231
+ f"Role '{role_id}' not found. "
232
+ "Use 'role_list' with service_id to see available roles."
233
+ )
234
+
235
+ # Create PENDING audit record
236
+ audit_id = await audit_logger.record_pending(
237
+ user_email=user_email,
238
+ operation_type="link",
239
+ object_type="role_unit",
240
+ params={
241
+ "role_id": str(role_id),
242
+ "institution_id": institution_id,
243
+ "unit_id": unit_id,
244
+ },
245
+ )
246
+
247
+ try:
248
+ result = await client.post(
249
+ "/role/{role_id}/role_unit",
250
+ path_params={"role_id": role_id},
251
+ json=payload,
252
+ resource_type="role_unit",
253
+ )
254
+
255
+ # Save rollback state
256
+ created_id = result.get("id") if isinstance(result, dict) else None
257
+ await audit_logger.save_rollback_state(
258
+ audit_id=audit_id,
259
+ object_type="role_unit",
260
+ object_id=str(created_id) if created_id else str(role_id),
261
+ previous_state={
262
+ "id": created_id,
263
+ "role_id": str(role_id),
264
+ "institution_id": institution_id,
265
+ "unit_id": unit_id,
266
+ "_operation": "create",
267
+ },
268
+ )
269
+
270
+ await audit_logger.mark_success(
271
+ audit_id,
272
+ result={
273
+ "id": created_id,
274
+ "role_id": str(role_id),
275
+ "institution_id": institution_id,
276
+ "unit_id": unit_id,
277
+ },
278
+ )
279
+
280
+ response = (
281
+ _transform_role_unit_response(result)
282
+ if isinstance(result, dict)
283
+ else {
284
+ "id": None,
285
+ "role_id": str(role_id),
286
+ "institution_id": institution_id,
287
+ "unit_id": unit_id,
288
+ }
289
+ )
290
+ response["audit_id"] = audit_id
291
+ return response
292
+
293
+ except BPAClientError as e:
294
+ await audit_logger.mark_failed(audit_id, str(e))
295
+ raise translate_error(e, resource_type="role_unit")
296
+
297
+ except ToolError:
298
+ raise
299
+ except BPAClientError as e:
300
+ raise translate_error(e, resource_type="role", resource_id=role_id)
301
+
302
+
303
+ async def roleunit_delete(role_unit_id: str | int) -> dict[str, Any]:
304
+ """Remove a unit assignment from a role. Audited write operation.
305
+
306
+ Args:
307
+ role_unit_id: Role unit assignment ID to delete.
308
+
309
+ Returns:
310
+ dict with deleted (bool), role_unit_id, deleted_unit, audit_id.
311
+ """
312
+ if not role_unit_id:
313
+ raise ToolError(
314
+ "Cannot delete role unit: 'role_unit_id' is required. "
315
+ "Use 'roleunit_list' to find valid IDs."
316
+ )
317
+
318
+ # Get authenticated user for audit
319
+ try:
320
+ user_email = get_current_user_email()
321
+ except NotAuthenticatedError as e:
322
+ raise ToolError(str(e))
323
+
324
+ audit_logger = AuditLogger()
325
+
326
+ try:
327
+ async with BPAClient() as client:
328
+ # Capture current state for rollback
329
+ try:
330
+ previous_state = await client.get(
331
+ "/role_unit/{role_unit_id}",
332
+ path_params={"role_unit_id": role_unit_id},
333
+ resource_type="role_unit",
334
+ resource_id=role_unit_id,
335
+ )
336
+ except BPANotFoundError:
337
+ raise ToolError(
338
+ f"Role unit '{role_unit_id}' not found. "
339
+ "Use 'roleunit_list' to see available assignments."
340
+ )
341
+
342
+ # Create PENDING audit record
343
+ audit_id = await audit_logger.record_pending(
344
+ user_email=user_email,
345
+ operation_type="unlink",
346
+ object_type="role_unit",
347
+ object_id=str(role_unit_id),
348
+ params={},
349
+ )
350
+
351
+ # Save rollback state
352
+ await audit_logger.save_rollback_state(
353
+ audit_id=audit_id,
354
+ object_type="role_unit",
355
+ object_id=str(role_unit_id),
356
+ previous_state=previous_state,
357
+ )
358
+
359
+ try:
360
+ await client.delete(
361
+ "/role_unit/{role_unit_id}",
362
+ path_params={"role_unit_id": role_unit_id},
363
+ resource_type="role_unit",
364
+ resource_id=role_unit_id,
365
+ )
366
+
367
+ await audit_logger.mark_success(
368
+ audit_id,
369
+ result={"deleted": True, "role_unit_id": str(role_unit_id)},
370
+ )
371
+
372
+ except BPAClientError as e:
373
+ await audit_logger.mark_failed(audit_id, str(e))
374
+ raise translate_error(
375
+ e, resource_type="role_unit", resource_id=role_unit_id
376
+ )
377
+
378
+ except ToolError:
379
+ raise
380
+ except BPAClientError as e:
381
+ raise translate_error(e, resource_type="role_unit", resource_id=role_unit_id)
382
+
383
+ return {
384
+ "deleted": True,
385
+ "role_unit_id": str(role_unit_id),
386
+ "deleted_unit": _transform_role_unit_response(previous_state),
387
+ "audit_id": audit_id,
388
+ }
389
+
390
+
391
+ def register_role_unit_tools(mcp: Any) -> None:
392
+ """Register role unit tools with the MCP server.
393
+
394
+ Args:
395
+ mcp: The FastMCP server instance.
396
+ """
397
+ mcp.tool()(roleunit_list)
398
+ mcp.tool()(roleunit_get)
399
+ mcp.tool()(roleunit_create)
400
+ mcp.tool()(roleunit_delete)