Enhance KorAP search
diff --git a/service/types.go b/service/types.go
index d5d4036..a6151b6 100644
--- a/service/types.go
+++ b/service/types.go
@@ -27,7 +27,7 @@
type SearchRequest struct {
Query string `json:"q"`
QueryLang string `json:"ql,omitempty"` // Query language (e.g., "poliqarp", "cosmas2", "annis")
- Collection string `json:"collection,omitempty"` // Collection filter
+ Collection string `json:"collection,omitempty"` // Corpus filter
Count int `json:"count,omitempty"` // Number of results to return
Offset int `json:"offset,omitempty"` // Offset for pagination
Context string `json:"context,omitempty"` // Context specification
diff --git a/tools/search.go b/tools/search.go
index c11caa9..7a0422d 100644
--- a/tools/search.go
+++ b/tools/search.go
@@ -3,6 +3,7 @@
import (
"context"
"fmt"
+ "strings"
"github.com/korap/korap-mcp/service"
"github.com/mark3labs/mcp-go/mcp"
@@ -68,26 +69,125 @@
Str("tool", s.Name()).
Msg("Executing search tool")
- // Extract required query parameter using the same pattern as main.go
+ // Extract required query parameter
query, err := request.RequireString("query")
if err != nil {
return nil, fmt.Errorf("query parameter is required: %w", err)
}
+ // Extract optional parameters with defaults
+ queryLang := request.GetString("query_language", "poliqarp")
+ corpus := request.GetString("corpus", "")
+ count := request.GetInt("count", 25)
+
log.Debug().
Str("query", query).
+ Str("query_language", queryLang).
+ Str("corpus", corpus).
+ Int("count", count).
Msg("Parsed search parameters")
- // For now, return a simple response to verify the tool works
- // TODO: Implement actual KorAP search functionality
- result := "KorAP Search Results\n"
- result += "====================\n\n"
- result += fmt.Sprintf("Query: %s\n", query)
- result += "Status: Tool is working, KorAP integration to be implemented\n"
+ // Check if client is available and authenticated
+ if s.client == nil {
+ return nil, fmt.Errorf("KorAP client not configured")
+ }
+
+ if !s.client.IsAuthenticated() {
+ log.Warn().Msg("Client not authenticated, attempting authentication")
+ if err := s.client.AuthenticateWithClientCredentials(ctx); err != nil {
+ return nil, fmt.Errorf("authentication failed: %w", err)
+ }
+ }
+
+ // Prepare search request
+ searchReq := service.SearchRequest{
+ Query: query,
+ QueryLang: queryLang,
+ Collection: corpus,
+ Count: count,
+ }
+
+ // Perform the search
+ var searchResp service.SearchResponse
+ err = s.client.PostJSON(ctx, "search", searchReq, &searchResp)
+ if err != nil {
+ log.Error().
+ Err(err).
+ Str("query", query).
+ Msg("Search request failed")
+ return nil, fmt.Errorf("search failed: %w", err)
+ }
log.Info().
Str("query", query).
- Msg("Search tool executed successfully")
+ Int("total_results", searchResp.Meta.TotalResults).
+ Int("returned_matches", len(searchResp.Matches)).
+ Float64("search_time", searchResp.Meta.SearchTime).
+ Msg("Search completed successfully")
+
+ // Format the response
+ result := s.formatSearchResults(&searchResp)
return mcp.NewToolResultText(result), nil
}
+
+// formatSearchResults formats the search response into a readable text format
+func (s *SearchTool) formatSearchResults(response *service.SearchResponse) string {
+ var result strings.Builder
+
+ result.WriteString("KorAP Search Results\n")
+ result.WriteString("====================\n\n")
+
+ // Query information
+ result.WriteString(fmt.Sprintf("Query: %s\n", response.Query.Query))
+ if response.Query.QueryLang != "" {
+ result.WriteString(fmt.Sprintf("Query Language: %s\n", response.Query.QueryLang))
+ }
+ if response.Query.Collection != "" {
+ result.WriteString(fmt.Sprintf("Corpus: %s\n", response.Query.Collection))
+ }
+ result.WriteString("\n")
+
+ // Result statistics
+ result.WriteString("Results Summary:\n")
+ result.WriteString(fmt.Sprintf(" Total Results: %d\n", response.Meta.TotalResults))
+ result.WriteString(fmt.Sprintf(" Shown: %d-%d\n",
+ response.Meta.StartIndex+1,
+ response.Meta.StartIndex+len(response.Matches)))
+ if response.Meta.SearchTime > 0 {
+ result.WriteString(fmt.Sprintf(" Search Time: %.3f seconds\n", response.Meta.SearchTime))
+ }
+ result.WriteString("\n")
+
+ // Individual matches
+ if len(response.Matches) > 0 {
+ result.WriteString("Matches:\n")
+ result.WriteString("--------\n")
+
+ for i, match := range response.Matches {
+ result.WriteString(fmt.Sprintf("\n%d. Text: %s\n", i+1, match.TextSigle))
+ if match.Snippet != "" {
+ result.WriteString(fmt.Sprintf(" Snippet: %s\n", match.Snippet))
+ }
+ if match.PubPlace != "" {
+ result.WriteString(fmt.Sprintf(" Publication: %s\n", match.PubPlace))
+ }
+ if match.MatchID != "" {
+ result.WriteString(fmt.Sprintf(" Match ID: %s\n", match.MatchID))
+ }
+ result.WriteString(fmt.Sprintf(" Position: %d\n", match.Position))
+ }
+ } else {
+ result.WriteString("No matches found.\n")
+ }
+
+ // Additional information
+ if response.Query.CutOff {
+ result.WriteString("\nNote: Results were cut off due to limits.\n")
+ }
+ if response.Query.TimeExceeded {
+ result.WriteString("\nNote: Search time limit was exceeded.\n")
+ }
+
+ return result.String()
+}
diff --git a/tools/search_test.go b/tools/search_test.go
index abd6bef..5966877 100644
--- a/tools/search_test.go
+++ b/tools/search_test.go
@@ -1,9 +1,11 @@
package tools
import (
+ "context"
"testing"
"github.com/korap/korap-mcp/service"
+ "github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
)
@@ -54,10 +56,173 @@
assert.Equal(t, client, tool.client)
}
-// TestSearchTool_Execute will be implemented once the Execute method is fixed
-func TestSearchTool_Execute_WillBeImplemented(t *testing.T) {
- t.Skip("Execute method needs to be fixed first")
+func TestSearchTool_Execute_MissingQuery(t *testing.T) {
+ client := &service.Client{}
+ tool := NewSearchTool(client)
- // This test will be implemented once we fix the linter errors
- // in the Execute method
+ // Create request without query parameter
+ request := mcp.CallToolRequest{
+ Params: mcp.CallToolParams{
+ Arguments: map[string]interface{}{},
+ },
+ }
+
+ _, err := tool.Execute(context.Background(), request)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "query parameter is required")
+}
+
+func TestSearchTool_Execute_NilClient(t *testing.T) {
+ tool := NewSearchTool(nil)
+
+ request := mcp.CallToolRequest{
+ Params: mcp.CallToolParams{
+ Arguments: map[string]interface{}{
+ "query": "test",
+ },
+ },
+ }
+
+ _, err := tool.Execute(context.Background(), request)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "KorAP client not configured")
+}
+
+func TestSearchTool_Execute_ParameterExtraction(t *testing.T) {
+ // This test verifies that parameters are extracted correctly
+ // It should fail with authentication error since we don't have a mock server
+ // but we can verify the parameters were parsed correctly by checking the log messages
+
+ client := &service.Client{}
+ tool := NewSearchTool(client)
+
+ tests := []struct {
+ name string
+ arguments map[string]interface{}
+ expectErr bool
+ }{
+ {
+ name: "minimal_query",
+ arguments: map[string]interface{}{
+ "query": "test",
+ },
+ expectErr: true, // Will fail at authentication
+ },
+ {
+ name: "full_parameters",
+ arguments: map[string]interface{}{
+ "query": "word",
+ "query_language": "cosmas2",
+ "corpus": "test-corpus",
+ "count": 10,
+ },
+ expectErr: true, // Will fail at authentication
+ },
+ {
+ name: "invalid_count_type",
+ arguments: map[string]interface{}{
+ "query": "test",
+ "count": "invalid", // Should use default
+ },
+ expectErr: true, // Will fail at authentication
+ },
+ }
+
+ 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.expectErr {
+ assert.Error(t, err)
+ }
+ })
+ }
+}
+
+func TestSearchTool_formatSearchResults(t *testing.T) {
+ client := &service.Client{}
+ tool := NewSearchTool(client)
+
+ // Test empty response
+ emptyResponse := &service.SearchResponse{
+ Query: service.SearchQuery{
+ Query: "test",
+ QueryLang: "poliqarp",
+ },
+ Meta: service.SearchMeta{
+ TotalResults: 0,
+ Count: 0,
+ StartIndex: 0,
+ },
+ Matches: []service.SearchMatch{},
+ }
+
+ result := tool.formatSearchResults(emptyResponse)
+ assert.Contains(t, result, "KorAP Search Results")
+ assert.Contains(t, result, "Query: test")
+ assert.Contains(t, result, "Query Language: poliqarp")
+ assert.Contains(t, result, "Total Results: 0")
+ assert.Contains(t, result, "No matches found")
+
+ // Test response with matches
+ responseWithMatches := &service.SearchResponse{
+ Query: service.SearchQuery{
+ Query: "word",
+ QueryLang: "poliqarp",
+ Collection: "test-corpus",
+ },
+ Meta: service.SearchMeta{
+ TotalResults: 5,
+ Count: 2,
+ StartIndex: 0,
+ SearchTime: 0.123,
+ },
+ Matches: []service.SearchMatch{
+ {
+ TextSigle: "text1",
+ Snippet: "This is a test snippet",
+ PubPlace: "Berlin",
+ MatchID: "match1",
+ Position: 10,
+ },
+ {
+ TextSigle: "text2",
+ Snippet: "Another test snippet",
+ Position: 25,
+ },
+ },
+ }
+
+ result = tool.formatSearchResults(responseWithMatches)
+ assert.Contains(t, result, "Query: word")
+ assert.Contains(t, result, "Query Language: poliqarp")
+ assert.Contains(t, result, "Corpus: test-corpus")
+ assert.Contains(t, result, "Total Results: 5")
+ assert.Contains(t, result, "Shown: 1-2")
+ assert.Contains(t, result, "Search Time: 0.123 seconds")
+ assert.Contains(t, result, "1. Text: text1")
+ assert.Contains(t, result, "Snippet: This is a test snippet")
+ assert.Contains(t, result, "Publication: Berlin")
+ assert.Contains(t, result, "2. Text: text2")
+ assert.Contains(t, result, "Position: 25")
+
+ // Test response with warnings
+ responseWithWarnings := &service.SearchResponse{
+ Query: service.SearchQuery{
+ Query: "test",
+ CutOff: true,
+ TimeExceeded: true,
+ },
+ Meta: service.SearchMeta{},
+ Matches: []service.SearchMatch{},
+ }
+
+ result = tool.formatSearchResults(responseWithWarnings)
+ assert.Contains(t, result, "Results were cut off due to limits")
+ assert.Contains(t, result, "Search time limit was exceeded")
}