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,674 @@
|
|
|
1
|
+
"""MCP tools for BPA service operations.
|
|
2
|
+
|
|
3
|
+
This module provides tools for listing, retrieving, creating, and updating BPA services.
|
|
4
|
+
|
|
5
|
+
Write operations follow the audit-before-write pattern:
|
|
6
|
+
1. Validate parameters (pre-flight, no audit record if validation fails)
|
|
7
|
+
2. Create PENDING audit record
|
|
8
|
+
3. Execute BPA API call
|
|
9
|
+
4. Update audit record to SUCCESS or FAILED
|
|
10
|
+
|
|
11
|
+
API Endpoints used:
|
|
12
|
+
- GET /service - List all services
|
|
13
|
+
- GET /service/{id} - Get service by ID
|
|
14
|
+
- POST /service - Create new service
|
|
15
|
+
- PUT /service - Update service
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
23
|
+
|
|
24
|
+
from mcp_eregistrations_bpa.audit.context import (
|
|
25
|
+
NotAuthenticatedError,
|
|
26
|
+
get_current_user_email,
|
|
27
|
+
)
|
|
28
|
+
from mcp_eregistrations_bpa.audit.logger import AuditLogger
|
|
29
|
+
from mcp_eregistrations_bpa.bpa_client import BPAClient
|
|
30
|
+
from mcp_eregistrations_bpa.bpa_client.errors import (
|
|
31
|
+
BPAClientError,
|
|
32
|
+
BPANotFoundError,
|
|
33
|
+
translate_error,
|
|
34
|
+
)
|
|
35
|
+
from mcp_eregistrations_bpa.tools.large_response import large_response_handler
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"service_list",
|
|
39
|
+
"service_get",
|
|
40
|
+
"service_create",
|
|
41
|
+
"service_update",
|
|
42
|
+
"service_publish",
|
|
43
|
+
"service_activate",
|
|
44
|
+
"register_service_tools",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@large_response_handler(
|
|
49
|
+
threshold_bytes=50 * 1024, # 50KB threshold for list tools
|
|
50
|
+
navigation={
|
|
51
|
+
"list_all": "jq '.services'",
|
|
52
|
+
"find_by_name": "jq '.services[] | select(.name | contains(\"search\"))'",
|
|
53
|
+
"find_by_status": "jq '.services[] | select(.status == \"ACTIVE\")'",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
async def service_list(
|
|
57
|
+
limit: int = 50,
|
|
58
|
+
offset: int = 0,
|
|
59
|
+
) -> dict[str, Any]:
|
|
60
|
+
"""List all BPA services.
|
|
61
|
+
|
|
62
|
+
Returns all services the authenticated user has access to.
|
|
63
|
+
Each service includes id, name, status, and registration_count.
|
|
64
|
+
Large responses (>50KB) are saved to file with navigation hints.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
limit: Maximum number of services to return (default: 50).
|
|
68
|
+
offset: Number of services to skip for pagination (default: 0).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
dict: List of services with total count.
|
|
72
|
+
- services: List of service objects
|
|
73
|
+
- total: Total number of services
|
|
74
|
+
- has_more: True if more services exist beyond current page
|
|
75
|
+
"""
|
|
76
|
+
# Normalize pagination parameters
|
|
77
|
+
if limit <= 0:
|
|
78
|
+
limit = 50
|
|
79
|
+
if offset < 0:
|
|
80
|
+
offset = 0
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
async with BPAClient() as client:
|
|
84
|
+
services_data = await client.get_list("/service", resource_type="service")
|
|
85
|
+
except BPAClientError as e:
|
|
86
|
+
raise translate_error(e, resource_type="service")
|
|
87
|
+
|
|
88
|
+
# Transform to consistent output format with registration_count
|
|
89
|
+
# Note: BPA list endpoint returns registrationCount=0 always. If registrations
|
|
90
|
+
# array is embedded (like in detail endpoint), use its length instead.
|
|
91
|
+
all_services = []
|
|
92
|
+
for svc in services_data:
|
|
93
|
+
# Prefer counting registrations array (if present) over BPA's registrationCount
|
|
94
|
+
registrations = svc.get("registrations")
|
|
95
|
+
if registrations is not None:
|
|
96
|
+
reg_count = len(registrations)
|
|
97
|
+
else:
|
|
98
|
+
reg_count = svc.get("registrationCount", 0)
|
|
99
|
+
|
|
100
|
+
all_services.append(
|
|
101
|
+
{
|
|
102
|
+
"id": svc.get("id"),
|
|
103
|
+
"name": svc.get("name"),
|
|
104
|
+
"status": svc.get("status"),
|
|
105
|
+
"registration_count": reg_count,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Calculate total before pagination
|
|
110
|
+
total = len(all_services)
|
|
111
|
+
|
|
112
|
+
# Apply pagination
|
|
113
|
+
paginated_services = all_services[offset : offset + limit]
|
|
114
|
+
|
|
115
|
+
# Calculate has_more
|
|
116
|
+
has_more = (offset + limit) < total
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"services": paginated_services,
|
|
120
|
+
"total": total,
|
|
121
|
+
"has_more": has_more,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def service_get(service_id: str | int) -> dict[str, Any]:
|
|
126
|
+
"""Get details of a BPA service by ID.
|
|
127
|
+
|
|
128
|
+
Returns complete service details including registrations summary.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
service_id: The unique identifier of the service.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
dict: Complete service details including:
|
|
135
|
+
- id, name, description, status, short_name
|
|
136
|
+
- registrations: List of registration summaries
|
|
137
|
+
- created_at, updated_at timestamps
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
async with BPAClient() as client:
|
|
141
|
+
try:
|
|
142
|
+
service_data = await client.get(
|
|
143
|
+
"/service/{id}",
|
|
144
|
+
path_params={"id": service_id},
|
|
145
|
+
resource_type="service",
|
|
146
|
+
resource_id=service_id,
|
|
147
|
+
)
|
|
148
|
+
except BPANotFoundError:
|
|
149
|
+
raise ToolError(
|
|
150
|
+
f"Service '{service_id}' not found. "
|
|
151
|
+
"Use 'service_list' to see available services."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Extract registrations embedded in service response
|
|
155
|
+
# Note: BPA API ignores serviceId param on /registration endpoint,
|
|
156
|
+
# returning ALL registrations globally. The correct approach is to
|
|
157
|
+
# use registrations already embedded in the service response.
|
|
158
|
+
registrations_data = service_data.get("registrations", [])
|
|
159
|
+
except ToolError:
|
|
160
|
+
# Re-raise ToolError (from BPANotFoundError handling above)
|
|
161
|
+
raise
|
|
162
|
+
except BPAClientError as e:
|
|
163
|
+
raise translate_error(e, resource_type="service", resource_id=service_id)
|
|
164
|
+
|
|
165
|
+
# Transform registrations to summary format (includes key per AC1)
|
|
166
|
+
registrations = [
|
|
167
|
+
{"id": reg.get("id"), "name": reg.get("name"), "key": reg.get("key")}
|
|
168
|
+
for reg in registrations_data
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"id": service_data.get("id"),
|
|
173
|
+
"name": service_data.get("name"),
|
|
174
|
+
"description": service_data.get("description"),
|
|
175
|
+
"status": service_data.get("status"),
|
|
176
|
+
"short_name": service_data.get("shortName"),
|
|
177
|
+
"registrations": registrations,
|
|
178
|
+
"created_at": service_data.get("createdAt"),
|
|
179
|
+
"updated_at": service_data.get("updatedAt"),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _validate_service_create_params(
|
|
184
|
+
name: str,
|
|
185
|
+
description: str | None,
|
|
186
|
+
short_name: str | None,
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
"""Validate service_create parameters (pre-flight).
|
|
189
|
+
|
|
190
|
+
Returns validated params dict or raises ToolError if invalid.
|
|
191
|
+
No audit record is created for validation failures.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
name: Service name (required).
|
|
195
|
+
description: Service description (optional).
|
|
196
|
+
short_name: Short name for the service (optional).
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
dict: Validated parameters ready for API call.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ToolError: If validation fails.
|
|
203
|
+
"""
|
|
204
|
+
errors = []
|
|
205
|
+
|
|
206
|
+
if not name or not name.strip():
|
|
207
|
+
errors.append("'name' is required and cannot be empty")
|
|
208
|
+
|
|
209
|
+
if name and len(name.strip()) > 255:
|
|
210
|
+
errors.append("'name' must be 255 characters or less")
|
|
211
|
+
|
|
212
|
+
if short_name and len(short_name.strip()) > 50:
|
|
213
|
+
errors.append("'short_name' must be 50 characters or less")
|
|
214
|
+
|
|
215
|
+
if errors:
|
|
216
|
+
error_msg = "; ".join(errors)
|
|
217
|
+
raise ToolError(f"Cannot create service: {error_msg}. Check required fields.")
|
|
218
|
+
|
|
219
|
+
params: dict[str, Any] = {
|
|
220
|
+
"name": name.strip(),
|
|
221
|
+
"active": True, # Services are active by default
|
|
222
|
+
}
|
|
223
|
+
if description:
|
|
224
|
+
params["description"] = description.strip()
|
|
225
|
+
if short_name:
|
|
226
|
+
params["shortName"] = short_name.strip()
|
|
227
|
+
|
|
228
|
+
return params
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def service_create(
|
|
232
|
+
name: str,
|
|
233
|
+
description: str | None = None,
|
|
234
|
+
short_name: str | None = None,
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
"""Create a new BPA service. Audited write operation.
|
|
237
|
+
|
|
238
|
+
Services are created as active by default.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
name: Service name.
|
|
242
|
+
description: Optional description.
|
|
243
|
+
short_name: Optional short name.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
dict with id, name, description, status, short_name, active, created_at,
|
|
247
|
+
audit_id.
|
|
248
|
+
"""
|
|
249
|
+
# Pre-flight validation (no audit record for validation failures)
|
|
250
|
+
validated_params = _validate_service_create_params(name, description, short_name)
|
|
251
|
+
|
|
252
|
+
# Get authenticated user for audit
|
|
253
|
+
try:
|
|
254
|
+
user_email = get_current_user_email()
|
|
255
|
+
except NotAuthenticatedError as e:
|
|
256
|
+
raise ToolError(str(e))
|
|
257
|
+
|
|
258
|
+
# Create audit record BEFORE API call (audit-before-write pattern)
|
|
259
|
+
audit_logger = AuditLogger()
|
|
260
|
+
audit_id = await audit_logger.record_pending(
|
|
261
|
+
user_email=user_email,
|
|
262
|
+
operation_type="create",
|
|
263
|
+
object_type="service",
|
|
264
|
+
params=validated_params,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
async with BPAClient() as client:
|
|
269
|
+
service_data = await client.post(
|
|
270
|
+
"/service",
|
|
271
|
+
json=validated_params,
|
|
272
|
+
resource_type="service",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Save rollback state (for create, save ID to enable deletion on rollback)
|
|
276
|
+
created_id = service_data.get("id")
|
|
277
|
+
await audit_logger.save_rollback_state(
|
|
278
|
+
audit_id=audit_id,
|
|
279
|
+
object_type="service",
|
|
280
|
+
object_id=str(created_id),
|
|
281
|
+
previous_state={
|
|
282
|
+
"id": created_id,
|
|
283
|
+
"name": service_data.get("name"),
|
|
284
|
+
"description": service_data.get("description"),
|
|
285
|
+
"shortName": service_data.get("shortName"),
|
|
286
|
+
"_operation": "create", # Marker for rollback to know to DELETE
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Mark audit as success
|
|
291
|
+
await audit_logger.mark_success(
|
|
292
|
+
audit_id,
|
|
293
|
+
result={
|
|
294
|
+
"service_id": created_id,
|
|
295
|
+
"name": service_data.get("name"),
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
"id": created_id,
|
|
301
|
+
"name": service_data.get("name"),
|
|
302
|
+
"description": service_data.get("description"),
|
|
303
|
+
"status": service_data.get("status"),
|
|
304
|
+
"short_name": service_data.get("shortName"),
|
|
305
|
+
"active": service_data.get("active", True),
|
|
306
|
+
"created_at": service_data.get("createdAt"),
|
|
307
|
+
"audit_id": audit_id,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
except BPAClientError as e:
|
|
311
|
+
# Mark audit as failed
|
|
312
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
313
|
+
raise translate_error(e, resource_type="service")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _validate_service_update_params(
|
|
317
|
+
service_id: str | int,
|
|
318
|
+
name: str | None,
|
|
319
|
+
description: str | None,
|
|
320
|
+
short_name: str | None,
|
|
321
|
+
) -> dict[str, Any]:
|
|
322
|
+
"""Validate service_update parameters (pre-flight).
|
|
323
|
+
|
|
324
|
+
Returns validated params dict or raises ToolError if invalid.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
service_id: ID of service to update (required).
|
|
328
|
+
name: New name (optional).
|
|
329
|
+
description: New description (optional).
|
|
330
|
+
short_name: New short name (optional).
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
dict: Validated parameters ready for API call.
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
ToolError: If validation fails.
|
|
337
|
+
"""
|
|
338
|
+
errors = []
|
|
339
|
+
|
|
340
|
+
if not service_id:
|
|
341
|
+
errors.append("'service_id' is required")
|
|
342
|
+
|
|
343
|
+
if name is not None and not name.strip():
|
|
344
|
+
errors.append("'name' cannot be empty when provided")
|
|
345
|
+
|
|
346
|
+
if name and len(name.strip()) > 255:
|
|
347
|
+
errors.append("'name' must be 255 characters or less")
|
|
348
|
+
|
|
349
|
+
if short_name and len(short_name.strip()) > 50:
|
|
350
|
+
errors.append("'short_name' must be 50 characters or less")
|
|
351
|
+
|
|
352
|
+
# At least one field must be provided for update
|
|
353
|
+
if name is None and description is None and short_name is None:
|
|
354
|
+
errors.append(
|
|
355
|
+
"At least one field (name, description, short_name) must be provided"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if errors:
|
|
359
|
+
error_msg = "; ".join(errors)
|
|
360
|
+
raise ToolError(f"Cannot update service: {error_msg}. Check required fields.")
|
|
361
|
+
|
|
362
|
+
params: dict[str, Any] = {"id": service_id}
|
|
363
|
+
if name is not None:
|
|
364
|
+
params["name"] = name.strip()
|
|
365
|
+
if description is not None:
|
|
366
|
+
params["description"] = description.strip()
|
|
367
|
+
if short_name is not None:
|
|
368
|
+
params["shortName"] = short_name.strip()
|
|
369
|
+
|
|
370
|
+
return params
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
async def service_update(
|
|
374
|
+
service_id: str | int,
|
|
375
|
+
name: str | None = None,
|
|
376
|
+
description: str | None = None,
|
|
377
|
+
short_name: str | None = None,
|
|
378
|
+
) -> dict[str, Any]:
|
|
379
|
+
"""Update an existing BPA service. Audited write operation.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
service_id: Service ID to update.
|
|
383
|
+
name: New name (optional).
|
|
384
|
+
description: New description (optional).
|
|
385
|
+
short_name: New short name (optional).
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
dict with id, name, description, status, short_name, updated_at,
|
|
389
|
+
previous_state, audit_id.
|
|
390
|
+
"""
|
|
391
|
+
# Pre-flight validation (no audit record for validation failures)
|
|
392
|
+
validated_params = _validate_service_update_params(
|
|
393
|
+
service_id, name, description, short_name
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Get authenticated user for audit
|
|
397
|
+
try:
|
|
398
|
+
user_email = get_current_user_email()
|
|
399
|
+
except NotAuthenticatedError as e:
|
|
400
|
+
raise ToolError(str(e))
|
|
401
|
+
|
|
402
|
+
# Capture current state for rollback BEFORE making changes
|
|
403
|
+
try:
|
|
404
|
+
async with BPAClient() as client:
|
|
405
|
+
try:
|
|
406
|
+
previous_state = await client.get(
|
|
407
|
+
"/service/{id}",
|
|
408
|
+
path_params={"id": service_id},
|
|
409
|
+
resource_type="service",
|
|
410
|
+
resource_id=service_id,
|
|
411
|
+
)
|
|
412
|
+
except BPANotFoundError:
|
|
413
|
+
raise ToolError(
|
|
414
|
+
f"Service '{service_id}' not found. "
|
|
415
|
+
"Use 'service_list' to see available services."
|
|
416
|
+
)
|
|
417
|
+
except ToolError:
|
|
418
|
+
raise
|
|
419
|
+
except BPAClientError as e:
|
|
420
|
+
raise translate_error(e, resource_type="service", resource_id=service_id)
|
|
421
|
+
|
|
422
|
+
# Create audit record BEFORE API call (audit-before-write pattern)
|
|
423
|
+
audit_logger = AuditLogger()
|
|
424
|
+
audit_id = await audit_logger.record_pending(
|
|
425
|
+
user_email=user_email,
|
|
426
|
+
operation_type="update",
|
|
427
|
+
object_type="service",
|
|
428
|
+
object_id=str(service_id),
|
|
429
|
+
params={
|
|
430
|
+
"changes": validated_params,
|
|
431
|
+
},
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Save rollback state for undo capability
|
|
435
|
+
await audit_logger.save_rollback_state(
|
|
436
|
+
audit_id=audit_id,
|
|
437
|
+
object_type="service",
|
|
438
|
+
object_id=str(service_id),
|
|
439
|
+
previous_state={
|
|
440
|
+
"id": previous_state.get("id"),
|
|
441
|
+
"name": previous_state.get("name"),
|
|
442
|
+
"description": previous_state.get("description"),
|
|
443
|
+
"shortName": previous_state.get("shortName"),
|
|
444
|
+
},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
async with BPAClient() as client:
|
|
449
|
+
# BPA API requires name in PUT body - merge previous state with changes
|
|
450
|
+
put_body = {
|
|
451
|
+
"id": service_id,
|
|
452
|
+
"name": validated_params.get("name", previous_state.get("name")),
|
|
453
|
+
}
|
|
454
|
+
if "description" in validated_params:
|
|
455
|
+
put_body["description"] = validated_params["description"]
|
|
456
|
+
elif previous_state.get("description"):
|
|
457
|
+
put_body["description"] = previous_state["description"]
|
|
458
|
+
if "shortName" in validated_params:
|
|
459
|
+
put_body["shortName"] = validated_params["shortName"]
|
|
460
|
+
elif previous_state.get("shortName"):
|
|
461
|
+
put_body["shortName"] = previous_state["shortName"]
|
|
462
|
+
|
|
463
|
+
service_data = await client.put(
|
|
464
|
+
"/service",
|
|
465
|
+
json=put_body,
|
|
466
|
+
resource_type="service",
|
|
467
|
+
resource_id=service_id,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# Mark audit as success
|
|
471
|
+
await audit_logger.mark_success(
|
|
472
|
+
audit_id,
|
|
473
|
+
result={
|
|
474
|
+
"service_id": service_data.get("id"),
|
|
475
|
+
"name": service_data.get("name"),
|
|
476
|
+
"changes_applied": {
|
|
477
|
+
k: v for k, v in validated_params.items() if k != "id"
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
"id": service_data.get("id"),
|
|
484
|
+
"name": service_data.get("name"),
|
|
485
|
+
"description": service_data.get("description"),
|
|
486
|
+
"status": service_data.get("status"),
|
|
487
|
+
"short_name": service_data.get("shortName"),
|
|
488
|
+
"updated_at": service_data.get("updatedAt"),
|
|
489
|
+
"previous_state": {
|
|
490
|
+
"name": previous_state.get("name"),
|
|
491
|
+
"description": previous_state.get("description"),
|
|
492
|
+
"short_name": previous_state.get("shortName"),
|
|
493
|
+
},
|
|
494
|
+
"audit_id": audit_id,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
except BPAClientError as e:
|
|
498
|
+
# Mark audit as failed
|
|
499
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
500
|
+
raise translate_error(e, resource_type="service", resource_id=service_id)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
async def service_publish(service_id: str | int) -> dict[str, Any]:
|
|
504
|
+
"""Publish a BPA service to make it visible in the frontend.
|
|
505
|
+
|
|
506
|
+
Audited write operation.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
service_id: Service ID to publish.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
dict with service_id, published (bool), audit_id.
|
|
513
|
+
"""
|
|
514
|
+
# Pre-flight validation
|
|
515
|
+
if not service_id:
|
|
516
|
+
raise ToolError("'service_id' is required. Provide the service ID to publish.")
|
|
517
|
+
|
|
518
|
+
# Get authenticated user for audit
|
|
519
|
+
try:
|
|
520
|
+
user_email = get_current_user_email()
|
|
521
|
+
except NotAuthenticatedError as e:
|
|
522
|
+
raise ToolError(str(e))
|
|
523
|
+
|
|
524
|
+
# Verify service exists
|
|
525
|
+
try:
|
|
526
|
+
async with BPAClient() as client:
|
|
527
|
+
try:
|
|
528
|
+
await client.get(
|
|
529
|
+
"/service/{id}",
|
|
530
|
+
path_params={"id": service_id},
|
|
531
|
+
resource_type="service",
|
|
532
|
+
resource_id=service_id,
|
|
533
|
+
)
|
|
534
|
+
except BPANotFoundError:
|
|
535
|
+
raise ToolError(
|
|
536
|
+
f"Service '{service_id}' not found. "
|
|
537
|
+
"Use 'service_list' to see available services."
|
|
538
|
+
)
|
|
539
|
+
except ToolError:
|
|
540
|
+
raise
|
|
541
|
+
except BPAClientError as e:
|
|
542
|
+
raise translate_error(e, resource_type="service", resource_id=service_id)
|
|
543
|
+
|
|
544
|
+
# Create audit record BEFORE API call
|
|
545
|
+
audit_logger = AuditLogger()
|
|
546
|
+
audit_id = await audit_logger.record_pending(
|
|
547
|
+
user_email=user_email,
|
|
548
|
+
operation_type="update",
|
|
549
|
+
object_type="service",
|
|
550
|
+
object_id=str(service_id),
|
|
551
|
+
params={"action": "publish"},
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
async with BPAClient() as client:
|
|
556
|
+
await client.post(
|
|
557
|
+
"/service/{id}/publish",
|
|
558
|
+
path_params={"id": service_id},
|
|
559
|
+
json={}, # Empty body required by BPA API
|
|
560
|
+
resource_type="service",
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Mark audit as success
|
|
564
|
+
await audit_logger.mark_success(
|
|
565
|
+
audit_id,
|
|
566
|
+
result={"service_id": service_id, "action": "published"},
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
"service_id": str(service_id),
|
|
571
|
+
"published": True,
|
|
572
|
+
"audit_id": audit_id,
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
except BPAClientError as e:
|
|
576
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
577
|
+
raise translate_error(e, resource_type="service", resource_id=service_id)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
async def service_activate(
|
|
581
|
+
service_id: str | int,
|
|
582
|
+
active: bool = True,
|
|
583
|
+
) -> dict[str, Any]:
|
|
584
|
+
"""Activate or deactivate a BPA service. Audited write operation.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
service_id: Service ID to activate/deactivate.
|
|
588
|
+
active: True to activate, False to deactivate (default: True).
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
dict with service_id, active (bool), audit_id.
|
|
592
|
+
"""
|
|
593
|
+
# Pre-flight validation
|
|
594
|
+
if not service_id:
|
|
595
|
+
raise ToolError(
|
|
596
|
+
"'service_id' is required. Provide the service ID to activate/deactivate."
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Get authenticated user for audit
|
|
600
|
+
try:
|
|
601
|
+
user_email = get_current_user_email()
|
|
602
|
+
except NotAuthenticatedError as e:
|
|
603
|
+
raise ToolError(str(e))
|
|
604
|
+
|
|
605
|
+
# Verify service exists
|
|
606
|
+
try:
|
|
607
|
+
async with BPAClient() as client:
|
|
608
|
+
try:
|
|
609
|
+
await client.get(
|
|
610
|
+
"/service/{id}",
|
|
611
|
+
path_params={"id": service_id},
|
|
612
|
+
resource_type="service",
|
|
613
|
+
resource_id=service_id,
|
|
614
|
+
)
|
|
615
|
+
except BPANotFoundError:
|
|
616
|
+
raise ToolError(
|
|
617
|
+
f"Service '{service_id}' not found. "
|
|
618
|
+
"Use 'service_list' to see available services."
|
|
619
|
+
)
|
|
620
|
+
except ToolError:
|
|
621
|
+
raise
|
|
622
|
+
except BPAClientError as e:
|
|
623
|
+
raise translate_error(e, resource_type="service", resource_id=service_id)
|
|
624
|
+
|
|
625
|
+
# Create audit record BEFORE API call
|
|
626
|
+
audit_logger = AuditLogger()
|
|
627
|
+
audit_id = await audit_logger.record_pending(
|
|
628
|
+
user_email=user_email,
|
|
629
|
+
operation_type="update",
|
|
630
|
+
object_type="service",
|
|
631
|
+
object_id=str(service_id),
|
|
632
|
+
params={"action": "activate", "active": active},
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
async with BPAClient() as client:
|
|
637
|
+
await client.put(
|
|
638
|
+
"/service/{service_id}/activate/{active}",
|
|
639
|
+
path_params={"service_id": service_id, "active": str(active).lower()},
|
|
640
|
+
resource_type="service",
|
|
641
|
+
resource_id=service_id,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Mark audit as success
|
|
645
|
+
await audit_logger.mark_success(
|
|
646
|
+
audit_id,
|
|
647
|
+
result={"service_id": service_id, "active": active},
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
"service_id": str(service_id),
|
|
652
|
+
"active": active,
|
|
653
|
+
"audit_id": audit_id,
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
except BPAClientError as e:
|
|
657
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
658
|
+
raise translate_error(e, resource_type="service", resource_id=service_id)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def register_service_tools(mcp: Any) -> None:
|
|
662
|
+
"""Register service tools with the MCP server.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
mcp: The FastMCP server instance.
|
|
666
|
+
"""
|
|
667
|
+
# Read operations
|
|
668
|
+
mcp.tool()(service_list)
|
|
669
|
+
mcp.tool()(service_get)
|
|
670
|
+
# Write operations (audit-before-write pattern)
|
|
671
|
+
mcp.tool()(service_create)
|
|
672
|
+
mcp.tool()(service_update)
|
|
673
|
+
mcp.tool()(service_publish)
|
|
674
|
+
mcp.tool()(service_activate)
|