From fd8d0e36464ddba0352da6eedb886ea463438e70 Mon Sep 17 00:00:00 2001 From: Pichinov-Jose Date: Mon, 18 May 2026 00:06:22 +0200 Subject: [PATCH] New: log MCP tools/call invocations to llx_ai_request_log (#38302) The AI Assistant web UI (parse_intent.php) persists every request to llx_ai_request_log via ai_log_request() in ai.lib.php. Administrators can browse the resulting log via the existing Log Viewer (htdocs/ai/admin/log_viewer.php). The MCP HTTP server (mcp_server.php) does NOT call this helper. As a result, external MCP client activity (Claude Desktop, Claude Code CLI, MCP Inspector, custom scripts, etc.) is invisible to administrators -- only dol_syslog() entries land in documents/dolibarr.log, which is much harder to filter and audit. This patch adds an mcp_log_request() helper alongside the existing auth/dispatch logic in mcp_server.php. The helper: - Persists every tools/call invocation to llx_ai_request_log - Filters out lifecycle methods (initialize, ping, notifications/initialized, tools/list, ...) to avoid log spam - Logs with provider='mcp' so administrators can filter MCP traffic in the Log Viewer - Captures the tool name + arguments, success/error status, raw request payload and raw response payload - Charges the elapsed time per-call (also per-call inside batches) The helper is gated by the same AI_LOG_REQUESTS toggle that already controls the web UI logging -- no new admin setting introduced. Tested with: search_products, create_other_document, search_invoice, get_sales_report from Claude Desktop and Claude Code CLI -- each call appears as a new row in the Log Viewer with the right tool name, status, payloads, and elapsed time. --- htdocs/ai/server/mcp_server.php | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/htdocs/ai/server/mcp_server.php b/htdocs/ai/server/mcp_server.php index 10b58ff4399..854170c07f6 100644 --- a/htdocs/ai/server/mcp_server.php +++ b/htdocs/ai/server/mcp_server.php @@ -146,8 +146,74 @@ if ($userId > 0) { exit; } +// Load the AI request log helper so we can persist tools/call invocations to +// llx_ai_request_log (same table the AI Assistant web UI logs to). This gives +// administrators a single place to audit external MCP client activity. +require_once DOL_DOCUMENT_ROOT . '/ai/lib/ai.lib.php'; + +/** + * Persist an MCP tools/call invocation to the llx_ai_request_log table. + * Mirrors what parse_intent.php does for the web AI Assistant. + * Only tools/call is logged -- lifecycle methods (initialize, ping, + * notifications/initialized, tools/list, ...) are skipped to avoid noise. + * + * @param array $req The JSON-RPC request + * @param mixed $resp The JSON-RPC response (null for notifications) + * @param float $tStart microtime(true) captured before processing + * @param string $rawInput Raw request body + * @return void + */ +function mcp_log_request(array $req, $resp, float $tStart, string $rawInput): void +{ + global $db, $serviceUser; + + $method = isset($req['method']) ? (string) $req['method'] : ''; + if ($method !== 'tools/call' || !function_exists('ai_log_request')) { + return; + } + + $params = isset($req['params']) && is_array($req['params']) ? $req['params'] : []; + $toolName = isset($params['name']) ? (string) $params['name'] : ''; + $toolArgs = isset($params['arguments']) ? $params['arguments'] : []; + + $argsJson = is_string($toolArgs) ? $toolArgs : (string) json_encode($toolArgs); + $query = '[MCP] ' . $toolName . ' ' . dol_substr($argsJson, 0, 1000); + + $responseShape = ['tool' => $toolName, 'arguments' => $toolArgs]; + + $status = 'Success'; + $errorMsg = ''; + if (is_array($resp) && isset($resp['error'])) { + $status = 'Error'; + $errorMsg = is_array($resp['error']) && isset($resp['error']['message']) + ? (string) $resp['error']['message'] + : (string) json_encode($resp['error']); + } elseif (is_array($resp) && isset($resp['result']['isError']) && $resp['result']['isError']) { + $status = 'Error'; + $errorMsg = is_array($resp['result']['content'] ?? null) ? (string) json_encode($resp['result']['content']) : ''; + } + + $rawResStr = is_string($resp) ? $resp : (string) json_encode($resp); + + ai_log_request( + $db, + $serviceUser, + $query, + $responseShape, + 'mcp', + microtime(true) - $tStart, + 1.0, + $status, + $errorMsg, + $rawInput, + $rawResStr + ); +} + // Request handling try { + $tStart = microtime(true); + // Basic payload size limit $rawInput = file_get_contents('php://input'); if ($rawInput === false || strlen($rawInput) > 1024 * 1024) { @@ -181,11 +247,15 @@ try { continue; } + $reqStart = microtime(true); $res = $server->handleRequest($req); if ($res !== null) { $responses[] = $res; } + + // Log each tools/call separately so the admin log viewer shows them individually. + mcp_log_request($req, $res, $reqStart, (string) json_encode($req)); } echo json_encode($responses); @@ -200,6 +270,10 @@ try { if ($response !== null) { echo json_encode($response); } + + // Log this tools/call to llx_ai_request_log (no-op unless AI_LOG_REQUESTS is enabled + // and the method is tools/call). + mcp_log_request($request, $response, $tStart, $rawInput); } } catch (Exception $e) { dol_syslog(