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:
parent
325000c398
commit
fd8d0e3646
1 changed files with 74 additions and 0 deletions
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue