[webmcp] Provide parameter descriptions This CL adds a new attribute (`toolparamdescription`), which can be used to provide 'description' [1] for individual tool parameters. As described by Khushal in Issue 22 [2], the value of the 'description' field is created from multiple sources, as follows: 1. We prefer an explicit 'toolparamdescription' attribute, if present. 2. Otherwise, the concatenation of associated label text, if any. 3. Otherwise, the aria-description attribute. Note that we currently don't need to update any information when relevant attributes or other DOM circumstance chances; CL:7428254 introduced code which recomputes the input schema aggressively. [1] https://json-schema.org/draft/2020-12/json-schema-validation#name-title-and-description [2] https://github.com/webmachinelearning/webmcp/issues/22#issuecomment-3726418984 Bug: 475972617 Change-Id: I5816e4e54b3162efc6deeaa044a98bccfb3563fb Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7510760 Reviewed-by: Dominic Farolino <dom@chromium.org> Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org> Cr-Commit-Position: refs/heads/main@{#1577100}
diff --git a/third_party/blink/renderer/core/html/forms/html_form_control_element.cc b/third_party/blink/renderer/core/html/forms/html_form_control_element.cc index f05b549d..85f1ad4 100644 --- a/third_party/blink/renderer/core/html/forms/html_form_control_element.cc +++ b/third_party/blink/renderer/core/html/forms/html_form_control_element.cc
@@ -55,7 +55,10 @@ #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" #include "third_party/blink/renderer/platform/runtime_enabled_features.h" +#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" #include "third_party/blink/renderer/platform/wtf/vector.h" +#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h" namespace blink { @@ -380,6 +383,43 @@ NOTREACHED(); } +String HTMLFormControlElement::GetWebMCPParameterDescription() { + // Prefer 'toolparamdescription' when present. + if (String description = + FastGetAttribute(html_names::kToolparamdescriptionAttr); + !description.empty()) { + return description; + } + + // Absent a 'toolparamdescription' attribute, use concatenated label text. + // + // TODO(crbug.com/475972617): Is this too expensive? The `labels()` function + // lazily creates a cached collection that may not be commonly created. + if (LiveNodeList* list = labels()) { + StringBuilder builder; + + for (wtf_size_t i = 0; i < list->length(); ++i) { + Element* label = list->item(i); + if (i != 0) { + builder.Append("; "); + } + builder.Append(label->textContent()); + } + + if (!builder.empty()) { + return builder.ReleaseString(); + } + } + + // Last resort: aria-description. + if (String description = FastGetAttribute(html_names::kAriaDescriptionAttr); + !description.empty()) { + return description; + } + + return g_null_atom; +} + bool HTMLFormControlElement::IsValidElement() { return ListedElement::IsValidElement(); }
diff --git a/third_party/blink/renderer/core/html/forms/html_form_control_element.h b/third_party/blink/renderer/core/html/forms/html_form_control_element.h index 1d98354..9bab036 100644 --- a/third_party/blink/renderer/core/html/forms/html_form_control_element.h +++ b/third_party/blink/renderer/core/html/forms/html_form_control_element.h
@@ -170,11 +170,9 @@ // Note that the return value should not contain top-level "description" // or "title" fields, as these are automatically added to all objects // at the call site. - // - // TODO(crbug.com/475972617): Or rather, "description" *will* be added, - // once we support it. virtual std::unique_ptr<JSONObject> GetWebMCPParameterSchema() const; virtual void FillWebMCPData(JSONValue& data); + String GetWebMCPParameterDescription(); protected: HTMLFormControlElement(const QualifiedName& tag_name, Document&);
diff --git a/third_party/blink/renderer/core/html/forms/html_form_element.cc b/third_party/blink/renderer/core/html/forms/html_form_element.cc index 0b86d70..461a15a3 100644 --- a/third_party/blink/renderer/core/html/forms/html_form_element.cc +++ b/third_party/blink/renderer/core/html/forms/html_form_element.cc
@@ -290,6 +290,11 @@ parameter_schema->SetString("title", title); } + if (String description = form_control->GetWebMCPParameterDescription(); + !description.empty()) { + parameter_schema->SetString("description", description); + } + properties->SetObject(name, std::move(parameter_schema)); if (form_control->IsRequired()) { required->PushString(name);
diff --git a/third_party/blink/renderer/core/html/forms/html_form_mcp_tool_test.cc b/third_party/blink/renderer/core/html/forms/html_form_mcp_tool_test.cc index adf01ee..5029860 100644 --- a/third_party/blink/renderer/core/html/forms/html_form_mcp_tool_test.cc +++ b/third_party/blink/renderer/core/html/forms/html_form_mcp_tool_test.cc
@@ -403,6 +403,184 @@ EXPECT_EQ(expected_json->ToJSONString(), actual); } +TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_Description) { + SetBodyInnerHTML( + R"HTML( + <form id="form" toolname="mytool" tooldescription="perform task"> + <input name="text1" type="text" toolparamdescription="Some nice text"> + </form> + )HTML"); + + HTMLFormElement* form_element = GetFormElement("form"); + ASSERT_TRUE(form_element); + ASSERT_TRUE(IsValidWebMCPForm(*form_element)); + String actual = ComputeInputSchema(*form_element); + std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( + { + "type": "object", + "properties": { + "text1": { + "type": "string", + "description": "Some nice text" + } + }, + "required": [] + } + )JSON"); + ASSERT_TRUE(expected_json); + EXPECT_EQ(expected_json->ToJSONString(), actual); +} + +TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_Label) { + SetBodyInnerHTML( + R"HTML( + <form id="form" toolname="mytool" tooldescription="perform task"> + <label for="text">Some text</label> + <input id="text" name="text1" type="text"> + </form> + )HTML"); + + HTMLFormElement* form_element = GetFormElement("form"); + ASSERT_TRUE(form_element); + ASSERT_TRUE(IsValidWebMCPForm(*form_element)); + String actual = ComputeInputSchema(*form_element); + std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( + { + "type": "object", + "properties": { + "text1": { + "type": "string", + "description": "Some text" + } + }, + "required": [] + } + )JSON"); + ASSERT_TRUE(expected_json); + EXPECT_EQ(expected_json->ToJSONString(), actual); +} + +TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_Label_Multiple) { + SetBodyInnerHTML( + R"HTML( + <form id="form" toolname="mytool" tooldescription="perform task"> + <label for="text">Label one</label> + <label for="text">Label two</label> + <input id="text" name="text1" type="text"> + </form> + )HTML"); + + HTMLFormElement* form_element = GetFormElement("form"); + ASSERT_TRUE(form_element); + ASSERT_TRUE(IsValidWebMCPForm(*form_element)); + String actual = ComputeInputSchema(*form_element); + std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( + { + "type": "object", + "properties": { + "text1": { + "type": "string", + "description": "Label one; Label two" + } + }, + "required": [] + } + )JSON"); + ASSERT_TRUE(expected_json); + EXPECT_EQ(expected_json->ToJSONString(), actual); +} + +TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_AriaDescription) { + SetBodyInnerHTML( + R"HTML( + <form id="form" toolname="mytool" tooldescription="perform task"> + <input name="text1" type="text" aria-description="ARIA"> + </form> + )HTML"); + + HTMLFormElement* form_element = GetFormElement("form"); + ASSERT_TRUE(form_element); + ASSERT_TRUE(IsValidWebMCPForm(*form_element)); + String actual = ComputeInputSchema(*form_element); + std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( + { + "type": "object", + "properties": { + "text1": { + "type": "string", + "description": "ARIA" + } + }, + "required": [] + } + )JSON"); + ASSERT_TRUE(expected_json); + EXPECT_EQ(expected_json->ToJSONString(), actual); +} + +TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_PreferLabelOverAria) { + SetBodyInnerHTML( + R"HTML( + <form id="form" toolname="mytool" tooldescription="perform task"> + <label for="text">Label</label> + <input id="text" name="text1" type="text" aria-description="ARIA"> + </form> + )HTML"); + + HTMLFormElement* form_element = GetFormElement("form"); + ASSERT_TRUE(form_element); + ASSERT_TRUE(IsValidWebMCPForm(*form_element)); + String actual = ComputeInputSchema(*form_element); + std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( + { + "type": "object", + "properties": { + "text1": { + "type": "string", + "description": "Label" + } + }, + "required": [] + } + )JSON"); + ASSERT_TRUE(expected_json); + EXPECT_EQ(expected_json->ToJSONString(), actual); +} + +TEST_F(HTMLFormMcpToolTest, ParameterSchema_TextInput_PreferAttrOverLabel) { + SetBodyInnerHTML( + R"HTML( + <form id="form" toolname="mytool" tooldescription="perform task"> + <label for="text">Label</label> + <input + id="text" + name="text1" + type="text" + toolparamdescription="ATTR" + aria-description="ARIA"> + </form> + )HTML"); + + HTMLFormElement* form_element = GetFormElement("form"); + ASSERT_TRUE(form_element); + ASSERT_TRUE(IsValidWebMCPForm(*form_element)); + String actual = ComputeInputSchema(*form_element); + std::unique_ptr<JSONValue> expected_json = ParseJSON(R"JSON( + { + "type": "object", + "properties": { + "text1": { + "type": "string", + "description": "ATTR" + } + }, + "required": [] + } + )JSON"); + ASSERT_TRUE(expected_json); + EXPECT_EQ(expected_json->ToJSONString(), actual); +} + TEST_F(HTMLFormMcpToolTest, ParameterSchema_Select) { SetBodyInnerHTML( R"HTML(
diff --git a/third_party/blink/renderer/core/html/html_attribute_names.json5 b/third_party/blink/renderer/core/html/html_attribute_names.json5 index 66dc4e42..e4a9d7a5 100644 --- a/third_party/blink/renderer/core/html/html_attribute_names.json5 +++ b/third_party/blink/renderer/core/html/html_attribute_names.json5
@@ -376,6 +376,7 @@ "toolautosubmit", "tooldescription", "toolname", + "toolparamdescription", "toolparamtitle", "topmargin", "translate",