Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c32fc14
feat(webmcp): register the WebMCP tool catalog on form load
rismehta Sep 10, 2026
1b5f2e0
feat(webmcp): expose form opt-in in container dialog
rismehta Sep 29, 2026
3ee49c3
docs(webmcp): clarify form dialog opt-in
rismehta Sep 29, 2026
555bbd4
fix(webmcp): unregister tools on form unload
rismehta Sep 29, 2026
421e524
feat(webmcp): support domain tools and stable form ids
rismehta Sep 29, 2026
d33c7f0
fix(webmcp): scope additional tools through GuideBridge
rismehta Oct 1, 2026
895410f
FORMS-28234 gate WebMCP authoring opt-in behind the shared epic FT
rismehta Oct 1, 2026
e1fc42a
FORMS-28234 keep authoring checks out of the WebMCP runtime suite
rismehta Oct 1, 2026
0c796d7
fix(webmcp): preserve form-scoped renderer focus on registration
rismehta Oct 1, 2026
7c035c2
chore: prepare WebMCP runtime dependencies for 1.0.6
rismehta Oct 1, 2026
32258ed
Lock released WebMCP runtime 1.0.6 and verify real integration
rismehta Oct 1, 2026
9f3464b
test: add skipped WebMCP authoring and preview coverage
rismehta Oct 1, 2026
738675c
fix: omit unauthored WebMCP opt-in from form JSON
rismehta Oct 1, 2026
d4391aa
test: enable verified WebMCP authoring coverage
rismehta Oct 1, 2026
80e5f03
test: isolate rule-editor equality fixture across retries
rismehta Oct 2, 2026
4605c01
fix(runtime): upgrade public runtime packages to 1.0.7
rismehta Oct 2, 2026
0fc4313
fix(tests): gate WebMCP authoring and scope component insertion to vi…
rismehta Oct 2, 2026
9e4c911
revert(tests): undo component insertion helper changes and regression…
rismehta Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ private ReservedProperties() {
public static final String FD_DRAFT_ID = "fd:draftId";
public static final String FD_CHANGE_EVENT_BEHAVIOUR = "fd:changeEventBehaviour";
public static final String FD_SET_PROPERTY_BEHAVIOUR = "fd:setPropertyBehaviour";
public static final String FD_WEB_MCP_ENABLED = "fd:webMcpEnabled";

public static final String PN_CQ_ANNOTATIONS = "cq:annotations";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ public class FormContainerImpl extends AbstractContainerImpl implements FormCont
@Nullable
private String setPropertyBehaviour;

@ValueMapValue(name = ReservedProperties.FD_WEB_MCP_ENABLED, injectionStrategy = InjectionStrategy.OPTIONAL)
@Nullable
private Boolean webMcpEnabled;

@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = ReservedProperties.PN_DATA)
@Nullable
private String data;
Expand Down Expand Up @@ -437,6 +441,9 @@ public String getLanguageDirection() {
if (StringUtils.isNotBlank(setPropertyBehaviour)) {
properties.put(ReservedProperties.FD_SET_PROPERTY_BEHAVIOUR, setPropertyBehaviour);
}
if (webMcpEnabled != null) {
properties.put(ReservedProperties.FD_WEB_MCP_ENABLED, webMcpEnabled);
}
properties.put(FD_FORM_DATA_ENABLED, formDataEnabled);
if (this.autoSaveConfig != null && this.autoSaveConfig.isEnableAutoSave()) {
properties.put(ReservedProperties.FD_AUTO_SAVE_PROPERTY_WRAPPER, this.autoSaveConfig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,46 @@ void testSetPropertyBehaviourAbsentWhenNotAuthored() throws Exception {
assertNull(formContainer.getProperties().get(ReservedProperties.FD_SET_PROPERTY_BEHAVIOUR));
}

@Test
void testWebMcpEnabledAbsentWhenNotAuthored() throws Exception {
FormContainer formContainer = Utils.getComponentUnderTest(PATH_FORM_1, FormContainer.class, context);
assertFalse(formContainer.getProperties().containsKey(ReservedProperties.FD_WEB_MCP_ENABLED));
JsonNode properties = new ObjectMapper().readTree(Utils.getJson(formContainer, Views.Publish.class)).get("properties");
assertFalse(properties.has(ReservedProperties.FD_WEB_MCP_ENABLED));
}

@Test
void testWebMcpEnabledFromNode() throws Exception {
assertWebMcpEnabledExport(true, true);
}

@Test
void testWebMcpDisabledFromNode() throws Exception {
assertWebMcpEnabledExport(false, false);
}

@Test
void testWebMcpEnabledFromStringNode() throws Exception {
assertWebMcpEnabledExport("true", true);
}

@Test
void testWebMcpDisabledFromStringNode() throws Exception {
assertWebMcpEnabledExport("false", false);
}

private void assertWebMcpEnabledExport(Object authoredValue, boolean enabled) throws Exception {
Resource resource = context.resourceResolver().getResource(PATH_FORM_1);
resource.adaptTo(ModifiableValueMap.class).put(ReservedProperties.FD_WEB_MCP_ENABLED, authoredValue);
FormContainer formContainer = Utils.getComponentUnderTest(PATH_FORM_1, FormContainer.class, context);
assertEquals(enabled, formContainer.getProperties().get(ReservedProperties.FD_WEB_MCP_ENABLED));
JsonNode property = new ObjectMapper().readTree(Utils.getJson(formContainer, Views.Publish.class))
.get("properties").get(ReservedProperties.FD_WEB_MCP_ENABLED);
assertNotNull(property);
assertTrue(property.isBoolean());
assertEquals(enabled, property.booleanValue());
}

@Test
void testCustomFunctionUrl() throws Exception {
FormContainer formContainer = Utils.getComponentUnderTest(PATH_FORM_1, FormContainer.class, context);
Expand Down
11 changes: 8 additions & 3 deletions docs/e2e-testing/feature-toggles.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ it/config/src/main/content/jcr_root/apps/system/config/
com.adobe.granite.toggle.impl.dev.DynamicToggleProviderImpl.cfg.json
```

Add the toggle name to the `enabled` array in that file to turn it on in IT.
Add the toggle name to the `enabledToggles` array in that file to turn it on in IT.

### Layer 2 — JVM System Property (server-side)

Expand Down Expand Up @@ -77,7 +77,7 @@ When adding a new toggle, the only config file that requires an update is:

| File | Change |
|------|--------|
| `com.adobe.granite.toggle.impl.dev.DynamicToggleProviderImpl.cfg.json` | Add toggle name to `enabled` array |
| `com.adobe.granite.toggle.impl.dev.DynamicToggleProviderImpl.cfg.json` | Add toggle name to `enabledToggles` array |

---

Expand Down Expand Up @@ -178,6 +178,11 @@ it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples
|------|----------------|
| `ui.tests/test-module/specs/fragment/fragment.featuretoggles.cy.js` | FT_FORMS-24087, FT_FORMS-24343, FT_FORMS-24358 |
| `ui.tests/test-module/specs/panelcontainer/panelcontainer.featuretoggles.cy.js` | FT_FORMS-24358 |
| `ui.tests/test-module/specs/webmcp/webmcp.authoring.cy.js` | FT_FORMS-28233 (latest addon) |

The enabled WebMCP authoring suite runs on the latest addon, uses the existing accessibility sample, enables **Enable AI assistant access (WebMCP)** in the container dialog, and opens the saved form in preview. A preview-scoped response interceptor installs a test-only `document.modelContext` host before runtime scripts load; the real runtime must register `list_forms` plus all 12 form tools and return the accessibility form's field summary. Authenticated browser cleanup restores the original opt-in property and its type, or deletes it when originally absent.

Running the authoring flow requires matching Java models and standard/XFA runtime clientlibs, IT content, the examples page component package, and the IT toggle configuration. The instance must have the development toggle provider installed and expose `FT_FORMS-28233` in `/etc.clientlibs/toggles.json`; configuring `enabledToggles` without an active provider does not enable the flag. When the flag is absent, including in without-FT jobs, the suite is reported as skipped without changing the opt-in property. With the flag enabled, the full authoring and preview assertions run.

---

Expand Down Expand Up @@ -219,7 +224,7 @@ curl -s -u admin:admin "http://localhost:4502/content/forms/af/core-components-i

1. **Define the constant** — add `public static final String FT_FORMS_XXXXX = "FT_FORMS-XXXXX";` to `FeatureToggleConstants.java`.

2. **Enable in Granite config** — add `"FT_FORMS-XXXXX"` to the `enabled` array in `DynamicToggleProviderImpl.cfg.json`.
2. **Enable in Granite config** — add `"FT_FORMS-XXXXX"` to the `enabledToggles` array in `DynamicToggleProviderImpl.cfg.json`.

3. **Provision test content** — decide whether to reuse an existing IT sample page or create a new one under `it/content/.../samples/`. Create new JCR content only when the toggled behavior requires it.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"FT_FORMS-24087",
"FT_FORMS-24343",
"FT_FORMS-25252",
"FT_FORMS-27648"
"FT_FORMS-27648",
"FT_FORMS-28233"
],
"disabledToggles": [
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@
text="Enable the hamburger menu for mobile view"
fieldDescription="Select the option to display the hamburger menu for efficient navigation on mobile devices."
value="{Boolean}true"/>
<webMcpEnabled
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./fd:webMcpEnabled"
text="Enable AI assistant access (WebMCP)"
fieldDescription="Allows supported browser AI assistants to securely understand and help users complete this form through WebMCP tools. When enabled, assistants can read form fields and, with user confirmation, update values or submit the form."
uncheckedValue="{Boolean}false"
value="{Boolean}true">
<granite:rendercondition
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/renderconditions/featuretoggle"
toggleName="FT_FORMS-28233"/>
</webMcpEnabled>
<fieldType
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
Expand Down
6 changes: 6 additions & 0 deletions ui.frontend/__tests__/guidebridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
******************************************************************************/

jest.mock('@aemforms/af-webmcp', () => ({registerFormWebMCP: jest.fn()}));

import {Constants} from "../src/index";
import FormContainer from "../src/view/FormContainer";
import formJson from './resources/form.json';
Expand Down Expand Up @@ -53,14 +55,18 @@ test('GuideBridge test', () => {
const resetButton = resultModel.getElement("reset-c7cccb1a5e");
expect(resetButton).not.toBeNull();
expect(resetButton.visible).toBeFalsy();
guideBridge.unloadAdaptiveForm("/a/b/c");
expect(unregisterWebMcp).toHaveBeenCalledTimes(1);
}});

});
const unregisterWebMcp = jest.fn();
var formContainer = new FormContainer({
_formJson: formJson,
_prefillData: {data: {"textinput1662370110841" : "initialData"}},
_path: "/a/b/c"
});
formContainer._setWebMcpUnregister(unregisterWebMcp);
const event = new CustomEvent(Constants.FORM_CONTAINER_INITIALISED, { "detail": formContainer });
document.dispatchEvent(event);
});
91 changes: 91 additions & 0 deletions ui.frontend/__tests__/webmcp.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*******************************************************************************
* Copyright 2026 Adobe
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/

import {createFormInstance} from '@aemforms/af-core';
import {registerFormWebMCP} from '@aemforms/af-webmcp';
import Utils from '../src/utils';

describe('published WebMCP runtime integration', () => {
let unregister;
let tools;
let handles;
let host;
let rum;

beforeEach(() => {
tools = new Map();
handles = [];
host = {registerTool: jest.fn(tool => {
tools.set(tool.name, tool);
const handle = {unregister: jest.fn(() => tools.delete(tool.name))};
handles.push(handle);
return handle;
})};
document.modelContext = host;
rum = jest.fn();
window.hlx = {sampleRUM: rum};
});

afterEach(() => {
if (unregister) { unregister(); }
unregister = undefined;
delete document.modelContext;
delete window.hlx;
document.body.innerHTML = '';
});

function form(enabled) {
return createFormInstance({
id: 'published-runtime',
properties: {'fd:webMcpEnabled': enabled},
items: [{id: 'name', name: 'name', type: 'string', fieldType: 'text-input', value: 'before'}]
});
}

test('registers actual tools, changes values, bridges DOM focus, and cleans up', async () => {
const model = form(true);
const input = document.createElement('input');
document.body.appendChild(input);
const view = {setFocus: jest.fn(() => input.focus())};
unregister = registerFormWebMCP(model, Utils.getWebMcpOptions(view));

expect(tools.get('validate_form_completeness').annotations)
.toEqual(expect.objectContaining({readOnlyHint: true, untrustedContentHint: true}));
expect(await tools.get('set_field_value').execute({field: 'name', value: 'after'}))
.toEqual(expect.objectContaining({success: true, changed: true}));
expect(model.getElement('name').value).toBe('after');
expect(rum).toHaveBeenCalledWith('fill', {source: 'af-webmcp', target: 'set_field_value'});
rum.mockClear();
await tools.get('set_field_value').execute({field: 'name', value: 'after'});
expect(rum).not.toHaveBeenCalled();

expect((await tools.get('focus_field').execute({field: 'name'})).success).toBe(true);
expect(view.setFocus).toHaveBeenCalledWith('name');
expect(document.activeElement).toBe(input);
expect(rum).toHaveBeenCalledWith('click', {source: 'af-webmcp', target: 'focus_field'});

unregister();
expect(tools.size).toBe(0);
handles.forEach(handle => expect(handle.unregister).toHaveBeenCalledTimes(1));
unregister = undefined;
});

test('does not expose tools for a form that did not opt in', () => {
unregister = registerFormWebMCP(form(false));
expect(host.registerTool).not.toHaveBeenCalled();
expect(tools.size).toBe(0);
});
});
Loading
Loading