feat(forms): hard-code readOnlyHint and untrustedContentHint for WebMCP implicit signal forms

Signal forms can safely assume `{readOnlyHint: false, untrustedContentHint: false}` given their context. A form _must_ alter page DOM (or else how would a user interact with it) and is therefore definitionally side-effectful. We also assume returned content is trusted, given it is currently hard-coded or derived from application errors.

In the future, we might want to consider cases where application errors are explicitly untrusted or where application developers choose to customize the response text with something which might be untrusted. For now, that's out of scope and we'll worry about it when a compelling use case arises.
This commit is contained in:
Doug Parker
2026-08-31 17:14:38 -07:00
committed by Matthew Beck
parent 91a2bf8425
commit 6e299da8cf
2 changed files with 22 additions and 0 deletions
@@ -55,6 +55,15 @@ async function initWebMcpForm(
name: options.name,
description: options.description,
inputSchema,
annotations: {
// Forms are assumed to implicitly mutate the DOM (otherwise how would a user interact with them?)
// and therefore are _never_ read-only.
readOnlyHint: false,
// Response text is currently hard-coded by the framework and trusted or derived from application
// errors which are considered trusted.
untrustedContentHint: false,
},
execute: async (args: Record<string, unknown>) => {
// Populate the form with changes from the agent.
node.value.set(args);
@@ -40,6 +40,9 @@ describe('Signal Forms WebMCP Integration', () => {
},
});
const modelContext = (globalThis.document as any).modelContext;
const registerSpy = spyOn(modelContext, 'registerTool').and.callThrough();
TestBed.runInInjectionContext(() => {
form(model, {
experimentalWebMcpTool: {
@@ -50,6 +53,16 @@ describe('Signal Forms WebMCP Integration', () => {
});
await TestBed.inject(ApplicationRef).whenStable();
expect(registerSpy).toHaveBeenCalledWith(
jasmine.objectContaining({
annotations: {
readOnlyHint: false,
untrustedContentHint: false,
},
}),
jasmine.anything(),
);
const registeredTools = globalThis.navigator.modelContextTesting!.listTools();
expect(registeredTools[0].name).toBe('testFormTool');
expect(registeredTools[0].description).toBe('A test form tool');