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