本文へ移動
cccskills
無料GitHub で公開

mcp-ai-wpoos-toolkit-audit

Per-toolkit hardening audit loop for NV oOS Pro tools. Systematically scans each addons/pro/includes/tools/<toolkit> (and its plugins/nvoos-content-graph-pro mirror) for the four recurring production failure classes — unguarded shell calls that fatal on exec-disabled hosts, calls to nonexistent client methods, string-assuming parsing of provider responses that may be arrays, and provider enum/service mismatches — then fixes, tests (Docker PHPUnit), and validates (phpcs both standards) before moving to the next toolkit. Use when asked to audit a toolkit, review the X tools for similar issues, do the same for the other toolkits, or continue the toolkit audit loop. The first case study (document-generation) found 3 fatal bugs + 1 broken regex.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md50.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

NV oOS Toolkit Hardening Audit Loop

Operational playbook for auditing each of the 53 Pro toolkits (addons/pro/includes/tools/<toolkit>/) for the failure classes below, fixing them, and validating. The loop is toolkit-at-a-time so each audit lands as its own reviewable cluster. First completed cluster: document-generation (2026-10-04 session — see Case study).

When to use this skill

  • "Audit the <toolkit> tools" / "review the X tools for similar issues"
  • "Continue the toolkit audit loop" / "do the next toolkit"
  • A production site reports a fatal in a toolkit tool and you need to sweep the whole toolkit (and its CG Pro mirror) for the same class of bug
  • Before opening a fix cluster PR touching a whole toolkit folder

The four failure classes (scan for all four, every toolkit)

  1. Unguarded shell calls — fatal on exec-disabled hosts. On PHP 8+, calling a function listed in disable_functions throws a fatal Error that @ cannot suppress. Scan for shell_exec, exec(, proc_open, passthru, system(, popen. Every hit must be either:

    • removed in favour of WP_MCP_AI\Services\WP_MCP_AI_Process_Service (is_command_available() / run_silent()), or
    • gated by function_exists( '<fn>' ) with a graceful fallback, or
    • behind WP_MCP_AI_ALLOW_SHELL_TOOLS and wp_mcp_ai_find_binary() (which itself guards function_exists('proc_open')) — the html_to_pdf/merge_pdfs pattern. Canonical helpers: wp_mcp_ai_check_nodejs_available() / wp_mcp_ai_get_nodejs_version() (addons/pro/includes/npm-integration-filters.php), and the private is_cli_tool_available() / run_cli_command() pair (exec-first + Process Service fallback, Windows-aware where/which) — see class-wp-mcp-ai-ocr-service.php and class-wp-mcp-ai-tool-extract-pdf-text.php. Never recommend enabling shell_exec on the server — the host hardening is correct; fix the code.
  2. Calls to nonexistent client methods — fatal Call to undefined method. The canonical example: WP_MCP_AI_Gemini_Client has no generate_content() method — its public API is create_chat_completion() (plus generate_image, edit_image, transcribe_audio, stream_chat_completion, count_tokens, …). WP_MCP_AI_Logger::log_activity() does not exist either (use log_event()). Scan every new WP_MCP_AI_*Client() call site and verify each invoked method against the client class.

  3. String-assuming parsing of provider responses — fatal preg_match(): Argument #2 ($subject) must be of type string, array given. WP_MCP_AI_Gemini_Client::normalize_response() returns choices[0].message.content as an array of parts; OpenAI-compatible gateways (DeepSeek, vLLM) can do the same. Any trim( $response['choices'][0]['message']['content'] ), preg_match(...), json_decode(...) or (string) cast on that field is a bug: either a fatal or silent "Array" garbage. Flatten first with a helper (see Fix patterns). Also verify provider-side regexes actually compile — the document-generation tools carried /```( ? ( :json)?…/s, a pattern with an unbalanced parenthesis that never compiled and warned on every call.

  4. Provider enum / service mismatches. A tool schema advertising a provider the backing service's switch does not handle → runtime Unknown OCR provider: X on the first call and a misleading "temporarily unavailable" cascade from the circuit breaker on subsequent calls. For every 'enum' list in a toolkit's schemas, diff it against the service/client switch or registry it feeds.

Scan procedure

Use the grep tool with the full project-rooted glob — a bare addons/pro/includes/tools/<toolkit>/*.php include_pattern silently matches zero files (false-negative scans!). Working forms:

mcp-ai-wpoos/addons/pro/includes/tools/<toolkit>/**/*.php
mcp-ai-wpoos/plugins/nvoos-content-graph-pro/src/tools/<toolkit>/**/*.php

Run these seven scans per toolkit (each covers classes 1–4):

shell_exec|exec\(|proc_open|passthru|system\(|popen
->generate_content|->generate\(|->log_activity|log_activity\(
preg_match.*content|trim\( \$response|choices\[0\]|choices'\]\[0\]|candidates\[0\]
(\(string\) \$data|\(string\) \$response|->create_chat_completion|new WP_MCP_AI_[A-Za-z_]+Client
enum|provider
wp_remote_post|wp_remote_get|message'\]\['content
wp_mcp_ai_[a-z_]+\(   (then verify each global helper has a function definition somewhere)

Note: the class-3 grep must include BOTH choices[0] and choices'][0] spellings — the crm toolkit wrote $result['choices'][0]['message']['content'] and a choices\[0\]-only scan produced a false negative.

Then read every hit. Do not trust "No matches found" until the glob is the rooted **/*.php form. Tools in a toolkit delegate to services under addons/pro/includes/services/ and shared clients under includes/ — the failure usually lives there (e.g., the OCR service and the self-hosted OCR client), so audit those when a toolkit routes through them.

Fix patterns

  • Flatten helpers. Add a small protected method next to the parsing site (used in class-wp-mcp-ai-ocr-service.php as extract_text_from_response() and in the pro_pdf/pro_word/pro_excel tools as flatten_response_content() / flatten_content()):

    if ( is_string( $content ) ) { return $content; }
    if ( is_array( $content ) ) {
        $text = '';
        foreach ( $content as $part ) {
            if ( is_array( $part ) && isset( $part['text'] ) ) { $text .= $part['text']; }
            elseif ( is_string( $part ) ) { $text .= $part; }
        }
        return $text;
    }
    return '';
    

    Call it at the response boundary and inside the JSON/regex parser defensively (if ( ! is_string( $content ) ) { … }), never (string) cast an array.

  • Guarded CLI helpers. Copy the is_cli_tool_available() / run_cli_command() pair into tools that need CLI binaries; probe which/where only under function_exists( 'exec' ) and fall back to the Process Service, so locked-down hosts degrade to the pure-PHP path.

  • Credential gates. WP_MCP_AI_Credential_Resolver::has_credentials( $provider ) before instantiating a provider client; return a no_api_key WP_Error.

  • Circuit-breaker awareness. In WP_MCP_AI_OCR_Service, validation errors (no_api_key, no_endpoint, file_not_found) do NOT open the circuit; other failures do, and subsequent calls then report provider_unavailable ("temporarily unavailable"). When a provider reports that message, the root cause is an earlier non-validation failure — check the logs for the first error in the page loop.

  • Enum honesty. Align every advertised enum with the service switch — either implement the missing provider or remove it from the schema.

Test conventions (per fixed toolkit)

Add regression tests that would have caught each bug:

  • Mock pre_http_request with BOTH response shapes: string message.content AND array-of-parts content. Assert the flattened text comes out and never "Array".
  • Reflection for protected call_provider / extract_with_* / flatten_* / try_parse_json_response methods.
  • Reset static state in setUp() (circuit breakers via ReflectionProperty::setValue( null, array() ), credential caches via WP_MCP_AI_Credential_Resolver::clear_cache(), WP_MCP_AI_Admin_Settings::reset_settings_cache()).
  • End-to-end where the environment allows: Imagick is loaded in the oos-wp container — build a 2-page PDF fixture and drive the whole convert → per-page OCR → concatenate flow through the mock.
  • Isolate provider keys: update_option( 'wp_mcp_ai_settings', … ) with only the key under test; delete_option( 'wp_mcp_ai_credentials' ).
  • Style gates: expand multi-item arrays (inline array( 'type' => 'text', 'text' => … ) pairs are PHPCS errors), capitalize docblock descriptions (even for function-name subjects), wp_delete_file() instead of @unlink, and phpcs:ignore comments for fixture base64_encode/file_put_contents. phpcbf can fix the mechanical ones.

Validation gates

php -l <each changed file>
# Docker PHPUnit (never two concurrent runs; same shared DB):
MSYS_NO_PATHCONV=1 docker exec -e WP_CORE_DIR=/var/www/html -e WP_DB_HOST=db -e WP_DB_NAME=wordpress_test -e WP_DB_USER=wordpress -e WP_DB_PASSWORD=wordpress oos-wp sh -c 'cd /var/www/html/wp-content/plugins/mcp-ai-wpoos && php -d memory_limit=1G vendor/bin/phpunit <paths> --no-coverage 2>&1'
# phpcs — the gate counts ERRORS, not warnings; two standards:
MSYS_NO_PATHCONV=1 docker exec oos-wp sh -c 'cd /var/www/html/wp-content/plugins/mcp-ai-wpoos && php vendor/bin/phpcs --standard=phpcs.xml.dist --error-severity=1 --report=summary <root paths>'
MSYS_NO_PATHCONV=1 docker exec oos-wp sh -c 'cd /var/www/html/wp-content/plugins/mcp-ai-wpoos && php vendor/bin/phpcs --standard=plugins/nvoos-content-graph-pro/phpcs.xml.dist --error-severity=1 --report=summary <cg-pro paths>'
  • Mirror discipline: every production fix lands byte-identically in plugins/nvoos-content-graph-pro/src/… (its own text domain) — see the mcp-ai-wpoos-ecosystem-port skill. Verify both trees lint + phpcs clean.
  • UTF-8 integrity: after editing, check mb_check_encoding() on every changed file (edit-tool CP1252 mangling passes php -l silently).
  • WP 7.1 matrix: /tmp/wp71 inside oos-wp is a container-local artifact lost on Docker restarts; when absent, note that CI covers it.
  • Watch for bonus finds: good tests expose adjacent production bugs (the broken fence regex was found by a new test's warning) — fix and regression-test them in the same cluster.
  • Expect OK, but there were issues! with only pre-existing warnings (feeds property notice) + deprecations = pass.

Cluster → PR workflow

Follow the mcp-ai-wpoos-test-suite skill's conventions: branch per toolkit from origin/alpha-working, stage explicit paths only (never vendor/), imperative subject ≤ 50 chars, PR base alpha-working, user merges manually, then move to the next toolkit.

Toolkit board

53 Pro toolkits under addons/pro/includes/tools/ (audited ✅ / not yet):

  • document-generation — 2026-10-04 (case study below)

  • ai-tool-builder

  • analytics

  • architect-agent (known shell allowlist, R-S-02 migration in flight — treat separately)

  • architectural-design

  • automotive

  • calendar-booking

  • capture

  • chat-channels

  • cloudways

  • comic-creation

  • composio

  • cre-debt

  • crm — 2026-10-04 (case study below)

  • developer

  • dietpi

  • dj-management

  • eca-management — 2026-10-04 (case study below)

  • ecommerce — 2026-10-04 (case study below)

  • email-marketing — 2026-10-04 (verified clean — case study below)

  • erp-ezuite

  • extended-cognition

  • fashion

  • financial-planning

  • flowhub

  • google-workspace

  • healthcare — 2026-10-04 (case study below)

  • image-production — 2026-10-04 (case study below)

  • infrastructure

  • jetengine

  • jev

  • law-firm

  • math

  • media — 2026-10-04 (case study below; CG Pro mirror not yet ported)

  • multilingual

  • okf

  • orchestration — 2026-10-04 (case study below; CG Pro mirror has no tool files yet — base-tree only)

  • outbound-booking

  • paper-store

  • places

  • project-management

  • quiz-management

  • regulatory-registration

  • remote-connections

  • research

  • shopify-sync — 2026-10-04 (case study below)

  • site-creator-toolkit

  • social-media

  • vault

  • vector-storage

  • video-production

  • vision-analysis

  • wp-all-import-export

  • Loose incident/maintenance tools (addons/pro/includes/tools/*.php) + shared addons/pro/includes/services/ sweep — 2026-10-04 (case study below)

Case study — loose incident tools + shared services sweep

What the ninth audit found (one latent compile-fatal family, two fatals, and nine data-quality/latent-fatal guards; classes 1 and 4 verified clean across all 34 service files):

  1. Loose incident tools were never loadable (compile fatal). All five files declared execute( array $arguments, … ) while WP_MCP_AI_Tool_Interface declares execute( array $arguments = array(), … ) — a missing-parameter-default incompatibility that fatals at class declaration; get_service_status also lacked get_capability_flags() and all five lacked the modern get_name() / get_description(). No loader requires these files (the Incident REST controller has its own callbacks), so the fatals were latent. Aligned every signature with the interface, added the missing methods (the four state-changing tools declare get_required_capability() → manage_options; the status tool keeps the trait default), and guarded the required-arg reads with honest errors (wp_mcp_ai_missing_title when title is absent).
  2. result-delivery-service::send_paper_store() — called WP_MCP_AI_Paper_Git_Sync::get_instance()->maybe_commit(), a method that never existed (the real API is sync()): every git_commit delivery fataled inside the try/catch and was mis-reported as a paper_store_error even though the record saved. Now calls sync() and logs sync failures without failing the delivery.
  3. hf-vision-inference-service::post_inference() — returned valid-JSON scalar bodies (null, strings) raw into the array-typed normalize_detection_result() / normalize_classification_result() → TypeError. Added an is_array guard with an honest WP_Error.
  4. crm-gmail-client — array-shaped Gmail header values reached strtolower()/preg_match() (TypeError) and a non-string access_token was (string)-cast; both now is_string-guarded. Mirrored to CG Pro.
  5. vector-store-adapter — qdrant_query() parsed non-2xx bodies as silent empty-success and iterated a non-array result; status gate + is_array guard added. qdrant_ensure_collection() ignored the collection-create response entirely; it now returns WP_Error|null (409 tolerated as already-exists) and qdrant_upsert() propagates. Mirrored to CG Pro.
  6. roboflow-inference-service — (string) cast of a possibly-array detection class produced literal "Array" labels; is_string guard with COCO fallback. Mirrored to CG Pro.
  7. pro-workflow-bridge::guard_agent_prompt() — the Phase-1 injection guardrail referenced WP_MCP_AI_Prompt_Injection_Detector, a class that exists nowhere in the repo; the class_exists guard made the filter permanently inert. Rewired to WP_MCP_AI_Guardrails::analyze_message() (blocks jailbreak/injection, mirroring the harness screen_message() policy; diversion signals never block).
  8. Data-quality guards: nv-cloud-billing-observer (array model → "Array" in the ledger; is_scalar guard), content-template-engine (array topic/primary_keyword into sprintf; scalar guards), finnhub-provider (strtolower( $data['s'] ) / count( $data['t'] ) without guards → TypeErrors on malformed payloads), markdown-converter ((string) cast of arrays), nodemailer-service (array template values flipped str_replace into search/replace pairing; non-scalars skipped).

Verified clean this cluster: zero unguarded shell across all 34 service files (the OCR service was hardened in the document-generation cluster; fluent-ffmpeg's cURL paths gate on the trait's curl_file_create availability check; jukebox/prettier route through the Process Service); every client method call exists (Ollama create_chat_completion/get_endpoint_url, Typesafe decide, OpenRouter create_decision, Language_Model_Router constructor + completion, WP_MCP_AI_Guardrails::analyze_message, the Paper Store repository/driver API); enums honest (incident phases vs VALID_TRANSITIONS, severity enum, roboflow Apache/PML aliases, jev transports); HTTP hardening present everywhere else (finnhub seam + key rotation + status checks, market-data status validation, skill-catalogue SSRF pinning, webhook is_wp_error + status). The result-delivery envelope boundary already (string)-casts at format_for_channel() and the schedule-manager producer stringifies full_response — no flatten needed there.

Tests landed: addons/pro/tests/test-loose-incident-tools-hardening.php (new, 12 tests / 36 assertions — full incident lifecycle through the CPT, transition-table rejection, timeline append, resolution timestamp, maintenance date/range validation, status registry wiring with a stub source) and addons/pro/tests/test-services-sweep-hardening.php (new, 21 tests / 54 assertions — gmail token/header shape matrix, Qdrant status/non-array-result/ensure-collection matrix via mocked pre_http_request, roboflow array-label fallback, HF-vision scalar-body guard, content-template coercion, finnhub malformed shapes via the wp_mcp_ai_market_data_http_response seam, markdown/nodemailer guards, ledger model guard, workflow-bridge jailbreak/safe matrix, Paper Store git_commit delivery regression). Validation: 33/33 green standalone and 229/229 green across the 21-suite related cluster (vector-store, gmail, result-delivery, service-status, guardrail, paper-store families) on the isolated DB wordpress_test_muted_moth; phpcs 0 errors on both standards; UTF-8 clean; WP 7.1 matrix not runnable locally (container artifact absent) — CI covers it.

Case study — orchestration

What the ninth audit found (class 3 confirmed from the CRM-audit follow-up queue, plus envelope canon, argument-shape, and enum-honesty issues; classes 1, 2, and provider enums verified clean):

  1. generate_research_report::perform_ai_research() — passed $result['choices'][0]['message']['content'] through raw, and parse_and_format_research() fed it straight into preg_match() and json_decode() — preg_match(): Argument #2 ($subject) must be of type string, array given on the first Gemini-served report (Gemini and OpenAI-compatible gateways return message.content as an array of parts). Added a flatten_response_content() helper at the response boundary plus a defensive flatten in the parser; empty content now returns the honest wp_mcp_ai_invalid_response WP_Error instead of a JSON parse error.
  2. Non-canonical array( 'success' => false, ... ) envelopes across 8 tools (15 sites: aggregate-research-data, convert-html-to-markdown, extract-structured-data, generate-password, get-session-status, get-task-plan, list-templates, update-task-plan) → all converted to WP_Error with prefixed codes. Consumer ripple (same as the media cluster): detect_completion_indicators::check_plan_completion() and get_session_status::get_task_plan() (pro AND base-tree mirrors in includes/tools/orchestration/) read $result['success'] off get-task-plan's result → Cannot use object of type WP_Error as array the moment the nested call failed. Guarded all four call sites with is_wp_error(). The base WP_MCP_AI_Tool_Get_Task_Plan already used canonical WP_Errors — the pro copy was the outlier.
  3. generate_password::execute() — the Vault service's generate_password() returns string|WP_Error, but the tool only checked ! $password (a WP_Error is truthy) → the error path fell through to calculate_password_strength( WP_Error ). Now returns the WP_Error directly (bonus find, same cluster).
  4. Argument-shape fatals (class 3, tool-argument variant): aggregate-research-data fed raw $source['content'] into md5()/ similar_text()/substr()/stripos()/preg_split() (now normalized to a string at both the dedupe and aggregate loops); verify-information fed $source['content'] into stripos() without an isset/is_string guard; convert-html-to-markdown and extract-structured-data fed raw $arguments['html']/$arguments['content'] into preg_replace()/ preg_match_all(); instantiate-template fed unvalidated variables (array values → str_replace TypeError), config_overrides, and json_decode() null into array_merge(). All now guarded with canonical WP_Error validation or safe normalization/skip.
  5. analyze_data_patterns advertised frequency|correlation|outliers in its enum but only implemented trend — the other three silently returned base stats. Implemented all three (bucket counts, Pearson r against position, 1.5xIQR fences + nearest-rank percentile helper).

Verified clean this cluster: zero shell/exec anywhere in the toolkit; all 14 provider clients instantiated by get_ai_client() have create_chat_completion() and the three provider switches (get_research_provider / get_research_model / get_ai_client) cover the same 14 providers (class 2/4 clean); Jev classifier methods and Paper Store manager/repository APIs all exist; create-pro-schedule / schedule-channel-broadcast channel enums match the chat-channels vocabulary; calculate-orchestration-capacity / configure-circuit-breaker / create-template enums match their switches; blueprint-installer JSON paths are is_array-guarded. CG Pro mirror: only the blueprint installer exists in plugins/nvoos-content-graph-pro/src/tools/orchestration/ — no tool files were changed there (ecosystem-port decision, out of scope).

Tests landed: addons/pro/tests/test-orchestration-toolkit-hardening.php (new, 26 tests / 92 assertions — flatten shape matrix + Gemini array-of-parts end-to-end through perform_ai_research via mocked pre_http_request, empty-content error paths, canonical WP_Error matrix for all 8 converted tools, consumer degradation guards, generate-password bounds + happy path, html/content/selector type rejection, aggregate/verify array-content tolerance + URL dedupe, analyze-data-patterns full enum matrix, instantiate-template end-to-end through a CPT fixture with a non-scalar variable). Validation: 26/26 green standalone and 41/41 with the Jev suites + 43/43 with the base-tree round-trip/signature suites (isolated DB wordpress_test_orchestration); phpcs 0 errors on both orchestration trees + the new test; UTF-8 clean.

Case study — media

What the eighth audit found (class 2 variant, envelope, security; classes 1 and 4 verified clean):

  1. process_collection::execute() — read the nested apply_media_template result with ! empty( $result['success'] ) although execute() returns a success array or a WP_Error (invalid template params, missing graphic_editor_plus, provider failure) → fatal Cannot use object of type WP_Error as array on the first failing item. The same array assumption sat in the admin bulk action (handle_bulk_actions) and the Quick Process AJAX (ajax_quick_process_collection), which read $result['success'] off process_collection's own WP_Error returns. Guarded with is_wp_error + per-item error propagation in the tool and is_wp_error checks in both admin callers.
  2. apply_collection_template::execute() — same WP_Error-as-array fatal on the nested process_collection call, plus non-canonical array( 'success' => false, ... ) validation envelopes → converted to WP_Error.
  3. scan_orphaned_media / cleanup_orphaned_media — non-canonical array( 'success' => false, ... ) envelopes; and scan_type / cleanup_type were never validated in execute() (bogus values reported success with zero results). Both now reject unknown enum values with a WP_Error; cleanup_type=ids requires attachment_ids.
  4. scan_orphaned_media::scan_unregistered_files() — year_month passed through sanitize_text_field and concatenated into an uploads path ($base_dir . '/' . trim( $year_month, '/' )) → ../-style values listed arbitrary directories (info disclosure). Now strictly validated as ^\d{4}/\d{2}$ before any path use.
  5. Defensive hardening: delete_file_safe containment upgraded to a path-component boundary (trailing separator, uploads-evil/ no longer passes), glob metacharacters escaped in delete_size_variants patterns, glob() false guards, and get_all_registered_files() no longer calls dirname( false ) for attachments without _wp_attached_file meta.
  6. Unreferenced-detection safety (bonus find, same cluster): the reference check only looked at post_parent / featured image / post_content, so delete_unreferenced=true could delete attachments referenced from post meta or options (WooCommerce _product_image_gallery, serialized ACF arrays, URL-in-meta, site icon / custom logo). Added a fail-safe is_attachment_referenced_in_meta() to both tools (comma-list, exact, serialized-int and quoted-ID LIKE patterns over postmeta + filename/URL patterns, self-rows excluded via post_id + reserved meta-key clause, plus targeted well-known options). Gotcha: do NOT do a table-wide options LIKE scan for i:<id>; — serialized blobs like the WooCommerce blocks-patterns transient contain i:<n>; for every index and classify every attachment as referenced (ID-dependent flakiness). Targeted options only.
  7. Test-fixture fix: both media suites pointed at tests/data/test-image.png — a gitignored file that never existed, so attachments were created with no physical file (getimagesize error notices; real-file branches never exercised). Switched to the committed tests/fixtures/sample-image.png (1×1 PNG).

Verified clean this cluster: zero shell/exec anywhere in the toolkit; ocr_image_classic pins tesseract which the OCR service switch handles (its CLI path was already hardened in the document-generation cluster); the 7-operation template enum matches graphic_editor_plus's full local + AI operation set (class 4); the Blueprint Installer methods used by the unregistered examples/ tool exist; the central registry capability gate covers these tools (no per-tool check gap — unlike the CRM/ECA pattern, no in-tool user_can needed).

Tests landed: addons/pro/tests/test-media-toolkit-hardening.php (new, 15 tests / 60 assertions — nested-tool WP_Error matrix through registry stubs, envelope/validation regressions, year_month traversal matrix, scan referenced-vs-unreferenced fixture, postmeta/option reference safety, admin bulk-action regression) plus addons/pro/tests/class-wp-mcp-ai-media-toolkit-stub.php (single configurable stub — the custom sniff allows one object structure per file). Validation: 34 media-cluster tests green (hardening + integration + tools)

  • 12 fashion-batch green; phpcs 0 errors; UTF-8 clean. CG Pro note: the media toolkit has no plugins/nvoos-content-graph-pro/src/tools/media/ mirror yet — porting is an ecosystem-port decision, out of scope for this audit.

Case study — shopify-sync

What the seventh audit found (a class-3-family error-path fatal and a class-4 descriptor/wiring mismatch; everything else verified clean):

  1. shopify_sync_orders::handle_order_analytics() — $edges was only assigned inside the ! is_wp_error( $orders_result ) branch, then count( $edges ) ran unconditionally — count(null) TypeError on exactly the error path meant to degrade gracefully (API down, missing token, rate limit). Initialized $edges = array() before the guarded block; the error path now returns the canonical envelope with zeros.
  2. WP_MCP_AI_Shopify_Sync_MCP_Server — three descriptor wiring mismatches fed by the shared WP_MCP_AI_Scheduled_Toolkit_Server_Trait:
    • get_sync_hook_name() advertised wp_mcp_ai_shopify_sync_full_sync, a hook the engine never schedules (it schedules per-connection wp_mcp_ai_shopify_full_sync_{conn_id}) → get_sync_status() always "unknown"/0;
    • the trait's get_sync_interval() read a slug-derived option (wp_mcp_ai_shopify-sync_settings) the toolkit never writes, in seconds, while the toolkit stores minutes in wp_mcp_ai_shopify_sync_toolkit_settings → advertised sync_interval_seconds stuck at the 300s default;
    • the trait's get_connection_status() read wp_mcp_ai_remote_connections, an option the toolkit never populates → always false. Overrode all three: per-connection last-sync option aggregation ({last_sync, status, row_count} shape preserved + the trait's wp_mcp_ai_scheduled_toolkit_sync_status filter), minutes→seconds interval conversion, and Remote-Sites-manager resolution of enabled synced Shopify connections. Mirrored byte-identically to the CG Pro MCP server file (text-domain deviation only); class_exists guards degrade gracefully there because the sync engine/CCT manager are not yet ported to CG Pro.

Verified clean this cluster: zero shell/exec in the toolkit and all backing services; every client/service method call exists on its class (WP_MCP_AI_Shopify_Client get_orders/get_order/get_shop_info/bulk_query/ catalog_request/get_api_mode/get_catalog_shop_id, CCT manager read/write/ sync methods, engine consts + dispatch pair, WP_MCP_AI_Sync_Log_Manager start_run/log_item/end_run, wp_mcp_ai_log(), WP_MCP_AI_Logger::log_event(), Remote Site Manager get_all_connections/get_connection/decrypt_value); HTTP paths already hardened (client graphql/bulk_query/catalog_request carry status+JSON+size validation via wp_safe_remote_*; webhook handler verifies HMAC with hash_equals + decrypted secret before any state change, with __return_true permission_callback as the documented webhook-auth pattern); all advertised enums match their switches (5 action enums, stock_status, product status, sync_direction, sync_interval diffed against the executing switch statements).

Tests landed: addons/pro/tests/test-shopify-sync-toolkit-hardening.php (new, 11 tests / 25 assertions — orders-analytics error-path regression + happy-path aggregation through mocked pre_http_request, MCP server hook prefix/interval-minutes/interval-default/status-aggregation/status-unknown/ status-stale/connection-status matrix). Validation: 126 tests / 1198 assertions green across the 9-suite shopify-sync cluster; CG Pro MCP server matrix green (7 tests / 64 assertions, 3 pre-existing standalone skips); phpcs 0 errors on both standards; UTF-8 clean. PR #6889.

Case study — email-marketing

What the eighth audit found: all four failure classes verified clean. This is a verified-clean cluster; the deliverable is the regression suite that locks the verified contracts, because the Brevo trio plus the blueprint importer previously had zero test coverage.

  • Class 1: zero shell/exec/proc_open/popen anywhere in the toolkit. The loose incident/maintenance tools (addons/pro/includes/tools/*.php, 5 files) also scanned clean on the same patterns.
  • Class 2: no provider clients are instantiated at all; the only cross-class calls are WP_MCP_AI_Logger::log_event() / log_error() and WP_MCP_AI_Blueprint_Installer::load_blueprint() / install(), all of which exist.
  • Class 3: no provider responses are parsed — every tool is a pure HTTP wrapper; all json_decode() sites are is_array()-guarded and every wp_remote_* call carries is_wp_error + status-code checks.
  • Class 4: every enum matches its switch — brevo statistics campaigns|transactional vs the execute() branch, brevo contacts 8-action enum vs the 8-case switch, mailjet contacts 4-action enum vs switch, mailjet statistics CounterSource/CounterTiming pass through to the Mailjet API vocabulary, blueprint enum vs BLUEPRINT_SLUGS.

Tests landed: addons/pro/tests/test-email-marketing-toolkit-hardening.php (new, 15 tests / 91 assertions — credential/capability gates, payload normalisation incl. recipient dedupe and tag truncation to 10, HTTP status/transport/shape error paths, an enum-to-handler routing loop driven from the live schema, per-type endpoint routing, blueprint slug rejection). Mailgun/Mailjet were already covered by tests/test-mailgun-tool.php and tests/test-mailjet-tool.php; Brevo had none. Validation: 15/15 green standalone and 29/29 green combined with the mailgun/mailjet suites on the isolated DB wordpress_test_muted_moth; phpcs 0 errors on the base standard; UTF-8 clean (ASCII-only).

CG Pro note: the email-marketing toolkit has not been ported to plugins/nvoos-content-graph-pro/src/tools/ yet — no mirror exists to sync, so this cluster is base-tree only (porting stays on the ecosystem-port track).

Case study — healthcare

What the sixth audit found (class 3; plus class-2 dead audit trail and two bonus TypeErrors exposed by the new tests; everything else verified clean):

  1. interpret_imaging_study::action_interpret() — concatenated $result['choices'][0]['message']['content'] straight into the interpretation text. Gemini (and OpenAI-compatible gateways such as vLLM) return that field as an array of parts → literal "Array" text in every Gemini-served interpretation. Added a flatten_response_content() helper at the response boundary; empty content now returns the honest imaging_ai_empty_response WP_Error.
  2. export_fhir_data + wellness init.php — called the nonexistent global wp_mcp_ai_log_activity() (no definition anywhere in the repo; guarded so it never fataled, but the HIPAA audit trail silently never fired). Rewired to WP_MCP_AI_Healthcare_Audit::record() (the unified PHI ledger every other healthcare tool writes to) and WP_MCP_AI_Logger::log_event() for the migration.
  3. Bonus (tests exposed): deidentify_health_record + extract_clinical_entities passed $user_id in the array $meta slot of WP_MCP_AI_Healthcare_Audit::record() → TypeError on every execution once the audit class loads. Moved user_id into the meta array. Also guarded the OpenMed response boundary (entities non-array → empty list; deidentified_text missing → '' in the completion action).

Verified clean this cluster: zero shell/exec anywhere in the toolkit; all client methods exist (create_chat_completion on OpenAI/Gemini/Anthropic, create_embedding(s) on Gemini/OpenAI, OpenMed client methods, Media Worker sidecar trait, imaging CPT/audit classes, batch iterator, migration class); all advertised enums match their service switches (species human/canine/feline in the engine reference tables, vitals units, DICOMweb auth types, EHR vendors, FHIR formats, NER models, deidentify methods); HTTP paths already hardened (DICOMweb status + JSON validation + SSRF guard on save, OpenMed client, EHR connect SSRF guard + status + JSON).

Tests landed: addons/pro/tests/test-healthcare-toolkit-hardening.php (new, 7 tests / 27 assertions — flatten shape matrix, Gemini array-of-parts end-to-end through execute(), OpenAI string passthrough, empty-content error, FHIR export audit-trail entry, OpenMed non-array entities guard, missing deidentified_text guard). Validation: 139 healthcare-cluster tests green (isolated DB wordpress_test_muted_moth), 38 CG Pro healthcare tests green (isolated DB wordpress_test_muted_moth_cg); phpcs 0 errors on both standards; UTF-8 clean. PR #6887.

Case study — ecommerce

What the fifth audit found (classes 1 and 3, plus two feature-dead Node paths):

  1. validate-image-for-product::validate_with_vision_ai() and validate-image-for-vehicle::validate_with_vision_ai() — trim( $response['choices'][0]['message']['content'] ) string-assuming (class 3): OpenAI-compatible gateways can return message.content as an array of parts, so trim() fatals on the first such response. Added a flatten_response_content() helper at the response boundary plus an honest empty-content wp_mcp_ai_invalid_response WP_Error.
  2. export_products_report::generate_excel_file() and generate_woocommerce_order_invoice_pdf::generate_pdf() — unguarded exec() (class 1) AND doubly broken invocations: the script path was WP_MCP_AI_PRO_PATH . 'addons/pro/scripts/...' (doubled — PRO_PATH already ends in addons/pro/), the invoice tool pointed at generate-invoice.js which does not exist anywhere in the repo, and both passed inline JSON on the command line while the bundled scripts take <json_file> <output_file> argv. Both are now gated behind WP_MCP_AI_ALLOW_SHELL_TOOLS + Node availability via the shared wp_mcp_ai_ecommerce_run_node_script() helper (helpers file, both trees) and execute through the Process Service (run_silent() array form — no shell interpolation); the Excel tool writes a JSON input file and the invoice tool renders an escaped HTML invoice through the bundled generate-pdf.js (copied into the CG Pro scripts dir).
  3. lookup_product_price — Crawl4AI markdown/text fields and submit_document_prompt text/content/response fields fed into preg_match()/explode()/json_decode() without a string guarantee (class 3, tool-result variant): parse_crawl_result_for_product(), parse_search_results(), extract_text_from_document(), extract_line_items_from_text() now flatten at every response boundary; extract_price_from_content() and parse_json_from_text() carry defensive ! is_string() guards.
  4. product_actualization::generate_ai_integrated_image_openai() — the OpenAI-url fallback branch downloaded the image without checking the HTTP status (error HTML saved as image bytes); added a status gate.

Verified clean this cluster: all Shopify/Printful/Roboflow client method calls exist on their classes (class 2); every advertised enum matches its service switch — Printful action enum ↔ switch, product-actualization provider enum ↔ detect_preferred_provider(), QuickBooks accounting-method and import-duty country enums validated against their maps (class 4); get-import-duty / quickbooks-report / quickbooks-desktop-sync HTTP paths carry is_wp_error + status + JSON validation; zero remaining shell calls in the toolkit.

Tests landed: addons/pro/tests/test-ecommerce-toolkit-hardening.php (new, 14 tests / 43 assertions — flatten shape matrix for both vision tools via mocked pre_http_request with string AND array-of-parts content, empty content honest-error paths, crawl-markdown flatten, price/JSON parse guards, shell-tools gate for the Node runner + both generation methods with temp file cleanup assertions, invoice HTML escaping, JSON input roundtrip). Validation: 250 ecommerce-cluster tests green; phpcs 0 errors on both standards; UTF-8 clean. Note: the test never defines `WP_MCP_AI_ALLOW_SHELL_TOOLS` — defining it would pollute every later suite in the shared process.

Case study — eca-management

What the fourth audit found (class 3; everything else verified clean):

  1. research_eca::perform_ai_research() — passed $result['choices'][0]['message']['content'] through raw; Gemini (and OpenAI-compatible gateways such as vLLM) return that field as an array of parts, and parse_research_results() fed it straight into preg_match() — preg_match(): Argument #2 ($subject) must be of type string, array given on the first Gemini-served research call. Added a flatten_response_content() helper at the response boundary plus a defensive flatten in parse_research_results(); empty content now returns the honest wp_mcp_ai_invalid_response WP_Error.

Verified clean this cluster: zero shell/exec; all 14 provider client classes in get_ai_client() exist with matching get_research_provider() selection (class 4 clean — no advertised enum, provider auto-selected from credentials); Google Classroom client methods (list_courses, list_students, list_coursework, list_student_submissions, create_course, create_coursework, get_course, list_guardians, paginate) and Google_Classroom_Credentials::require_scope / Google_Classroom_Push all exist in includes/google/; iSAMS/SOCS/Gmail HTTP paths carry is_wp_error + status + JSON validation; CSV import gates on manage_options.

Port-level note (tracked as issue #6884): the CG Pro mirror's get_research_provider() calls WP_MCP_AI_Credential_Resolver::has_credentials() and WP_MCP_AI_Tool_Registry::get_instance() unguarded, while the crm upwork mirror carries the wave-proof class_exists guard deviation. The unguarded pattern predates this audit and spans many CG Pro toolkits (ai-tool-builder, chat-channels, crm, quiz-management, site-creator-toolkit) — queued as a port-wide sweep rather than a per-toolkit fix.

Tests landed: addons/pro/tests/test-eca-toolkit-hardening.php (new, 6 tests / 19 assertions — flatten shape matrix, Gemini array-of-parts end-to-end through the real client, fenced-JSON parse, empty-content error). Validation: 44 eca-cluster tests green; phpcs 0 errors on both standards; UTF-8 clean.

Case study — crm

What the third audit found (classes 2 and 3; everything else verified clean):

  1. draft_upwork_proposal::generate_proposal() — trim( $result['choices'][0]['message']['content'] ) string-assuming (class 3): Gemini normalize_response() returns message.content as an array of {type,text} parts, so the isset() guard passes and trim() fatals on the first Gemini-served proposal. Added a flatten_response_content() helper at the response boundary; empty content now returns the honest wp_mcp_ai_invalid_ai_response WP_Error instead of an empty-string “success”.
  2. draft_lead_reply::generate_ai_draft() — called the nonexistent global wp_mcp_ai_chat_completion() (class 2, function variant): no such function exists anywhere in the repo, so the AI path was permanently dead (silent template fallback on every call). Rewrote around the OpenAI/Gemini/Anthropic provider clients (Credential_Resolver → provider/model/client plumbing, same as the upwork tool) with the same flatten helper; template fallback preserved on any WP_Error.
  3. Scan-regression note: the standard class-3 grep choices[0] missed this toolkit's hits because the code writes ['choices'][0] — also scan for choices'\]\[0\]. And scan for wp_mcp_ai_*() global-helper calls with no function definition anywhere in the repo (dead-code smell).

Verified clean this cluster: zero shell/exec anywhere in the toolkit; all client methods exist (Gmail service search_leads, LinkedIn get_me/search_jobs, Upwork graphql, MCP App clients, WP_MCP_AI_Validator_Service::is_email/is_phone_number) with hardened HTTP paths (is_wp_error + status + JSON validation); IMAP listener is pure-PHP socket with guarded ext-imap fallback; classifiers (intent/support) are heuristic; enum/service surfaces match.

Follow-up queue for later clusters (same ['choices'][0]['message']['content'] string-assumption found globally): eca-management research-eca ✅ (done, this file), ecommerce validate-image-for-product/vehicle ✅ (trim + json_decode, done 2026-10-04), healthcare interpret-imaging-study ✅ (done 2026-10-04), multilingual auto-translate-content (also raw wp_remote_post), orchestration generate-research-report ✅ (done 2026-10-04), places research-place.

Tests landed: addons/pro/tests/test-crm-toolkit-hardening.php (new, 7 tests / 19 assertions — response-shape matrix for both tools via mocked pre_http_request, Gemini array-of-parts end-to-end through the real clients, template-fallback regression). Validation: 7/7 green + 354 crm/docgen cluster green; phpcs 0 errors on both standards; UTF-8 clean.

Case study — image-production

What the second audit found (classes 1, 3, 4; plus defensive fixes):

  1. text_to_image_prompt_optimizer — raw wp_remote_post() to a hardcoded api.openai.com/v1/chat/completions URL bypassing the OpenAI client (custom openai_base_url, org/project headers, Credential_Resolver all ignored), trim( $data['choices'][0]['message']['content'] ) string-assuming (fatal on array-of-parts content from OpenAI-compatible gateways), no HTTP status check, and fenced json payloads never parsed (the model is asked for JSON; fences are common). Rewrote around WP_MCP_AI_OpenAI_Client::create_chat_completion() with a flatten_response_content() helper, a get_api_key() credential gate, and fence-stripping in parse_optimization_response().
  2. generate_image_ai Stability path — list( $w, $h ) = explode( 'x', $size ) left $height undefined (→ 0) for malformed sizes, and non-200 responses fell through to the generic missing-artifacts error. Fixed with array_pad + absint guards and a status check that surfaces Stability's own message field.
  3. optimize_image_sharp::optimize_via_sidecar() — (int) $sidecar['optimized_size'] undefined-index notice + 0 when the worker omits the field. Guarded with a filesize() fallback.
  4. Harmonization ai_edit_image() / generate_background() — the OpenAI-url fallback branch downloaded the image without checking the HTTP status (error HTML would be saved as image bytes), and a neither-b64-nor-url response fell through to the misleading "no supported provider" error. Added a status gate + an honest wp_mcp_ai_empty_result.

Verified-clean this cluster: the WP_MCP_AI_NodeJS_Subprocess trait (already Process Service-only), the Media Worker sidecar trait, the remove.bg helper (upload-dir containment + status-aware error parsing), the harmonization URL downloader (wp_safe_remote_get + image content-type check), and all Gemini/OpenAI generate_image/edit_image call sites (signatures and return shapes match the real client APIs).

Documented, out of scope: upscale_image_ai / colorize_image / enhance_image_quality / apply_artistic_style are placeholder implementations (no actual AI processing — they re-save the source and claim success); enhance_image_quality's sharpen/color/contrast/denoise helpers are no-ops. Not failure-class bugs; a feature-completeness track. Wave 1 landed (2026-10-04): enhance_image_quality + upscale_image_ai now run real Sharp processing via the dual-path WP_MCP_AI_Sharp_Image_Processing trait (local subprocess → worker sidecar → honest error). Wave 2 landed (2026-10-04): colorize_image + apply_artistic_style now run real AI edits via the worker /api/image/edit route (Gemini/OpenAI/Replicate) or the PHP provider clients through the WP_MCP_AI_Provider_Image_Edit trait — see issue #6877 and docs/project/plans/image-production-sidecar-cluster-plan.md. Wave 3 (Real-ESRGAN) remains deferred.

Tests landed: addons/pro/tests/test-image-production-hardening.php (new, 12 tests / 39 assertions — prompt-optimizer response-shape matrix, fence-stripping, Stability dimension/error paths, Gemini+OpenAI image-edit round-trips through mocked HTTP, sidecar missing-field guard). Validation: 12+22 green across the image-production cluster; phpcs 0 errors on both standards; UTF-8 clean.

Case study — document-generation

What the first audit found (all four classes, one toolkit):

  1. extract_pdf_text — unguarded shell_exec('which pdftotext…') + raw exec() → fatal on the exec-disabled production host (missed by the v1.1.70 PR #6295 hardening wave). Fixed with guarded CLI helpers + Process Service fallback; pure-PHP pdfparser remains the last-resort path.
  2. pro_document_ocr / ocr_pdf_text — $client->generate_content() on WP_MCP_AI_Gemini_Client (method does not exist) → fatal. Rewrote extract_with_gemini() around create_chat_completion() with an inline-base64 image segment (added data key support to format_image_part() in the base Gemini client).
  3. pro_pdf / pro_word / pro_excel_document — Gemini array-of-parts message.content fed into preg_match() → fatal; plus the broken markdown-fence JSON regex (missing closing parenthesis, warned on every call, fenced JSON never parsed). Fixed with flatten_response_content()
    • defensive flatten in try_parse_json_response() + a compilable /```(?:json)?\s*(\{.*?\})\s*```/s.
  4. pro_document_ocr schema advertised anthropic while the OCR service switch had no anthropic case → Unknown OCR provider then the circuit-breaker "temporarily unavailable" cascade. Implemented extract_with_anthropic() (data-URL image block via WP_MCP_AI_Anthropic_Client::create_chat_completion).
  5. WP_MCP_AI_Self_Hosted_OCR_Client — (string) cast of possibly-array message.content → literal "Array" OCR text. Added flatten_content().

Tests landed: addons/pro/tests/test-ocr-service.php (+6), addons/pro/tests/test-extract-pdf-text-tool.php (new), addons/pro/tests/test-docgen-provider-content-flattening.php (new), tests/test-self-hosted-ocr-client.php (new), tests/test-gemini-client.php (+1). Validation: 126 tests / 532 assertions green in the 9-suite cluster; phpcs 0 errors on both standards; UTF-8 clean.

References

  • Test environment + cluster workflow: mcp-ai-wpoos-test-suite skill
  • Building a NEW toolkit from scratch (scaffold, registration surfaces, oracle testing): mcp-ai-wpoos-toolkit-creation skill — run its four-failure-class scan (§8) as the final gate on new toolkits before merge
  • CG Pro byte-identity mirroring: mcp-ai-wpoos-ecosystem-port skill
  • v1.1.70 hardening precedent: PR #6295, addons/pro/includes/npm-integration-filters.php
  • Array-content normalization precedent: test-suite skill pattern 56
  • Production evidence: nirmanawellness get_system_logs failures (2026-10-04) — sse_tool_fatal_error entries from these exact bugs

Related non-toolkit hardening (stream hygiene)

Toolkit fatals are one half of the nirmanawellness story — the other was the chat SSE stream being reset by Cloudflare/nginx during long tool runs (net::ERR_HTTP2_PROTOCOL_ERROR). That fix (inter-step SSE keepalive comment frames) and its test contract are documented as test-suite skill pattern 64; the durable follow-up (deadline-triggered offload to the job stream) is proposed in docs/project/proposals/sse-stream-hardening-long-run-offload-proposal.md. When a toolkit audit surfaces tools that block for tens of seconds (OCR, document generation, video), the chat-stream impact of those tools is the same class of production risk — keep the two loops linked.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

Control a headless browser to navigate URLs, click elements, fill forms, extract content from JavaScript-rendered pages, take screenshots, and automate end-to-end web workflows.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

Review code for quality, simplicity, and maintainability. Identifies unnecessary abstractions, duplicated logic, oversized functions, missing error handling, and naming issues — then fixes them automatically before presenting the result.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

WooCommerce store operations through NV oOS Pro — product research, catalog cross-referencing, inventory lookups, and Shopify or Flowhub comparisons where integrations are configured.

日本語の概要は準備中です。原文の説明を表示しています。

nvdigitalsolutions/mcp-ai-wpoos72026年10月13日 更新

nvdigitalsolutions のスキルをすべて見る

このスキルの問題を報告する