MCP server for Gravity Forms (primary) and GravityKit products (secondary). It exposes 26 always-on Gravity Forms tools plus a dynamic set of GravityKit product tools generated from the connected site's Foundation Abilities catalog — GravityView is the only GravityKit product implemented so far.
This is the single canonical doc for the project (agents and humans). CLAUDE.md simply re-exports it via @AGENTS.md.
- Package:
@gravitykit/mcpv2.4.1 - Type: Node.js MCP server (ESM)
- Purpose: Full Gravity Forms REST API v2 coverage (26 Gravity Forms tools), plus dynamic GravityKit product tools (GravityView so far) via the WordPress Abilities API
- Repo: https://github.com/GravityKit/MCP
What this is: A Node.js MCP (Model Context Protocol) server with two independent capability planes:
- Plane A — Gravity Forms (
gf_*), primary. 26 static tools wrapping the Gravity Forms REST API v2 (forms, entries, feeds, notifications, submissions, field filters, results, and intelligent field management). Always available when Gravity Forms REST credentials work — on any Gravity Forms site. - Plane B — GravityKit, secondary. Tools generated at runtime from the connected site's GravityKit Foundation Abilities catalog; each product registers tools under its own server-owned prefix. They appear only when Foundation is active. GravityView is the only product wired up so far, using the
gv_*prefix (View authoring, fields, widgets, search, layouts). The plane is product-agnostic: any GravityKit product that registers Foundation abilities shows up automatically under its own prefix.
The two planes are independent: a GF-only site gets the full gf_* surface with no abilities; a GravityKit site without GF REST keys still gets its GravityKit tools.
Main entry point: src/index.js
Architecture style: MCP SDK server with stdio transport, one HTTP client per plane, composable validation
Key dependency: @modelcontextprotocol/sdk ^1.0.0
MCP/
├── package.json # @gravitykit/mcp, ESM, npm scripts
├── mcp.json # MCP manifest (tool catalog, auth config)
├── .env.example # All env vars documented
├── AGENTS.md # Canonical agent + developer docs (this file)
├── CLAUDE.md # Claude Code entry point — re-exports AGENTS.md (@AGENTS.md)
├── src/
│ ├── index.js # Server bootstrap, two-plane init, tool registration, handler routing
│ ├── gravity-forms-client.js # GravityFormsClient: GF REST HTTP client, all gf_* API methods
│ ├── wp-client.js # WordPressClient: product-agnostic authenticated WP transport (Plane B)
│ ├── version.js # VERSION + USER_AGENT, single-sourced from package.json
│ ├── server-runtime.js # Pure helpers: runPlaneInit, buildToolList, classifyAbilityCall
│ ├── abilities/
│ │ └── loader.js # loadAbilitiesAsTools() — turns the live Abilities catalog into product tools (GravityView → gv_*)
│ ├── gravityview/ # GravityView test/demo harness (NOT runtime — gv_* come from abilities/)
│ │ ├── inspector-client.js # Client for /wp-json/gravityview/v1 (only when DOING_GRAVITYVIEW_TESTS)
│ │ └── view-validator.js # Client-side structural + schema-aware validation for the inspector
│ ├── field-operations/ # Intelligent field management layer (gf_* field tools)
│ │ ├── index.js # Factory, tool definitions, handler functions
│ │ ├── field-manager.js # FieldManager: CRUD orchestrator
│ │ ├── field-dependencies.js # DependencyTracker: conditional logic/merge tag scanning
│ │ └── field-positioner.js # PositionEngine: page-aware field positioning
│ ├── field-definitions/
│ │ ├── field-registry.js # 46 field types with metadata, validation, storage patterns
│ │ └── loader.js # Registry loader
│ ├── config/
│ │ ├── auth.js # BasicAuthHandler, OAuth1Handler, AuthManager
│ │ ├── validation.js # ValidationFactory, BaseValidator, domain validators
│ │ ├── validation-chain.js # Composable rule chain system
│ │ ├── validation-rules.js # Individual validation rules
│ │ ├── validation-config.js # Validation constants and enums
│ │ ├── validators.js # Domain-specific validators (forms, entries, feeds, etc.)
│ │ ├── field-validation.js # FieldAwareValidator for field-specific rules
│ │ └── test-config.js # Dual test/live environment config, TestFormManager
│ └── utils/
│ ├── compact.js # stripEmpty() — recursive null/empty/false stripping for token optimization
│ ├── logger.js # MCP-safe logger (stderr in MCP mode, console in test)
│ └── sanitize.js # Credential masking for safe logging
├── test/ # Test suites — top-level, NOT published (see Packaging)
│ ├── run.js # Custom test runner (npm run test:unit)
│ ├── helpers.js # Mock data generators, test utilities
│ ├── integration.test.js # Live API integration tests (npm test)
│ ├── views.test.js, views-stress.test.js # GravityView inspector + abilities coverage
│ ├── abilities-loader.test.js # Abilities catalog → gv_* tool generation
│ └── *.test.js # forms, entries, feeds, fields, validation, submissions, compact, sanitize, …
├── scripts/
│ ├── check-env.js # Environment validation script
│ ├── check-docs.mjs # Doc-freshness guard for AGENTS.md (offline; npm run lint:docs)
│ ├── verify-tool-names.mjs # Cross-check doc/instruction tool names vs registered tools (needs live site)
│ ├── lib/
│ │ └── ability-catalog.mjs # collectAbilityNames() — paginated Abilities catalog reader
│ ├── stress-abilities.mjs # Synthetic abilities-loader stress/contract test
│ ├── setup-test-data.js # Test data seeding
│ ├── test-field-ops.js # Field operations smoke test
│ ├── test-server-output.js # Server output verification
│ └── verify-field-tools.js # Field tool registration check
└── .github/workflows/
├── publish.yml # npm publish workflow
├── security.yml # Security scanning
└── test.yml # CI test runner
The server registers tools from two independent sources, initialized separately so a failure in one never blocks the other:
- Plane A — Gravity Forms (
gf_*). Static tool definitions insrc/index.js(GF_TOOL_DEFINITIONS) plus the field tools fromsrc/field-operations/index.js(fieldOperationTools). Backed byGravityFormsClientagainst the GF REST API v2. 26 tools, always present once GF credentials validate. - Plane B — GravityKit. Generated at runtime by
src/abilities/loader.jsfrom the connected site's Abilities catalog, backed byWordPressClient; each product's tools carry its own prefix (GravityView →gv_*). The catalog is fetched in the background after startup; tools appear once it loads (the server advertisestools.listChanged). A single built-in tool,gk_reload_abilities, forces a re-fetch.
src/index.jsloads env vars via dotenv (CWD first, then project dir).- Creates the MCP
Serverwithtools.listChangedcapability and the two-plane server instructions. - Plane A:
initializeClient()constructsGravityFormsClientfromprocess.env; itsAuthManagerselects Basic or OAuth;validateRestApiAccess()probes Forms/Entries/Feeds; field operations (FieldManager,DependencyTracker,PositionEngine) are wired up. - Plane B: constructs
WordPressClientand kicks off a fire-and-forget abilities catalog fetch (loadAbilitiesAsTools). Startup never blocks on it; failures self-heal on the nextgv_*call (after a cooldown) or viagk_reload_abilities. - Server connects to
StdioServerTransport.
GravityFormsClient (gravity-forms-client.js): Single class wrapping all GF API endpoints. Each method uses the validateAndCall(toolName, input, apiCall) pattern — validates input via ValidationFactory, then executes the HTTP call. Update operations (forms, entries, feeds) fetch-then-merge to preserve existing data. Returns minimal payloads.
WordPressClient (wp-client.js): Product-agnostic authenticated WordPress transport for Plane B. The abilities loader rides it to reach the Foundation catalog (/wp-json/gravitykit/v1/...) and the WP core Abilities API (/wp-json/wp-abilities/v1/...). Auth is a WordPress Application Password via HTTP Basic; when GRAVITYKIT_WP_* creds aren't set it falls back to GRAVITY_FORMS_CONSUMER_KEY/SECRET (commonly the same WP user + app password).
Abilities loader (abilities/loader.js): loadAbilitiesAsTools(wpClient) builds the GravityKit product tool definitions + handlers from the live catalog (GravityView's carry the gv_* prefix). Source-preference chain: (1) Foundation catalog /wp-json/gravitykit/v1/abilities (server-filtered to GravityKit, server-owned tool names), (2) WP core catalog /wp-json/wp-abilities/v1/abilities (filtered client-side on Foundation's stamped meta.gk_registered_by), (3) throw if neither is reachable (caller leaves gv_* unregistered and retries — self-healing). Tool names are owned by the server via each ability's mcp_tool_name (from the product's mcp_prefix, or the full product slug); abilities without one are skipped with a warning rather than client-invented. Handlers execute abilities at /wp-abilities/v1/abilities/{name}/run with the HTTP method derived from annotations (readonly → GET, destructive+idempotent → DELETE, else POST).
GravityView harness (gravityview/inspector-client.js, view-validator.js): The Inspector client and validator target /wp-json/gravityview/v1 routes that exist only when DOING_GRAVITYVIEW_TESTS is defined server-side. They are the integration-test and demo harness — not a runtime dependency. Runtime gv_* tools come from the abilities loader.
AuthManager (config/auth.js): Credential-aware selection between BasicAuthHandler and OAuth1Handler — app-password creds get Basic (HTTPS or local URLs); ck_/cs_ key pairs get Basic on HTTPS, OAuth on plain HTTP (matching the GF server's is_ssl() gate for key Basic auth). Explicit GRAVITY_FORMS_AUTH_METHOD overrides.
ValidationFactory (config/validation.js): Central validation dispatcher. validateToolInput(toolName, input) routes to domain-specific validators. Composable rule chains (validation-chain.js) for reusable validation logic.
FieldManager (field-operations/field-manager.js): Handles field CRUD within REST API v2 constraints (fields are properties of form objects, not separate endpoints). Generates integer IDs via max+1, creates compound sub-inputs for address/name/creditcard fields.
Field Registry (field-definitions/field-registry.js): Metadata for all 46 Gravity Forms field types — categories, storage patterns (simple/compound/special), validation rules, variants, and capability flags.
MCP Client → stdio → Server.CallToolRequestSchema handler
→ switch(name):
gf_* / field tools → wrapHandler(GravityFormsClient.method)
→ validateAndCall → ValidationFactory → axios (auth interceptor)
→ minimal result
gv_* → ability handler → WordPressClient → /wp-abilities/v1/.../run
gk_reload_abilities → force catalog re-fetch
→ JSON.stringify(result) → compact MCP content block (no pretty-print)
← { content: [{ type: "text", text: "..." }] }
Responses are optimized for minimal token usage:
- Compact JSON:
JSON.stringify(result)— no pretty-printing (nonull, 2). - Minimal payloads: No redundant
message,created/updatedbooleans, or echo-back of input IDs. GET methods return{ resource: data }; mutations return only what can't be inferred (e.g., delete returns{ deleted: true, id, permanently }). - Summary/detail modes:
gf_list_field_typesdefaults to summary mode (type,label,category). Passdetail=truefor full metadata; addinclude_variants=truefor variant data. - Compact mode (default on):
stripEmpty()(utils/compact.js) recursively removesnulland""from all responses.falseis preserved (semantic meaning). Entry tools also strip plugin-added meta keys (e.g.,gv_revision_*,helpscout_conversation_id) viastripEntryMeta(), keeping only core properties and numbered field values. Passcompact=falsefor full raw data. - Terse descriptions: All tool and property descriptions are kept terse to reduce
tools/listoverhead.
Plane A — Gravity Forms (gf_*), 26 static tools:
| Category | Tools | Client Methods |
|---|---|---|
| Forms | gf_list_forms, gf_get_form, gf_create_form, gf_update_form, gf_delete_form, gf_validate_form |
listForms, getForm, createForm, updateForm, deleteForm, validateForm |
| Entries | gf_list_entries, gf_get_entry, gf_create_entry, gf_update_entry, gf_delete_entry |
listEntries, getEntry, createEntry, updateEntry, deleteEntry |
| Submissions | gf_submit_form_data, gf_validate_submission |
submitFormData, validateSubmission |
| Notifications | gf_send_notifications |
sendNotifications |
| Feeds | gf_list_feeds, gf_get_feed, gf_create_feed, gf_update_feed, gf_patch_feed, gf_delete_feed |
listFeeds, getFeed, createFeed, updateFeed, patchFeed, deleteFeed |
| Utilities | gf_get_field_filters, gf_get_results |
getFieldFilters, getResults |
| Field Ops | gf_add_field, gf_update_field, gf_delete_field, gf_list_field_types |
via fieldOperationHandlers → FieldManager |
Plane B — GravityKit, dynamic. Generated from the catalog, so the exact set depends on the connected site's GravityKit products and versions — each product under its own prefix; discover at runtime, don't hard-code. GravityView (prefix gv_*) currently contributes tool families for View lifecycle (gv_view_create, gv_view_config_apply, gv_view_delete, …), fields (gv_view_field_add/patch/move/remove), grid rows, widgets, search fields, and discovery/schema (gv_layouts_list, gv_widgets_list, gv_field_type_schema_get, gv_available_fields_get, …). Plus the built-in gk_reload_abilities. Use the gv_*_list discovery tools and gv_field_type_schema_get to introspect what's available; the server instructions string documents the GravityView authoring flow. To re-verify that prose tool names still match the live catalog, run npm run verify:tool-names (see Releasing).
GET/list methods return just the data:
{ form: responseData } // gf_get_form
{ forms: responseData, total_count, total_pages } // gf_list_forms
{ entries: responseData, total_count } // gf_list_entries
{ entry: responseData } // gf_get_entry
{ feed: responseData } // gf_get_feed, gf_create_feed, gf_update_feed, gf_patch_feed
{ feeds: responseData } // gf_list_feeds (pass form_id to scope to one form)Mutation methods return minimal confirmation:
{ deleted: true, form_id, permanently } // gf_delete_form, gf_delete_entry
{ deleted: true, feed_id } // gf_delete_feed
{ valid: true/false, validation_messages } // gf_validate_form, gf_validate_submission
{ success: true/false, entry_id, confirmation_message, validation_messages } // gf_submit_form_data
{ sent: true, notifications_sent } // gf_send_notifications- Files:
kebab-case.js(e.g.,field-manager.js,gravity-forms-client.js) - Classes:
PascalCase(e.g.,GravityFormsClient,FieldManager,WordPressClient) - Exports: Named exports for classes, default export for the primary class per file
- Test files:
{module-name}.test.jsin the top-leveltest/directory
- ESM throughout (
"type": "module"in package.json) - All imports use
.jsextension (required for ESM) __dirnameshimmed viafileURLToPath(import.meta.url)where needed
All tool handlers use wrapHandler() in src/index.js:
- Checks client initialization
- Wraps the result in MCP content blocks
{ content: [{ type: "text", text: JSON.stringify(result) }] } - Catches errors →
createErrorResponse()with sanitized details - Error details pass through
sanitize()to mask credentials
Every GravityFormsClient method follows this pattern:
async methodName(params) {
return this.validateAndCall('tool_name', params, async (validated) => {
const response = await this.httpClient.get/post/put/delete(path, data);
return { resource: response.data }; // Minimal return — no redundant fields
});
}Update methods (forms, entries, feeds) always fetch-then-merge to preserve existing data:
const existing = await this.httpClient.get(`/resource/${id}`);
const merged = { ...existing.data, ...updates };
await this.httpClient.put(`/resource/${id}`, merged);All GF delete operations (deleteForm, deleteEntry, deleteFeed) check this.allowDelete first, controlled by GRAVITY_FORMS_ALLOW_DELETE=true. Without it, deletes throw immediately.
utils/logger.js routes all logs to stderr in MCP mode (keeps stdout clean for JSON-RPC). In test mode it uses console.log. Sensitive data is masked via utils/sanitize.js. Never use console.log in server code.
- Define the tool schema in
src/index.js(inGF_TOOL_DEFINITIONS, surfaced by theListToolsRequestSchemahandler) with a concise description:{ name: 'gf_new_tool', description: 'Short description', // Keep terse for token efficiency inputSchema: { type: 'object', properties: {...}, required: [...] } }
- Add the client method in
gravity-forms-client.jsusingvalidateAndCall, returning minimal data. - Add validation in
config/validation.jsinsideValidationFactory.validateToolInput(). - Add the handler route in the
CallToolRequestSchemaswitch insrc/index.js. - Write the failing test first (TDD — see Test-Driven Development above), then implement steps 1–4 to make it pass. Tests live in
test/, importing the source under test as../src/…(seeforms.test.js).
GravityKit product tools (e.g. GravityView's gv_*) are not defined in this repo — they come from the connected site's Foundation Abilities catalog. To add or change them, register/modify abilities in the relevant GravityKit product (the server stamps each ability's mcp_tool_name); the loader picks them up automatically. After a catalog change, run gk_reload_abilities (live) or npm run verify:tool-names to confirm names.
Add an entry in field-definitions/field-registry.js:
newfield: {
type: 'newfield',
label: 'My New Field',
category: 'standard', // standard | advanced | pricing | post
supportsRequired: true,
supportsConditionalLogic: true,
storage: { type: 'string', format: 'single' }, // or 'compound'
validation: { maxLength: 255 },
variants: { default: { label: 'Default', settings: {} } }
}For compound fields (multi-input like address/name), set storage.type: 'compound' and add sub-input generation logic in field-manager.js (generateSubInputs()).
- Create the rule class in
config/validation-rules.js - Add the chainable method in
config/validation-chain.js - Use it in validators via
validate('fieldName').newRule()
All development here is test-first — features, bug fixes, refactors, behavior changes. The cycle is non-negotiable:
- RED — write one failing test that pins the intended behavior, and run it to watch it fail for the right reason. No production code before this.
- GREEN — write the minimal code to make it pass; keep the rest of the suite green.
- REFACTOR — clean up with the tests staying green.
A test that passes the first time you run it proves nothing — if you can't point to the RED run, it isn't TDD. Extract logic into a testable unit instead of burying it inline where it can't be exercised (e.g. feedUnavailable and collectAbilityNames were extracted so their behavior is covered, RED-then-GREEN). Bug fixes start with a failing test that reproduces the bug.
Pure-function/unit tests use node:test and live in test/ (run via npm run test:node); see Testing. Never wire a fix into the codebase ahead of its failing test.
npm install
cp .env.example .env # Edit with your Gravity Forms API credentials
npm run check-env # Verify environment
npm run dev # Dev with auto-reload
npm run inspect # Debug with MCP InspectorGRAVITY_FORMS_BASE_URL=https://... # WordPress site URL (no trailing slash)
# Recommended — WordPress application password (Users > Profile):
GRAVITY_FORMS_CONSUMER_KEY=wp_username
GRAVITY_FORMS_CONSUMER_SECRET="xxxx xxxx xxxx xxxx xxxx xxxx"
# Or a Gravity Forms API key (Forms > Settings > REST API), e.g. read-only:
# GRAVITY_FORMS_CONSUMER_KEY=ck_...
# GRAVITY_FORMS_CONSUMER_SECRET=cs_...
# Either way: check "Enable access to the API" on Forms > Settings > REST API once.
Shorthand aliases GF_CONSUMER_KEY, GF_CONSUMER_SECRET, GF_URL are also supported (resolved in test-config.js).
# Optional — only needed for gv_* tools. Falls back to GRAVITY_FORMS_* when unset.
GRAVITYKIT_WP_URL=https://... # WordPress site URL (usually same as GF)
GRAVITYKIT_WP_USERNAME=wp_username
GRAVITYKIT_WP_APP_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx"
WordPressClient resolves the base URL from GRAVITYKIT_WP_URL or GRAVITY_FORMS_BASE_URL, and credentials from GRAVITYKIT_WP_* or the GRAVITY_FORMS_CONSUMER_KEY/SECRET fallback. On most single-site setups the GF credentials already double as the WP app password, so no extra config is needed.
# GRAVITY_FORMS_AUTH_METHOD=basic # Override auto-selection only (see Gotcha #3)
# GRAVITY_FORMS_ALLOW_HTTP_BASIC_AUTH=false # Basic to a REMOTE plain-HTTP host
GRAVITY_FORMS_ALLOW_DELETE=false # Must be 'true' to enable delete operations
GRAVITY_FORMS_TIMEOUT=30000 # Request timeout in ms
GRAVITY_FORMS_MAX_RETRIES=3 # Max retry attempts
GRAVITY_FORMS_DEBUG=false # Enable debug logging (stderr)
GRAVITY_FORMS_ALLOW_SELF_SIGNED_CERTS=false # Allow self-signed certs (local dev only)
Note: GRAVITY_FORMS_RETRY_DELAY, GRAVITY_FORMS_RATE_LIMIT, and GRAVITY_FORMS_RATE_WINDOW appear in older docs but are NOT implemented in source code.
GRAVITY_FORMS_TEST_BASE_URL= # Test site URL
GRAVITY_FORMS_TEST_CONSUMER_KEY= # Test site API key
GRAVITY_FORMS_TEST_CONSUMER_SECRET= # Test site API secret
GRAVITY_FORMS_TEST_AUTH_METHOD= # Override auth method for test site
GRAVITY_FORMS_TEST_TIMEOUT= # Override timeout for test site
GRAVITYKIT_MCP_TEST_MODE=true # Enable test mode (remaps TEST_* vars)
Shorthand aliases: TEST_GF_URL, TEST_GF_CONSUMER_KEY, TEST_GF_CONSUMER_SECRET, TEST_WP_USER, TEST_WP_PASSWORD. Legacy: GRAVITYMCP_TEST_MODE and GRAVITY_FORMS_TEST_URL are also supported. Test mode also activates when NODE_ENV=test.
npm run test:unit # Unit tests via custom runner
npm run test:node # node:test unit suites (field ops, helpers, ability-catalog, bench grader/runner)
npm run test:auth # Authentication tests
npm run test:forms # Forms endpoint tests
npm run test:entries # Entries endpoint tests
npm run test:feeds # Feeds endpoint tests
npm run test:tools # Tool registration validation
npm run test:views # GravityView inspector/validator tests
npm run test:all # Run everything sequentially
npm test # Integration tests (requires live API)Two unit harnesses run side by side: the custom TestRunner (suites export a runner and are registered in test/run.js, run via npm run test:unit) and node:test (suites are listed in the test:node script in package.json, run via npm run test:node) — the latter includes the dev-only bench grader/runner tests (test/bench-*.test.js). A new suite only runs once it is registered in the matching place. test/helpers.js provides mock data generators (generateMockForm, generateMockEntry, generateMockFeed). For integration tests, set GRAVITY_FORMS_TEST_* env vars pointing to a test WordPress site; test forms are prefixed with TEST_ and auto-cleaned via TestFormManager. See test/AGENTS.md for which harness to use, how to register a new test, and how the bench/ AI gate relates to the unit suites.
No build step — pure ESM JavaScript, runs directly with node src/index.js. Requires Node.js >= 18.
-
Fields are form properties, not separate endpoints. The Gravity Forms REST API has no direct field CRUD endpoints. All field operations fetch the entire form, modify the fields array, then PUT the whole form back. This is why
FieldManagerexists as a layer on top ofGravityFormsClient. -
Stdout is reserved for JSON-RPC. In MCP mode, ALL logging must go to stderr.
console.logcorrupts the transport. Uselogger.info/error/warn. -
Auth method is credential-aware.
AuthManagerpicks the transport from the credential shape: app-password creds use Basic over HTTPS or local URLs;ck_/cs_key pairs use Basic over HTTPS and OAuth 1.0a over plain HTTP (Gravity Forms only checks key-pair Basic auth whenis_ssl()). An explicitGRAVITY_FORMS_AUTH_METHODis always honored — includingbasicover remote HTTP, so don't set it in.env"just in case". Remote-HTTP Basic without an explicit method needsGRAVITY_FORMS_ALLOW_HTTP_BASIC_AUTH=true. -
Update operations fetch-then-merge.
updateForm,updateEntry, andupdateFeedGET the existing resource, merge, then PUT — two HTTP calls per update. If the resource changes between GET and PUT, the intermediate change is overwritten. -
Field ID generation uses max+1. If field ID 10 is deleted, the next field gets ID 11, not 10. IDs are never reused within a form.
-
Compound field sub-input IDs use dot notation. Address field 5 has sub-inputs
5.1(street),5.2(line 2), etc. These IDs are strings, not numbers. Entry data uses these dot-notation keys. -
Delete operations are disabled by default.
GRAVITY_FORMS_ALLOW_DELETE=truemust be set explicitly, ordeleteForm/deleteEntry/deleteFeedthrow. Intentional safety. -
mcp.jsonmay be stale. The runtime source of truth isGF_TOOL_DEFINITIONS+ theListToolsRequestSchemahandler insrc/index.js,fieldOperationToolsinfield-operations/index.js(26gf_*tools), the built-ingk_reload_abilities, and the dynamicgv_*tools from the abilities loader. -
Self-signed certs for local dev. Set
GRAVITY_FORMS_ALLOW_SELF_SIGNED_CERTS=trueto bypass certificate validation for local WordPress (Laravel Valet, Local WP, etc.). Never in production. -
Validation has legacy and new patterns. A
BaseValidatorlegacy layer wraps the newerValidationChainand domain validators. Both paths are active. New code should use the chain system invalidation-chain.js. -
gf_list_field_typesdefaults to summary mode. Returns onlytype,label,category. Passdetail=truefor full metadata; addinclude_variants=truefor variants. Prevents dumping thousands of tokens for all 46 field types. -
Test mode resolves env vars at client construction. When
GRAVITYKIT_MCP_TEST_MODE=true(or legacyGRAVITYMCP_TEST_MODE=true),testConfig.resolveEnv()remapsGRAVITY_FORMS_TEST_*→GRAVITY_FORMS_*. The rest of the client and AuthManager work unchanged. -
gv_*tools load asynchronously and self-heal. The abilities catalog is fetched in the background after startup, sogv_*tools may be absent for a moment (the server emits atools/listChangedonce they arrive). If a catalog fetch fails, it retries after a cooldown or immediately ongk_reload_abilities. Thesrc/gravityview/Inspector client is a test/demo harness only — runtimegv_*come from the abilities loader.
- Don't let the bench hit a REMOTE site.
config.mjsresolveTarget()readsGRAVITY_FORMS_TEST_BASE_URLBEFOREGRAVITY_FORMS_BASE_URL, and the machine's shell env (~/.monokit/.env) setsGRAVITY_FORMS_TEST_*to a remote*.try.gravitykit.comsite. So setting onlyGRAVITY_FORMS_BASE_URLsilently runs the bench against that REMOTE site — and write tasks (create/patch) would create data there. Either use--mint(self-contained siteminter target; ignores the env) or pin ALL ofGRAVITY_FORMS_TEST_BASE_URL/_CONSUMER_KEY/_CONSUMER_SECRET+GRAVITYKIT_WP_URL/_USERNAME/_APP_PASSWORDto your local site. [2026-07] - A minted site needs GF REST v2 enabled for
gf_*+ the grader's GF calls (gravityformsaddon_gravityformswebapi_settings.enabled=1).--mint'sprovisionSitesets it; a hand-minted siteminter site does NOT (forms endpoint 404s until you flip it). [2026-07] - Bench any product, not just GravityView:
BENCH_PLUGINS=<GF>,<Foundation>,<product>(absolute paths) overrides the minted plugin list. GravityCharts abilities:BENCH_PLUGINS=…/gravityforms,…/Foundation,…/gravitycharts BENCH_SITE=gcbench node bench/run.mjs --mint --keep --task charts. PHP is symlinked, so ability refinements go live on the kept site with no re-mint. [2026-07] - The grader's
client.ability(name,input)returns{status, data}— read the ability payload at.data(e.g.res.data.charts), and its data-point objects can be{value,label,x}, not bare numbers. [2026-07]
What ships to npm is governed solely by the files allowlist in package.json — there is intentionally no .npmignore (with a files field present npm ignores it, so keeping one is misleading). Allowlist, not denylist: a new file ships only if it matches files.
- Ships:
src/(runtime),mcp.json,.env.example,README.md,LICENSE,CLAUDE.md,AGENTS.md. - Excluded by omission:
test/(tests are top-level, not undersrc/),scripts/(dev tooling),.github/,package-lock.json. npm run lint:packageruns publint to validate package correctness;npm run lint:docsruns the offline doc-freshness guard (scripts/check-docs.mjs).prepublishOnlyruns the offline test suites + both linters, so a broken, mis-packaged, or stale-documented build can't be published. It deliberately omits the live integration test (npm test) to avoid hitting a real site during publish.- Verify before publishing:
npm pack --dry-runlists exactly what will ship.
Every version tag MUST include a CHANGELOG.md update. Follow this checklist:
- Update
CHANGELOG.md— add a new## [X.Y.Z] - YYYY-MM-DDsection with all changes since the last release. Follow Keep a Changelog format (Added, Changed, Fixed, Removed). - Bump
versioninpackage.json - Update version in
AGENTS.md(Project Identity → Package line) - Add link at bottom of
CHANGELOG.md:[X.Y.Z]: https://github.com/GravityKit/MCP/releases/tag/vX.Y.Z - Commit:
git commit -m "chore(release): bump version to X.Y.Z" - Tag:
git tag vX.Y.Z - Push:
git push origin main --tags
Skipping any step (especially CHANGELOG) leaves the release history incomplete.
Before tagging:
- Run
npm run lint:docs— the offline doc-freshness guard (repo-map coverage, tool/field counts, no line citations).prepublishOnlyruns it too. - Run
npm run verify:tool-namesagainst a live site — thegv_*tools are generated from the installed GravityView/Foundation Abilities catalog, so a catalog rename can silently leave the serverinstructionsstring, README, or the demo referencing tools that no longer exist. The script cross-checks everygf_/gv_name in prose against what the server registers and exits non-zero on a mismatch. Needs WordPress credentials (see GravityKit Environment). Dev-only — not shipped in the npm package. - Run
npm run bench— the AI release gate (bench/). It drives the whole flow (forms/entries/views/fields/widgets/search/grid CRUD) through a small model (claude-haiku-4-5) over the MCP and exits non-zero if any task falls below the success threshold. A small model failing means the tool surface is too hard for an agent to use — fix descriptions/schemas/errors, not the gate. Needs theclaudeCLI + aGRAVITY_FORMS_TEST_*/GRAVITY_FORMS_*site running the code under test; slow + token-costly, so it's a release gate, not per-commit CI. Dev-only — not shipped.
- CLAUDE.md — Claude Code entry point; re-exports this file via
@AGENTS.md - README.md — User-facing setup and usage guide
- .env.example — Complete environment variable reference
- mcp.json — MCP manifest (tool catalog, auth requirements)
- Gravity Forms REST API v2 docs
- MCP SDK docs