PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
← All changes | app/Modules/MCP/AbilitiesRegistrar.php +84 -36 1.5.3 → 1.7.0 View file →
@@ -2,8 +2,9 @@
2 2
3 3 namespace FluentCart\App\Modules\MCP;
4 4
5 5 use FluentCart\App\Modules\MCP\Tools\ContextTools;
6 +use FluentCart\App\Modules\MCP\Tools\SearchTools;
6 7 use FluentCart\App\Modules\MCP\Tools\OrderTools;
7 8 use FluentCart\App\Modules\MCP\Tools\CustomerTools;
8 9 use FluentCart\App\Modules\MCP\Tools\ProductTools;
9 10 use FluentCart\App\Modules\MCP\Tools\SubscriptionTools;
@@ -17,11 +18,12 @@
17 18 /**
18 19 * Single source of truth for every FluentCart MCP ability.
19 20 *
20 21 * Each tool class owns its own `definitions()` slice (schema next to code);
21 - * this class merges them, wraps every execute_callback so unhandled exceptions
22 - * become structured WP_Errors the agent can read (instead of the adapter's
23 - * generic "Tool execution failed"), and registers each as a WP ability.
22 + * this class merges them, wraps every execute_callback — rejecting undeclared
23 + * input params up front and converting unhandled exceptions into structured
24 + * WP_Errors the agent can read (instead of the adapter's generic "Tool
25 + * execution failed") — and registers each as a WP ability.
24 26 *
25 27 * Pro tools are NOT listed here — FluentCart Pro pushes its abilities via the
26 28 * `fluent_cart/mcp_loaded` action + `fluent_cart/mcp_ability_names` filter.
27 29 */
@@ -31,8 +33,9 @@
31 33 private static function toolClasses()
32 34 {
33 35 return [
34 36 ContextTools::class,
37 + SearchTools::class,
35 38 OrderTools::class,
36 39 CustomerTools::class,
37 40 ProductTools::class,
38 41 SubscriptionTools::class,
@@ -180,43 +183,80 @@
180 183 return $out;
181 184 }
182 185
183 186 /**
184 - * Portable params an agent naturally carries from one tool to a sibling but
185 - * which only some tools accept. input_schema sets no additionalProperties, so
186 - * an unsupported one is silently ignored and the agent gets a full,
187 - * wrong-shaped result with no signal it wasn't filtered. We surface exactly
188 - * these as meta.warnings. We deliberately do NOT warn on every unknown key:
189 - * that risks false positives against a param a tool reads but doesn't declare,
190 - * and would turn a typo into noise instead of a helpful correction.
187 + * Reject any input param this tool does not declare, instead of executing
188 + * with it silently dropped. input_schema sets no additionalProperties, so an
189 + * unknown key would otherwise pass validation and the agent would get a
190 + * full, plausible-looking result that is NOT filtered the way it asked —
191 + * the worst failure mode for an agent (a wrong number reads as a right one;
192 + * an error is recoverable). Sibling tools also name overlapping concepts
193 + * differently (list-orders: created_after/type; query-orders: start_date/
194 + * order_type dimension), so carried-over names are a common, realistic slip,
195 + * not a rare typo. The error lists the accepted params so the agent can
196 + * self-correct in one step — richer than the schema validator's message,
197 + * which is why this is enforced here rather than via additionalProperties.
198 + *
199 + * @param string $toolName
200 + * @param mixed $params the raw input params
201 + * @param array $declaredParams input_schema property names this tool declares
202 + * @return \WP_Error|null null when all params are declared
191 203 */
192 - const PORTABLE_PARAMS = ['product_id', 'variation_id', 'summary_only', 'fields', 'mode'];
204 + private static function rejectUnknownParams($toolName, $params, $declaredParams)
205 + {
206 + if (!is_array($params)) {
207 + return null;
208 + }
193 209
210 + $unknown = [];
211 + foreach (array_keys($params) as $key) {
212 + if (!in_array((string) $key, $declaredParams, true)) {
213 + $unknown[] = (string) $key;
214 + }
215 + }
216 +
217 + if (empty($unknown)) {
218 + return null;
219 + }
220 +
221 + return \FluentCart\App\Modules\MCP\Support\MCPHelper::error(
222 + 'unknown_param',
223 + sprintf(
224 + /* translators: 1: rejected parameter names, 2: tool name, 3: accepted parameter names */
225 + __('Unknown parameter(s) [%1$s] — %2$s does not support them, and running without them would return a result that is NOT filtered the way you asked. Accepted parameters: [%3$s]. Rename or remove the unknown parameter(s) and retry.', 'fluent-cart'),
226 + implode(', ', $unknown),
227 + $toolName,
228 + $declaredParams ? implode(', ', $declaredParams) : __('none — this tool takes no parameters', 'fluent-cart')
229 + ),
230 + ['fields' => $unknown, 'accepted' => $declaredParams, 'tool' => $toolName]
231 + );
232 + }
233 +
194 234 /**
195 - * Append a meta.warnings entry for each portable param the caller passed that
196 - * this tool does not declare (and therefore ignored). Untouched when the
197 - * result isn't a success envelope (e.g. a WP_Error) or nothing was ignored,
198 - * so a tool's own warnings (list-reference-data) are preserved.
235 + * Append a meta.warnings entry when a requested per_page exceeded the tool's
236 + * ceiling and was clamped (the descriptions state each cap, but a stated cap
237 + * still deserves a runtime signal — the agent asked for 500 rows and must
238 + * know it got a 100-row page, not the full set). Detected generically by
239 + * comparing the request against meta.page.per_page, so every list and
240 + * query tool is covered without per-tool changes. Untouched when the result
241 + * isn't a paged success envelope.
199 242 *
200 - * @param mixed $result the tool's return value
201 - * @param mixed $params the raw input params
202 - * @param array $declaredParams input_schema property names this tool declares
243 + * @param mixed $result the tool's return value
244 + * @param mixed $params the raw input params
203 245 * @return mixed
204 246 */
205 - private static function annotateIgnoredParams($result, $params, $declaredParams)
247 + private static function annotateClampedPerPage($result, $params)
206 248 {
207 - if (!is_array($result) || !isset($result['meta']) || !is_array($result['meta']) || !is_array($params)) {
249 + if (
250 + !is_array($params) || !isset($params['per_page'])
251 + || !is_array($result) || !isset($result['meta']['page']['per_page'])
252 + ) {
208 253 return $result;
209 254 }
210 255
211 - $ignored = [];
212 - foreach (self::PORTABLE_PARAMS as $p) {
213 - if (array_key_exists($p, $params) && !in_array($p, $declaredParams, true)) {
214 - $ignored[] = $p;
215 - }
216 - }
217 -
218 - if (empty($ignored)) {
256 + $requested = (int) $params['per_page'];
257 + $actual = (int) $result['meta']['page']['per_page'];
258 + if ($actual < 1 || $requested <= $actual) {
219 259 return $result;
220 260 }
221 261
222 262 $warnings = (isset($result['meta']['warnings']) && is_array($result['meta']['warnings']))
@@ -222,15 +262,14 @@
222 262 $warnings = (isset($result['meta']['warnings']) && is_array($result['meta']['warnings']))
223 263 ? $result['meta']['warnings']
224 264 : [];
225 265
226 - foreach ($ignored as $p) {
227 - $warnings[] = sprintf(
228 - /* translators: %1$s: the parameter name that was ignored */
229 - __('The "%1$s" parameter is not supported by this tool and was ignored — the result is not filtered by it. Check the tool schema for the parameters this tool accepts.', 'fluent-cart'),
230 - $p
231 - );
232 - }
266 + $warnings[] = sprintf(
267 + /* translators: 1: requested per_page, 2: the maximum this tool returned */
268 + __('per_page %1$d exceeds this tool\'s maximum; %2$d rows per page were returned. Use meta.page (total/pages/has_more) to page through the rest.', 'fluent-cart'),
269 + $requested,
270 + $actual
271 + );
233 272
234 273 $result['meta']['warnings'] = $warnings;
235 274
236 275 return $result;
@@ -245,10 +284,19 @@
245 284 private static function wrapExecuteCallback($toolName, $callback, $declaredParams = [])
246 285 {
247 286 return function ($params) use ($toolName, $callback, $declaredParams) {
248 287 try {
288 + // Before executing: an undeclared param means the caller asked for
289 + // a filter this tool can't apply — error out rather than return a
290 + // confidently wrong (unfiltered) result. Runs before the callback
291 + // so write tools never partially execute on a malformed call.
292 + $unknownError = self::rejectUnknownParams($toolName, $params, $declaredParams);
293 + if ($unknownError !== null) {
294 + return $unknownError;
295 + }
296 +
249 297 $result = call_user_func($callback, $params);
250 - return self::annotateIgnoredParams($result, $params, $declaredParams);
298 + return self::annotateClampedPerPage($result, $params);
251 299 } catch (\Throwable $e) {
252 300 /**
253 301 * Fires when an MCP tool throws. Lets sites log/alert before the
254 302 * structured error reaches the agent.