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(