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,399 @@
1
+ """MCP tools for viewing the audit log.
2
+
3
+ This module provides tools for querying and viewing audit log entries.
4
+ Audit entries are stored locally in SQLite, tracking all write operations
5
+ performed through this MCP server.
6
+
7
+ These tools query local data only - no BPA API calls are made.
8
+ No authentication is required (the audit log is local to this MCP instance).
9
+
10
+ API Endpoints used: None (local SQLite queries only)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from datetime import datetime
17
+ from typing import Any
18
+
19
+ from mcp.server.fastmcp.exceptions import ToolError
20
+
21
+ from mcp_eregistrations_bpa.db import get_connection
22
+ from mcp_eregistrations_bpa.tools.large_response import large_response_handler
23
+
24
+ __all__ = [
25
+ "audit_list",
26
+ "audit_get",
27
+ "register_audit_tools",
28
+ "VALID_OPERATION_TYPES",
29
+ "VALID_STATUSES",
30
+ ]
31
+
32
+ # Valid values for operation_type filter
33
+ VALID_OPERATION_TYPES = frozenset(["create", "update", "delete", "link", "unlink"])
34
+
35
+ # Valid values for status filter
36
+ VALID_STATUSES = frozenset(["pending", "success", "failed"])
37
+
38
+ # Maximum limit allowed
39
+ MAX_LIMIT = 100
40
+
41
+ # Default limit
42
+ DEFAULT_LIMIT = 50
43
+
44
+
45
+ def _parse_date(date_str: str) -> datetime:
46
+ """Parse an ISO 8601 date or datetime string for validation.
47
+
48
+ This function validates date format only. The original string is passed
49
+ to SQLite's datetime() function for actual filtering, so timezone handling
50
+ is delegated to SQLite.
51
+
52
+ Supports formats:
53
+ - YYYY-MM-DD (date only, treated as start of day)
54
+ - YYYY-MM-DDTHH:MM:SS (datetime without timezone)
55
+ - YYYY-MM-DDTHH:MM:SSZ (datetime with Z timezone)
56
+ - YYYY-MM-DDTHH:MM:SS+HH:MM (datetime with offset)
57
+
58
+ Args:
59
+ date_str: ISO 8601 date/datetime string.
60
+
61
+ Returns:
62
+ Parsed datetime object (may be naive or aware depending on input).
63
+
64
+ Raises:
65
+ ValueError: If the format is invalid.
66
+ """
67
+ # Try date-only format first
68
+ try:
69
+ return datetime.strptime(date_str, "%Y-%m-%d")
70
+ except ValueError:
71
+ pass
72
+
73
+ # Try datetime formats
74
+ for fmt in [
75
+ "%Y-%m-%dT%H:%M:%S",
76
+ "%Y-%m-%dT%H:%M:%SZ",
77
+ "%Y-%m-%dT%H:%M:%S%z",
78
+ ]:
79
+ try:
80
+ return datetime.strptime(date_str, fmt)
81
+ except ValueError:
82
+ continue
83
+
84
+ # Try fromisoformat as fallback (handles more variants)
85
+ try:
86
+ return datetime.fromisoformat(date_str.replace("Z", "+00:00"))
87
+ except ValueError:
88
+ pass
89
+
90
+ raise ValueError(f"Invalid date format: {date_str}")
91
+
92
+
93
+ def _validate_audit_list_params(
94
+ from_date: str | None,
95
+ to_date: str | None,
96
+ operation_type: str | None,
97
+ status: str | None,
98
+ limit: int,
99
+ ) -> dict[str, Any]:
100
+ """Validate audit_list parameters (pre-flight).
101
+
102
+ Args:
103
+ from_date: Filter entries from this date.
104
+ to_date: Filter entries up to this date.
105
+ operation_type: Filter by operation type.
106
+ status: Filter by status.
107
+ limit: Maximum entries to return.
108
+
109
+ Returns:
110
+ Dict with validated parameters.
111
+
112
+ Raises:
113
+ ToolError: If validation fails.
114
+ """
115
+ validated: dict[str, Any] = {}
116
+
117
+ # Validate from_date
118
+ if from_date:
119
+ try:
120
+ _parse_date(from_date)
121
+ validated["from_date"] = from_date
122
+ except ValueError:
123
+ raise ToolError(
124
+ f"Invalid from_date format '{from_date}'. "
125
+ "Use ISO 8601 format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ."
126
+ )
127
+
128
+ # Validate to_date
129
+ if to_date:
130
+ try:
131
+ _parse_date(to_date)
132
+ validated["to_date"] = to_date
133
+ except ValueError:
134
+ raise ToolError(
135
+ f"Invalid to_date format '{to_date}'. "
136
+ "Use ISO 8601 format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ."
137
+ )
138
+
139
+ # Validate date range order
140
+ if from_date and to_date:
141
+ from_dt = _parse_date(from_date)
142
+ to_dt = _parse_date(to_date)
143
+ if from_dt > to_dt:
144
+ raise ToolError(
145
+ f"from_date '{from_date}' must be before or equal to "
146
+ f"to_date '{to_date}'."
147
+ )
148
+
149
+ # Validate operation_type
150
+ if operation_type:
151
+ if operation_type not in VALID_OPERATION_TYPES:
152
+ raise ToolError(
153
+ f"Invalid operation_type '{operation_type}'. "
154
+ f"Valid types: {', '.join(sorted(VALID_OPERATION_TYPES))}."
155
+ )
156
+ validated["operation_type"] = operation_type
157
+
158
+ # Validate status
159
+ if status:
160
+ if status not in VALID_STATUSES:
161
+ raise ToolError(
162
+ f"Invalid status '{status}'. "
163
+ f"Valid statuses: {', '.join(sorted(VALID_STATUSES))}."
164
+ )
165
+ validated["status"] = status
166
+
167
+ # Validate and clamp limit
168
+ if limit <= 0:
169
+ raise ToolError("limit must be a positive integer.")
170
+ validated["limit"] = min(limit, MAX_LIMIT)
171
+
172
+ return validated
173
+
174
+
175
+ def _build_audit_query(
176
+ from_date: str | None,
177
+ to_date: str | None,
178
+ operation_type: str | None,
179
+ object_type: str | None,
180
+ status: str | None,
181
+ limit: int,
182
+ ) -> tuple[str, list[Any]]:
183
+ """Build SQL query with filters for audit_list.
184
+
185
+ Args:
186
+ from_date: Filter entries from this date.
187
+ to_date: Filter entries up to this date.
188
+ operation_type: Filter by operation type.
189
+ object_type: Filter by object type.
190
+ status: Filter by status.
191
+ limit: Maximum entries to return.
192
+
193
+ Returns:
194
+ Tuple of (SQL query string, list of parameters).
195
+ """
196
+ base_query = """
197
+ SELECT id, timestamp, user_email, operation_type, object_type,
198
+ object_id, status
199
+ FROM audit_logs
200
+ """
201
+
202
+ conditions: list[str] = []
203
+ params: list[Any] = []
204
+
205
+ if from_date:
206
+ conditions.append("timestamp >= datetime(?)")
207
+ params.append(from_date)
208
+
209
+ if to_date:
210
+ # For date-only format (YYYY-MM-DD), include the entire day by using < next day.
211
+ # For datetime format (contains "T"), use <= for exact boundary matching.
212
+ # This means entries at exactly the specified datetime are included.
213
+ if "T" not in to_date:
214
+ conditions.append("timestamp < datetime(?, '+1 day')")
215
+ else:
216
+ conditions.append("timestamp <= datetime(?)")
217
+ params.append(to_date)
218
+
219
+ if operation_type:
220
+ conditions.append("operation_type = ?")
221
+ params.append(operation_type)
222
+
223
+ if object_type:
224
+ conditions.append("object_type = ?")
225
+ params.append(object_type)
226
+
227
+ if status:
228
+ conditions.append("status = ?")
229
+ params.append(status)
230
+
231
+ if conditions:
232
+ base_query += " WHERE " + " AND ".join(conditions)
233
+
234
+ base_query += " ORDER BY timestamp DESC LIMIT ?"
235
+ params.append(limit)
236
+
237
+ return base_query, params
238
+
239
+
240
+ def _transform_audit_entry_summary(row: dict[str, Any]) -> dict[str, Any]:
241
+ """Transform database row to summary response format.
242
+
243
+ Args:
244
+ row: Database row dictionary.
245
+
246
+ Returns:
247
+ Transformed summary with snake_case keys.
248
+ """
249
+ return {
250
+ "id": row["id"],
251
+ "timestamp": row["timestamp"],
252
+ "user_email": row["user_email"],
253
+ "operation_type": row["operation_type"],
254
+ "object_type": row["object_type"],
255
+ "object_id": row["object_id"],
256
+ "status": row["status"],
257
+ }
258
+
259
+
260
+ @large_response_handler(
261
+ threshold_bytes=50 * 1024, # 50KB threshold for list tools
262
+ navigation={
263
+ "list_all": "jq '.entries'",
264
+ "find_by_operation": "jq '.entries[] | select(.operation_type == \"create\")'",
265
+ "find_by_object_type": "jq '.entries[] | select(.object_type == \"service\")'",
266
+ "find_by_status": "jq '.entries[] | select(.status == \"success\")'",
267
+ },
268
+ )
269
+ async def audit_list(
270
+ from_date: str | None = None,
271
+ to_date: str | None = None,
272
+ operation_type: str | None = None,
273
+ object_type: str | None = None,
274
+ status: str | None = None,
275
+ limit: int = DEFAULT_LIMIT,
276
+ ) -> dict[str, Any]:
277
+ """List audit log entries with optional filters. Local data only.
278
+
279
+ Large responses (>50KB) are saved to file with navigation hints.
280
+
281
+ Args:
282
+ from_date: ISO 8601 date to filter from.
283
+ to_date: ISO 8601 date to filter to.
284
+ operation_type: create, update, delete, link, or unlink.
285
+ object_type: service, registration, role, bot, determinant, cost, form, etc.
286
+ status: pending, success, or failed.
287
+ limit: Max entries (default 50, max 100).
288
+
289
+ Returns:
290
+ dict with entries, total, filters_applied.
291
+ """
292
+ # Validate parameters (will raise ToolError if invalid)
293
+ validated = _validate_audit_list_params(
294
+ from_date, to_date, operation_type, status, limit
295
+ )
296
+ effective_limit = validated.get("limit", DEFAULT_LIMIT)
297
+
298
+ # Build and execute query
299
+ query, params = _build_audit_query(
300
+ from_date, to_date, operation_type, object_type, status, effective_limit
301
+ )
302
+
303
+ async with get_connection() as conn:
304
+ cursor = await conn.execute(query, params)
305
+ rows = await cursor.fetchall()
306
+
307
+ # Transform to response format
308
+ entries = [_transform_audit_entry_summary(dict(row)) for row in rows]
309
+
310
+ # Build filters_applied dict (only include non-None filters)
311
+ filters_applied: dict[str, Any] = {}
312
+ if from_date:
313
+ filters_applied["from_date"] = from_date
314
+ if to_date:
315
+ filters_applied["to_date"] = to_date
316
+ if operation_type:
317
+ filters_applied["operation_type"] = operation_type
318
+ if object_type:
319
+ filters_applied["object_type"] = object_type
320
+ if status:
321
+ filters_applied["status"] = status
322
+ if limit != DEFAULT_LIMIT:
323
+ filters_applied["limit"] = effective_limit
324
+
325
+ return {
326
+ "entries": entries,
327
+ "total": len(entries),
328
+ "filters_applied": filters_applied,
329
+ }
330
+
331
+
332
+ async def audit_get(audit_id: str) -> dict[str, Any]:
333
+ """Get full audit entry details. Local data only.
334
+
335
+ Args:
336
+ audit_id: Audit entry UUID.
337
+
338
+ Returns:
339
+ dict with id, timestamp, user_email, operation_type, object_type,
340
+ object_id, params, status, result, rollback_available.
341
+ """
342
+ if not audit_id or not audit_id.strip():
343
+ raise ToolError(
344
+ "Cannot get audit entry: 'audit_id' is required. "
345
+ "Use 'audit_list' to see available entries."
346
+ )
347
+
348
+ async with get_connection() as conn:
349
+ cursor = await conn.execute(
350
+ """
351
+ SELECT id, timestamp, user_email, operation_type, object_type,
352
+ object_id, params, status, result, rollback_state_id
353
+ FROM audit_logs
354
+ WHERE id = ?
355
+ """,
356
+ (audit_id.strip(),),
357
+ )
358
+ row = await cursor.fetchone()
359
+
360
+ if row is None:
361
+ raise ToolError(
362
+ f"Audit entry '{audit_id}' not found. "
363
+ "Use 'audit_list' to see available entries."
364
+ )
365
+
366
+ row_dict = dict(row)
367
+
368
+ # Parse JSON fields
369
+ params = json.loads(row_dict["params"]) if row_dict["params"] else {}
370
+ result = json.loads(row_dict["result"]) if row_dict["result"] else None
371
+
372
+ # Determine rollback availability
373
+ # Rollback is available if: status is success AND rollback_state_id exists
374
+ rollback_available = (
375
+ row_dict["status"] == "success" and row_dict["rollback_state_id"] is not None
376
+ )
377
+
378
+ return {
379
+ "id": row_dict["id"],
380
+ "timestamp": row_dict["timestamp"],
381
+ "user_email": row_dict["user_email"],
382
+ "operation_type": row_dict["operation_type"],
383
+ "object_type": row_dict["object_type"],
384
+ "object_id": row_dict["object_id"],
385
+ "params": params,
386
+ "status": row_dict["status"],
387
+ "result": result,
388
+ "rollback_available": rollback_available,
389
+ }
390
+
391
+
392
+ def register_audit_tools(mcp: Any) -> None:
393
+ """Register audit tools with the MCP server.
394
+
395
+ Args:
396
+ mcp: The FastMCP server instance.
397
+ """
398
+ mcp.tool()(audit_list)
399
+ mcp.tool()(audit_get)