From 6e299da8cfc7df49029d5d8bb7d5739560e1aaa4 Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Mon, 31 Aug 2026 17:14:38 -0700 Subject: [PATCH] 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. --- packages/forms/signals/src/webmcp/registration.ts | 9 +++++++++ packages/forms/signals/test/web/webmcp.spec.ts | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/forms/signals/src/webmcp/registration.ts b/packages/forms/signals/src/webmcp/registration.ts index 5653d3c814f..bc38529a879 100644 --- a/packages/forms/signals/src/webmcp/registration.ts +++ b/packages/forms/signals/src/webmcp/registration.ts @@ -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) => { // Populate the form with changes from the agent. node.value.set(args); diff --git a/packages/forms/signals/test/web/webmcp.spec.ts b/packages/forms/signals/test/web/webmcp.spec.ts index b92d763d785..6e6ba789183 100644 --- a/packages/forms/signals/test/web/webmcp.spec.ts +++ b/packages/forms/signals/test/web/webmcp.spec.ts @@ -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');