=== Ppok AI Toolkit ===
Contributors: ppok
Tags: mcp, ai, model-context-protocol, claude, llm
Requires at least: 6.4
Tested up to: 6.7
Requires PHP: 8.1
Stable tag: 1.0.0
License: GPL-2.0-or-later (plus proprietary components — see LICENSE.md)
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Turn this WordPress site into a Model Context Protocol (MCP) endpoint. Ships an OAuth 2.1 Authorization Server so AI clients can connect directly via the streamable-HTTP MCP transport — no intermediate bridge required.

== Description ==

Ppok AI Toolkit registers WordPress Abilities for MCP-driven content authoring + introspection and ships a complete OAuth 2.1 surface (authorization server + protected resource server) so AI clients such as Claude.ai's native "Add custom integration" flow can connect to your site directly. App Password auth keeps working on the rest of the WP REST surface; MCP routes require OAuth.

Features:

* OAuth 2.1 + PKCE (S256) authorization server with Dynamic Client Registration (RFC 7591), refresh-token rotation with reuse detection, and a one-call revocation endpoint (RFC 7009).
* Discovery via `.well-known/oauth-authorization-server` (RFC 8414) and `.well-known/oauth-protected-resource` (RFC 9728), so Claude.ai and other MCP clients can complete the connection autonomously.
* Per-ability scopes (`mcp.ping`, `mcp.rest-call`, `mcp.set-post-blocks`, etc.) presented on a grouped Read/Write consent screen. Reads pre-checked, writes require an explicit tick.
* Audited authorization events (`client_registered`, `consent_granted`, `code_issued`, `token_issued`, `token_refreshed`, `token_revoked`, `reuse_detected`) in a dedicated `ppok_ai_oauth_events` table.
* Ability registrar layers an OAuth scope gate above the existing L1-L5 (capability / schema / block allowlist / scope / audit) write gates.

== Licensing ==

This plugin ships under a split license:

* WordPress-integrated code (everything outside `includes/lib/`) — GPL-2.0-or-later.
* Standalone library code in `includes/lib/` — proprietary.

See `LICENSE.md` at the plugin root for the full policy.

== Installation ==

1. Upload the `ppok-ai-toolkit` folder to `/wp-content/plugins/`.
2. Run `composer install --no-dev` inside the plugin folder.
3. Activate the plugin in the WordPress admin.
4. Visit *Settings → AI Toolkit* to configure MCP.

== Changelog ==

= 1.0.0 =
* Phase 18: full OAuth 2.1 surface — paste your `/wp-json/mcp/mcp-adapter-default-server` URL into Claude.ai's "Add custom integration" and the connection completes via PKCE + dynamic client registration + per-ability consent without any intermediate Node bridge. Five new tables (`ppok_ai_oauth_clients`, `_auth_codes`, `_tokens`, `_consents`, `_events`). New REST endpoints under `ppok/v1`: `.well-known/oauth-authorization-server`, `.well-known/oauth-protected-resource`, `oauth/register`, `oauth/authorize`, `oauth/token`, `oauth/revoke`. Opaque access tokens (default 1h TTL) + refresh tokens (default 30d) stored only as SHA-256 hashes; refresh rotates one-time-use with full-family revocation on reuse-detection. Per-ability scopes (`mcp.ping`, `mcp.rest-call`, ...) presented on a grouped Read/Write consent screen at `wp-admin/admin.php?page=ppok-oauth-authorize` — reads pre-checked, writes require an explicit tick. Bearer token validation on MCP routes (`/mcp/*` by default; filterable via `ppok_ai_toolkit_oauth_protected_route_prefixes`) emits 401 + WWW-Authenticate with a resource_metadata pointer so MCP clients discover the OAuth chain autonomously. Ability registrar layers a new scope-gate wrap above the existing L1-L5 gates so under-scoped OAuth callers get rejected before the capability check. Daily WP-Cron prune of expired auth codes + tokens past a 30-day grace window. HTTPS hard-required on all OAuth endpoints (localhost exception); filterable for reverse-proxy TLS-termination setups via `ppok_ai_toolkit_oauth_allow_insecure`. App Passwords keep working on the non-MCP WP REST surface unchanged; only the MCP routes are OAuth-gated. The `Automattic/mcp-wordpress-remote` Node bridge path is gracefully deprecated — new installs should point clients at the OAuth flow.

= 0.4.6 =
* Phase 15: new read-only ppok/block-template ability — empirical schema introspection for a block by sampling real-world usage. Walks up to sample_size existing instances (default 5, max 20; reuses blocks-find-usage's candidate-query SQL), aggregates observed attrs into keys_observed (with present_rate_pct and up to 3 redacted sample values per key), required_keys (present_rate=100), paired_keys (Jaccard co-occurrence ≥ 80% over ≥ 3 instances; role_hint via key-name patterns + URL-shape detection + length-differential heuristic), and variant_signals (distinct values 2-6 AND ≤ instances/2). Two-level flattening on associative attrs so frameworks that nest data under a container key (ACF Blocks' `data` map) surface their inner shape. The cure for cold-start authoring on blocks whose user-editable data lives outside the registered attributes_schema — works for Gutenbricks gb-* slots, ACF Block data fields, Spectra / Stackable / GenerateBlocks Pro custom keys, and any future framework whose registered schema under-specifies the real attr shape.
* Phase 15: new framework-adapter extension seam under includes/framework-adapters/. Framework_Adapter interface (name / is_available / describe_block) lets adapters enrich the empirical baseline with framework-native field metadata (label, type, choices). Ships with two built-ins: ACF_Adapter (active when acf_get_field_groups / acf_get_fields are callable — works for both ACF and ACF Pro) projects ACF field groups whose location rules target the queried block; Gutenbricks_Adapter is a documented placeholder (is_available returns false today because Gutenbricks does not expose a stable PHP introspection API). Third-party plugins extend the system by hooking the new ppok_ai_toolkit_framework_adapters filter and pushing their own Framework_Adapter instances onto the array.

= 0.4.5 =
* Update check: Update_Channel gains an on_update_plugins() handler hooked on `update_plugins_ppok-ai.com` so the plugin self-consumes update.json instead of relying on an external companion plugin. Without this, WP 5.8+'s `Update URI:` header is just a hostname discriminator with no callback to fulfill — sites never see the "Update available" prompt. Translates the JSON shape into the stdClass WP's `update_plugins` site transient expects; accepts both `version` and `new_version` keys so the hosting-side JSON can use either naming. Cached for 1 hour (negative 1m on HTTP failure) and overridable via the `ppok_ai_toolkit_update_json_url` filter. Sites running v0.4.4 or earlier WILL NOT auto-upgrade to this version — install the v0.4.5 zip once manually and the self-update channel takes over from there.

= 0.4.4 =
* Phase 14: new read-only ppok/page-outline ability — returns a depth-tracked flat outline of a post's block tree as `[{block_name, depth, brief_data_summary, anchor?}]`. No rendered HTML, no full attrs, no innerContent. Cheap structural reconnaissance (typically under 2KB on a page where get-post-blocks returns 100KB+). brief_data_summary falls back from stripped innerHTML to text-shaped attrs (content / text / title / alt / caption / label) so custom blocks (Gutenbricks, ACF Blocks, Spectra) summarize cleanly. Empty-freeform parser artifacts are filtered out so the outline reflects structure, not whitespace.
* View Details modal: new Update_Channel hooks `plugins_api` for the plugin's own slug, fetching https://ppok-ai.com/plugins/ai-toolkit/info.json and translating the info-shape into WP's plugin_information stdClass. Cached in a 6-hour site transient (1-minute negative cache on failure) so opening the modal doesn't round-trip on every click. URL overridable via the `ppok_ai_toolkit_info_json_url` filter.

= 0.4.3 =
* Self-hosted updates: added `Update URI: https://ppok-ai.com/plugins/ai-toolkit/update.json` header. WP 5.8+ delegates this slug's upgrade check to the `update_plugins_ppok-ai.com` hostname-scoped filter (handled hosting-side), bypassing WordPress.org entirely.
* Phase 17(a): ppok/set-post-blocks gains `validate_only` (default false). When true, the call runs L3 block-allowlist + L4 scope + a serialize_blocks/parse_blocks round-trip and returns `{validate_only, bytes_written_would_be, unknown_blocks, schema_violations, roundtrip_drift_blocks}` — diagnostics collected without halting on the first failure and without calling wp_update_post or wp_save_post_revision. Lets agents verify authoring intent against the live WP environment before committing to a draft.
* Phase 17(a): ppok/replace-in-content `dry_run` (already default true) now also predicts the per-candidate apply-path verdict and surfaces it as a `would_skip` field on each `changes[]` entry (`permission_denied` | `scope_blocked`). The preview is now honest about which posts would actually change on apply. Intentionally no L3 here — existing posts may legitimately contain unregistered blocks.
* Phase 17(b): ppok/rest-call accepts `params.validate_only=true` on POST/PUT/PATCH. By default it short-circuits with a structured `validate_only_not_supported` WP_Error (status 501) since no WP core REST endpoint honors the flag. Hook the new `ppok_ai_toolkit_validate_only_routes` filter to allowlist custom endpoints that do (exact match or trailing `*` wildcard prefix). On GET/DELETE the flag is silently dropped — there is nothing to dry-run on a read. For WP core authoring today, create with `status: "draft"` then inspect, or use ppok/set-post-blocks with `validate_only=true` for block-tree authoring.

= 0.4.2 =
* Release tooling: `composer build` now packages a distributable zip into the directory specified by `.build.json` (gitignored). Slims `vendor/` via `composer install --no-dev --optimize-autoloader`, stages a clean copy excluding dev artifacts, writes `ppok-ai-toolkit-{version}.zip`, then restores fat dev deps via try/finally. Cross-checks plugin-header `Version`, `PPOK_AI_TOOLKIT_VERSION` constant, and `readme.txt` `Stable tag` and aborts on mismatch or if the target zip already exists. No runtime plugin changes.

= 0.4.1 =
* Phase 13a (bug fix): ppok/get-post-blocks permission denials now return a structured WP_Error with code `ppok_cap_denied` carrying data.failed_gate / required_cap / target_post_id / target_post_type, instead of a bare "Permission denied" via permission_callback. The per-post edit_post cap check moved inside execute_callback to match ppok/rest-call's gating model (logged-in at the ability layer; per-post cap enforced after the post is loaded). Agents can now self-recover.
* Phase 13b: ppok/blocks-registered gains an `include_schema` parameter (default true). Set false to omit `attributes_schema` from each row — catalog enumeration drops from ~87KB to ~10KB on typical installs.
* Phase 13c: ppok/blocks-find-usage description now explicitly documents the default `post_status=['publish','pending']` so agents widen the net for draft-reconnaissance instead of getting silent zero-hit responses on draft-only matches.
* Phase 13d: ppok/rest-call accepts `route` as a synonym for `path` (both top-level args). Either is fine; if both are provided, `path` wins. Missing both returns a structured `rest_invalid_param` error.
* Phase 13e: ppok/rest-call honors `params.slim_response_keys` — an array of top-level keys to drop from each row after the default strip pass. Lets agents shrink responses on endpoints that don't honor `_fields`.
* Phase 13f: ppok/rest-call strips server-rendered shapes (content.rendered, title.rendered, excerpt.rendered, guid.rendered) from POST/PUT/PATCH responses by default. The caller just authored the content — echoing back 100-200KB of server-rendered HTML is pure waste. `params.include_extended=true` preserves them.
* Phase 13g: ppok/list-posts gains `orderby` (`date` | `modified` | `title` | `menu_order`) + `order` (`ASC` | `DESC`) params, with a stable secondary sort by ID to break ties. DEFAULT flipped from `orderby=date DESC` to `orderby=modified DESC` — agents looking for "current voice" overwhelmingly want most-recently-touched.
* Phase 13i: ppok/blocks-registered response gains a `namespaces:[{name, count}]` summary derived from grouping the catalog by slash-prefix. Lets agents preview the block ecosystem cheaply before pulling rows.
* Phase 13j: documented in the ppok/rest-call description that WP REST's native `params._fields` is already honored via pass-through (no plugin code change needed) — verified working on dotfoundry where slim `_fields` responses meant the agent never had to dump to a file. The example() payload now demonstrates `_fields`.
* Phase 20: maintenance — split `.context/project.json` into a tree of focused Markdown files (README + principles + plugin + licensing + architecture + sftp + git + structure + build + upstream-findings + completed + phase-evolution + todo-policy + features + enhancements). Editability + GitHub rendering + grep all improve; the single ~74KB file had grown past comfortable JSON-editing thresholds. CLAUDE.md updated to point at the new `.context/README.md`; five PHP doc-comments updated to cite the new file locations.

= 0.4.0 =
* Phase 5: new read-only ppok/blocks-registered ability — dumps WP_Block_Type_Registry as {name, title, description, category, is_dynamic, attributes_schema}. Optional namespace-prefix filter.
* Phase 5: new read-only ppok/blocks-find-usage ability — given a canonical block_name, returns posts containing it with per-post counts. SQL LIKE prefilter + parse_blocks recursive walk so nested usage counts and innerHTML false-positives don't. Core-namespace shortform alias handled.
* Phase 5: new read-only ppok/patterns-registered ability — dumps WP_Block_Patterns_Registry (core + theme + plugin-registered) with full content; optional category filter; include_content=false enumerates the catalog cheaply.
* Discovery hygiene: Abilities_List aggregator now also surfaces the example() payload for ppok/preview-url (Phase-4 omission).

= 0.3.0 =
* Phase 4: new ppok/preview-url ability — mints a short-lived (5-minute) HMAC-signed URL that bypasses the draft/pending/private redirect for visual review without admin login. Write-flagged so every mint is audited and admin opt-in via abilities_enabled is required. Companion pre_get_posts handler expands post_status when a valid token is on the request.

= 0.2.1 =
* Phase 3: rest-call slim default response (strips yoast_head, yoast_head_json, permalink_template, class_list, generated_slug) — pass params.include_extended=true to keep them.
* Phase 3: rest-call cursor pagination — collection responses with more pages return has_more + opaque next_cursor; pass back as params.cursor.
* Phase 3: new ppok/abilities aggregator — one call returns {name, title, description, input_schema, output_schema, example} for every registered ability.
* Phase 6 partial: new write ability ppok/set-post-blocks — accepts a parsed block tree, runs serialize_blocks + wp_update_post, returns revision_id for one-call rollback. Gated by L1 cap + L3 block-allowlist + L4 plugin-options scope (write_post_types, write_post_statuses).
* Phase 6 partial: new write ability ppok/replace-in-content — block-aware bulk find/replace; dry_run defaults true; capped at 500 candidate posts per call; opt-in attrs_text_keys parameter to traverse block attrs (Gutenbricks, ACF Blocks).
* Transport defenses: rest-call returns structured response_too_large WP_Error (default 1MB cap, filterable) instead of letting the bridge drop with "Connection closed"; try/catch around dispatch surfaces PHP exceptions as rest_call_dispatch_exception; correlation_id stamped onto every WP_Error data array; Event_Logger caps payload at 256KB with a stub.
* Bug fix: replace-in-content schema now uses anyOf for post_type/post_status (oneOf mis-validated valid string input on WP REST).

= 0.2.0 =
* Wire Request_Context at rest_pre_dispatch so audit rows carry correlation_id, mcp_client, and ip (previously NULL on every row).
* Fix Activator option seeds to match the v1 settings keys (abilities_enabled, write_post_types, write_post_statuses, block_allowlist_mode, block_curated_allowlist, log_retention_days).
* Settings page rebuilt on the WP Settings API; abilities_enabled toggles meta.mcp.public per write ability at registration.
* New Tools → Ppok Activity viewer over wp_ppok_ai_events with filters, free-text reason search, and a row-detail screen.

= 0.1.0 =
* Initial scaffold.
