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,241 @@
1
+ """MCP tools for BPA notification operations.
2
+
3
+ This module provides tools for listing and creating BPA notifications
4
+ (email/SMS alerts configured for service events).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from mcp.server.fastmcp.exceptions import ToolError
12
+
13
+ from mcp_eregistrations_bpa.audit.context import (
14
+ NotAuthenticatedError,
15
+ get_current_user_email,
16
+ )
17
+ from mcp_eregistrations_bpa.audit.logger import AuditLogger
18
+ from mcp_eregistrations_bpa.bpa_client import BPAClient
19
+ from mcp_eregistrations_bpa.bpa_client.errors import (
20
+ BPAClientError,
21
+ BPANotFoundError,
22
+ translate_error,
23
+ )
24
+ from mcp_eregistrations_bpa.tools.large_response import large_response_handler
25
+
26
+ __all__ = [
27
+ "notification_list",
28
+ "notification_create",
29
+ "register_notification_tools",
30
+ ]
31
+
32
+
33
+ def _transform_notification(notification: dict[str, Any]) -> dict[str, Any]:
34
+ """Transform notification to snake_case format."""
35
+ return {
36
+ "id": notification.get("id"),
37
+ "name": notification.get("name") or notification.get("title"),
38
+ "type": notification.get("type") or notification.get("notificationType"),
39
+ "trigger": notification.get("trigger") or notification.get("triggerEvent"),
40
+ "enabled": notification.get("enabled", True),
41
+ "template": notification.get("template") or notification.get("body"),
42
+ "subject": notification.get("subject"),
43
+ }
44
+
45
+
46
+ @large_response_handler(
47
+ threshold_bytes=50 * 1024, # 50KB threshold for list tools
48
+ navigation={
49
+ "list_all": "jq '.notifications'",
50
+ "find_by_type": "jq '.notifications[] | select(.type == \"email\")'",
51
+ "find_by_trigger": "jq '.notifications[] | select(.trigger == \"on_submit\")'",
52
+ "enabled_only": "jq '.notifications[] | select(.enabled == true)'",
53
+ },
54
+ )
55
+ async def notification_list(
56
+ service_id: str | int,
57
+ limit: int = 50,
58
+ offset: int = 0,
59
+ ) -> dict[str, Any]:
60
+ """List notifications configured for a service.
61
+
62
+ Large responses (>50KB) are saved to file with navigation hints.
63
+
64
+ Args:
65
+ service_id: The service ID to list notifications for.
66
+ limit: Maximum to return (default: 50).
67
+ offset: Skip count (default: 0).
68
+
69
+ Returns:
70
+ dict with notifications, total, has_more, service_id.
71
+ """
72
+ # Normalize pagination parameters
73
+ if limit <= 0:
74
+ limit = 50
75
+ if offset < 0:
76
+ offset = 0
77
+
78
+ try:
79
+ async with BPAClient() as client:
80
+ try:
81
+ notifications_data = await client.get_list(
82
+ "/service/{service_id}/notifications",
83
+ path_params={"service_id": service_id},
84
+ resource_type="notification",
85
+ )
86
+ except BPANotFoundError:
87
+ raise ToolError(
88
+ f"Service '{service_id}' not found. "
89
+ "Use 'service_list' to see available services."
90
+ )
91
+ except ToolError:
92
+ raise
93
+ except BPAClientError as e:
94
+ raise translate_error(e, resource_type="notification")
95
+
96
+ # Transform to consistent output format
97
+ all_notifications = [_transform_notification(n) for n in notifications_data]
98
+
99
+ # Sort by name for consistent ordering
100
+ all_notifications.sort(key=lambda n: n.get("name") or "")
101
+
102
+ # Calculate total before pagination
103
+ total = len(all_notifications)
104
+
105
+ # Apply pagination
106
+ paginated = all_notifications[offset : offset + limit]
107
+
108
+ # Calculate has_more
109
+ has_more = (offset + limit) < total
110
+
111
+ return {
112
+ "notifications": paginated,
113
+ "total": total,
114
+ "has_more": has_more,
115
+ "service_id": str(service_id),
116
+ }
117
+
118
+
119
+ async def notification_create(
120
+ service_id: str | int,
121
+ name: str,
122
+ notification_type: str,
123
+ trigger: str,
124
+ template: str | None = None,
125
+ subject: str | None = None,
126
+ enabled: bool = True,
127
+ ) -> dict[str, Any]:
128
+ """Create a notification for a service. Audited write operation.
129
+
130
+ Args:
131
+ service_id: The service ID to create the notification for.
132
+ name: The notification name.
133
+ notification_type: Type of notification (email, sms).
134
+ trigger: Event that triggers the notification (on_submit, on_approve, etc.).
135
+ template: The notification body template (default: None).
136
+ subject: The email subject (default: None).
137
+ enabled: Whether the notification is enabled (default: True).
138
+
139
+ Returns:
140
+ dict with id, name, type, trigger, enabled, audit_id.
141
+ """
142
+ # Pre-flight validation
143
+ if not name or not name.strip():
144
+ raise ToolError("Notification name is required.")
145
+ if not notification_type or not notification_type.strip():
146
+ raise ToolError("Notification type is required (e.g., 'email', 'sms').")
147
+ if not trigger or not trigger.strip():
148
+ raise ToolError(
149
+ "Notification trigger is required (e.g., 'on_submit', 'on_approve')."
150
+ )
151
+
152
+ # Get authenticated user for audit
153
+ try:
154
+ user_email = get_current_user_email()
155
+ except NotAuthenticatedError as e:
156
+ raise ToolError(str(e))
157
+
158
+ # Build payload
159
+ payload: dict[str, Any] = {
160
+ "name": name.strip(),
161
+ "type": notification_type.strip(),
162
+ "trigger": trigger.strip(),
163
+ "enabled": enabled,
164
+ }
165
+ if template:
166
+ payload["template"] = template
167
+ if subject:
168
+ payload["subject"] = subject
169
+
170
+ try:
171
+ async with BPAClient() as client:
172
+ # Create audit record BEFORE API call
173
+ audit_logger = AuditLogger()
174
+ audit_id = await audit_logger.record_pending(
175
+ user_email=user_email,
176
+ operation_type="create",
177
+ object_type="notification",
178
+ params={"service_id": str(service_id), **payload},
179
+ )
180
+
181
+ try:
182
+ notification_data = await client.post(
183
+ "/service/{service_id}/notification",
184
+ path_params={"service_id": service_id},
185
+ json=payload,
186
+ resource_type="notification",
187
+ )
188
+
189
+ # Save rollback state (for create, save ID to enable deletion)
190
+ created_id = notification_data.get("id")
191
+ await audit_logger.save_rollback_state(
192
+ audit_id=audit_id,
193
+ object_type="notification",
194
+ object_id=str(created_id),
195
+ previous_state={
196
+ "id": created_id,
197
+ "service_id": str(service_id),
198
+ "name": notification_data.get("name"),
199
+ "_operation": "create",
200
+ },
201
+ )
202
+
203
+ await audit_logger.mark_success(
204
+ audit_id=audit_id,
205
+ result={"id": created_id, "name": notification_data.get("name")},
206
+ )
207
+
208
+ except BPANotFoundError:
209
+ await audit_logger.mark_failed(
210
+ audit_id=audit_id,
211
+ error_message=f"Service '{service_id}' not found",
212
+ )
213
+ raise ToolError(
214
+ f"Service '{service_id}' not found. "
215
+ "Use 'service_list' to see available services."
216
+ )
217
+ except BPAClientError as e:
218
+ await audit_logger.mark_failed(audit_id=audit_id, error_message=str(e))
219
+ raise translate_error(e, resource_type="notification")
220
+
221
+ except ToolError:
222
+ raise
223
+ except BPAClientError as e:
224
+ raise translate_error(e, resource_type="notification")
225
+
226
+ # Transform response
227
+ result = _transform_notification(notification_data)
228
+ result["audit_id"] = audit_id
229
+ result["service_id"] = str(service_id)
230
+
231
+ return result
232
+
233
+
234
+ def register_notification_tools(mcp: Any) -> None:
235
+ """Register notification tools with the MCP server.
236
+
237
+ Args:
238
+ mcp: The FastMCP server instance.
239
+ """
240
+ mcp.tool()(notification_list)
241
+ mcp.tool()(notification_create)