Skip to content

feat: GravityKit abilities plane (dynamic product tools via Foundation) + credential-aware auth#5

Merged
zackkatz merged 37 commits into
mainfrom
feature/abilities-api
Jun 16, 2026
Merged

feat: GravityKit abilities plane (dynamic product tools via Foundation) + credential-aware auth#5
zackkatz merged 37 commits into
mainfrom
feature/abilities-api

Conversation

@zackkatz

@zackkatz zackkatz commented Jun 15, 2026

Copy link
Copy Markdown
Member

Summary

Adds a second, independent capability plane to the MCP server and hardens the whole package for release.

  • Plane A — Gravity Forms (gf_*), primary. 26 static tools over the GF REST API v2 (forms, entries, feeds, notifications, submissions, fields). Works on any Gravity Forms site.
  • Plane B — GravityKit (dynamic), secondary. Tools generated at runtime from the connected site's GravityKit Foundation Abilities catalog — each product registers under its own server-owned prefix. GravityView is the first product (prefix gv_*); other add-ons appear automatically. Built-in gk_reload_abilities refetches the catalog.

The two planes initialize and degrade independently. Much of the non-feature work here was surfaced by live-testing against a real WordPress site (minted with Siteminter) and an adversarial Codex review.

What's new — Plane B (the headline)

  • src/abilities/loader.jsloadAbilitiesAsTools() builds product tools from the live catalog: Foundation (/wp-json/gravitykit/v1/abilities) → WP-core (/wp-json/wp-abilities/v1/abilities) fallback → throw + self-heal. Server-owned tool names (each product's mcp_prefix); paginated.
  • src/wp-client.jsWordPressClient, a product-agnostic authenticated WP transport (app-password Basic). Refuses Basic over a remote plain-HTTP URL unless GRAVITY_FORMS_ALLOW_HTTP_BASIC_AUTH=true.
  • Background catalog load with tools.listChanged; per-call self-heal.
  • src/gravityview/ (inspector-client + view-validator) is a test/demo harness, not a runtime dependency.

Plane A — auth + feeds

  • Credential-aware auth: app-password creds → Basic (HTTPS or local); ck_/cs_ keys → Basic on HTTPS, OAuth 1.0a on plain HTTP. OAuth signing fixed for array/nested query params.
  • gf_list_feeds tolerates a site with no feed add-on (GF's missing_table WP_Error → []).

Hardening (live testing + Codex review)

  • Plane independence: WP plane no longer gated on the GF REST probe; gf_* tools advertised only when the GF plane is live; call dispatch routes by ability-handler-map membership (any product prefix), not a hard-coded gv_ check.
  • Naming: the cross-product reload tool is gk_reload_abilitiesgv_ is GravityView's prefix only.
  • Stricter validation (integer layoutGridColumnSpan; non-empty/number field_id), paginated catalog read in the tool-name verifier, and assorted test-robustness fixes.

Packaging

  • files allowlist now ships runtime only (src minus tests, mcp.json, .env.example, README, LICENSE, CLAUDE.md, AGENTS.md). Deleted the dead .npmignore; tests relocated to top-level test/. Tarball 61 → 31 files (no tests/dev scripts).
  • publint (npm run lint:package) + an offline doc-freshness guard (npm run lint:docs) wired into prepublishOnly.
  • User-Agent single-sourced from package.json (src/version.js).

Docs

  • AGENTS.md is the single canonical doc; CLAUDE.md re-exports it via @AGENTS.md. Two-plane architecture documented; no fragile file:line citations (the doc guard enforces this and repo-map coverage).
  • README: npx @gravitykit/mcp install/config, the GravityKit plane documented, gv_ vs GravityKit prefix corrected.
  • TDD is required (RED→GREEN) per AGENTS.md; src/index.js plane/list/dispatch logic extracted to src/server-runtime.js so it's unit-testable.

Tests

  • Offline (prepublishOnly): test:unit 269 · test:node 165 · test:views 27 · test:field-validation 20 · publint · lint:docs — all green.
  • Live (minted WP site): integration 27/27; verify:tool-names 49/49 names match the catalog; MCP smoke (SDK client → node src/index.js) lists 76 tools (26 gf_ + 49 gv_ + gk_reload_abilities) and dispatches gv_* tools.

Notes

  • main already merged in (ba6cc40).
  • Codex review output lives under reports/ (gitignored / untracked, not shipped).

zackkatz and others added 18 commits May 15, 2026 12:13
Adds 30 gv_* tools alongside the existing gf_* surface, covering full
View authoring against the GravityView Inspector REST API.

Authoring flow: gv_create_view (defaults to gravityview-layout-builder,
accepts per-zone template_ids for Multiple/Single Entry divergence) →
gv_create_grid_row (surface=fields|widgets) → gv_apply_view_config for
bulk one-shot writes OR surgical tools (gv_add_view_field /
gv_patch_view_field / gv_move_view_field) for incremental edits. For
search bars: gv_add_search_field / gv_patch_search_field /
gv_remove_search_field write the modern search_fields_section tree;
existing legacy widgets auto-migrate on first save through this API.

Discovery: gv_list_templates / gv_list_widgets / gv_list_grid_row_types
/ gv_list_widget_zones / gv_list_search_zones / gv_list_view_forms /
gv_list_available_fields / gv_get_view_areas / gv_get_view_field_schemas.
gv_get_field_type_schema dispatches by kind (field | widget |
search_field) so one tool covers every type catalogue.

Move semantics borrowed from block-mcp: to.before_slot / to.after_slot
for ref-relative placement, position="start"|"end"|integer for symbolic.
Concurrency via ifMatch="auto" pulls from a per-view version cache.
Compact responses by default (compact=false for raw).

Validator pre-flights structural mistakes (mode enum, required fields,
LB area row_uid existence) and optionally schema-aware setting validation
when validateAgainstSchemas: true is passed.

Files:
- src/gravityview-client.js: HTTP client (Basic auth via WP App
  Password, ETag/If-Match cache, all endpoint methods).
- src/view-operations/{index,view-validator}.js: tool definitions +
  handler routing + client-side validation.
- src/index.js: server bootstrap, gv_* dispatch via wrapViewHandler
  that surfaces the inspector REST envelope (gv_rest_* error codes)
  rather than generic axios errors.
- src/tests/views.test.js: 25 tests covering construction, auth
  fallback, discovery, create + version cache, apply with If-Match,
  Layout Builder area-key encoding, allow-delete guard, render
  GET-vs-POST, and validator (structural + schema-aware).
- package.json: test:views script wired into test:all.
Replaces the hand-maintained viewToolDefinitions array with a dynamic
loader: on MCP startup, fetch /wp-json/wp-abilities/v1/abilities, filter
to the gk-gravityview/* namespace, and synthesise a {name, description,
inputSchema} tuple per ability. HTTP method is derived from annotations
(readonly→GET, destructive→DELETE, otherwise POST). Tool naming is the
existing convention: gk-gravityview/list-layouts → gv_list_layouts.
The hand-maintained tool defs stay as a fallback for older WP installs
without the abilities-api package or when the catalog is unreachable.

GET/DELETE inputs ship as bracketed query params (input[key][nested]=v)
so WordPress REST rebuilds the nested object straight off the URL —
the abilities-api controller otherwise hands ?input=… straight to the
schema validator and complains "input is not of type object".

Drops the GRAVITYVIEW_ALLOW_DELETE env gate now that no "delete the
whole View" ability exists; field/widget/row removal is normal authoring
(reversible by re-adding) and status: trash is the only soft-remove
path, gated server-side by the WP delete_post capability. The
GRAVITY_FORMS_ALLOW_DELETE gate stays for entry/form/feed deletes —
those genuinely destroy data.

Adds src/tests/views-stress.test.js — 114 live tests against dev.test
covering the full catalog: hostile payloads, optimistic-concurrency
edge cases, schema-driven validation per setting type, search-field
domain delegation, area-key prefix/length/control-char validation,
duplicate-view + set-view-status round-trips, and an anti-test
proving no permanent-delete ability is registered.

Adds demo-abilities.mjs — a cold-start walk through the abilities
surface for onboarding + manual smoke tests.
Adds 9 new live stress tests covering the new abilities + behaviour
shipped in the GravityView feature/3.0-config-editor branch:

* gv_list_views — substring search lands a freshly-minted View;
  form_id filter narrows correctly; pagination metadata round-trips.
* gv_get_view_config `include` projection — narrows the response to
  just the requested top-level keys; view_id always present.
* Dry-run on gv_apply_view_config — meta unchanged after dry; flag +
  would_apply stamped on the response.
* Dry-run on gv_patch_view_field — second write at dry_run=true does
  not overwrite the value persisted by the prior real write.
* Dry-run on gv_add_view_field — slot count unchanged after dry-run.
* Catalog: every gk-gravityview ability advertises a non-empty
  next_steps array, each entry is { ability:gk-gravityview/*, when:* }.
* Discovery bridge: list-layouts has_grid description names
  list-grid-row-types AND list-view-areas as the next steps.
* Field presets: default catalog is empty (filter-driven, core ships
  none).
* Field presets: apply-field-preset returns 404 for an unknown
  preset id.

123/123 passing.
…ess tests

Loader change:
* abilities-loader.js now accepts an array of namespace prefixes
  (default ['gk-gravityview', 'gk-multiple-forms']) instead of a
  single string. Both namespaces collapse into the `gv_*` MCP tool
  prefix because they're conceptually one product family from the
  agent's point of view. Backward compatible — string args still
  work via the array-or-string normalisation at the top of
  loadAbilitiesAsTools.

Tests:

Basic coverage (12 tests):
* catalog exposes gv_list_joins / gv_apply_joins / gv_list_joinable_fields
* list-joins on a no-joins View returns empty + count=0
* list-joinable-fields enumerates form fields + entry-property aliases
* apply-joins dry_run flags response + does NOT persist
* apply-joins persists + list-joins inflates form/field labels
* apply-joins is replace-not-merge (3 → 1 → 0 cycle)
* apply-joins rejects malformed rows with HTTP 400 (no partial write)
* apply-view-config writes joins via the cross-plugin filter
* get-view-config include=[joins] projection narrows shape
* list-views match_joined surfaces Views joining a form
* list-available-fields includes joined_form_fields tagged with form_id
* every gk-multiple-forms/* ability advertises a next_steps annotation

Deep authoring coverage — exercises mixed-form field placement
in real View areas (5 tests):
* field slots from primary AND joined forms coexist in one area
  (with field-id collision: both forms have id=1)
* 3-form join + fields from each form land in distinct areas
* apply-view-config bulk: joins + fields from both forms in one call
* dry_run on mixed-form bulk apply does NOT persist any slot
* apply-joins clears + replaces, list-joins reflects each step

Total: 140 passed, 0 failed (was 123 baseline → +17 MFV tests).
All run live against dev.test admin/admin.
The startup-time blocking load was sticky on failure: once the cert /
network / WP-not-yet-booted path failed, abilityToolDefinitions stayed
null for the lifetime of the Node process and only Claude Code restart
(not /mcp reconnect, which doesn't re-fork) recovered.

This rewires the loader to:

1. Fire-and-forget eager kickoff in initializeClient() — no startup
   latency, no blocking the MCP handshake on a slow / down WP.
2. Single-flight ensureAbilitiesLoaded() promise. Concurrent callers
   share it; on rejection the cache clears so the NEXT call retries.
3. ListTools awaits up to 2s — covers a warm cold-start fetch on
   dev.test (~800ms) without ever feeling like a hang.
4. Every gv_* tool call awaits with no timeout (caller is willing).
   Cache hit on the success path = zero overhead. Failures self-heal
   on next call (sleep/wake, valet still booting, cert mid-fix all
   recover without an MCP restart).
5. tools/list_changed notification on successful load — clients
   refetch the catalog so abilities-derived schemas (joins on
   apply-view-config, gv_apply_joins, gv_list_joins) replace the
   legacy ones in their cache.
6. New gv_reload_abilities tool — manual escape hatch when you fixed
   a WP issue and want the refresh now without firing another gv_*.

Verified: 140/140 views-stress tests pass against dev.test.
Tools 29–36 (auto-generated from the abilities catalog) emitted
`inputSchema` as a raw parameter array; another tool emitted
`properties` as an array (PHP-empty-assoc → JSON `[]`). Both forms
fail Zod validation during MCP `tools/list`, which broke every
gv_* tool on reconnect.

`normalizeInputSchema()` in abilities-loader coerces both shapes
into `{ type: "object", properties: <Record<string,JSONSchema>>, … }`,
deriving keys from each descriptor's name/slug/key when wrapping an
array, lifting `required: true` to the outer `required` array, and
preserving any sibling JSON Schema keys (additionalProperties,
description, etc.).

Adds src/tests/abilities-loader.test.js (16 tests) including the
MCP-contract assertion every generated tool's inputSchema must
satisfy: object root, type === 'object', properties is a non-array
object. Reproduces both wire-format failures via a synthetic
catalog so we never regress on the empty-properties or
array-input-schema shapes again.
Source chain: Foundation catalog (/wp-json/gravitykit/v1/abilities) first
— server-side GK filtering, disabled omitted, paginated — with WP core
(/wp-abilities/v1/abilities) as fallback for connections that can't pass
the catalog's manage_options gate, filtered on meta.gk_registered_by.

The server now owns tool naming exclusively (mcp_tool_name from
Foundation's MCP_TOOL_PREFIXES). Removed the client-side gv_ name
derivation and the hardcoded namespace allow-list — both were compat
with code that never shipped (main/npm 2.1.1 are GF-only, no abilities).
Abilities arriving without mcp_tool_name are skipped with a warning.

Removed the never-released hand-maintained gv_* tool definitions and
handlers (src/view-operations/index.js) and all fallback wiring; the
abilities catalogs are the only source of gv_* tools. When unreachable,
gv_* tools are absent and the per-call self-heal / gv_reload_abilities
retries.

Added a tool-name collision guard (first claimant wins, later ones
logged and skipped) and updated the server instructions string to the
ability-derived tool names (gv_view_create, gv_layouts_list, …).

Requires server-side: Foundation to stamp mcp_tool_name into ability
meta (for the WP-core fallback path) and/or relax the gravitykit/v1
catalog permission so non-admin authors can list abilities.
src/abilities/loader.js   (was src/view-operations/abilities-loader.js)
  The dynamic tool pipeline is product-agnostic — it serves every
  GravityKit product from the Foundation catalog, so it no longer
  lives under a GravityView-named folder.

src/wp-client.js (new)    WordPressClient
  Extracted the authenticated WP-root transport (base URL, app-password
  auth, TLS, timeout) that the abilities loader rides. Runtime no
  longer constructs anything GravityView-specific.

src/gravityview/          (product-specific test harness)
  inspector-client.js     GravityViewInspectorClient extends
                          WordPressClient, mounts /gravityview/v1.
                          Test/demo harness only — the Inspector routes
                          are registered server-side solely under
                          DOING_GRAVITYVIEW_TESTS. Also fixed three
                          stale ability slugs (available-fields-get,
                          field-type-schema-get, search-input-types-list).
  view-validator.js       moved alongside its product harness.

Env vars renamed (never released): GRAVITYVIEW_BASE_URL → GRAVITYKIT_WP_URL,
GRAVITYVIEW_WP_USERNAME/_APP_PASSWORD → GRAVITYKIT_WP_USERNAME/_APP_PASSWORD,
GRAVITYVIEW_TIMEOUT → GRAVITYKIT_TIMEOUT. GF fallbacks unchanged.
Documented in .env.example.

Tests: 264 unit + 21 loader, all passing.
…ation

Plane A (Gravity Forms): the 26 static gf_* tools over GF REST v2 —
works on any Gravity Forms site, no Foundation/WP 6.9 required. Tool
literals extracted to GF_TOOL_DEFINITIONS at module scope.

Plane B (GravityKit abilities): dynamic tools from the Foundation
catalog (all GK products) with WP-core fallback. Lights up only when
Foundation is active on the connected site.

Changes:
- initializeClient() split into per-plane initializers. A GravityView
  site without GF REST keys now still gets abilities tools; a GF-only
  site is unaffected. Throws only when NEITHER plane has credentials.
  Per-plane 60s retry cooldown prevents re-validation storms.
- Abilities load failures now back off for 60s instead of re-fetching
  on every tools/list (Foundation-less sites no longer pay two failed
  requests per list, forever). gv_reload_abilities bypasses cooldown.
- RESERVED_TOOL_NAMES: the loader's collision guard is seeded with all
  built-in tool names, so a future catalog-served gk-gravity-forms
  ability can never shadow the released gf_* contract.
- Server instructions now state plane availability (gf_* works
  anywhere; product tools require Foundation on the site).
Four interlocking changes that get the live stress suite green:

1. methodForAbility (loader.js): require BOTH `destructive` AND
   `idempotent` for DELETE. Foundation's run controller (in WordPress
   core's wp-rest-abilities-v1-run-controller) only accepts DELETE
   when both flags are set, matching WP-REST conventions. view-delete
   is destructive but soft-trashes by default (force=false), so it is
   not idempotent — sending DELETE got a 405. Now POSTs.

2. abilities-loader.test.js: extend the methodForAbility unit test to
   cover the new logic, with an explicit regression case for
   `destructive + !idempotent → POST`.

3. views-stress.test.js: re-apply the verb-first → noun-first rename
   across 243 call sites (gv_apply_view_config → gv_view_config_apply
   etc.). Foundation's `/wp-json/gravitykit/v1/abilities` endpoint
   emits the canonical `mcp_tool_name` in noun-first shape, and the
   loader (post-7474ab0) treats that as authoritative.

4. views-stress.test.js: correct three stale assertions that no longer
   match Foundation's current contract:
   - "NO permanent-delete ability exists" → reframed as "default
     invocation soft-trashes" (mode=trash, deleted=true, force=false).
     view-delete IS shipped, with a safe-by-default soft-delete path.
   - dry-run response no longer flagged `would_apply` — Foundation
     FIX-22/66 (commit 05053d3d on the Foundation feature branch)
     intentionally dropped it as redundant with `dry_run`.
   - discovery-bridge test now looks up `gk-gravityview/layouts-list`
     (was `list-layouts`) and expects `grid-row-types-list` /
     `view-areas-get` in the has_grid description (was the old
     `list-grid-row-types` / `list-view-areas`).
- scripts/stress-abilities.mjs: synthetic 1,205-item paginated catalog
  exercising the products-filter naming model (declared gv_* prefixes +
  full-product-slug fallback names), collision/reserved-name guards,
  disabled/unnamed/foreign filtering, all schema normalization shapes,
  GET/POST/DELETE execution wire formats, the stamped meta.mcp_tool_name
  WP-core fallback path, and the empty-catalog self-heal throw.
  19 checks; ~1ms per 1,205-item load, >1M handler calls/s.
- loader.js: update comments — tool prefixes now come from each product's
  required mcp_prefix on gk/foundation/abilities/products (full-slug
  fallback), and mcp_tool_name is stamped into ability meta on both
  catalogs. No code changes needed: the loader was already shape-compatible.
Cross-checked against gravityforms/gravityforms#3716 (25 abilities +
bundled MCP Adapter, registered on wp_abilities_api_init with
show_in_rest:true under the gravityforms/* namespace).

- Coexistence test: GF's abilities DO appear in the WP core catalog our
  fallback path reads; pin that the gk_registered_by metadata filter
  excludes them (core gravityforms/* and the two-slash add-on
  convention gravityforms/{addon}/{action}).
- Layout grid normalization in FieldManager (ported from
  GF_Abilities_Handler_Forms::normalize_layout_group_ids): friendly
  layoutGroupId names hash to the editor's 8-char hex format — stable
  per form so sequential gf_add_field calls can share a row — and
  layoutGridColumnSpan clamps to the 1-12 grid.
- Doc fix: loader header now states the destructive+idempotent DELETE
  rule (the methodForAbility code/tests were already fixed).

Note: field-manager.test.js has 11 pre-existing failures under direct
node --test (stale mock response shapes); not run by the custom runner.
createMockApiClient predated two client changes: getForm() resolving
{ form } and the 1.4.1 replaceForm() direct-PUT path. All 11 failures
were mock drift, not product bugs — field-manager.test.js now 31/31.

New test:field-ops script runs the four node:test-based files
(field-manager, field-registry, field-dependencies, field-positioner),
which were invisible to both the custom runner and test:all. Chained
into test:all so they can't silently rot again.
…y-feeds normalization

Found by running the live integration suite against a minted GF 2.10.3
site (Siteminter, http://localhost — the OAuth fallback path):

- OAuth signatures stringified arrays ({ include: [3] } signed as
  include=3) while axios sent include[]=3 on the wire — every OAuth GET
  with array params failed with 'invalid signature'. Affects released
  2.1.1 on any non-HTTPS connection. Fix: shared flattenParams() turns
  params into PHP bracket-index pairs (include[0]=3, paging[page_size]=2)
  used by BOTH the signature base and a matching axios paramsSerializer,
  with strict RFC 3986 encoding (rawurlencode parity: !'()* escaped) per
  RFC 5849 §3.4.1.3.2 (encode, then sort by encoded name/value).

- listFeeds: GF returns a serialized WP_Error ({errors:{not_found:[...]}})
  with HTTP 200 when a site has no feeds — normalize to feeds: [] so
  callers always get an array.

Verified: full live integration suite 24/24 against GF 2.10.3 over
OAuth; unit 266, auth 22, feeds 25, field-ops 131 — all green.
…Auth

Basic auth no longer hard-requires HTTPS client-side. Selection now
matches what the Gravity Forms server actually accepts:

- ck_/cs_ key pairs on plain HTTP sign with OAuth 1.0a automatically —
  GF only checks key-pair Basic auth over SSL
  (class-gf-rest-authentication.php: if ( is_ssl() )), so Basic with
  keys on http authenticates as nobody.
- WordPress app-password credentials (username + application password)
  use Basic on local URLs (localhost, *.localhost, 127.x, ::1, *.test,
  *.local) with no opt-in — WP core authenticates them and GF's
  capability checks take over. No OAuth involved.
- Explicit GRAVITY_FORMS_AUTH_METHOD always wins; remote-HTTP Basic
  needs GRAVITY_FORMS_ALLOW_HTTP_BASIC_AUTH=true and logs a warning.

Integration harness: honor GRAVITY_FORMS_TEST_AUTH_METHOD and stop
forcing an explicit 'basic' default that defeated auto-selection.

New security coverage (skip cleanly without fixtures): unauthenticated
requests denied; authenticated user without GF capabilities denied;
read-only API key can read but not write.

Live verification on minted GF 2.10.3 (Siteminter, http://localhost):
27/27 in all four configurations — auto→OAuth (keys), auto→Basic (app
password), explicit oauth, explicit basic. Mocked suites: unit 269,
auth 25, all green.
App passwords are now the recommended first-run credential (README,
.env.example, AGENTS.md): one credential powers gf_* and, with
Foundation, the GravityKit product tools; access follows WP
capabilities. GF API keys move to the scoped-access (e.g. read-only)
path. Documented the one unavoidable GF requirement — 'Enable access
to the API' gates route registration for every credential type.

Removed the active GRAVITY_FORMS_AUTH_METHOD=basic line from
.env.example: with explicit method now honored everywhere, shipping it
as a default forces Basic on remote HTTP. Replaced the stale
'silently falls back to OAuth' gotcha with the credential-aware rules.
Brings in cfd5261 (AI-facing input hints for server instructions and
tool descriptions).

Conflict resolution — src/index.js:
- Server `instructions`: kept the feature-branch version. It already
  documents the same GF input behavior main added ("checkbox/multiselect
  arrays auto-normalized; multiselect values with commas get split by GF
  REST API") plus the full GravityView/abilities tool guidance.
- gf_create_entry / gf_update_entry: re-applied main's improved
  descriptions into the refactored GF_TOOL_DEFINITIONS array (feature
  extracted the inline tool list into that constant).

src/field-operations/index.js merged cleanly (main's gf_list_field_types
summary-mode entry_input hints preserved).

GF tool set verified identical (22 tools) before/after; no tools dropped.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds GravityView Abilities API integration: a new WordPressClient transport, an src/abilities/loader.js that dynamically converts WordPress ability catalogs into MCP tools, a GravityViewInspectorClient with full view CRUD and optimistic concurrency, a ViewValidator, and FieldManager layout property normalization. The MCP server is refactored from eager single-plane to two-plane lazy/self-healing initialization. Auth selection gains credential-aware Basic/OAuth1 routing and RFC3986 encoding. Tests, demo scripts, verification tooling, package wiring, and documentation are updated throughout.

Changes

GravityView Abilities Integration & Auth Overhaul

Layer / File(s) Summary
Auth selection, RFC3986 encoding, and WP/GF transports
src/config/auth.js, src/wp-client.js, src/gravity-forms-client.js
Adds isLocalUrl, HTTP-permissive BasicAuthHandler, rfc3986Encode/flattenParams for OAuth signing and query serialization, credential-aware AuthManager auto-selecting Basic vs OAuth1, a new WordPressClient with multi-source credential resolution, and GravityFormsClient updates for dual self-signed-cert env vars and listFeeds WP_Error normalization.
Abilities loader: catalog fetch, schema normalization, and tool generation
src/abilities/loader.js
Adds methodForAbility, normalizeInputSchema for coercing WordPress input_schema shapes, paginated Foundation catalog fetch with WP-core fallback, collision/reserved-name guards, and executeAbility routing GET bracket params vs POST body.
GravityViewInspectorClient, ViewValidator, and views tests
src/gravityview/inspector-client.js, src/gravityview/view-validator.js, test/views.test.js
Adds GravityViewInspectorClient (discovery, per-view reads, view create/apply with If-Match concurrency, surgical field/grid-row/search-field/widget CRUD, assertSearchInputType with cached validation) and ViewValidator (structural and schema-aware payload validation). Tests cover constructor, CRUD routes, ETag caching, surgical ops, and validator schema resolution.
FieldManager layout property normalization
src/field-operations/field-manager.js, test/field-manager.test.js
Adds normalizeLayoutProperties to clamp layoutGridColumnSpan (1–12) and hash layoutGroupId to 8-char hex via MD5. Called in addField and updateField. Tests cover hashing stability, collision avoidance, clamping, and empty/missing passthrough.
MCP server dual-plane lazy init and gv_* routing
src/index.js
Rewrites server from eager single-plane to two-plane lazy/self-healing init with ensureAbilitiesLoaded (single-flight, cooldown, tools/list_changed). tools/list returns combined GF + field-ops + gv_* + gv_reload_abilities. CallTool adds wrapViewHandler and default dispatch for gv_* through abilityToolHandlers. Startup defers initialization to first tool request.
Abilities loader tests and stress script
test/abilities-loader.test.js, scripts/stress-abilities.mjs, test/run.js
Adds comprehensive normalizeInputSchema unit tests, loadAbilitiesAsTools integration tests (route selection, filtering, collision, pagination, MCP contract), methodForAbility smoke tests, and a synthetic stress runner validating catalog edge cases, handler wire shapes, throughput, fallback, and empty-catalog throw behavior.
Demo script and tool-name verification script
demo-abilities.mjs, scripts/verify-tool-names.mjs
Adds end-to-end demo-abilities.mjs exercising catalog discovery, method routing, view create/apply/read/stale-write round-trip, and cleanup. Adds scripts/verify-tool-names.mjs that cross-checks tool-name references in docs/source against the live WordPress catalog, exiting non-zero on mismatches.
Integration and authentication tests
test/integration.test.js, test/authentication.test.js
Adds security integration tests for unauthenticated, low-privilege, and read-only access. Reworks MailChimp feed creation to skip gracefully on missing add-on. Adds AuthManager credential-selection tests for explicit-basic, key-pair OAuth fallback, and app-password auto-selection.
Package wiring, publish config, and test import path fixes
package.json, .npmignore, test/*.test.js
Switches all test entrypoints from src/tests/* to test/*, adds test:field-ops, integrates into test:all/prepublishOnly, adjusts files whitelist, adds publint, removes .npmignore. Updates all test import paths from root-relative to ../src/....
Documentation, env templates, and manifest updates
README.md, AGENTS.md, CLAUDE.md, .env.example, mcp.json, .mcp.json
Updates tool count to 26, adds Authentication Flow docs, corrects GRAVITY_FORMS_ALLOW_SELF_SIGNED_CERTS variable name, removes gf_list_form_feeds, expands .env.example with abilities/security sections, removes rate-limiting variables from manifests, and adds verify:tool-names release step to AGENTS.md.

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant MCPServer as src/index.js
  participant AuthManager
  participant WordPressPlane as WordPressClient
  participant ensureAbilitiesLoaded
  participant AbilitiesLoader as src/abilities/loader.js
  participant WP_Abilities_API

  MCPClient->>MCPServer: tools/list
  MCPServer->>AuthManager: initializeGravityFormsPlane (lazy)
  MCPServer->>WordPressPlane: initializeWordPressPlane (lazy)
  MCPServer->>ensureAbilitiesLoaded: start with 2s timeout
  ensureAbilitiesLoaded->>AbilitiesLoader: loadAbilitiesAsTools(wpClient, {reservedNames})
  AbilitiesLoader->>WP_Abilities_API: GET /gravitykit/v1/abilities (Foundation, paginated)
  alt Foundation reachable
    WP_Abilities_API-->>AbilitiesLoader: catalog pages
  else Foundation 404/empty
    AbilitiesLoader->>WP_Abilities_API: GET /wp-abilities/v1/abilities (WP-core)
    WP_Abilities_API-->>AbilitiesLoader: catalog filtered by gk_registered_by
  end
  AbilitiesLoader->>AbilitiesLoader: normalizeInputSchema + collision guard + build handlers
  AbilitiesLoader-->>ensureAbilitiesLoaded: {definitions, handlers}
  ensureAbilitiesLoaded-->>MCPServer: cached tools
  MCPServer-->>MCPClient: GF tools + field-ops + gv_* tools + gv_reload_abilities

  MCPClient->>MCPServer: call gv_view_create {title, form_id}
  MCPServer->>ensureAbilitiesLoaded: self-heal check
  MCPServer->>AbilitiesLoader: abilityToolHandlers.get("gv_view_create")
  AbilitiesLoader->>WP_Abilities_API: POST /wp-abilities/v1/abilities/gv_view_create/run {input}
  WP_Abilities_API-->>AbilitiesLoader: {data, etag}
  AbilitiesLoader-->>MCPServer: result via wrapViewHandler
  MCPServer-->>MCPClient: tool result
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/abilities-api

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
AGENTS.md (1)

127-127: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update tool count from 28 to 26.

Line 127 still mentions "All 28 tool descriptions" but the tool count is now 26 (after removing gf_list_form_feeds). This should read "All 26 tool descriptions" for consistency with the rest of the document and the actual tool set.

Proposed fix
-- **Concise tool descriptions**: All 28 tool descriptions and property descriptions are terse to reduce tool-list overhead
+- **Concise tool descriptions**: All 26 tool descriptions and property descriptions are terse to reduce tool-list overhead
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 127, Update the tool count reference in the line
containing "All 28 tool descriptions and property descriptions are terse" by
changing "28" to "26" to reflect the current number of tools after the removal
of gf_list_form_feeds, ensuring the documentation accurately represents the
actual tool set.
🧹 Nitpick comments (2)
.env.example (1)

57-58: 💤 Low value

Reorder environment variables for consistency.

Line 58 (GRAVITY_FORMS_MAX_RETRIES) should appear before line 57 (GRAVITY_FORMS_TIMEOUT) to maintain alphabetical/logical ordering, as flagged by dotenv-linter.

Proposed reordering
-GRAVITY_FORMS_TIMEOUT=30000
-GRAVITY_FORMS_MAX_RETRIES=3
+GRAVITY_FORMS_MAX_RETRIES=3
+GRAVITY_FORMS_TIMEOUT=30000
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 57 - 58, The environment variables in .env.example
are not in alphabetical order. Swap the positions of GRAVITY_FORMS_MAX_RETRIES
and GRAVITY_FORMS_TIMEOUT so that GRAVITY_FORMS_MAX_RETRIES (line 57) appears
before GRAVITY_FORMS_TIMEOUT (line 58), maintaining alphabetical consistency as
required by dotenv-linter.

Source: Linters/SAST tools

src/gravity-forms-client.js (1)

28-29: 💤 Low value

User-Agent version mismatch.

GravityFormsClient uses version 2.1.0 while WordPressClient uses 2.1.1. These should be consistent, ideally derived from a single source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gravity-forms-client.js` around lines 28 - 29, The User-Agent header in
GravityFormsClient is set to version 2.1.0 but WordPressClient uses 2.1.1,
creating an inconsistency. Extract the version number into a shared constant
(e.g., a VERSION constant or from package.json) and update both
GravityFormsClient and WordPressClient to reference this single source of truth,
ensuring they both report the same version in their User-Agent headers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@demo-abilities.mjs`:
- Around line 9-15: Replace the absolute file paths in the import statements for
WordPressClient and the loader functions with relative imports that will work
from any machine, using relative path syntax like '../src/wp-client.js' and
'../src/abilities/loader.js' based on the script's location. Additionally,
update the documentation comment at the beginning of the file to correct the
filename reference from '/tmp/abilities-demo.mjs' to 'demo-abilities.mjs' and
clarify the correct command to run the script from the project root with the
correct relative path or how to invoke it properly so other developers
understand how to use this demo script.

In `@src/field-operations/field-manager.js`:
- Around line 227-233: In the layoutGridColumnSpan validation block, replace the
parseInt(field.layoutGridColumnSpan, 10) call with
Number(field.layoutGridColumnSpan) to strictly validate numeric values, and
change the Number.isFinite(span) check to Number.isInteger(span) to reject
floats and partial numeric strings like "6wide". This ensures only proper
integers are accepted and non-numeric values (including mixed strings) are
dropped via the delete statement. Note that Number("") returns 0 which will be
kept rather than dropped, but if strict rejection of empty strings is required,
you can add an additional check for non-empty strings before the Number
conversion.

In `@src/gravityview/inspector-client.js`:
- Around line 491-498: Fix indentation issues in
src/gravityview/inspector-client.js at two locations. At lines 491-498 in the
removeSearchField method, indent the lines containing const config, config.data,
const response, and the httpClient.delete call to properly align with the method
body indentation level. Apply the same indentation fix to lines 537-543 in the
removeViewWidget method, ensuring const config, config.data, const response, and
the httpClient.delete call are consistently indented to match the surrounding
method body structure.
- Around line 402-411: Fix the indentation inconsistency in the deleteGridRow
method. Review the entire method body starting from the comment about
axios.delete and ensure all lines (the config declaration, the if statement for
surface, the httpClient.delete call with its parameters, the cacheVersion call,
and the return statement) use consistent indentation that matches the
indentation style used in the rest of the class.
- Around line 345-351: The `removeViewField` method has inconsistent indentation
where the `const response` declaration line is missing leading indentation,
breaking the established code style. Add proper indentation (typically 4 spaces
or one tab level) to the `const response` line so it aligns consistently with
the other statements in the method body like `this.cacheVersion` and `return
response.data`.

In `@src/gravityview/view-validator.js`:
- Around line 104-106: The field_id validation in the error-checking condition
at line 104 is insufficient and allows invalid values like false, objects,
arrays, and whitespace-only strings to pass validation. Enhance the validation
check for field_id to ensure it is a non-empty string by verifying that it
exists, is of string type, and is not empty or whitespace-only. Replace the
current condition that only checks for the key existence and null/empty string
with a stricter validation that rejects falsy values, non-string types, and
whitespace-only strings before throwing the validation error.

---

Outside diff comments:
In `@AGENTS.md`:
- Line 127: Update the tool count reference in the line containing "All 28 tool
descriptions and property descriptions are terse" by changing "28" to "26" to
reflect the current number of tools after the removal of gf_list_form_feeds,
ensuring the documentation accurately represents the actual tool set.

---

Nitpick comments:
In @.env.example:
- Around line 57-58: The environment variables in .env.example are not in
alphabetical order. Swap the positions of GRAVITY_FORMS_MAX_RETRIES and
GRAVITY_FORMS_TIMEOUT so that GRAVITY_FORMS_MAX_RETRIES (line 57) appears before
GRAVITY_FORMS_TIMEOUT (line 58), maintaining alphabetical consistency as
required by dotenv-linter.

In `@src/gravity-forms-client.js`:
- Around line 28-29: The User-Agent header in GravityFormsClient is set to
version 2.1.0 but WordPressClient uses 2.1.1, creating an inconsistency. Extract
the version number into a shared constant (e.g., a VERSION constant or from
package.json) and update both GravityFormsClient and WordPressClient to
reference this single source of truth, ensuring they both report the same
version in their User-Agent headers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f09fcdf6-d652-49b3-b517-3338f271fd99

📥 Commits

Reviewing files that changed from the base of the PR and between cfd5261 and ba6cc40.

📒 Files selected for processing (24)
  • .env.example
  • .mcp.json
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • demo-abilities.mjs
  • mcp.json
  • package.json
  • scripts/stress-abilities.mjs
  • src/abilities/loader.js
  • src/config/auth.js
  • src/field-operations/field-manager.js
  • src/gravity-forms-client.js
  • src/gravityview/inspector-client.js
  • src/gravityview/view-validator.js
  • src/index.js
  • src/tests/abilities-loader.test.js
  • src/tests/authentication.test.js
  • src/tests/field-manager.test.js
  • src/tests/integration.test.js
  • src/tests/run.js
  • src/tests/views-stress.test.js
  • src/tests/views.test.js
  • src/wp-client.js

Comment thread demo-abilities.mjs Outdated
Comment thread src/field-operations/field-manager.js
Comment thread src/gravityview/inspector-client.js Outdated
Comment thread src/gravityview/inspector-client.js Outdated
Comment thread src/gravityview/inspector-client.js Outdated
Comment thread src/gravityview/view-validator.js Outdated
zackkatz added 5 commits June 15, 2026 17:07
The constructor throws 'WordPress client requires credentials…' but the
test asserted the substring 'WordPress credentials', which never appears.
Align with the actual message (and with the sibling base-URL test, which
asserts a literal substring of its error).
Gravity Forms returns a serialized WP_Error with HTTP 200 when feeds can't
be enumerated. listFeeds already normalized the not_found variant to []; a
fresh GF install with no feed-based add-on instead returns missing_table
(the wp_gf_addon_feed table only exists once a GFFeedAddOn's upgrade_base()
has run). Generalize the normalization to any HTTP-200 WP_Error so
gf_list_feeds always returns an array on any site.

Integration test: the "Create test feed" pre-check listed MailChimp feeds
to detect availability, but with the normalization above that now returns
{feeds: []} on a fresh site and the check falsely concluded MailChimp was
present, then hard-failed on create. Replace the brittle pre-check with an
attempt-and-skip: try the create, and skip (not fail) when the error shows
the add-on or its table isn't available. Verified live against a Siteminter
WP site across no-table, table-present, and add-on-inactive states (27/27).
The abilities catalog renamed tools from verb-noun to noun-verb
(list-layouts -> layouts-list, create-view -> view-create, etc.); the demo
still used the old ability and gv_* handler names and crashed at step 1c.
Update all names to match the live catalog, replace hardcoded absolute
import paths with relative ones, and make the View round-trip self-contained
by minting a throwaway form (when GRAVITYKIT_DEMO_FORM_ID is unset) and
cleaning up both the View and the form at the end. Verified end-to-end
against a live GravityView 3.0.0 site.
…i skip]

gf_list_form_feeds was removed (gf_list_feeds with form_id covers it); the
Response Shapes section still listed it as a tool. Verified every gf_*/gv_*
name in the server instructions, demo, README, AGENTS.md and mcp.json
against the 75 registered tools (26 gf_* + 49 live gv_*) — all match.
The published package was shipping 25 test files and 5 dev scripts: the
`files` allowlist listed `src/` and `scripts/` wholesale, and with a
`files` field present npm ignores `.npmignore` entirely — so its test/dev
excludes silently did nothing.

Packaging (industry-standard allowlist, no .npmignore):
- Relocate tests src/tests/ -> top-level test/ so the `src` allowlist entry
  no longer pulls them in. Rewrite their `../` imports to `../src/` and fix
  bug-fixes.test.js path anchors (srcDir/projectDir) for the new depth.
- Tighten `files` to a precise allowlist (src, mcp.json, .env.example,
  README, LICENSE, CLAUDE.md, AGENTS.md) and delete the dead `.npmignore`.
  Tarball: 61 -> 31 files, 0 tests, 0 dev scripts.
- Add publint (npm run lint:package) and a prepublishOnly gate that runs
  the offline suites + publint before publish (omits the live integration
  test so publishing never hits a real site).

Dev tooling:
- Add scripts/verify-tool-names.mjs: cross-checks every gf_/gv_ name in the
  server instructions, docs and demo against the tools the server actually
  registers (gv_* are generated from the live abilities catalog and can
  drift). Run via npm run verify:tool-names against a live site.
- Document packaging and the verifier in AGENTS.md.

Verified: full test:all incl. live integration green; npm pack --dry-run
clean (31 files); publint passes; verify:tool-names passes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
test/integration.test.js (2)

751-780: 💤 Low value

Silent .catch(() => {}) may mask unrelated initialization failures.

Line 766 swallows all initialization errors. If the read-only key is invalid or there's a network issue, the test proceeds and listForms may fail with a confusing error. Consider logging or at least checking the error type:

-  await roClient.initialize().catch(() => {});
+  await roClient.initialize().catch((e) => {
+    // Read-only keys may fail certain init checks; continue to test actual permissions
+    console.log(`  Note: initialize() threw: ${e.message}`);
+  });

This preserves observability while allowing the test to continue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration.test.js` around lines 751 - 780, The `.catch(() => {})` on
the roClient.initialize() call silently swallows all errors without any logging
or inspection, which can mask unrelated initialization failures and cause
confusing errors later when listForms is called. Replace the silent catch
handler with code that logs the error and optionally checks the error type
before deciding whether to suppress it. This will preserve observability of what
actually happened during initialization while still allowing the test to
continue to the read/write verification steps.

377-416: 💤 Low value

Regex pattern may not catch all MailChimp unavailability scenarios.

The regex at line 403 covers common cases but could miss variations like "addon_slug gravityformsmailchimp is not registered" or "Feed add-on not active". Consider also matching feed.*not or gravityformsmailchimp.*not patterns if the API returns those messages.

That said, the current regex is reasonable for known error formats, and the fallback behavior (re-throw) ensures genuine failures aren't silently ignored.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration.test.js` around lines 377 - 416, The regex pattern assigned
to the unavailable constant needs to be enhanced to catch additional error
message variations that may be returned by the MailChimp API. The current
pattern covers common cases like "table does not exist" and "not installed", but
should also match variations such as "feed" followed by "not" (for messages like
"Feed add-on not active") and "gravityformsmailchimp" followed by "not" (for
messages like "addon_slug gravityformsmailchimp is not registered"). Update the
regex in the unavailable variable to include these additional patterns using
alternation or broader matching to ensure the skip condition is triggered for
all documented unavailability scenarios while still allowing genuine errors to
be re-thrown.
test/authentication.test.js (1)

371-377: ⚡ Quick win

isMain detection may fail on Windows paths or certain Node invocations.

The current logic import.meta.url.endsWith(process.argv[1].replace(/.*\//, "")) strips only forward slashes. On Windows, process.argv[1] uses backslashes. A more robust approach:

-const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/.*\//, ""));
+const isMain = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;

This matches the pattern used in test/integration.test.js at line 783.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/authentication.test.js` around lines 371 - 377, The isMain detection in
test/authentication.test.js uses a regex pattern that only strips forward
slashes, which fails on Windows paths that contain backslashes. Update the regex
pattern in the isMain assignment (around line 371) to handle both forward and
backward slashes, matching the robust approach already implemented in
test/integration.test.js at line 783. Replace the current replace pattern to use
a regex that matches either forward or backward slash separators, ensuring
cross-platform compatibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/verify-tool-names.mjs`:
- Around line 52-57: The ability-names discovery is fetching only a single page
from the `/wp-json/wp-abilities/v1/abilities` endpoint, causing valid
gk-gravityview/* names on subsequent pages to be missed during verification.
Implement pagination logic in the request call to fetch all available pages of
the abilities catalog and accumulate all matching ability names into the
abilityNames set, rather than building it from just the initial response. Check
the API response for pagination metadata (such as total pages or a next page
link) and continue fetching until all pages have been processed and added to the
abilityNames set.

---

Nitpick comments:
In `@test/authentication.test.js`:
- Around line 371-377: The isMain detection in test/authentication.test.js uses
a regex pattern that only strips forward slashes, which fails on Windows paths
that contain backslashes. Update the regex pattern in the isMain assignment
(around line 371) to handle both forward and backward slashes, matching the
robust approach already implemented in test/integration.test.js at line 783.
Replace the current replace pattern to use a regex that matches either forward
or backward slash separators, ensuring cross-platform compatibility.

In `@test/integration.test.js`:
- Around line 751-780: The `.catch(() => {})` on the roClient.initialize() call
silently swallows all errors without any logging or inspection, which can mask
unrelated initialization failures and cause confusing errors later when
listForms is called. Replace the silent catch handler with code that logs the
error and optionally checks the error type before deciding whether to suppress
it. This will preserve observability of what actually happened during
initialization while still allowing the test to continue to the read/write
verification steps.
- Around line 377-416: The regex pattern assigned to the unavailable constant
needs to be enhanced to catch additional error message variations that may be
returned by the MailChimp API. The current pattern covers common cases like
"table does not exist" and "not installed", but should also match variations
such as "feed" followed by "not" (for messages like "Feed add-on not active")
and "gravityformsmailchimp" followed by "not" (for messages like "addon_slug
gravityformsmailchimp is not registered"). Update the regex in the unavailable
variable to include these additional patterns using alternation or broader
matching to ensure the skip condition is triggered for all documented
unavailability scenarios while still allowing genuine errors to be re-thrown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 384dc9cc-538d-4fce-879a-d21164b5b12c

📥 Commits

Reviewing files that changed from the base of the PR and between bda10bb and 9dfda15.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • .npmignore
  • AGENTS.md
  • package.json
  • scripts/verify-tool-names.mjs
  • test/abilities-loader.test.js
  • test/authentication.test.js
  • test/bug-fixes.test.js
  • test/checkbox-expansion.test.js
  • test/compact.test.js
  • test/entries.test.js
  • test/feeds.test.js
  • test/field-dependencies.test.js
  • test/field-manager.test.js
  • test/field-operations-e2e.test.js
  • test/field-operations-integration.test.js
  • test/field-positioner.test.js
  • test/field-registry.test.js
  • test/field-validation.test.js
  • test/forms.test.js
  • test/helpers.js
  • test/integration.test.js
  • test/mutex.test.js
  • test/run.js
  • test/sanitize.test.js
  • test/server-tools.test.js
  • test/submissions.test.js
  • test/validation.test.js
  • test/views-stress.test.js
  • test/views.test.js
💤 Files with no reviewable changes (2)
  • test/run.js
  • .npmignore
✅ Files skipped from review due to trivial changes (6)
  • test/mutex.test.js
  • test/sanitize.test.js
  • test/entries.test.js
  • test/forms.test.js
  • test/field-validation.test.js
  • test/field-operations-integration.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Comment thread scripts/verify-tool-names.mjs Outdated
zackkatz added 4 commits June 15, 2026 18:55
…[ci skip]

CLAUDE.md is now a one-line `@AGENTS.md` import. Moved the only unique
CLAUDE.md content — the Project Identity block (package + version) — into
AGENTS.md, retargeted the release checklist's version-bump step to
AGENTS.md, added AGENTS.md to the repo map, and dropped CLAUDE.md from the
verify-tool-names surfaces (it re-exports AGENTS.md, already checked).
Everything else (commands, env, critical rules, release steps) was already
covered in AGENTS.md.
The reload tool operates on the whole GravityKit abilities plane (the
product-agnostic Foundation catalog), not GravityView specifically — `gv_`
is GravityView's product prefix. Rename the built-in to `gk_` so the
GravityKit-wide control tool is distinct from the `gv_*` product tools.
Also surface it in the server instructions so an agent knows how to (re)load
the catalog when gv_* tools are missing. Pre-release; no public API impact.
AGENTS.md is now the single source of truth, so accuracy matters. Recast it
around two planes — Gravity Forms (primary, 26 gf_* tools) and GravityKit
(secondary, dynamic gv_* from the Foundation Abilities catalog; GravityView
the only product so far) — documenting the previously-undocumented abilities
plane (WordPressClient, abilities loader, gk_reload_abilities, the
test/demo-only gravityview/ harness). Removed all brittle file:line
citations (cite symbols instead), corrected counts (45 field types; 26 GF
tools), and fixed the stale test/ path.

Add scripts/check-docs.mjs (npm run lint:docs): an offline guard that fails
on doc drift — repo-map coverage (via `git ls-files --cached --others
--exclude-standard`, so it respects .gitignore without a hand-rolled
parser), tool/field-count mismatches, and any file:line citation. Wired
into prepublishOnly alongside publint.

Verified: lint:docs, lint:package, and the offline test suites all pass.
Live verify:tool-names confirmed the gv_* names against the catalog earlier
this session (re-run currently blocked by an unrelated GravityView Composer
autoload fatal in the local plugin checkout).
Review follow-ups (verified live against the running site):

- verify-tool-names: the WP Abilities endpoint paginates (default per_page
  50; the site has 51 abilities over 2 pages), so the single-page fetch
  missed gk-gravityview/* names on page 2 — abilityNames was 48 vs the
  loader's 49. Walk all pages (per_page=100 + X-WP-TotalPages). Now 49/49.

- authentication.test.js: isMain basename strip only handled "/", failing
  direct `node` execution on Windows paths. Strip "/" or "\". (Same pattern
  exists in ~10 sibling test files; fixed only the flagged one per minimal
  scope.) Note: the suggested integration.test.js reference (file:// exact
  match) isn't Windows-robust either, so used a separator-agnostic regex.

- integration.test.js: read-only-key test swallowed initialize() errors
  silently; now logs them so a real init failure stays visible.

- integration.test.js: the create-feed "unavailable" skip regex required a
  space after "add-?on", so "addon_slug ... is not registered" wasn't
  caught; dropped the space. ("Feed add-on not active" was already covered
  by the existing "not active" branch.)

Verified: verify:tool-names 49/49 all-match; test:auth 25; test:unit 269;
live integration 27; lint:docs green.
zackkatz added 8 commits June 15, 2026 20:34
…S.md

Redid the prior review fixes test-first (RED watched, then GREEN), pulling
the logic out of inline test/script bodies into units with real coverage:

- test/helpers.js: isMainModule (POSIX+Windows path basename), feedUnavailable
  (feed add-on/infra "unavailable" detection), settleWithReport (report, don't
  swallow, a rejected init). New test/helpers.test.js — 13 cases; each bug
  case was confirmed failing against the old behavior before the fix.
- scripts/lib/ability-catalog.mjs: collectAbilityNames() walks every page of
  the paginated WP Abilities catalog. New test/ability-catalog.test.js — the
  multi-page case failed against the single-page stub, then passed.
- Wired call sites to the helpers (authentication.test.js, integration.test.js,
  verify-tool-names.mjs), removing the inline copies. No STUB placeholders
  remain — each was replaced during GREEN.
- package.json: test:lib runs the node:test units; added to test:all and
  prepublishOnly.
- AGENTS.md: new "Test-Driven Development (required)" section mandating
  RED/GREEN/REFACTOR; Extension Patterns now says write the failing test
  first; repo map + Testing list updated.

Verified: test:lib 16, test:unit 269, test:field-ops 131, live integration 27,
verify:tool-names 49/49 all-match, lint:docs green, npm pack clean (31 files).
The two node:test suites had near-duplicate scripts (test:field-ops +
test:lib). Merge into a single honestly-named test:node listing all six
node:test files; update test:all, prepublishOnly, and the AGENTS.md Testing
list / TDD section. No test logic changed (147 node:test cases still run).
Verified each finding against current code; fixed the still-valid ones.

Behavioral (test-first, RED watched then GREEN):
- field-manager: layoutGridColumnSpan now validated with Number()+
  Number.isInteger() (was parseInt/isFinite), so "6.5"/"6wide"/floats/
  empty/whitespace are dropped instead of coerced. (test/field-manager.test.js)
- view-validator: field_id must be a finite number or non-empty string —
  rejects false/objects/arrays/whitespace that previously passed. Kept
  numeric support (it's coerced via String(item.field_id)); the reviewer's
  "require string type" would have broken numeric ids. (test/views.test.js)
- User-Agent: single-sourced via new src/version.js (USER_AGENT from
  package.json). GravityFormsClient said 2.1.0, WordPressClient 2.1.1;
  now both match the package version. (test/user-agent.test.js)

Non-behavioral (TDD-exempt):
- inspector-client: fixed four flush-left lines (removeViewField,
  deleteGridRow comment, removeSearchField, removeViewWidget).
- .env.example: alphabetized GRAVITY_FORMS_MAX_RETRIES before _TIMEOUT.

Skipped — already addressed earlier:
- demo-abilities.mjs absolute imports + /tmp header (fixed in ded240c).
- AGENTS.md "28 tool descriptions" (the AGENTS.md rewrite dropped the count).

Wiring: test:node now includes user-agent.test.js; version.js added to the
AGENTS.md repo map. Verified: test:node 151, test:views 27, test:unit 269,
live integration 27, verify:tool-names 49/49, lint:docs + publint green.
README's Features + Available Tools were Gravity-Forms-only. Add a Features
bullet and a 'GravityKit Products (gv_*, dynamic)' section covering the
runtime-generated GravityView tools and gk_reload_abilities. Verified all
README tool names resolve against the live catalog (28 referenced, 0 unknown).
gv_* is GravityView's prefix specifically; the GravityKit plane is
product-agnostic — each add-on registers tools under its own server-owned
prefix (that's why the cross-product reload tool is gk_reload_abilities,
not gv_). Reframe the plane as 'GravityKit (dynamic)' in README + AGENTS.md,
with GravityView noted as the first product (prefix gv_*).
Replace the clone + .env + 'node /path/to/MCP/src/index.js' setup with the
published-package flow: 'npx -y @gravitykit/mcp' in the MCP client's command,
credentials in the client's env block. Drops the clone/npm-install steps from
Quick Start (local-checkout dev still covered under Contributing); notes key-
pair, self-signed, and version-pin options; Configuration + Troubleshooting
now point at the env block (npx) or .env (clone).
…— TDD

All four findings fixed test-first (RED watched, then GREEN). Logic for the
three index.js issues was extracted into src/server-runtime.js so it's
unit-testable (index.js self-runs main() and isn't importable).

- #1 serial init: WordPress plane now starts before the Gravity Forms REST
  probe is awaited, so a slow/bad GF config no longer stalls the WP plane or
  the abilities load. (runPlaneInit)
- #2 tool advertising: gf_* + field-op tools are listed only when the GF
  plane is live, so a WP-only install doesn't advertise tools that error on
  call. (buildToolList, gated on gravityFormsClient)
- #3 dispatch: the call router no longer hard-codes name.startsWith('gv_').
  It routes by ability-handler-map membership, so any GravityKit product
  prefix (gc_, …) dispatches, not just GravityView's gv_. (classifyAbilityCall)
- #4 wp-client: WordPressClient refuses to send Basic auth over a remote
  plain-HTTP URL (credential exposure) unless GRAVITY_FORMS_ALLOW_HTTP_BASIC_AUTH
  =true, reusing isLocalUrl — matching the GF plane's guard.

Tests: test/server-runtime.test.js (10), test/wp-client.test.js (4), wired
into test:node. server-runtime.js added to the AGENTS.md repo map.

Verified: test:node 165, test:unit 269, prepublishOnly gate green; live MCP
smoke (SDK client → node src/index.js) shows 76 tools (26 gf_ + 49 gv_ +
gk_reload_abilities) and gv_layouts_list dispatches.
- gf_* tools are 'static, listed whenever Gravity Forms credentials are
  valid' (not unconditionally 'always available') — matches the now-gated
  tool advertising; note the two planes are independent.
- GravityKit Product Tools: note they refuse remote plain-HTTP Basic auth
  unless GRAVITY_FORMS_ALLOW_HTTP_BASIC_AUTH=true (HTTPS/local always fine),
  matching the new WordPressClient guard.
@zackkatz zackkatz changed the title feat: Abilities API — auto-generated GravityView (gv_*) tools + credential-aware auth feat: GravityKit abilities plane (dynamic product tools via Foundation) + credential-aware auth Jun 16, 2026
@zackkatz

Copy link
Copy Markdown
Member Author

@coderabbitai Review again, please

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

@zackkatz Sure! I'll kick off a full review of the PR right away.

✅ Action performed

Full review finished.

zackkatz added 2 commits June 15, 2026 21:53
…names

Confirmed against the live catalog + the gravityview-multiple-forms source:
gk_apply_joins / gk_list_joins are NOT real tools. Multiple Forms registers
abilities in the gk-multiple-forms namespace but declares no Foundation
product / mcp_prefix, so those abilities have mcp_tool_name=null and the
loader skips them (live tool set is 49 gv_*, zero join tools). They were a
speculative code comment + a synthetic test fixture.

- src/index.js: drop the made-up "gk_apply_joins, gk_list_joins" from the
  list_changed comment (those tools don't exist).
- ability-catalog.mjs: collectAbilityNames default prefix gk-gravityview/ ->
  gk- so it covers every GravityKit product namespace, not just GravityView.
  (test/ability-catalog.test.js: RED→GREEN for a gk-multiple-forms name.)
- verify-tool-names.mjs: ABIL_RE generalized to gk-<product>/; log/comment
  updated. TOOL_RE kept narrow (gf_/gv_ are the only prefixes surfacing real
  tools today) with a comment explaining why broadening risks false positives.

Verified live: verify:tool-names 0 unknown across all surfaces; abilities now
52 (gk-gravityview 49 + gk-multiple-forms 3); test:node 166; lint:docs green.
@zackkatz zackkatz merged commit 937acd3 into main Jun 16, 2026
4 checks passed
@zackkatz zackkatz deleted the feature/abilities-api branch June 16, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants