Skip to content

Commit eea28ea

Browse files
authored
triv: #no-task mcp logging (#193)
* triv: #no-task mcp logging * triv: #no-task mcp logging * triv: #no-task mcp logging * triv: #no-task mcp logging * triv: #no-task mcp logging
1 parent 009beb6 commit eea28ea

15 files changed

Lines changed: 1083 additions & 7 deletions

File tree

1.88 KB
Binary file not shown.

src/django_smartbase_admin/locale/sk/LC_MESSAGES/django.po

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,3 +1192,122 @@ msgstr "Strana"
11921192
msgctxt "pagination"
11931193
msgid "Go"
11941194
msgstr "Prejsť"
1195+
1196+
# MCP request log / AI Assistant Module dashboard
1197+
1198+
msgid "AI Assistant Module"
1199+
msgstr "AI asistent"
1200+
1201+
msgid "Calls"
1202+
msgstr "Volania"
1203+
1204+
msgid "Errors"
1205+
msgstr "Chyby"
1206+
1207+
msgid "Avg duration (ms)"
1208+
msgstr "Priem. trvanie (ms)"
1209+
1210+
msgid "Items returned"
1211+
msgstr "Vrátené položky"
1212+
1213+
msgid "Request (KB)"
1214+
msgstr "Požiadavka (KB)"
1215+
1216+
msgid "Response (KB)"
1217+
msgstr "Odpoveď (KB)"
1218+
1219+
msgid "No data"
1220+
msgstr "Žiadne dáta"
1221+
1222+
msgid "Recent calls"
1223+
msgstr "Posledné volania"
1224+
1225+
msgid "Time"
1226+
msgstr "Čas"
1227+
1228+
msgid "Tool"
1229+
msgstr "Nástroj"
1230+
1231+
msgid "Duration (ms)"
1232+
msgstr "Trvanie (ms)"
1233+
1234+
msgid "Error"
1235+
msgstr "Chyba"
1236+
1237+
msgid "Request"
1238+
msgstr "Požiadavka"
1239+
1240+
msgid "Response"
1241+
msgstr "Odpoveď"
1242+
1243+
msgid "Request size (B)"
1244+
msgstr "Veľkosť požiadavky (B)"
1245+
1246+
msgid "Response size (B)"
1247+
msgstr "Veľkosť odpovede (B)"
1248+
1249+
msgid "Result status"
1250+
msgstr "Stav výsledku"
1251+
1252+
msgid "Total"
1253+
msgstr "Spolu"
1254+
1255+
msgid "Fields"
1256+
msgstr "Polia"
1257+
1258+
msgid "Inlines"
1259+
msgstr "Vnorené"
1260+
1261+
msgid "Inline rows"
1262+
msgstr "Riadky vnorených"
1263+
1264+
msgid "Error type"
1265+
msgstr "Typ chyby"
1266+
1267+
msgid "Error message"
1268+
msgstr "Chybová správa"
1269+
1270+
msgid "Last %(days)s days"
1271+
msgstr "Posledných %(days)s dní"
1272+
1273+
msgid "Duration"
1274+
msgstr "Trvanie"
1275+
1276+
msgid "Changed"
1277+
msgstr "Zmenené"
1278+
1279+
msgid "rows"
1280+
msgstr "riadkov"
1281+
1282+
msgid "Audit log"
1283+
msgstr "Audit"
1284+
1285+
msgid "Record count"
1286+
msgstr "Počet záznamov"
1287+
1288+
msgid "Detail field count"
1289+
msgstr "Počet polí detailu"
1290+
1291+
msgid "Detail inline count"
1292+
msgstr "Počet inlinov detailu"
1293+
1294+
msgid "Detail inline rows"
1295+
msgstr "Riadky inlinov detailu"
1296+
1297+
msgid "Returned field count"
1298+
msgstr "Počet vrátených polí"
1299+
1300+
msgid "Returned inline count"
1301+
msgstr "Počet vrátených inlinov"
1302+
1303+
msgid "Returned inline rows"
1304+
msgstr "Počet vrátených riadkov inlinov"
1305+
1306+
msgid "MCP calls"
1307+
msgstr "MCP volania"
1308+
1309+
msgid "Grouping"
1310+
msgstr "Zoskupenie"
1311+
1312+
msgid "By user"
1313+
msgstr "Podľa používateľa"

src/django_smartbase_admin/mcp/instructions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
``list_rows`` requires non-empty ``fields``. Write results:
3838
``{"status": "ok", ...}`` or ``{"status": "invalid", "errors": ...}`` (no DB change).
3939
Inline dict keys use ``inline_name`` from ``list_admins`` (inline class name).
40+
Send nested arguments (``inlines``, ``field_values``, ``object_ids``, ``filter_data``,
41+
``sort``) as real JSON objects/arrays, never stringified JSON; pass ids as JSON
42+
numbers exactly as returned (e.g. ``174``, not ``"174"``) so rows match.
4043
Create flow: ``fetch_add_form`` then ``create_object``; inline FK ids on add often
4144
need ``autocomplete`` on another admin with the same ``filter.target_model``.
4245

src/django_smartbase_admin/mcp/mcp.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,16 +259,40 @@ def _guarded_tool_call(method):
259259
leaks across calls and tests, causing audit rows to be written under
260260
stale users.
261261
"""
262+
import time
262263
from functools import wraps
263264

265+
from django_smartbase_admin.mcp.signals import mcp_tool_called
266+
264267
@wraps(method)
265268
def wrapper(self, *args, **kwargs):
266-
if not sb_admin_site.has_permission(self.request):
267-
raise PermissionDenied
269+
start = time.perf_counter()
270+
result = None
271+
error = None
268272
try:
269-
return method(self, *args, **kwargs)
273+
if not sb_admin_site.has_permission(self.request):
274+
raise PermissionDenied
275+
result = method(self, *args, **kwargs)
276+
return result
277+
except Exception as exc:
278+
error = exc
279+
raise
270280
finally:
281+
# Clear the bound request BEFORE notifying listeners so a logging
282+
# receiver's own DB writes don't run inside the SBAdmin/audit
283+
# context (which would emit an AdminAuditLog row per log row).
271284
SBAdminThreadLocalService.clear_request()
285+
# send_robust: a failing receiver must not break the tool call.
286+
mcp_tool_called.send_robust(
287+
sender=type(self),
288+
request=getattr(self, "request", None),
289+
tool_name=method.__name__,
290+
tool_args=args,
291+
tool_kwargs=kwargs,
292+
result=result,
293+
error=error,
294+
duration_ms=int((time.perf_counter() - start) * 1000),
295+
)
272296

273297
wrapper.sbadmin_mcp_tool = True
274298
return wrapper
@@ -967,14 +991,18 @@ def update_detail(
967991
fetched payload back. Unknown or readonly names raise
968992
``LookupError``.
969993
inlines: ``{inline_name: [row_op, ...]}`` keyed by the same
970-
``inline_name`` used by ``fetch_detail``. Each op is:
994+
``inline_name`` used by ``fetch_detail``. Pass it as a real
995+
JSON object/array — never a stringified one. Each op is:
971996
972997
* ``{"id": <pk>, ...overrides}`` — update an existing row.
973998
* ``{"id": <pk>, "_delete": true}`` — delete the row.
974999
* ``{...field values}`` (no ``id``) — create a new row.
9751000
976-
Inlines not mentioned are passed through unchanged. Unknown
977-
inline names or row ids raise ``LookupError``.
1001+
``id`` is the **integer** pk exactly as ``fetch_detail``
1002+
returns it — send it as a JSON number (``174``), not a string
1003+
(``"174"``), or the row won't match. Inlines not mentioned are
1004+
passed through unchanged. Unknown inline names or row ids raise
1005+
``LookupError``.
9781006
9791007
Returns ``{"status": "ok", "id": ..., "fields": ...,
9801008
"inlines": ...}`` mirroring ``fetch_detail`` after the save, or

src/django_smartbase_admin/mcp/service.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,15 @@ def _detail_form_value(cls, bound_field, request):
647647
if not value:
648648
return []
649649
ids = list(value)
650-
objects = {item.pk: item for item in form_field.queryset.filter(pk__in=ids)}
650+
try:
651+
objects = {
652+
item.pk: item for item in form_field.queryset.filter(pk__in=ids)
653+
}
654+
except (ValueError, TypeError):
655+
# An id that can't match the related pk type (e.g. a non-numeric
656+
# value where the pk is integer) must not 500 the read-back; it
657+
# falls back to str() below as an unresolved id.
658+
objects = {}
651659
# Preserve submitted order; unresolved ids fall back to str()
652660
# rather than getting silently dropped.
653661
return [
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Signals emitted by the MCP tool layer.
2+
3+
``mcp_tool_called`` fires once per dispatched tool. Receiver kwargs:
4+
request, tool_name, tool_args, tool_kwargs, result, error, duration_ms
5+
"""
6+
7+
import django.dispatch
8+
9+
mcp_tool_called = django.dispatch.Signal()

src/django_smartbase_admin/mcp_log/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django.apps import AppConfig
2+
3+
4+
class MCPLogConfig(AppConfig):
5+
"""Optional MCP request logging.
6+
7+
Opt-in via ``INSTALLED_APPS``; ``ready()`` connects the logger to
8+
``mcp_tool_called`` and registers the admin view. The host project
9+
schedules retention (``MCPRequestLog.prune``).
10+
"""
11+
12+
name = "django_smartbase_admin.mcp_log"
13+
label = "sbadmin_mcp_log"
14+
verbose_name = "SBAdmin MCP Log"
15+
default_auto_field = "django.db.models.BigAutoField"
16+
17+
def ready(self):
18+
from django_smartbase_admin.admin.site import sb_admin_site
19+
from django_smartbase_admin.mcp.signals import mcp_tool_called
20+
from django_smartbase_admin.mcp_log.models import MCPRequestLog
21+
from django_smartbase_admin.mcp_log.logger import on_mcp_tool_called
22+
from django_smartbase_admin.mcp_log.sb_admin import MCPRequestLogAdmin
23+
24+
mcp_tool_called.connect(
25+
on_mcp_tool_called, dispatch_uid="sbadmin_mcp_log_logger"
26+
)
27+
28+
if not sb_admin_site.is_registered(MCPRequestLog):
29+
sb_admin_site.register(MCPRequestLog, MCPRequestLogAdmin)

0 commit comments

Comments
 (0)