Improved parameter validation
Change-Id: If2e7ec1b063a6e114a6c5582463af784b75c37b8
diff --git a/tools/metadata.go b/tools/metadata.go
index c3da813..46eb221 100644
--- a/tools/metadata.go
+++ b/tools/metadata.go
@@ -42,16 +42,22 @@
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
- "description": "Type of metadata to retrieve: 'list' for corpus list, 'statistics' for corpus statistics",
+ "description": "Type of metadata operation to perform. 'list' retrieves all available corpora with their basic information, 'statistics' provides detailed corpus statistics.",
"enum": []string{"list", "statistics"},
"default": "list",
+ "examples": []string{"list", "statistics"},
},
"corpus": map[string]interface{}{
"type": "string",
- "description": "Virtual corpus query to filter results (optional, when not provided refers to all data available to the user)",
+ "description": "Virtual corpus query to filter results based on metadata fields. For 'list' action, this parameter is ignored. For 'statistics' action, specifies which subset of data to analyze using metadata queries with boolean operations (& | !), comparison operators (= != < > in), and regular expressions (/pattern/). When not provided with 'statistics', returns statistics for all accessible data.",
+ "pattern": "^[a-zA-Z0-9._\\-\\s&|!=<>()/*\"']+$",
+ "examples": []string{"corpusSigle = \"GOE\"", "textClass = \"politics\" & pubDate in 2020", "textType = \"news\" | textType = \"blog\"", "availability = /CC.*/ & textClass != \"fiction\""},
},
},
- "required": []string{"action"},
+ "required": []string{"action"},
+ "additionalProperties": false,
+ "title": "KorAP Metadata Parameters",
+ "description": "Parameters for retrieving corpus metadata and statistics from KorAP, including corpus lists and detailed statistical information.",
}
}
diff --git a/tools/metadata_test.go b/tools/metadata_test.go
index e5d60cd..04a067c 100644
--- a/tools/metadata_test.go
+++ b/tools/metadata_test.go
@@ -2,6 +2,7 @@
import (
"context"
+ "strings"
"testing"
"github.com/korap/korap-mcp/service"
@@ -32,6 +33,9 @@
// Verify it's an object type
assert.Equal(t, "object", schema["type"])
+ assert.Equal(t, "KorAP Metadata Parameters", schema["title"])
+ assert.Contains(t, schema["description"], "Parameters for retrieving corpus metadata")
+ assert.Equal(t, false, schema["additionalProperties"])
// Verify properties exist
properties, ok := schema["properties"].(map[string]interface{})
@@ -43,6 +47,7 @@
action, ok := properties["action"].(map[string]interface{})
assert.True(t, ok)
assert.Equal(t, "string", action["type"])
+ assert.Contains(t, action["description"], "Type of metadata operation")
enum, ok := action["enum"].([]string)
assert.True(t, ok)
@@ -50,10 +55,23 @@
assert.Contains(t, enum, "statistics")
assert.Equal(t, "list", action["default"])
+ actionExamples, ok := action["examples"].([]string)
+ assert.True(t, ok)
+ assert.Contains(t, actionExamples, "list")
+ assert.Contains(t, actionExamples, "statistics")
+
+ // Verify corpus property details
+ corpus, ok := properties["corpus"].(map[string]interface{})
+ assert.True(t, ok)
+ assert.Equal(t, "string", corpus["type"])
+ assert.Contains(t, corpus["description"], "Virtual corpus query")
+ assert.Contains(t, corpus["examples"], "corpusSigle = \"GOE\"")
+
// Verify required fields
required, ok := schema["required"].([]string)
assert.True(t, ok)
assert.Contains(t, required, "action")
+ assert.Len(t, required, 1) // Only action should be required
}
func TestNewMetadataTool(t *testing.T) {
@@ -284,3 +302,126 @@
assert.Contains(t, result, "Documents: 100")
assert.Contains(t, result, "Tokens: 50000")
}
+
+func TestMetadataTool_SchemaCompliance(t *testing.T) {
+ // Test various parameter combinations against the schema
+ client := &service.Client{}
+ tool := NewMetadataTool(client)
+
+ tests := []struct {
+ name string
+ arguments map[string]interface{}
+ expectValid bool
+ errorMsg string
+ }{
+ {
+ name: "valid_list_minimal",
+ arguments: map[string]interface{}{
+ "action": "list",
+ },
+ expectValid: true,
+ },
+ {
+ name: "valid_statistics_minimal",
+ arguments: map[string]interface{}{
+ "action": "statistics",
+ },
+ expectValid: true,
+ },
+ {
+ name: "valid_statistics_with_corpus",
+ arguments: map[string]interface{}{
+ "action": "statistics",
+ "corpus": "test-corpus",
+ },
+ expectValid: true,
+ },
+ {
+ name: "valid_list_with_corpus_ignored",
+ arguments: map[string]interface{}{
+ "action": "list",
+ "corpus": "test-corpus", // Should be ignored for list action
+ },
+ expectValid: true,
+ },
+ {
+ name: "missing_required_action",
+ arguments: map[string]interface{}{
+ "corpus": "test-corpus",
+ },
+ expectValid: false,
+ errorMsg: "action parameter is required",
+ },
+ {
+ name: "invalid_action",
+ arguments: map[string]interface{}{
+ "action": "invalid",
+ },
+ expectValid: false,
+ errorMsg: "invalid action",
+ },
+ {
+ name: "empty_action",
+ arguments: map[string]interface{}{
+ "action": "",
+ },
+ expectValid: false,
+ errorMsg: "action is required and cannot be empty",
+ },
+ {
+ name: "invalid_corpus_format",
+ arguments: map[string]interface{}{
+ "action": "statistics",
+ "corpus": "invalid@corpus#format",
+ },
+ expectValid: false,
+ errorMsg: "collection query contains invalid characters",
+ },
+ {
+ name: "valid_corpus_with_boolean",
+ arguments: map[string]interface{}{
+ "action": "statistics",
+ "corpus": "corpus1 & corpus2",
+ },
+ expectValid: true,
+ },
+ {
+ name: "valid_collection_query",
+ arguments: map[string]interface{}{
+ "action": "statistics",
+ "corpus": "textClass = \"politics\" & pubDate in 2020",
+ },
+ expectValid: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ request := mcp.CallToolRequest{
+ Params: mcp.CallToolParams{
+ Arguments: tt.arguments,
+ },
+ }
+
+ _, err := tool.Execute(context.Background(), request)
+
+ if tt.expectValid {
+ // For valid requests, we expect authentication/client errors, not validation errors
+ if err != nil {
+ assert.NotContains(t, err.Error(), "validation")
+ // Should fail at authentication or client configuration
+ assert.True(t,
+ strings.Contains(err.Error(), "authentication") ||
+ strings.Contains(err.Error(), "KorAP client not configured"),
+ "Expected authentication or client error, got: %s", err.Error())
+ }
+ } else {
+ // For invalid requests, we expect validation errors
+ assert.Error(t, err)
+ if tt.errorMsg != "" {
+ assert.Contains(t, err.Error(), tt.errorMsg)
+ }
+ }
+ })
+ }
+}
diff --git a/tools/schema_test.go b/tools/schema_test.go
new file mode 100644
index 0000000..4365537
--- /dev/null
+++ b/tools/schema_test.go
@@ -0,0 +1,362 @@
+package tools
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/korap/korap-mcp/service"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// TestAllToolsSchemaCompliance ensures all tools have properly structured schemas
+func TestAllToolsSchemaCompliance(t *testing.T) {
+ client := &service.Client{}
+
+ tools := []Tool{
+ NewSearchTool(client),
+ NewMetadataTool(client),
+ }
+
+ for _, tool := range tools {
+ t.Run(tool.Name(), func(t *testing.T) {
+ schema := tool.InputSchema()
+
+ // Verify basic schema structure
+ validateBasicSchemaStructure(t, schema, tool.Name())
+
+ // Verify schema is valid JSON
+ validateSchemaIsValidJSON(t, schema, tool.Name())
+
+ // Verify schema documentation completeness
+ validateSchemaDocumentation(t, schema, tool.Name())
+ })
+ }
+}
+
+// validateBasicSchemaStructure checks that the schema has the required top-level properties
+func validateBasicSchemaStructure(t *testing.T, schema map[string]interface{}, toolName string) {
+ // Must be object type
+ assert.Equal(t, "object", schema["type"], "Tool %s schema must be object type", toolName)
+
+ // Must have properties
+ properties, exists := schema["properties"]
+ assert.True(t, exists, "Tool %s schema must have properties", toolName)
+ assert.IsType(t, map[string]interface{}{}, properties, "Tool %s properties must be an object", toolName)
+
+ // Must have required array
+ required, exists := schema["required"]
+ assert.True(t, exists, "Tool %s schema must have required array", toolName)
+ assert.IsType(t, []string{}, required, "Tool %s required must be string array", toolName)
+
+ // Should have title and description
+ assert.Contains(t, schema, "title", "Tool %s schema should have title", toolName)
+ assert.Contains(t, schema, "description", "Tool %s schema should have description", toolName)
+
+ // Should prevent additional properties for strict validation
+ assert.Contains(t, schema, "additionalProperties", "Tool %s schema should specify additionalProperties", toolName)
+ assert.Equal(t, false, schema["additionalProperties"], "Tool %s should not allow additional properties", toolName)
+}
+
+// validateSchemaIsValidJSON ensures the schema can be properly serialized to JSON
+func validateSchemaIsValidJSON(t *testing.T, schema map[string]interface{}, toolName string) {
+ jsonBytes, err := json.Marshal(schema)
+ require.NoError(t, err, "Tool %s schema must be serializable to JSON", toolName)
+
+ var unmarshalled map[string]interface{}
+ err = json.Unmarshal(jsonBytes, &unmarshalled)
+ require.NoError(t, err, "Tool %s schema JSON must be valid", toolName)
+
+ // Verify the unmarshalled schema has the same structure
+ assert.Equal(t, schema["type"], unmarshalled["type"], "Tool %s schema type must survive JSON round-trip", toolName)
+}
+
+// validateSchemaDocumentation checks that all properties have proper documentation
+func validateSchemaDocumentation(t *testing.T, schema map[string]interface{}, toolName string) {
+ properties, ok := schema["properties"].(map[string]interface{})
+ require.True(t, ok, "Tool %s properties must be accessible", toolName)
+
+ for propName, propSchema := range properties {
+ propMap, ok := propSchema.(map[string]interface{})
+ require.True(t, ok, "Tool %s property %s must be an object", toolName, propName)
+
+ // Must have type
+ assert.Contains(t, propMap, "type", "Tool %s property %s must have type", toolName, propName)
+
+ // Must have description
+ assert.Contains(t, propMap, "description", "Tool %s property %s must have description", toolName, propName)
+
+ description, ok := propMap["description"].(string)
+ assert.True(t, ok, "Tool %s property %s description must be string", toolName, propName)
+ assert.NotEmpty(t, description, "Tool %s property %s description must not be empty", toolName, propName)
+ assert.Greater(t, len(description), 10, "Tool %s property %s description should be descriptive", toolName, propName)
+
+ // String properties should have examples
+ if propType, ok := propMap["type"].(string); ok && propType == "string" {
+ if enumValues, hasEnum := propMap["enum"]; hasEnum {
+ // Enum properties should have examples matching enum values
+ assert.Contains(t, propMap, "examples", "Tool %s property %s with enum should have examples", toolName, propName)
+ examples, ok := propMap["examples"].([]string)
+ assert.True(t, ok, "Tool %s property %s examples must be string array", toolName, propName)
+
+ enumArray, ok := enumValues.([]string)
+ assert.True(t, ok, "Tool %s property %s enum must be string array", toolName, propName)
+
+ // All examples should be valid enum values
+ for _, example := range examples {
+ assert.Contains(t, enumArray, example, "Tool %s property %s example '%s' must be valid enum value", toolName, propName, example)
+ }
+ } else {
+ // Regular string properties should have examples
+ assert.Contains(t, propMap, "examples", "Tool %s property %s should have examples", toolName, propName)
+ }
+ }
+
+ // Integer properties should have examples and constraints
+ if propType, ok := propMap["type"].(string); ok && propType == "integer" {
+ assert.Contains(t, propMap, "examples", "Tool %s property %s should have examples", toolName, propName)
+
+ // Should have minimum/maximum constraints
+ assert.Contains(t, propMap, "minimum", "Tool %s property %s should have minimum", toolName, propName)
+ assert.Contains(t, propMap, "maximum", "Tool %s property %s should have maximum", toolName, propName)
+ }
+ }
+}
+
+// TestSchemaExamples verifies that schema examples are realistic and valid
+func TestSchemaExamples(t *testing.T) {
+ client := &service.Client{}
+
+ t.Run("SearchTool", func(t *testing.T) {
+ tool := NewSearchTool(client)
+ schema := tool.InputSchema()
+
+ properties := schema["properties"].(map[string]interface{})
+
+ // Test query examples
+ queryProp := properties["query"].(map[string]interface{})
+ examples := queryProp["examples"].([]string)
+
+ for _, example := range examples {
+ assert.NotEmpty(t, example, "Query example should not be empty")
+ assert.LessOrEqual(t, len(example), 100, "Query example should be reasonably short")
+ }
+
+ // Test corpus examples
+ corpusProp := properties["corpus"].(map[string]interface{})
+ corpusExamples := corpusProp["examples"].([]string)
+
+ for _, example := range corpusExamples {
+ assert.NotEmpty(t, example, "Corpus example should not be empty")
+ // Should match the pattern defined in the schema
+ pattern := corpusProp["pattern"].(string)
+ assert.NotEmpty(t, pattern, "Corpus should have validation pattern")
+ }
+ })
+
+ t.Run("MetadataTool", func(t *testing.T) {
+ tool := NewMetadataTool(client)
+ schema := tool.InputSchema()
+
+ properties := schema["properties"].(map[string]interface{})
+
+ // Test action examples
+ actionProp := properties["action"].(map[string]interface{})
+ examples := actionProp["examples"].([]string)
+ enumValues := actionProp["enum"].([]string)
+
+ for _, example := range examples {
+ assert.Contains(t, enumValues, example, "Action example should be valid enum value")
+ }
+ })
+}
+
+// TestSchemaConstraints verifies that schema constraints are reasonable
+func TestSchemaConstraints(t *testing.T) {
+ client := &service.Client{}
+
+ t.Run("SearchTool", func(t *testing.T) {
+ tool := NewSearchTool(client)
+ schema := tool.InputSchema()
+ properties := schema["properties"].(map[string]interface{})
+
+ // Test query constraints
+ queryProp := properties["query"].(map[string]interface{})
+ minLength := queryProp["minLength"].(int)
+ maxLength := queryProp["maxLength"].(int)
+
+ assert.Greater(t, minLength, 0, "Query minimum length should be positive")
+ assert.Greater(t, maxLength, minLength, "Query maximum length should be greater than minimum")
+ assert.LessOrEqual(t, maxLength, 10000, "Query maximum length should be reasonable")
+
+ // Test count constraints
+ countProp := properties["count"].(map[string]interface{})
+ minimum := countProp["minimum"].(int)
+ maximum := countProp["maximum"].(int)
+ defaultValue := countProp["default"].(int)
+
+ assert.GreaterOrEqual(t, minimum, 0, "Count minimum should be non-negative")
+ assert.Greater(t, maximum, minimum, "Count maximum should be greater than minimum")
+ assert.GreaterOrEqual(t, defaultValue, minimum, "Count default should be >= minimum")
+ assert.LessOrEqual(t, defaultValue, maximum, "Count default should be <= maximum")
+ })
+}
+
+// TestSchemaVersioning ensures schema structure is backwards compatible
+func TestSchemaVersioning(t *testing.T) {
+ client := &service.Client{}
+
+ // This test would be extended when we add new versions
+ // For now, we ensure the basic structure is stable
+
+ tools := []Tool{
+ NewSearchTool(client),
+ NewMetadataTool(client),
+ }
+
+ for _, tool := range tools {
+ t.Run(tool.Name(), func(t *testing.T) {
+ schema := tool.InputSchema()
+
+ // Core schema structure must remain stable
+ assert.Equal(t, "object", schema["type"])
+ assert.Contains(t, schema, "properties")
+ assert.Contains(t, schema, "required")
+
+ // Tool-specific stability checks
+ switch tool.Name() {
+ case "korap_search":
+ properties := schema["properties"].(map[string]interface{})
+ assert.Contains(t, properties, "query", "Search tool must always have query parameter")
+
+ required := schema["required"].([]string)
+ assert.Contains(t, required, "query", "Search tool must always require query parameter")
+
+ case "korap_metadata":
+ properties := schema["properties"].(map[string]interface{})
+ assert.Contains(t, properties, "action", "Metadata tool must always have action parameter")
+
+ required := schema["required"].([]string)
+ assert.Contains(t, required, "action", "Metadata tool must always require action parameter")
+ }
+ })
+ }
+}
+
+// BenchmarkSchemaGeneration measures schema generation performance
+func BenchmarkSchemaGeneration(b *testing.B) {
+ client := &service.Client{}
+
+ tools := []Tool{
+ NewSearchTool(client),
+ NewMetadataTool(client),
+ }
+
+ for _, tool := range tools {
+ b.Run(tool.Name(), func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ schema := tool.InputSchema()
+ _ = schema // Prevent optimization
+ }
+ })
+ }
+}
+
+// TestSchemaDeepCopy ensures schemas are independent instances
+func TestSchemaDeepCopy(t *testing.T) {
+ client := &service.Client{}
+ tool := NewSearchTool(client)
+
+ // Get two schema instances
+ schema1 := tool.InputSchema()
+ schema2 := tool.InputSchema()
+
+ // They should be equal in content but different instances
+ assert.True(t, reflect.DeepEqual(schema1, schema2), "Schemas should have equal content")
+
+ // Modifying one should not affect the other
+ schema1["modified"] = true
+ assert.NotContains(t, schema2, "modified", "Schema instances should be independent")
+}
+
+// TestSchemaHelpGeneration verifies schema can be used to generate help text
+func TestSchemaHelpGeneration(t *testing.T) {
+ client := &service.Client{}
+
+ tools := []Tool{
+ NewSearchTool(client),
+ NewMetadataTool(client),
+ }
+
+ for _, tool := range tools {
+ t.Run(tool.Name(), func(t *testing.T) {
+ schema := tool.InputSchema()
+
+ // Generate help text from schema
+ helpText := generateHelpText(schema, tool.Name())
+
+ // Verify help text contains essential information
+ assert.Contains(t, helpText, tool.Name(), "Help text should contain tool name")
+ assert.Contains(t, helpText, "Parameters:", "Help text should list parameters")
+
+ // Should contain information about required parameters
+ required := schema["required"].([]string)
+ for _, param := range required {
+ assert.Contains(t, helpText, param, "Help text should mention required parameter %s", param)
+ assert.Contains(t, helpText, "(required)", "Help text should mark required parameters")
+ }
+ })
+ }
+}
+
+// generateHelpText creates human-readable help text from a schema
+func generateHelpText(schema map[string]interface{}, toolName string) string {
+ help := fmt.Sprintf("Tool: %s\n", toolName)
+
+ if desc, ok := schema["description"].(string); ok {
+ help += fmt.Sprintf("Description: %s\n\n", desc)
+ }
+
+ properties, ok := schema["properties"].(map[string]interface{})
+ if !ok {
+ return help
+ }
+
+ required, _ := schema["required"].([]string)
+ requiredMap := make(map[string]bool)
+ for _, req := range required {
+ requiredMap[req] = true
+ }
+
+ help += "Parameters:\n"
+ for paramName, paramSchema := range properties {
+ paramMap, ok := paramSchema.(map[string]interface{})
+ if !ok {
+ continue
+ }
+
+ requiredText := ""
+ if requiredMap[paramName] {
+ requiredText = " (required)"
+ }
+
+ paramType, _ := paramMap["type"].(string)
+ description, _ := paramMap["description"].(string)
+
+ help += fmt.Sprintf(" %s (%s)%s: %s\n", paramName, paramType, requiredText, description)
+
+ // Add enum values if present
+ if enum, ok := paramMap["enum"].([]string); ok {
+ help += fmt.Sprintf(" Allowed values: %v\n", enum)
+ }
+
+ // Add examples if present
+ if examples, ok := paramMap["examples"]; ok {
+ help += fmt.Sprintf(" Examples: %v\n", examples)
+ }
+ }
+
+ return help
+}
diff --git a/tools/search.go b/tools/search.go
index 2a78f8e..002dc86 100644
--- a/tools/search.go
+++ b/tools/search.go
@@ -42,27 +42,37 @@
"properties": map[string]interface{}{
"query": map[string]interface{}{
"type": "string",
- "description": "The search query (word, phrase, or pattern)",
+ "description": "The search query. Supports different query languages like Poliqarp, CosmasII, or Annis depending on the selected query_language parameter.",
+ "minLength": 1,
+ "maxLength": 1000,
+ "examples": []string{"Haus", "[pos=NN]", "der /w1:5 Mann"},
},
"query_language": map[string]interface{}{
"type": "string",
- "description": "Query language: 'poliqarp' (default), 'cosmas2', or 'annis'",
- "enum": []string{"poliqarp", "cosmas2", "annis"},
+ "description": "Query language to use for parsing the search query. Supported languages: 'poliqarp' (default; extended Poliqarp QL), 'cosmas2' (corpus query syntax of COSMAS II), 'annis' (multi-layer annotation queries), 'cql' (corpus query language), 'cqp' (Corpus Query Processor syntax), 'fcsql' (Federated Content Search queries).",
+ "enum": []string{"poliqarp", "cosmas2", "annis", "cql", "cqp", "fcsql"},
"default": "poliqarp",
+ "examples": []string{"poliqarp", "cosmas2", "annis", "cql", "cqp", "fcsql"},
},
"corpus": map[string]interface{}{
"type": "string",
- "description": "Virtual corpus query to filter search results (optional, when not provided searches all available data)",
+ "description": "Virtual corpus query to filter search results based on metadata fields. Supports boolean operations (& | !), comparison operators (= != < > in), and regular expressions (/pattern/). Use metadata fields like corpusSigle, textClass, pubDate, textType, availability, etc. When not provided, searches all available data accessible to the user.",
+ "pattern": "^[a-zA-Z0-9._\\-\\s&|!=<>()/*\"']+$",
+ "examples": []string{"corpusSigle = \"GOE\"", "textClass = \"politics\" & pubDate in 2020", "textType = \"news\" | textType = \"blog\"", "availability = /CC.*/ & textClass != \"fiction\""},
},
"count": map[string]interface{}{
"type": "integer",
- "description": "Number of results to return (max 100)",
- "minimum": 1,
- "maximum": 100,
+ "description": "Maximum number of search results to return. Higher values may increase response time. Use smaller values for faster responses when doing exploratory searches.",
+ "minimum": 0,
+ "maximum": 10000,
"default": 25,
+ "examples": []interface{}{10, 25, 50, 100},
},
},
- "required": []string{"query"},
+ "required": []string{"query"},
+ "additionalProperties": false,
+ "title": "KorAP Search Parameters",
+ "description": "Parameters for searching text corpora using KorAP's powerful query languages and filtering capabilities.",
}
}
diff --git a/tools/search_test.go b/tools/search_test.go
index 5966877..b80915a 100644
--- a/tools/search_test.go
+++ b/tools/search_test.go
@@ -32,6 +32,9 @@
// Verify it's an object type
assert.Equal(t, "object", schema["type"])
+ assert.Equal(t, "KorAP Search Parameters", schema["title"])
+ assert.Contains(t, schema["description"], "Parameters for searching text corpora")
+ assert.Equal(t, false, schema["additionalProperties"])
// Verify properties exist
properties, ok := schema["properties"].(map[string]interface{})
@@ -40,12 +43,168 @@
assert.Contains(t, properties, "query_language")
assert.Contains(t, properties, "corpus")
assert.Contains(t, properties, "count")
- // Note: offset and context will be added in future iterations
+
+ // Verify query property details
+ query, ok := properties["query"].(map[string]interface{})
+ assert.True(t, ok)
+ assert.Equal(t, "string", query["type"])
+ assert.Contains(t, query["description"], "search query")
+ assert.Equal(t, 1, query["minLength"])
+ assert.Equal(t, 1000, query["maxLength"])
+ examples, ok := query["examples"].([]string)
+ assert.True(t, ok)
+ assert.Contains(t, examples, "Haus")
+
+ // Verify query_language property details
+ queryLang, ok := properties["query_language"].(map[string]interface{})
+ assert.True(t, ok)
+ assert.Equal(t, "string", queryLang["type"])
+ assert.Contains(t, queryLang["description"], "Query language to use for parsing")
+ enum, ok := queryLang["enum"].([]string)
+ assert.True(t, ok)
+ assert.Contains(t, enum, "poliqarp")
+ assert.Contains(t, enum, "cosmas2")
+ assert.Contains(t, enum, "annis")
+ assert.Contains(t, enum, "cql")
+ assert.Contains(t, enum, "cqp")
+ assert.Contains(t, enum, "fcsql")
+ assert.Equal(t, "poliqarp", queryLang["default"])
+
+ // Verify corpus property details
+ corpus, ok := properties["corpus"].(map[string]interface{})
+ assert.True(t, ok)
+ assert.Equal(t, "string", corpus["type"])
+ assert.Contains(t, corpus["description"], "Virtual corpus query")
+ assert.NotEmpty(t, corpus["pattern"])
+ corpusExamples, ok := corpus["examples"].([]string)
+ assert.True(t, ok)
+ assert.Contains(t, corpusExamples, "corpusSigle = \"GOE\"")
+
+ // Verify count property details
+ count, ok := properties["count"].(map[string]interface{})
+ assert.True(t, ok)
+ assert.Equal(t, "integer", count["type"])
+ assert.Contains(t, count["description"], "Maximum number")
+ assert.Equal(t, 0, count["minimum"])
+ assert.Equal(t, 10000, count["maximum"])
+ assert.Equal(t, 25, count["default"])
+ countExamples, ok := count["examples"].([]interface{})
+ assert.True(t, ok)
+ assert.Contains(t, countExamples, 25)
// Verify required fields
required, ok := schema["required"].([]string)
assert.True(t, ok)
assert.Contains(t, required, "query")
+ assert.Len(t, required, 1) // Only query should be required
+}
+
+func TestSearchTool_SchemaCompliance(t *testing.T) {
+ // Test various parameter combinations against the schema
+ client := &service.Client{}
+ tool := NewSearchTool(client)
+
+ tests := []struct {
+ name string
+ arguments map[string]interface{}
+ expectValid bool
+ errorMsg string
+ }{
+ {
+ name: "valid_minimal",
+ arguments: map[string]interface{}{
+ "query": "test",
+ },
+ expectValid: true,
+ },
+ {
+ name: "valid_full",
+ arguments: map[string]interface{}{
+ "query": "word",
+ "query_language": "cosmas2",
+ "corpus": "test-corpus",
+ "count": 10,
+ },
+ expectValid: true,
+ },
+ {
+ name: "missing_required_query",
+ arguments: map[string]interface{}{
+ "query_language": "poliqarp",
+ },
+ expectValid: false,
+ errorMsg: "query parameter is required",
+ },
+ {
+ name: "invalid_query_language",
+ arguments: map[string]interface{}{
+ "query": "test",
+ "query_language": "invalid",
+ },
+ expectValid: false,
+ errorMsg: "invalid query language",
+ },
+ {
+ name: "invalid_count_negative",
+ arguments: map[string]interface{}{
+ "query": "test",
+ "count": -1,
+ },
+ expectValid: false,
+ errorMsg: "count must be",
+ },
+ {
+ name: "invalid_count_too_large",
+ arguments: map[string]interface{}{
+ "query": "test",
+ "count": 20000,
+ },
+ expectValid: false,
+ errorMsg: "count must be",
+ },
+ {
+ name: "empty_query",
+ arguments: map[string]interface{}{
+ "query": "",
+ },
+ expectValid: false,
+ errorMsg: "query is required and cannot be empty",
+ },
+ {
+ name: "count_zero_valid",
+ arguments: map[string]interface{}{
+ "query": "test",
+ "count": 0,
+ },
+ expectValid: true, // Zero count should be valid (uses default behavior)
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ request := mcp.CallToolRequest{
+ Params: mcp.CallToolParams{
+ Arguments: tt.arguments,
+ },
+ }
+
+ _, err := tool.Execute(context.Background(), request)
+
+ if tt.expectValid {
+ // For valid requests, we expect authentication/client errors, not validation errors
+ if err != nil {
+ assert.NotContains(t, err.Error(), "validation")
+ assert.Contains(t, err.Error(), "authentication")
+ }
+ } else {
+ // For invalid requests, we expect validation errors
+ assert.Error(t, err)
+ if tt.errorMsg != "" {
+ assert.Contains(t, err.Error(), tt.errorMsg)
+ }
+ }
+ })
+ }
}
func TestNewSearchTool(t *testing.T) {