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"