@@ -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
0 commit comments