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,391 @@
1
+ """Local HTTP server for OAuth callback.
2
+
3
+ This module provides a lightweight async HTTP server to receive
4
+ the authorization code from Keycloak after browser login.
5
+ """
6
+ # ruff: noqa: E501, N802 # E501: HTML templates contain long lines, N802: do_GET required by BaseHTTPRequestHandler
7
+
8
+ import asyncio
9
+ import logging
10
+ from http.server import BaseHTTPRequestHandler
11
+ from socketserver import TCPServer
12
+ from threading import Thread
13
+ from urllib.parse import parse_qs, urlparse
14
+
15
+ from mcp_eregistrations_bpa.exceptions import AuthenticationError
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Default timeout for waiting for callback
20
+ CALLBACK_TIMEOUT = 60.0 # seconds
21
+
22
+
23
+ class CallbackHandler(BaseHTTPRequestHandler):
24
+ """HTTP handler for OAuth callback."""
25
+
26
+ def log_message(self, format: str, *args: object) -> None:
27
+ """Suppress default HTTP logging."""
28
+ pass
29
+
30
+ def do_GET(self) -> None:
31
+ """Handle GET request from OAuth redirect."""
32
+ logger.info("Received callback request: %s", self.path)
33
+ parsed = urlparse(self.path)
34
+
35
+ if parsed.path != "/callback":
36
+ logger.warning("Invalid callback path: %s", parsed.path)
37
+ self.send_response(404)
38
+ self.end_headers()
39
+ return
40
+
41
+ # Parse query parameters
42
+ params = parse_qs(parsed.query)
43
+ code = params.get("code", [None])[0]
44
+ state = params.get("state", [None])[0]
45
+ error = params.get("error", [None])[0]
46
+ error_description = params.get("error_description", [None])[0]
47
+
48
+ logger.debug(
49
+ "Callback params: code=%s, state=%s, error=%s",
50
+ "present" if code else "missing",
51
+ "present" if state else "missing",
52
+ error,
53
+ )
54
+
55
+ # Store results on server instance
56
+ server = self.server
57
+ if hasattr(server, "_callback_result"):
58
+ if error:
59
+ logger.error("OAuth error: %s - %s", error, error_description)
60
+ server._callback_result = {
61
+ "error": error,
62
+ "error_description": error_description,
63
+ }
64
+ else:
65
+ logger.info("OAuth callback successful, code received")
66
+ server._callback_result = {
67
+ "code": code,
68
+ "state": state,
69
+ }
70
+
71
+ # Send success response to browser
72
+ self.send_response(200)
73
+ self.send_header("Content-Type", "text/html")
74
+ self.end_headers()
75
+
76
+ if error:
77
+ html = f"""<!DOCTYPE html>
78
+ <html lang="en">
79
+ <head>
80
+ <meta charset="UTF-8">
81
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
82
+ <title>Access Denied - eRegistrations BPA</title>
83
+ <link rel="preconnect" href="https://fonts.googleapis.com">
84
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
85
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
86
+ <style>
87
+ :root {{
88
+ --bg-color: #ffffff;
89
+ --text-primary: #1a1a1a;
90
+ --text-secondary: #5e5e5e;
91
+ --brand-red: #e60000;
92
+ --border-color: #dcdcdc;
93
+ }}
94
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
95
+ body {{
96
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
97
+ min-height: 100vh;
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ background-color: #f4f4f4;
102
+ color: var(--text-primary);
103
+ padding: 24px;
104
+ }}
105
+ .container {{
106
+ background: var(--bg-color);
107
+ width: 100%;
108
+ max-width: 480px;
109
+ padding: 48px;
110
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
111
+ border-top: 4px solid var(--brand-red);
112
+ }}
113
+ .header {{
114
+ margin-bottom: 32px;
115
+ }}
116
+ h1 {{
117
+ font-size: 32px;
118
+ font-weight: 700;
119
+ letter-spacing: -0.02em;
120
+ line-height: 1.1;
121
+ margin-bottom: 16px;
122
+ }}
123
+ .status-line {{
124
+ display: inline-block;
125
+ font-size: 14px;
126
+ font-weight: 600;
127
+ text-transform: uppercase;
128
+ letter-spacing: 0.05em;
129
+ color: var(--text-secondary);
130
+ margin-bottom: 8px;
131
+ }}
132
+ p {{
133
+ font-size: 18px;
134
+ line-height: 1.5;
135
+ color: var(--text-secondary);
136
+ margin-bottom: 32px;
137
+ }}
138
+ .error-code {{
139
+ font-family: monospace;
140
+ background: #f0f0f0;
141
+ padding: 8px 12px;
142
+ font-size: 14px;
143
+ color: var(--text-primary);
144
+ display: inline-block;
145
+ margin-top: 16px;
146
+ }}
147
+ .footer {{
148
+ margin-top: 48px;
149
+ border-top: 1px solid var(--border-color);
150
+ padding-top: 24px;
151
+ font-size: 12px;
152
+ color: #999;
153
+ display: flex;
154
+ justify-content: space-between;
155
+ }}
156
+ </style>
157
+ </head>
158
+ <body>
159
+ <div class="container">
160
+ <div class="header">
161
+ <span class="status-line">Authorization Status</span>
162
+ <h1>Access Denied</h1>
163
+ </div>
164
+ <p>The system could not verify your credentials. Ensure your security token is valid and try again.</p>
165
+
166
+ <div class="error-details">
167
+ <div class="status-line" style="font-size: 12px;">System Response</div>
168
+ <div style="font-weight: 600;">{error_description or "Unknown Error"}</div>
169
+ {f'<div class="error-code">{error}</div>' if error else ""}
170
+ </div>
171
+
172
+ <div class="footer">
173
+ <span>AI-Native Digital Government System</span>
174
+ <span>BPA MCP</span>
175
+ </div>
176
+ </div>
177
+ </body>
178
+ </html>"""
179
+ else:
180
+ html = """<!DOCTYPE html>
181
+ <html lang="en">
182
+ <head>
183
+ <meta charset="UTF-8">
184
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
185
+ <title>Access Authorized - eRegistrations BPA</title>
186
+ <link rel="preconnect" href="https://fonts.googleapis.com">
187
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
188
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
189
+ <style>
190
+ :root {
191
+ --bg-color: #ffffff;
192
+ --text-primary: #1a1a1a;
193
+ --text-secondary: #5e5e5e;
194
+ --brand-green: #107c10;
195
+ --border-color: #dcdcdc;
196
+ }
197
+ * { margin: 0; padding: 0; box-sizing: border-box; }
198
+ body {
199
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
200
+ min-height: 100vh;
201
+ display: flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ background-color: #f4f4f4;
205
+ color: var(--text-primary);
206
+ padding: 24px;
207
+ }
208
+ .container {
209
+ background: var(--bg-color);
210
+ width: 100%;
211
+ max-width: 480px;
212
+ padding: 48px;
213
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
214
+ border-top: 4px solid var(--brand-green);
215
+ }
216
+ .header {
217
+ margin-bottom: 32px;
218
+ }
219
+ h1 {
220
+ font-size: 32px;
221
+ font-weight: 700;
222
+ letter-spacing: -0.02em;
223
+ line-height: 1.1;
224
+ margin-bottom: 16px;
225
+ }
226
+ .status-line {
227
+ display: inline-block;
228
+ font-size: 14px;
229
+ font-weight: 600;
230
+ text-transform: uppercase;
231
+ letter-spacing: 0.05em;
232
+ color: var(--text-secondary);
233
+ margin-bottom: 8px;
234
+ }
235
+ p {
236
+ font-size: 18px;
237
+ line-height: 1.5;
238
+ color: var(--text-secondary);
239
+ margin-bottom: 32px;
240
+ }
241
+ .footer {
242
+ margin-top: 64px;
243
+ border-top: 1px solid var(--border-color);
244
+ padding-top: 24px;
245
+ font-size: 12px;
246
+ color: #999;
247
+ display: flex;
248
+ justify-content: space-between;
249
+ }
250
+ /* USB-style clean chevron */
251
+ .check-icon {
252
+ display: inline-block;
253
+ width: 16px;
254
+ height: 16px;
255
+ background-color: var(--brand-green);
256
+ color: white;
257
+ border-radius: 50%;
258
+ text-align: center;
259
+ line-height: 16px;
260
+ font-size: 10px;
261
+ margin-right: 8px;
262
+ }
263
+ </style>
264
+ </head>
265
+ <body>
266
+ <div class="container">
267
+ <div class="header">
268
+ <span class="status-line">Authorization Status</span>
269
+ <h1>Access Authorized</h1>
270
+ </div>
271
+ <p>Your AI agent now has secure access to BPA backend functions.</p>
272
+
273
+ <div style="font-size: 15px; color: var(--text-secondary); display: flex; align-items: center;">
274
+ <span class="check-icon">✓</span> You can close this window.
275
+ </div>
276
+
277
+ <div class="footer">
278
+ <span>AI-Native Digital Government System</span>
279
+ <span>BPA MCP</span>
280
+ </div>
281
+ </div>
282
+ </body>
283
+ </html>"""
284
+
285
+ self.wfile.write(html.encode())
286
+
287
+
288
+ class CallbackServer:
289
+ """Local HTTP server to receive OAuth callback.
290
+
291
+ This server listens on localhost with a dynamic port and waits
292
+ for the OAuth callback containing the authorization code.
293
+ """
294
+
295
+ def __init__(self, port: int = 0) -> None:
296
+ """Initialize callback server.
297
+
298
+ Args:
299
+ port: Port to listen on. Use 0 for dynamic port assignment.
300
+ """
301
+ self._requested_port = port
302
+ self._server: TCPServer | None = None
303
+ self._thread: Thread | None = None
304
+
305
+ @property
306
+ def port(self) -> int:
307
+ """Get the actual port the server is listening on."""
308
+ if self._server is None:
309
+ raise RuntimeError("Server not started")
310
+ return self._server.server_address[1]
311
+
312
+ @property
313
+ def redirect_uri(self) -> str:
314
+ """Get the redirect URI for OAuth configuration."""
315
+ return f"http://127.0.0.1:{self.port}/callback"
316
+
317
+ def start(self) -> None:
318
+ """Start the callback server in a background thread."""
319
+ self._server = TCPServer(("127.0.0.1", self._requested_port), CallbackHandler)
320
+ self._server._callback_result = None # type: ignore[attr-defined]
321
+ self._thread = Thread(target=self._server.handle_request, daemon=True)
322
+ self._thread.start()
323
+
324
+ def stop(self) -> None:
325
+ """Stop the callback server."""
326
+ if self._server:
327
+ # Note: Don't call shutdown() - it's for serve_forever(), not handle_request()
328
+ # shutdown() hangs waiting for a flag that handle_request() never sets
329
+ self._server.server_close()
330
+ self._server = None
331
+ if self._thread:
332
+ self._thread.join(timeout=1.0)
333
+ self._thread = None
334
+
335
+ async def wait_for_callback(
336
+ self, expected_state: str, timeout: float = CALLBACK_TIMEOUT
337
+ ) -> str:
338
+ """Wait for OAuth callback and return authorization code.
339
+
340
+ Args:
341
+ expected_state: The state parameter to validate against.
342
+ timeout: Maximum time to wait for callback in seconds.
343
+
344
+ Returns:
345
+ The authorization code from the callback.
346
+
347
+ Raises:
348
+ AuthenticationError: If callback fails or times out.
349
+ """
350
+ start_time = asyncio.get_event_loop().time()
351
+
352
+ while True:
353
+ # Check if we have a result
354
+ if self._server and self._server._callback_result: # type: ignore[attr-defined]
355
+ result = self._server._callback_result # type: ignore[attr-defined]
356
+
357
+ # Handle error response from Keycloak
358
+ if "error" in result:
359
+ error = result.get("error", "unknown")
360
+ description = result.get("error_description", "")
361
+ raise AuthenticationError(
362
+ f"Authentication failed: {error}. {description}. "
363
+ "Please try again."
364
+ )
365
+
366
+ # Validate state to prevent CSRF
367
+ if result.get("state") != expected_state:
368
+ raise AuthenticationError(
369
+ "Authentication failed: Security validation failed "
370
+ "(state mismatch). Please try auth_login again."
371
+ )
372
+
373
+ code = result.get("code")
374
+ if not code:
375
+ raise AuthenticationError(
376
+ "Authentication failed: No authorization code received. "
377
+ "Please try again."
378
+ )
379
+
380
+ return str(code)
381
+
382
+ # Check timeout
383
+ elapsed = asyncio.get_event_loop().time() - start_time
384
+ if elapsed >= timeout:
385
+ raise AuthenticationError(
386
+ f"Authentication timed out: No response received within "
387
+ f"{int(timeout)} seconds. Please try auth_login again."
388
+ )
389
+
390
+ # Wait a bit before checking again
391
+ await asyncio.sleep(0.1)