The MCP server (htdocs/ai/server/mcp_server.php) loads a service user
from AI_MCP_USER_ID but does not expose it as the PHP global $user.
Several tool classes call 'global $user' to identify the caller --
they therefore see no authenticated user in HTTP MCP context (no PHP
web session exists) and return 'User not authenticated' even though
authentication via X-API-Key/Bearer has already succeeded.
This breaks roughly 40% of the MCP tools shipped by the module when
called from external clients (Claude Desktop Custom Connectors,
Claude Code CLI, MCP Inspector, custom scripts).
Two complementary fixes applied:
1. mcp_server.php: after successfully loading the service user,
assign it to $GLOBALS['user'] and the local $user variable. This
acts as a safety net for any tool (current or third-party) that
relies on the legacy 'global $user' pattern.
2. Three tool classes harmonize their constructor signatures with
the McpHandler contract -- new $className($db, $user, $conf):
- ToolCrudObjects (htdocs/ai/tools/crud_objects.class.php)
- ToolInvoices (htdocs/ai/tools/invoices.class.php)
- ToolReports (htdocs/ai/tools/reports.class.php)
These now accept $user via constructor (matching the pattern
already used in ToolThirdParty, ToolCategories, ToolProducts)
and store it as $this->user. Methods that previously called
'global $user' (execute, validateInvoice, payInvoice) now use
the injected $this->user, with a fallback to the global for
web-session contexts.
Tested with:
- create_other_document (proposal, supplier_invoice): works
- search_invoice, validate_invoice, pay_invoice: works
- get_sales_report, get_purchase_report: works
- AI Assistant web UI: unchanged, no regression
The get_sales_report tool declares a group_by parameter with enum
values [thirdparty, product, month] in its inputSchema, but the
implementation ignored $args['group_by'] entirely and always
returned a flat list of invoices.
Meanwhile getPurchaseReport() in the same class already implemented
the correct grouped pattern (s.nom or DATE_FORMAT date) -- this
patch ports that pattern to getSalesReport(), with three modes:
- With a socid: detailed invoice list for that customer (unchanged
behavior, kept for backward compatibility).
- Without socid, group_by=thirdparty (default): aggregate by
customer with COUNT and SUM(total_ttc) -> Top-customers report.
- Without socid, group_by=month: aggregate by DATE_FORMAT(datef,
'%Y-%m') -> monthly revenue report.
- Without socid, group_by=product: aggregate by product reference
via INNER JOIN on facturedet, with COALESCE(p.ref, fd.description)
so free-text lines without a product link still appear under
their description. SUM(fd.total_ttc) gives per-product revenue,
COUNT(DISTINCT f.rowid) gives the number of invoices that
contained the product.
Tested:
- group_by=thirdparty: top customers correctly ranked by total.
- group_by=month: 2026-04 and 2026-05 buckets correctly populated.
- group_by=product: top product (SAM-BIK3-CAS-1430-24-N) correctly
ranked at 1 276 EUR over 5 invoices.
- With socid: unchanged detailed listing.
Note: the 'product' grouping is also missing from getPurchaseReport()
in the current code. That can be addressed in a follow-up patch on
the same pattern.