diff --git a/enterprise/e2e/html/hurl/mcp-2025-11-25-resources.all.hurl b/enterprise/e2e/html/hurl/mcp-2025-11-25-resources.all.hurl
index eb36be69..76a1465d 100644
--- a/enterprise/e2e/html/hurl/mcp-2025-11-25-resources.all.hurl
+++ b/enterprise/e2e/html/hurl/mcp-2025-11-25-resources.all.hurl
@@ -123,7 +123,7 @@ jsonpath "$.result.resources[20].uri" == "{{base}}/self/v1/schemas/api/schemas/s
jsonpath "$.result.resources[20].name" == "rpc"
jsonpath "$.result.resources[20].description" == "Search for schemas in the catalog by query term"
jsonpath "$.result.resources[20].mimeType" == "application/schema+json"
-jsonpath "$.result.resources[20].size" == 1120
+jsonpath "$.result.resources[20].size" == 1166
jsonpath "$.result.resources[21].uri" == "{{base}}/self/v1/schemas/api/schemas/stats/response"
jsonpath "$.result.resources[21].name" == "response"
jsonpath "$.result.resources[21].description" == "The response format for the schema statistics API endpoint"
diff --git a/enterprise/e2e/html/hurl/mcp-2025-11-25-search_schemas.all.hurl b/enterprise/e2e/html/hurl/mcp-2025-11-25-search_schemas.all.hurl
index e976a4bf..ef0d3419 100644
--- a/enterprise/e2e/html/hurl/mcp-2025-11-25-search_schemas.all.hurl
+++ b/enterprise/e2e/html/hurl/mcp-2025-11-25-search_schemas.all.hurl
@@ -964,3 +964,111 @@ POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
HTTP 200
[Asserts]
jsonpath "$.valid" == true
+
+# An empty query is rejected.
+POST {{base}}/self/v1/mcp
+MCP-Protocol-Version: 2025-11-25
+Content-Type: application/json
+```
+{
+ "jsonrpc": "2.0",
+ "id": 1140,
+ "method": "tools/call",
+ "params": {
+ "name": "search_schemas",
+ "arguments": { "q": "" }
+ }
+}
+```
+HTTP 200
+Content-Type: application/json
+Access-Control-Allow-Origin: {{base}}
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+[Asserts]
+jsonpath "$.jsonrpc" == "2.0"
+jsonpath "$.id" == 1140
+jsonpath "$.error.code" == -32602
+jsonpath "$.error.message" == "Invalid params"
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+[Asserts]
+jsonpath "$.valid" == true
+
+# A whitespace-only query is rejected.
+POST {{base}}/self/v1/mcp
+MCP-Protocol-Version: 2025-11-25
+Content-Type: application/json
+```
+{
+ "jsonrpc": "2.0",
+ "id": 1141,
+ "method": "tools/call",
+ "params": {
+ "name": "search_schemas",
+ "arguments": { "q": " " }
+ }
+}
+```
+HTTP 200
+Content-Type: application/json
+Access-Control-Allow-Origin: {{base}}
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+[Asserts]
+jsonpath "$.jsonrpc" == "2.0"
+jsonpath "$.id" == 1141
+jsonpath "$.error.code" == -32602
+jsonpath "$.error.message" == "Invalid params"
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+[Asserts]
+jsonpath "$.valid" == true
+
+# Tabs, newlines, and other whitespace characters are also rejected.
+POST {{base}}/self/v1/mcp
+MCP-Protocol-Version: 2025-11-25
+Content-Type: application/json
+```
+{
+ "jsonrpc": "2.0",
+ "id": 1142,
+ "method": "tools/call",
+ "params": {
+ "name": "search_schemas",
+ "arguments": { "q": "\t\n\r " }
+ }
+}
+```
+HTTP 200
+Content-Type: application/json
+Access-Control-Allow-Origin: {{base}}
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+[Asserts]
+jsonpath "$.jsonrpc" == "2.0"
+jsonpath "$.id" == 1142
+jsonpath "$.error.code" == -32602
+jsonpath "$.error.message" == "Invalid params"
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+[Asserts]
+jsonpath "$.valid" == true
diff --git a/enterprise/e2e/path/hurl/mcp-2025-11-25-resources.all.hurl b/enterprise/e2e/path/hurl/mcp-2025-11-25-resources.all.hurl
index 0bb6f725..40440e9f 100644
--- a/enterprise/e2e/path/hurl/mcp-2025-11-25-resources.all.hurl
+++ b/enterprise/e2e/path/hurl/mcp-2025-11-25-resources.all.hurl
@@ -123,7 +123,7 @@ jsonpath "$.result.resources[20].uri" == "{{base}}/v1/catalog/self/v1/schemas/ap
jsonpath "$.result.resources[20].name" == "rpc"
jsonpath "$.result.resources[20].description" == "Search for schemas in the catalog by query term"
jsonpath "$.result.resources[20].mimeType" == "application/schema+json"
-jsonpath "$.result.resources[20].size" == 1131
+jsonpath "$.result.resources[20].size" == 1177
jsonpath "$.result.resources[21].uri" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/stats/response"
jsonpath "$.result.resources[21].name" == "response"
jsonpath "$.result.resources[21].description" == "The response format for the schema statistics API endpoint"
diff --git a/enterprise/e2e/path/hurl/mcp-2025-11-25-search_schemas.all.hurl b/enterprise/e2e/path/hurl/mcp-2025-11-25-search_schemas.all.hurl
index 4ed72083..b146a581 100644
--- a/enterprise/e2e/path/hurl/mcp-2025-11-25-search_schemas.all.hurl
+++ b/enterprise/e2e/path/hurl/mcp-2025-11-25-search_schemas.all.hurl
@@ -169,3 +169,39 @@ POST {{base}}/v1/catalog/self/v1/api/schemas/evaluate{{schema_path}}
HTTP 200
[Asserts]
jsonpath "$.valid" == true
+
+# A whitespace-only query is rejected.
+POST {{base}}/v1/catalog/self/v1/mcp
+MCP-Protocol-Version: 2025-11-25
+Content-Type: application/json
+```
+{
+ "jsonrpc": "2.0",
+ "id": 1203,
+ "method": "tools/call",
+ "params": {
+ "name": "search_schemas",
+ "arguments": { "q": " " }
+ }
+}
+```
+HTTP 200
+Content-Type: application/json
+Access-Control-Allow-Origin: {{base}}
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "]+)>"
+[Asserts]
+jsonpath "$.jsonrpc" == "2.0"
+jsonpath "$.id" == 1203
+jsonpath "$.error.code" == -32602
+jsonpath "$.error.message" == "Invalid params"
+
+POST {{base}}/v1/catalog/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+[Asserts]
+jsonpath "$.valid" == true
diff --git a/src/actions/action_schema_search_v1.h b/src/actions/action_schema_search_v1.h
index bd60634e..63d1afcd 100644
--- a/src/actions/action_schema_search_v1.h
+++ b/src/actions/action_schema_search_v1.h
@@ -71,6 +71,15 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::RouterAction {
return;
}
+ if (query.find_first_not_of(" \t\n\r\f\v") == std::string_view::npos) {
+ sourcemeta::one::json_error(
+ request, response, sourcemeta::one::STATUS_BAD_REQUEST,
+ "invalid-search-query",
+ "The search query must contain at least one non-whitespace character",
+ this->error_schema_);
+ return;
+ }
+
constexpr std::size_t MAXIMUM_QUERY_LENGTH{256};
if (query.size() > MAXIMUM_QUERY_LENGTH) {
sourcemeta::one::json_error(
diff --git a/src/self/v1/schemas/api/schemas/search/rpc.json b/src/self/v1/schemas/api/schemas/search/rpc.json
index 584f9ea5..deeacd07 100644
--- a/src/self/v1/schemas/api/schemas/search/rpc.json
+++ b/src/self/v1/schemas/api/schemas/search/rpc.json
@@ -21,7 +21,9 @@
"q": {
"description": "Case-insensitive substring to search for",
"type": "string",
- "maxLength": 256
+ "pattern": "\\S",
+ "maxLength": 256,
+ "minLength": 1
},
"limit": {
"description": "Maximum number of results to return",
diff --git a/test/e2e/headless/hurl/schemas-search.all.hurl b/test/e2e/headless/hurl/schemas-search.all.hurl
index 5cb92821..aadabe7f 100644
--- a/test/e2e/headless/hurl/schemas-search.all.hurl
+++ b/test/e2e/headless/hurl/schemas-search.all.hurl
@@ -42,6 +42,50 @@ Link: ; rel="describedby"
[Asserts]
jsonpath "$.valid" == true
+GET {{base}}/self/v1/api/schemas/search?q=%20%20%20
+HTTP 400
+Content-Type: application/problem+json
+Access-Control-Allow-Origin: *
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+[Asserts]
+jsonpath "$.status" == 400
+jsonpath "$.title" == "sourcemeta:one/invalid-search-query"
+jsonpath "$.detail" == "The search query must contain at least one non-whitespace character"
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+Link: ; rel="describedby"
+[Asserts]
+jsonpath "$.valid" == true
+
+GET {{base}}/self/v1/api/schemas/search?q=%09%0A%0D
+HTTP 400
+Content-Type: application/problem+json
+Access-Control-Allow-Origin: *
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+[Asserts]
+jsonpath "$.status" == 400
+jsonpath "$.title" == "sourcemeta:one/invalid-search-query"
+jsonpath "$.detail" == "The search query must contain at least one non-whitespace character"
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+Link: ; rel="describedby"
+[Asserts]
+jsonpath "$.valid" == true
+
POST {{base}}/self/v1/api/schemas/search?q=foo
HTTP 405
Content-Type: application/problem+json
diff --git a/test/e2e/html/hurl/schemas-search.all.hurl b/test/e2e/html/hurl/schemas-search.all.hurl
index 0cb6ca7c..0fd1bdb7 100644
--- a/test/e2e/html/hurl/schemas-search.all.hurl
+++ b/test/e2e/html/hurl/schemas-search.all.hurl
@@ -42,6 +42,50 @@ Link: ; rel="describedby"
[Asserts]
jsonpath "$.valid" == true
+GET {{base}}/self/v1/api/schemas/search?q=%20%20%20
+HTTP 400
+Content-Type: application/problem+json
+Access-Control-Allow-Origin: *
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+[Asserts]
+jsonpath "$.status" == 400
+jsonpath "$.title" == "sourcemeta:one/invalid-search-query"
+jsonpath "$.detail" == "The search query must contain at least one non-whitespace character"
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+Link: ; rel="describedby"
+[Asserts]
+jsonpath "$.valid" == true
+
+GET {{base}}/self/v1/api/schemas/search?q=%09%0A%0D
+HTTP 400
+Content-Type: application/problem+json
+Access-Control-Allow-Origin: *
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+[Asserts]
+jsonpath "$.status" == 400
+jsonpath "$.title" == "sourcemeta:one/invalid-search-query"
+jsonpath "$.detail" == "The search query must contain at least one non-whitespace character"
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+Link: ; rel="describedby"
+[Asserts]
+jsonpath "$.valid" == true
+
POST {{base}}/self/v1/api/schemas/search?q=foo
HTTP 405
Content-Type: application/problem+json
diff --git a/test/e2e/path/hurl/search.all.hurl b/test/e2e/path/hurl/search.all.hurl
index 9e7f2b33..ee5cea11 100644
--- a/test/e2e/path/hurl/search.all.hurl
+++ b/test/e2e/path/hurl/search.all.hurl
@@ -38,3 +38,12 @@ Content-Type: application/problem+json
Access-Control-Allow-Origin: *
[Asserts]
jsonpath "$.status" == 400
+
+GET {{base}}/v1/catalog/self/v1/api/schemas/search?q=%20%20%20
+HTTP 400
+Content-Type: application/problem+json
+Access-Control-Allow-Origin: *
+[Asserts]
+jsonpath "$.status" == 400
+jsonpath "$.title" == "sourcemeta:one/invalid-search-query"
+jsonpath "$.detail" == "The search query must contain at least one non-whitespace character"