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