28if (!defined(
'NOTOKENRENEWAL')) {
29 define(
'NOTOKENRENEWAL', 1);
31if (!defined(
'NOREQUIREMENU')) {
32 define(
'NOREQUIREMENU', 1);
34if (!defined(
'NOREQUIREHTML')) {
35 define(
'NOREQUIREHTML', 1);
37if (!defined(
'NOREQUIREAJAX')) {
38 define(
'NOREQUIREAJAX', 1);
40if (!defined(
'NOCSRFCHECK')) {
41 define(
'NOCSRFCHECK', 1);
45require
'../../main.inc.php';
51require_once DOL_DOCUMENT_ROOT .
'/ai/class/mcp_protocol.class.php';
52require_once DOL_DOCUMENT_ROOT .
'/ai/class/mcpauth.class.php';
54while (ob_get_level()) {
60 http_response_code(503);
63 "error" => [
"code" => -32000,
"message" =>
"MCP Server Disabled"]
74header(
'Content-Type: application/json');
75header(
'X-Content-Type-Options: nosniff');
78$headers = function_exists(
'getallheaders') ? getallheaders() : [];
79$headers = array_change_key_case($headers, CASE_LOWER);
83if ($mcpAuth->authenticate() < 0) {
84 if ($mcpAuth->httpcode == 401) {
86 header(
'WWW-Authenticate: ' . $mcpAuth->getWwwAuthenticateHeader());
89 http_response_code($mcpAuth->httpcode);
92 "error" => [
"code" => -32000,
"message" => $mcpAuth->error]
101$serviceUser = $mcpAuth->user;
103if ($serviceUser ===
null) {
107 http_response_code(503);
110 "error" => [
"code" => -32000,
"message" =>
"MCP Server Misconfigured: authenticate with a user API key, or set AI_MCP_USER_ID for the shared server key"]
115 $serviceUser =
new User($db);
116 if ($serviceUser->fetch($userId) <= 0) {
117 http_response_code(500);
120 "error" => [
"code" => -32000,
"message" =>
"MCP Service User not found"]
124 $serviceUser->loadRights();
138require_once DOL_DOCUMENT_ROOT .
'/ai/lib/ai.lib.php';
154 global $db, $serviceUser;
156 $method = isset($req[
'method']) ? (string) $req[
'method'] :
'';
157 if ($method !==
'tools/call' || !function_exists(
'ai_log_request')) {
161 $params = isset($req[
'params']) && is_array($req[
'params']) ? $req[
'params'] : [];
162 $toolName = isset($params[
'name']) ? (string) $params[
'name'] :
'';
163 $toolArgs = isset($params[
'arguments']) ? $params[
'arguments'] : [];
165 $argsJson = is_string($toolArgs) ? $toolArgs : (string) json_encode($toolArgs);
166 $query =
'[MCP] ' . $toolName .
' ' .
dol_substr($argsJson, 0, 1000);
168 $responseShape = [
'tool' => $toolName,
'arguments' => $toolArgs];
172 if (is_array($resp) && isset($resp[
'error'])) {
174 $errorMsg = is_array($resp[
'error']) && isset($resp[
'error'][
'message'])
175 ? (string) $resp[
'error'][
'message']
176 : (string) json_encode($resp[
'error']);
177 } elseif (is_array($resp) && isset($resp[
'result'][
'isError']) && $resp[
'result'][
'isError']) {
179 $errorMsg = is_array($resp[
'result'][
'content'] ??
null) ? (string) json_encode($resp[
'result'][
'content']) :
'';
182 $rawResStr = is_string($resp) ? $resp : (string) json_encode($resp);
190 microtime(
true) - $tStart,
201 $tStart = microtime(
true);
204 $rawInput = file_get_contents(
'php://input');
205 if ($rawInput ===
false || strlen($rawInput) > 1024 * 1024) {
206 throw new Exception(
"Invalid or too large request");
209 $request = json_decode($rawInput,
true);
211 if (json_last_error() !== JSON_ERROR_NONE) {
218 if (is_array($request) && array_keys($request) === range(0, count($request) - 1)) {
220 if (count($request) > 20) {
221 http_response_code(413);
224 "error" => [
"code" => -32000,
"message" =>
"Batch too large"]
237 foreach ($request as $precheck) {
238 if (!is_array($precheck)) {
241 $headerError = $server->validateTransportHeaders($headers, $precheck);
242 if ($headerError !==
null || $server->getHttpStatus() !== 200) {
243 http_response_code($server->getHttpStatus());
244 if ($headerError ===
null) {
248 $headerError = [
"jsonrpc" =>
"2.0",
"id" =>
null,
"error" => [
"code" => -32020,
"message" =>
"Transport header does not match a batch item"]];
250 echo json_encode($headerError);
256 foreach ($request as $req) {
257 if (!is_array($req)) {
261 $reqStart = microtime(
true);
262 $res = $server->handleRequest($req);
272 echo json_encode($responses);
275 if (!is_array($request)) {
276 throw new Exception(
"Invalid request format");
279 $response = $server->validateTransportHeaders($headers, $request);
280 if ($response ===
null && $server->getHttpStatus() === 200) {
281 $response = $server->handleRequest($request);
284 if ($server->getHttpStatus() !== 200) {
285 http_response_code($server->getHttpStatus());
286 if ($response ===
null) {
290 $response = [
"jsonrpc" =>
"2.0",
"id" =>
null,
"error" => [
"code" => -32020,
"message" =>
"Transport header does not match the request"]];
294 if ($response !==
null) {
295 echo json_encode($response);
304 '[MCP Server] Fatal error: ' . $e->getMessage(),
313 "message" =>
"Parse error"
ai_log_request($db, $user, $query, array $response, $provider, float $time, float $confidence, $status, $error='', $rawReq='', $rawRes='', array $context=array(), &$logId=null)
Log AI Request with Raw Payloads.
Class to manage Dolibarr users.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_substr($string, $start, $length=null, $stringencoding='', $trunconbytes=0)
Make a substring.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
mcp_log_request(array $req, $resp, float $tStart, string $rawInput)
Persist an MCP tools/call invocation to the llx_ai_request_log table.