[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",