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,335 @@
1
+ """MCP tools for rollback operations.
2
+
3
+ This module provides tools for rolling back write operations and viewing
4
+ rollback state history. These tools enable service designers to undo
5
+ mistakes by restoring objects to their previous state.
6
+
7
+ Tools provided:
8
+ - rollback: Rollback a write operation to restore previous state
9
+ - rollback_history: View the state change history for an object
10
+ - rollback_cleanup: Clean up old rollback states based on retention policy
11
+
12
+ These tools query local SQLite data and call BPA API for restoration.
13
+ Authentication is required for rollback operations that call BPA API.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import uuid
21
+ from datetime import UTC, datetime, timedelta
22
+ from typing import Any
23
+
24
+ from mcp.server.fastmcp.exceptions import ToolError
25
+
26
+ from mcp_eregistrations_bpa.db import get_connection, get_db_path
27
+ from mcp_eregistrations_bpa.rollback.manager import (
28
+ RollbackError,
29
+ RollbackManager,
30
+ RollbackNotPossibleError,
31
+ )
32
+
33
+ # Default retention period in days (configurable via environment variable)
34
+ DEFAULT_RETENTION_DAYS = 90
35
+
36
+ __all__ = [
37
+ "rollback",
38
+ "rollback_history",
39
+ "rollback_cleanup",
40
+ "register_rollback_tools",
41
+ ]
42
+
43
+
44
+ async def rollback(audit_id: str) -> dict[str, Any]:
45
+ """Rollback a write operation to restore previous state. Requires authentication.
46
+
47
+ Undoes: create→DELETE, update→PUT previous state, delete→POST recreate.
48
+ Creates a new audit record for the rollback operation itself.
49
+
50
+ Args:
51
+ audit_id: Audit entry UUID to rollback.
52
+
53
+ Returns:
54
+ dict with status, message, original_operation, restored_state,
55
+ rollback_audit_id.
56
+ """
57
+ # Validate audit_id
58
+ if not audit_id or not audit_id.strip():
59
+ raise ToolError(
60
+ "Cannot rollback: 'audit_id' is required. "
61
+ "Use 'audit_list' to see available entries."
62
+ )
63
+
64
+ audit_id = audit_id.strip()
65
+ db_path = get_db_path()
66
+ manager = RollbackManager(db_path=db_path)
67
+
68
+ # Create pending audit record for the rollback operation FIRST
69
+ rollback_audit_id = str(uuid.uuid4())
70
+ timestamp = datetime.now(UTC).isoformat()
71
+
72
+ try:
73
+ # Validate rollback can be performed
74
+ # This raises RollbackNotPossibleError if validation fails
75
+ entry = await manager.validate_rollback(audit_id)
76
+
77
+ # Create pending audit entry for the rollback
78
+ async with get_connection(db_path) as conn:
79
+ await conn.execute(
80
+ """
81
+ INSERT INTO audit_logs (
82
+ id, timestamp, user_email, operation_type, object_type,
83
+ object_id, params, status, result, rollback_state_id
84
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
85
+ """,
86
+ (
87
+ rollback_audit_id,
88
+ timestamp,
89
+ entry["user_email"],
90
+ "rollback",
91
+ entry["object_type"],
92
+ entry["object_id"],
93
+ json.dumps({"rolled_back_audit_id": audit_id}),
94
+ "pending",
95
+ None,
96
+ None,
97
+ ),
98
+ )
99
+ await conn.commit()
100
+
101
+ # Perform the rollback
102
+ result = await manager.perform_rollback(audit_id)
103
+
104
+ # Mark rollback audit as success
105
+ async with get_connection(db_path) as conn:
106
+ await conn.execute(
107
+ "UPDATE audit_logs SET status = ?, result = ? WHERE id = ?",
108
+ (
109
+ "success",
110
+ json.dumps(
111
+ {
112
+ "rolled_back_audit_id": audit_id,
113
+ "action": result.get("message", "rollback completed"),
114
+ }
115
+ ),
116
+ rollback_audit_id,
117
+ ),
118
+ )
119
+ await conn.commit()
120
+
121
+ # Mark original as rolled back
122
+ await manager._mark_rolled_back(
123
+ audit_id=audit_id,
124
+ rollback_audit_id=rollback_audit_id,
125
+ rolled_back_at=timestamp,
126
+ )
127
+
128
+ # Add rollback_audit_id to response
129
+ result["rollback_audit_id"] = rollback_audit_id
130
+ return result
131
+
132
+ except RollbackNotPossibleError as e:
133
+ # Validation failed - don't create failed audit entry
134
+ # (we only create audit entries for operations that were attempted)
135
+ # Clean up the pending audit entry if it was created
136
+ try:
137
+ async with get_connection(db_path) as conn:
138
+ await conn.execute(
139
+ "DELETE FROM audit_logs WHERE id = ? AND status = 'pending'",
140
+ (rollback_audit_id,),
141
+ )
142
+ await conn.commit()
143
+ except Exception:
144
+ pass # Best effort cleanup
145
+ raise ToolError(str(e))
146
+
147
+ except RollbackError as e:
148
+ # Execution failed - mark audit as failed
149
+ try:
150
+ async with get_connection(db_path) as conn:
151
+ await conn.execute(
152
+ "UPDATE audit_logs SET status = ?, result = ? WHERE id = ?",
153
+ ("failed", json.dumps({"error": str(e)}), rollback_audit_id),
154
+ )
155
+ await conn.commit()
156
+ except Exception:
157
+ pass # Best effort
158
+ raise ToolError(f"Rollback failed: {e}")
159
+
160
+
161
+ async def rollback_history(object_type: str, object_id: str) -> dict[str, Any]:
162
+ """Get state change history for an object. Local data only.
163
+
164
+ Args:
165
+ object_type: service, registration, role, bot, determinant, cost, etc.
166
+ object_id: Object ID.
167
+
168
+ Returns:
169
+ dict with object_type, object_id, states, total.
170
+ """
171
+ # Validate inputs
172
+ if not object_type or not object_type.strip():
173
+ raise ToolError("Cannot get rollback history: 'object_type' is required.")
174
+
175
+ if not object_id or not object_id.strip():
176
+ raise ToolError("Cannot get rollback history: 'object_id' is required.")
177
+
178
+ object_type = object_type.strip()
179
+ object_id = object_id.strip()
180
+ db_path = get_db_path()
181
+
182
+ async with get_connection(db_path) as conn:
183
+ # Join rollback_states with audit_logs to get operation context
184
+ cursor = await conn.execute(
185
+ """
186
+ SELECT
187
+ rs.id as rollback_state_id,
188
+ rs.audit_log_id as audit_id,
189
+ rs.previous_state,
190
+ rs.created_at,
191
+ al.operation_type
192
+ FROM rollback_states rs
193
+ LEFT JOIN audit_logs al ON rs.audit_log_id = al.id
194
+ WHERE rs.object_type = ? AND rs.object_id = ?
195
+ ORDER BY rs.created_at ASC
196
+ """,
197
+ (object_type, object_id),
198
+ )
199
+ rows = await cursor.fetchall()
200
+
201
+ # Transform to response format
202
+ states = []
203
+ for row in rows:
204
+ previous_state = (
205
+ json.loads(row["previous_state"]) if row["previous_state"] else None
206
+ )
207
+ states.append(
208
+ {
209
+ "rollback_state_id": row["rollback_state_id"],
210
+ "audit_id": row["audit_id"],
211
+ "operation_type": row["operation_type"],
212
+ "previous_state": previous_state,
213
+ "created_at": row["created_at"],
214
+ }
215
+ )
216
+
217
+ return {
218
+ "object_type": object_type,
219
+ "object_id": object_id,
220
+ "states": states,
221
+ "total": len(states),
222
+ }
223
+
224
+
225
+ def _get_retention_days() -> int:
226
+ """Get retention days from environment variable or use default.
227
+
228
+ Returns:
229
+ Number of days to retain rollback states.
230
+ """
231
+ env_value = os.environ.get("BPA_ROLLBACK_RETENTION_DAYS")
232
+ if env_value:
233
+ try:
234
+ days = int(env_value)
235
+ if days > 0:
236
+ return days
237
+ except ValueError:
238
+ pass
239
+ return DEFAULT_RETENTION_DAYS
240
+
241
+
242
+ async def rollback_cleanup(
243
+ retention_days: int | None = None,
244
+ dry_run: bool = False,
245
+ ) -> dict[str, Any]:
246
+ """Delete old rollback states. IRREVERSIBLE - use dry_run=True to preview.
247
+
248
+ Rollback states are snapshots saved before write operations, enabling undo.
249
+ Cleanup removes old snapshots to manage storage.
250
+
251
+ Args:
252
+ retention_days: Days to keep (default: BPA_ROLLBACK_RETENTION_DAYS or 90).
253
+ dry_run: Preview only without deleting.
254
+
255
+ Returns:
256
+ dict with deleted_count, retention_days, cutoff_date, dry_run, deleted_states,
257
+ message.
258
+ """
259
+ # Determine retention period
260
+ if retention_days is None:
261
+ retention_days = _get_retention_days()
262
+
263
+ if retention_days <= 0:
264
+ raise ToolError(
265
+ "Cannot cleanup rollback states: 'retention_days' must be positive. "
266
+ f"Got: {retention_days}"
267
+ )
268
+
269
+ # Calculate cutoff date
270
+ cutoff_date = datetime.now(UTC) - timedelta(days=retention_days)
271
+ cutoff_iso = cutoff_date.isoformat()
272
+
273
+ db_path = get_db_path()
274
+
275
+ async with get_connection(db_path) as conn:
276
+ # First, get summary of what will be deleted
277
+ cursor = await conn.execute(
278
+ """
279
+ SELECT
280
+ object_type,
281
+ COUNT(*) as count,
282
+ MIN(created_at) as oldest,
283
+ MAX(created_at) as newest
284
+ FROM rollback_states
285
+ WHERE created_at < ?
286
+ GROUP BY object_type
287
+ ORDER BY count DESC
288
+ """,
289
+ (cutoff_iso,),
290
+ )
291
+ rows = await cursor.fetchall()
292
+
293
+ deleted_states = []
294
+ total_count = 0
295
+ for row in rows:
296
+ deleted_states.append(
297
+ {
298
+ "object_type": row["object_type"],
299
+ "count": row["count"],
300
+ "oldest": row["oldest"],
301
+ "newest": row["newest"],
302
+ }
303
+ )
304
+ total_count += row["count"]
305
+
306
+ # Perform deletion if not dry run
307
+ if not dry_run and total_count > 0:
308
+ await conn.execute(
309
+ "DELETE FROM rollback_states WHERE created_at < ?",
310
+ (cutoff_iso,),
311
+ )
312
+ await conn.commit()
313
+
314
+ return {
315
+ "deleted_count": total_count,
316
+ "retention_days": retention_days,
317
+ "cutoff_date": cutoff_iso,
318
+ "dry_run": dry_run,
319
+ "deleted_states": deleted_states,
320
+ "message": (
321
+ f"{'Would delete' if dry_run else 'Deleted'} {total_count} "
322
+ f"rollback states older than {retention_days} days"
323
+ ),
324
+ }
325
+
326
+
327
+ def register_rollback_tools(mcp: Any) -> None:
328
+ """Register rollback tools with the MCP server.
329
+
330
+ Args:
331
+ mcp: The FastMCP server instance.
332
+ """
333
+ mcp.tool()(rollback)
334
+ mcp.tool()(rollback_history)
335
+ mcp.tool()(rollback_cleanup)