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