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.
This commit is contained in:
Pichinov-Jose 2026-05-18 00:06:22 +02:00 committed by GitHub
parent 325000c398
commit fd8d0e3646
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<string,mixed> $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(