Skip to content

Commit fea0977

Browse files
committed
fix(fields): accept custom and third-party field types in gf_add_field
gf_add_field threw "Unknown field type" for any type not in the static 46-key registry, rejecting third-party fields (Gravity Perks, Gravity Wiz, etc.) that Gravity Forms itself accepts. The registry is now an enhancement, not a gate: unknown types are created with graceful degradation plus a warning. Also stop leaking the internal _unknown flag to Gravity Forms on form create/update, and align validateFieldConfig to tolerate unknown types. Convert the orphan field-operations-integration harness to exercise the real field-operations layer. Bump 2.4.0 -> 2.4.1. Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
1 parent 5b7e888 commit fea0977

12 files changed

Lines changed: 168 additions & 165 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This is the single canonical doc for the project (agents and humans). `CLAUDE.md
66

77
## Project Identity
88

9-
- **Package:** `@gravitykit/mcp` v2.4.0
9+
- **Package:** `@gravitykit/mcp` v2.4.1
1010
- **Type:** Node.js MCP server (ESM)
1111
- **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
1212
- **Repo:** https://github.com/GravityKit/MCP

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to GravityKit MCP (formerly GravityMCP) will be documented i
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.4.1] - 2026-06-25
9+
10+
This release lets the field tools work with custom and third-party field types, not just the ones built into Gravity Forms.
11+
12+
### 🐛 Fixed
13+
- **`gf_add_field` now accepts custom and third-party field types.** Field types added by other plugins (Gravity Perks, Gravity Wiz, and similar add-ons) were previously rejected because they aren't in the built-in registry. They are now created normally, with a note when type-specific defaults or sub-inputs aren't available; pass `inputs`/`choices` explicitly for custom compound or choice fields.
14+
- **Unrecognized field types no longer leave an internal marker in saved forms** when creating or updating a form.
15+
816
## [2.4.0] - 2026-06-19
917

1018
This update adds additional guards to make sure Gravity Forms entries save correctly for every field type, so an AI assistant gets the entry format right the first time. Adds explicit support for the `password` field type and Gravity Wiz Nested Forms, plus a benchmark suite that ensures the MCP works well with small models.
@@ -236,6 +244,7 @@ A correctness pass on the Gravity Forms (`gf_*`) plane, verified against Gravity
236244
- Field filters (1 tool)
237245
- Results/Analytics (1 tool)
238246

247+
[2.4.1]: https://github.com/GravityKit/MCP/releases/tag/v2.4.1
239248
[2.4.0]: https://github.com/GravityKit/MCP/releases/tag/v2.4.0
240249
[2.3.0]: https://github.com/GravityKit/MCP/releases/tag/v2.3.0
241250
[2.2.0]: https://github.com/GravityKit/MCP/releases/tag/v2.2.0

mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gravitykit-mcp",
3-
"version": "2.4.0",
3+
"version": "2.4.1",
44
"description": "MCP server for Gravity Forms",
55
"author": "GravityKit",
66
"license": "MIT",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@gravitykit/mcp",
3-
"version": "2.4.0",
3+
"version": "2.4.1",
44
"description": "Full-featured MCP server for Gravity Forms",
55
"main": "src/index.js",
66
"type": "module",

src/config/field-validation.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,12 @@ export class FieldAwareValidator {
8989
logger.warn(`[FieldValidator] Unknown field type '${field.type}' at ${path}`);
9090
}
9191

92-
// Allow unknown types but mark them
92+
// Allow unknown types (third-party add-ons, GravityKit, custom fields).
93+
// Gravity Forms accepts them on save; pass the field through unchanged. No
94+
// internal flag is added: it would be PUT verbatim and nothing reads it.
9395
return {
9496
isValid: true,
95-
field: { ...field, _unknown: true }
97+
field: { ...field }
9698
};
9799
}
98100

src/field-definitions/field-registry.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -928,12 +928,12 @@ export function detectFieldVariant(field) {
928928
*/
929929
export function validateFieldConfig(field) {
930930
const definition = fieldRegistry[field.type];
931-
931+
932+
// Unknown / third-party types are tolerated, not rejected (Gravity Forms
933+
// accepts them on save). With no config schema to check them against there is
934+
// nothing to validate, so report valid and let GF own it.
932935
if (!definition) {
933-
return {
934-
isValid: false,
935-
error: `Unknown field type: ${field.type}`
936-
};
936+
return { isValid: true };
937937
}
938938

939939
// Check required properties

src/field-operations/field-manager.js

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,48 +27,59 @@ export class FieldManager {
2727
* @returns {object} Field creation result with warnings
2828
*/
2929
async addField(formId, fieldType, properties = {}, position = {}) {
30-
// 1. Validate field type against registry
31-
const fieldDef = this.registry[fieldType];
32-
if (!fieldDef) {
33-
throw new Error(`Unknown field type: ${fieldType}`);
34-
}
30+
// The registry is an ENHANCEMENT source, not a gate. Known types get
31+
// type-specific defaults and sub-inputs. Unknown types (third-party add-ons,
32+
// GravityKit, custom fields) are still created (Gravity Forms accepts them on
33+
// save), just without those extras. Callers can pass `inputs`/`choices`
34+
// explicitly for custom compound/choice fields.
35+
const fieldDef = this.registry[fieldType] || null;
36+
const isKnownType = fieldDef !== null;
3537

36-
// 2. Fetch current form via REST API
38+
// Fetch current form via REST API
3739
const { form } = await this.api.getForm({ id: formId });
3840

39-
// 3. Generate unique integer field ID (max + 1 pattern)
41+
// Generate unique integer field ID (max + 1 pattern)
4042
const fieldId = this.generateFieldId(form.fields || []);
4143

42-
// 4. Create field with type-specific defaults
43-
const field = this.createField(fieldId, fieldType, properties, fieldDef);
44-
45-
// 5. Generate compound sub-inputs if needed (address.1, name.3, etc.)
46-
if (fieldDef.storage?.type === 'compound') {
44+
// Create field with type-specific defaults (none for unknown types)
45+
const field = this.createField(fieldId, fieldType, properties, fieldDef || {});
46+
47+
// Generate compound sub-inputs only for known compound types (address.1,
48+
// name.3, …). Unknown types keep any caller-supplied `inputs` untouched.
49+
const isCompoundType = fieldDef?.storage?.type === 'compound';
50+
if (isCompoundType) {
4751
field.inputs = this.generateSubInputs(field, fieldDef);
4852
}
4953

50-
// 5b. Normalize layout grid properties (layoutGroupId, layoutGridColumnSpan)
54+
// Normalize layout grid properties (layoutGroupId, layoutGridColumnSpan)
5155
this.normalizeLayoutProperties(field, formId);
5256

53-
// 6. Calculate insertion position (page-aware)
57+
// Calculate insertion position (page-aware)
5458
const insertIndex = this.positionEngine?.calculatePosition(
5559
form.fields || [],
5660
position,
5761
form.pagination
5862
) || form.fields?.length || 0;
5963

60-
// 7. Insert field at calculated position
64+
// Insert field at calculated position
6165
if (!form.fields) form.fields = [];
6266
form.fields.splice(insertIndex, 0, field);
6367

64-
// 8. Replace form via direct PUT (no re-fetch — we already have the full state)
65-
const updatedForm = await this.api.replaceForm(formId, form);
66-
67-
// 9. Return result with validation warnings
68+
// Replace form via direct PUT (no re-fetch; we already have the full state)
69+
await this.api.replaceForm(formId, form);
70+
71+
// Surface field-shape warnings, plus a heads-up when the type is unrecognized.
72+
const warnings = this.validator.getWarnings(field);
73+
if (!isKnownType) {
74+
warnings.unshift(
75+
`Field type '${fieldType}' is not in the known field registry; created without type-specific defaults or sub-inputs. Pass 'inputs'/'choices' explicitly if this type needs them.`
76+
);
77+
}
78+
6879
return {
6980
success: true,
7081
field: field,
71-
warnings: this.validator.getWarnings(field),
82+
warnings,
7283
form_id: formId,
7384
position: {
7485
index: insertIndex,

test/field-manager.test.js

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,17 +225,57 @@ test('FieldManager - addField', async (t) => {
225225
assert.strictEqual(result.position.index, 3);
226226
});
227227

228-
await t.test('rejects unknown field type', async () => {
228+
// Custom / third-party field types (Gravity Perks, add-ons, GravityKit, the
229+
// MailPot field a customer hit) are not in the static registry, yet Gravity
230+
// Forms accepts them on the form PUT. addField must not gate on the registry;
231+
// it degrades gracefully: create from caller properties, skip registry-derived
232+
// defaults/sub-inputs, and warn.
233+
await t.test('accepts an unknown field type instead of throwing', async () => {
229234
const apiClient = createMockApiClient();
230235
const registry = createMockRegistry();
231236
const validator = createMockValidator();
232237
const manager = new FieldManager(apiClient, registry, validator);
233238

234-
await assert.rejects(
235-
async () => await manager.addField(1, 'unknown_type', {}),
236-
/Unknown field type: unknown_type/
239+
const result = await manager.addField(1, 'mailpot_custom', { label: 'Custom Field' });
240+
241+
assert.strictEqual(result.success, true);
242+
assert.strictEqual(result.field.type, 'mailpot_custom');
243+
assert.strictEqual(result.field.label, 'Custom Field');
244+
assert.strictEqual(result.field.id, 4); // Next ID after 1,2,3
245+
});
246+
247+
await t.test('warns when the field type is not in the registry', async () => {
248+
const apiClient = createMockApiClient();
249+
const registry = createMockRegistry();
250+
const validator = createMockValidator();
251+
const manager = new FieldManager(apiClient, registry, validator);
252+
253+
const result = await manager.addField(1, 'mailpot_custom', { label: 'Custom Field' });
254+
255+
assert.ok(Array.isArray(result.warnings));
256+
assert.ok(
257+
result.warnings.some((m) => /mailpot_custom/.test(m) && /registr/i.test(m)),
258+
'expected a warning naming the unrecognized field type'
237259
);
238260
});
261+
262+
await t.test('preserves caller-supplied inputs for an unknown compound-like type', async () => {
263+
const apiClient = createMockApiClient();
264+
const registry = createMockRegistry();
265+
const validator = createMockValidator();
266+
const manager = new FieldManager(apiClient, registry, validator);
267+
268+
const result = await manager.addField(1, 'custom_compound', {
269+
label: 'Custom Compound',
270+
inputs: [{ id: '4.1', label: 'Part A' }, { id: '4.2', label: 'Part B' }]
271+
});
272+
273+
assert.strictEqual(result.success, true);
274+
assert.deepStrictEqual(result.field.inputs, [
275+
{ id: '4.1', label: 'Part A' },
276+
{ id: '4.2', label: 'Part B' }
277+
]);
278+
});
239279
});
240280

241281
test('FieldManager - updateField', async (t) => {

0 commit comments

Comments
 (0)