* Copyright (C) 2022 Alice Adminson * Copyright (C) 2024 MDW * Copyright (C) 2024 Frédéric France * Coryright (C) 2024 Alexandre Spangaro * Copyright (C) 2026 Nick Fragoulis * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY, without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file htdocs/ai/admin/server_mcp.php * \ingroup ai * \brief MCP Server & Assistant Configuration Page */ /** * @var Conf $conf * @var DoliDB $db * @var HookManager $hookmanager * @var Translate $langs * @var User $user * @var Form $form */ require '../../main.inc.php'; require_once DOL_DOCUMENT_ROOT . "/core/lib/admin.lib.php"; require_once DOL_DOCUMENT_ROOT . "/core/class/html.form.class.php"; require_once DOL_DOCUMENT_ROOT . "/core/class/doleditor.class.php"; require_once DOL_DOCUMENT_ROOT . "/ai/lib/ai.lib.php"; require_once DOL_DOCUMENT_ROOT . "/core/lib/functions2.lib.php"; require_once DOL_DOCUMENT_ROOT . "/core/lib/security.lib.php"; $langs->loadLangs(array("admin", "website", "other")); // Access control if (!$user->admin) { accessforbidden(); } if (!isModEnabled('ai')) { accessforbidden('Module AI not activated.'); } // Parameters $action = GETPOST('action', 'aZ09'); /* * ACTIONS */ // Main Settings if ($action == 'update') { $error = 0; if (GETPOSTISSET('AI_ASK_FOR_CONFIRMATION')) { $res = dolibarr_set_const($db, "AI_ASK_FOR_CONFIRMATION", GETPOSTINT("AI_ASK_FOR_CONFIRMATION"), 'int', 0, '', $conf->entity); if ($res <= 0) $error++; } if (GETPOSTISSET('AI_LOG_RETENTION')) { $res = dolibarr_set_const($db, "AI_LOG_RETENTION", GETPOST("AI_LOG_RETENTION"), 'int', 0, '', $conf->entity); if ($res <= 0) { $error++; } } if (GETPOSTISSET('AI_DEFAULT_INPUT_MODE')) { $res = dolibarr_set_const($db, "AI_DEFAULT_INPUT_MODE", GETPOST("AI_DEFAULT_INPUT_MODE"), 'chaine', 0, '', $conf->entity); if ($res <= 0) { $error++; } } if (GETPOSTISSET('AI_INTENT_PROMPT')) { $res = dolibarr_set_const($db, "AI_INTENT_PROMPT", GETPOST("AI_INTENT_PROMPT"), 'chaine', 0, '', $conf->entity); if ($res <= 0) { $error++; } } if ($error) { setEventMessages($langs->trans("ErrorSavingSettings"), null, 'errors'); } else { setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); } header("Location: ".$_SERVER["PHP_SELF"]."?mainmenu=home"); exit; } // Test connection if ($action == 'test_provider') { $service = GETPOST('service_key', 'aZ09'); if ($service) { $credential = getDolGlobalString('AI_API_' . strtoupper($service) . '_KEY'); $url = getDolGlobalString('AI_API_' . strtoupper($service) . '_URL'); // Decrypt if needed if (preg_match('/^crypted:/', $credential)) { $credential = dol_decode(substr($credential, 8)); } elseif (preg_match('/^dolcrypt:/', $credential)) { $credential = dolDecrypt($credential, ''); } // Only proceed if the key is valid (decrypted or not encrypted) if ($credential !== null) { $res = testAIConnection($service, $credential, $url); if ($res['success']) { setEventMessages($langs->trans("ConnectionSuccessful") . $res['message'], null, 'mesgs'); } else { setEventMessages($langs->trans("ConnectionFailed") . $res['message'], null, 'errors'); } } } } // External Access Settings if ($action == 'update_external') { $error = 0; $user_id = GETPOSTINT("AI_MCP_USER_ID"); if (GETPOSTISSET('AI_MCP_USER_ID')) { $res = dolibarr_set_const($db, "AI_MCP_USER_ID", $user_id, 'int', 0, '', $conf->entity); if ($res <= 0) { $error++; } } if ($error) { setEventMessages($langs->trans("ErrorSavingSettings"), null, 'errors'); } else { setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); } header("Location: ".$_SERVER["PHP_SELF"]."?mainmenu=home"); exit; } // Generate New API Key if ($action == 'generate_key') { $newKey = dolGetRandomBytes(64); if (empty($newKey)) { setEventMessages($langs->trans("KeyGenerationFailed"), null, 'errors'); } else { if (dolibarr_set_const($db, 'AI_MCP_API_KEY', $newKey, 'chaine', 0, '', $conf->entity) > 0) { setEventMessages($langs->trans("KeyGenerationSuccessfull"), null, 'mesgs'); } else { setEventMessages($langs->trans("KeySaveFailed"), null, 'errors'); } } } /* * VIEW */ $help_url = ''; $title = "AIMCPConfig"; llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-ai page-admin'); $linkback = ''.$langs->trans("BackToModuleList").''; print load_fiche_titre($title, $linkback, 'title_setup'); $head = aiAdminPrepareHead(); print dol_get_fiche_head($head, 'servermcp', "MCP Server", -1, "ai"); print '' . $langs->trans("ConfigHelp") . '

'; $form = new Form($db); // Load Current Values $apiKey = getDolGlobalString('AI_MCP_API_KEY'); // Default Prompt Logic $defaultPromptText = " ROLE: Dolibarr ERP AI. GOAL: Map user intent to specific JSON commands. CONSTRAINTS: 1. OUTPUT: Single valid JSON object ONLY. No Markdown. No text. Format: {\"tool\": \"tool_name\", \"arguments\": {\"argument_name\": \"argument_value\", ...}} 2. TOOLS: Use ONLY provided tools. If no tool or thirdparty fits, you must first use 'respond_to_user' to inform user. 3. ARGS: strict adherence to schema. Do not invent parameters. 4. MISSING INFO: If a required argument (like an ID) is missing, use 'ask_for_clarification'. 5. SAFETY: For DELETE/UPDATE actions, you MUST use 'ask_for_confirmation'."; $currentPrompt = getDolGlobalString('AI_INTENT_PROMPT', $defaultPromptText); // Settings print '
'; print ''; // Enable/Disable print ''; print ''; print ''; print ''; print '
' . $langs->trans('EnableMCPServer') . ''; print ajax_constantonoff('AI_MCP_ENABLED'); print ' ' . $langs->trans('DisableMCPAI') . ''; print '
'; print '
'; print load_fiche_titre($langs->trans("PrivateModeTitle"), '', 'fas fa-lock'); print '
'; print ''; print ''; print '
'; print ''; // Input Mode print ''; print ''; print ''; print ''; // Enhanced Privacy control print ''; print ''; print ''; print ''; // Confirmation Level print ''; print ''; print ''; print ''; // Logging print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; // System Prompt print ''; print ''; print ''; print '
Default Interface Mode'; $input_modes = [ 'text' => '💬 ' . $langs->trans('OptionTextOnly'), 'native' => '☁️ ' . $langs->trans('OptionCloudFast') . ' - ' . $langs->trans('OptionCloudFasthelp'), 'whisper' => '🔒 ' . $langs->trans('OptionWhisperLocal') . ' - ' . $langs->trans('OptionWhisperLocalhelp') ]; print $form->selectarray('AI_DEFAULT_INPUT_MODE', $input_modes, getDolGlobalString('AI_DEFAULT_INPUT_MODE'), 0, 0, 0); print '
' . $langs->trans("InputMethodHelp") . ''; print '
' . $langs->trans('ObfuscatePIIData') . ''; print ajax_constantonoff('AI_PRIVACY_REDACTION'); print ' ' . $langs->trans("RedactionHelp") . ''; print '
' . $langs->trans("AskConfirmation"); print ' '; $confirmation_options = [ '0' => $langs->trans("ConfirmNever"), '1' => $langs->trans("ConfirmWriteOnly"), '2' => $langs->trans("ConfirmAlways") ]; print $form->selectarray('AI_ASK_FOR_CONFIRMATION', $confirmation_options, getDolGlobalInt('AI_ASK_FOR_CONFIRMATION', 1), 0, 0, 0); print '
' . $langs->trans('EnableLogging') . ''; print ajax_constantonoff('AI_LOG_REQUESTS'); print ' View Logs'; print '
' . $langs->trans("LogRetention") . ' (0 = Forever)
'; print '' . $langs->trans("SystemPrompt") . '
'; print '' . $langs->trans("SystemPromptHelp") . '
'; $doleditor = new DolEditor('AI_INTENT_PROMPT', $currentPrompt, '', 250, 'dolibarr_notes', 'In', false, false, true, ROWS_8, '90%'); $doleditor->Create(); print '
'; print '
'; print '
'; print '
'; // AI Provider Config and Connection testing $services = getListOfAIServices(); $currentService = getDolGlobalString('AI_API_SERVICE'); print load_fiche_titre($langs->trans("AIProviderConfigTitle"), '', 'fa fa-plug'); if ((string) $currentService == '-1') { print '
'.$langs->trans("NoAIProviderSelected").' '.$langs->trans("ConfigureHere").'
'; } else { print '
'; print ''; print ''; $prefix = 'AI_API_'.strtoupper($currentService); $modelVal = getDolGlobalString($prefix.'_MODEL', $services[$currentService]['textgeneration']); print ''; print '
'.$langs->trans("AIProvider").''.$services[$currentService]['label'].'
'.$langs->trans("AI_API_MODEL").''.$modelVal.'
'; print '
'; if ($currentService && $currentService !== '-1') { print ' Test Connection'; } print '
'; } print ''; print ''; // External Access Configuration print load_fiche_titre($langs->trans("AiMcpExternalAccess"), '', 'fas fa-lock-open text-danger'); print '
'; print ''; print ''; print '
'; print ''; // Service User print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print '
Service User '; print '
'; print $form->select_dolusers(getDolGlobalInt('AI_MCP_USER_ID'), 'AI_MCP_USER_ID', 1); print ' '; print '
'; print '' . $langs->trans("DedicatedUserRecommendation") . ''; print '
API Key'; if ($apiKey) { print ''; print ' '; print ' Generate New Key'; } else { print '' . $langs->trans("NoKeyWarning") . ''; print ' ' . $langs->trans("GenerateKey") . ''; } print '
Endpoint URL'; $endpoint = dol_buildpath('/ai/server/mcp_server.php', 2); print ''; print '
'; print '
'; print '
'; // Configuration Examples print '
'; print '
'; print '' . $langs->trans("ClaudeDesktopConfig") . '
'; print '
';
echo htmlspecialchars('{
  "mcpServers": {
    "dolibarr": {
      "command": "node",
      "args": ["/path/to/mcp-bridge.js"],
      "env": {
        "DOLIBARR_URL": "'.$endpoint.'",
        "DOLIBARR_API_KEY": "'.($apiKey ? $apiKey : "YOUR_KEY_HERE").'"
      }
    }
  }
}');
print '
'; print '
'; print dol_get_fiche_end(); llxFooter(); $db->close();