# Directory Structure ``` ├── .gitignore ├── config │ └── typesense.go ├── go.mod ├── go.sum ├── handlers │ └── search.go ├── main.go ├── README.md ├── services │ └── typesense.go └── tools └── tools.go ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | tb-mcp-server 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Environment files 19 | .env 20 | 21 | # IDE specific files 22 | .idea 23 | .vscode 24 | *.swp 25 | *.swo 26 | 27 | # OS specific files 28 | .DS_Store 29 | .DS_Store? 30 | ._* 31 | .Spotlight-V100 32 | .Trashes 33 | ehthumbs.db 34 | Thumbs.db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Typesense MCP Server 2 | 3 | A Model Control Protocol (MCP) server for interacting with Typesense, a fast, typo-tolerant search engine. This server provides a standardized interface for performing searches across any Typesense collection. 4 | 5 | ## Features 6 | 7 | - Generic search interface for any Typesense collection 8 | - Support for all Typesense search parameters 9 | - Typo-tolerant search 10 | - Filtering and faceting support 11 | - Pagination 12 | 13 | ## Configuration 14 | 15 | The server can be configured using the following environment variables: 16 | 17 | - `TYPESENSE_HOST`: Typesense server host (default: "localhost") 18 | - `TYPESENSE_PORT`: Typesense server port (default: 8108) 19 | - `TYPESENSE_PROTOCOL`: Protocol to use (http/https) (default: "http") 20 | - `TYPESENSE_API_KEY`: Typesense API key (default: "xyz") 21 | 22 | ## Available Tools 23 | 24 | ### typesense_search 25 | 26 | Search documents in any Typesense collection. 27 | 28 | Parameters: 29 | - `collection` (required): Name of the Typesense collection to search in 30 | - `q` (required): Search query to find documents 31 | - `query_by` (optional): Comma-separated list of fields to search in (default: "*") 32 | - `filter_by` (optional): Filter expressions (e.g., "field:value", "num_field:>100") 33 | - `page` (required): Page number for pagination (1-based) 34 | - `per_page` (required): Number of results per page (default: 10, max: 100) 35 | 36 | ## Development 37 | 38 | ### Prerequisites 39 | 40 | - Go 1.23 or later 41 | - Access to a Typesense server 42 | 43 | ### Building 44 | 45 | ```bash 46 | go build -o typesense-mcp-server 47 | ``` 48 | 49 | ### Running 50 | 51 | ```bash 52 | ./typesense-mcp-server 53 | ``` 54 | ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "typesense-mcp-server/tools" 8 | 9 | "github.com/mark3labs/mcp-go/server" 10 | ) 11 | 12 | func init() { 13 | // Set up logging to file 14 | logFile, err := os.OpenFile("server.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 15 | if err != nil { 16 | log.Printf("ERROR: Failed to open log file: %v", err) 17 | } else { 18 | log.SetOutput(logFile) 19 | } 20 | } 21 | 22 | func main() { 23 | // Create MCP server 24 | s := server.NewMCPServer( 25 | "Typesense MCP Server", 26 | "1.0.0", 27 | // server.WithResourceCapabilities(true, true), 28 | // server.WithToolCapabilities(true), 29 | ) 30 | 31 | // Register tools 32 | tools.RegisterTools(s) 33 | 34 | // Start the stdio server 35 | if err := server.ServeStdio(s); err != nil { 36 | fmt.Printf("Server error: %v\n", err) 37 | } 38 | } 39 | ``` -------------------------------------------------------------------------------- /config/typesense.go: -------------------------------------------------------------------------------- ```go 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | type TypesenseConfig struct { 10 | Host string 11 | Port int 12 | Protocol string 13 | APIKey string 14 | } 15 | 16 | func NewTypesenseConfig() *TypesenseConfig { 17 | return &TypesenseConfig{ 18 | Host: getEnvOrDefault("TYPESENSE_HOST", "localhost"), 19 | Port: getEnvIntOrDefault("TYPESENSE_PORT", 8108), 20 | Protocol: getEnvOrDefault("TYPESENSE_PROTOCOL", "http"), 21 | APIKey: getEnvOrDefault("TYPESENSE_API_KEY", "xyz"), 22 | } 23 | } 24 | 25 | func (c *TypesenseConfig) URL() string { 26 | return fmt.Sprintf("%s://%s:%d", c.Protocol, c.Host, c.Port) 27 | } 28 | 29 | func getEnvOrDefault(key, defaultValue string) string { 30 | if value := os.Getenv(key); value != "" { 31 | return value 32 | } 33 | return defaultValue 34 | } 35 | 36 | func getEnvIntOrDefault(key string, defaultValue int) int { 37 | if value := os.Getenv(key); value != "" { 38 | if intValue, err := strconv.Atoi(value); err == nil { 39 | return intValue 40 | } 41 | } 42 | return defaultValue 43 | } 44 | ``` -------------------------------------------------------------------------------- /services/typesense.go: -------------------------------------------------------------------------------- ```go 1 | package services 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "typesense-mcp-server/config" 8 | 9 | "github.com/sirupsen/logrus" 10 | "github.com/typesense/typesense-go/typesense" 11 | "github.com/typesense/typesense-go/typesense/api" 12 | ) 13 | 14 | // TypesenseService is an interface for the Typesense service 15 | type TypesenseService interface { 16 | Search(ctx context.Context, collection string, request *api.SearchCollectionParams) (*api.SearchResult, error) 17 | GetCollections(ctx context.Context) ([]*api.CollectionResponse, error) 18 | } 19 | 20 | // typesenseService is a service that provides a client for Typesense 21 | type typesenseService struct { 22 | client *typesense.Client 23 | } 24 | 25 | // NewTypesenseService creates a new Typesense service 26 | func NewTypesenseService(config *config.TypesenseConfig) TypesenseService { 27 | client := typesense.NewClient( 28 | typesense.WithServer(config.URL()), 29 | typesense.WithAPIKey(config.APIKey), 30 | ) 31 | 32 | return &typesenseService{ 33 | client: client, 34 | } 35 | } 36 | 37 | // GetCollections gets all collections from Typesense 38 | func (s *typesenseService) GetCollections(ctx context.Context) ([]*api.CollectionResponse, error) { 39 | result, err := s.client.Collections().Retrieve() 40 | if err != nil { 41 | logrus.Errorf("failed to get collections: %v", err) 42 | return nil, fmt.Errorf("failed to get collections: %v", err) 43 | } 44 | return result, nil 45 | } 46 | 47 | // Search searches the collection for the given request 48 | func (s *typesenseService) Search(ctx context.Context, collection string, request *api.SearchCollectionParams) (*api.SearchResult, error) { 49 | result, err := s.client.Collection(collection).Documents().Search(request) 50 | if err != nil { 51 | logrus.Errorf("failed to search documents in collection %s: %v", collection, err) 52 | return nil, fmt.Errorf("failed to search documents in collection %s: %v", collection, err) 53 | } 54 | 55 | return result, nil 56 | } 57 | ``` -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "typesense-mcp-server/config" 5 | "typesense-mcp-server/handlers" 6 | "typesense-mcp-server/services" 7 | 8 | "github.com/mark3labs/mcp-go/mcp" 9 | "github.com/mark3labs/mcp-go/server" 10 | ) 11 | 12 | // RegisterTools registers all tools with the server 13 | func RegisterTools(s *server.MCPServer) { 14 | // Initialize configuration 15 | typesenseConfig := config.NewTypesenseConfig() 16 | 17 | // Initialize services 18 | typesenseService := services.NewTypesenseService(typesenseConfig) 19 | 20 | // Initialize handlers 21 | searchHandler := handlers.NewSearchHandler(typesenseService) 22 | 23 | collectionsTool := mcp.NewTool("typesense_collections", 24 | mcp.WithDescription("Get all collections with their details such as schema etc. from Typesense"), 25 | ) 26 | s.AddTool(collectionsTool, searchHandler.GetTypesenseCollections) 27 | 28 | // Add search tool for Typesense collections 29 | searchTool := mcp.NewTool("typesense_search", 30 | mcp.WithDescription("Search documents in a Typesense collection using powerful search capabilities. "+ 31 | "Supports typo-tolerant search, filtering, faceting, and more."), 32 | mcp.WithString("collection", 33 | mcp.Required(), 34 | mcp.Description("Name of the Typesense collection to search in."), 35 | ), 36 | mcp.WithString("q", 37 | mcp.Required(), 38 | mcp.Description("Search query. Can be keywords, phrases, or natural language queries."), 39 | ), 40 | mcp.WithString("query_by", 41 | mcp.Description("Comma-separated list of fields to search in."), 42 | mcp.DefaultString("*"), 43 | ), 44 | mcp.WithString("filter_by", 45 | mcp.Description("Filter expressions. Example: field:value, num_field:>100"), 46 | ), 47 | mcp.WithNumber("page", 48 | mcp.Description("Page number for pagination (1-based)"), 49 | mcp.DefaultNumber(1), 50 | mcp.Required(), 51 | ), 52 | mcp.WithNumber("per_page", 53 | mcp.Description("Number of results per page (default: 10, max: 100)"), 54 | mcp.DefaultNumber(10), 55 | mcp.Required(), 56 | ), 57 | ) 58 | s.AddTool(searchTool, searchHandler.SearchInTypesenseCollection) 59 | } 60 | ``` -------------------------------------------------------------------------------- /handlers/search.go: -------------------------------------------------------------------------------- ```go 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "typesense-mcp-server/services" 9 | 10 | "github.com/mark3labs/mcp-go/mcp" 11 | "github.com/sirupsen/logrus" 12 | "github.com/typesense/typesense-go/typesense/api" 13 | ) 14 | 15 | // SearchHandler handles Typesense search requests 16 | type SearchHandler struct { 17 | typesenseService services.TypesenseService 18 | } 19 | 20 | // NewSearchHandler creates a new instance of SearchHandler 21 | func NewSearchHandler(typesenseService services.TypesenseService) *SearchHandler { 22 | return &SearchHandler{ 23 | typesenseService: typesenseService, 24 | } 25 | } 26 | 27 | // SearchResponse represents the formatted search response 28 | type SearchResponse struct { 29 | Found int `json:"found"` 30 | Page int `json:"page"` 31 | PerPage int `json:"per_page"` 32 | Documents []map[string]interface{} `json:"documents"` 33 | FacetCount []api.FacetCounts `json:"facet_counts,omitempty"` 34 | } 35 | 36 | // GetTypesenseCollections handles the request to get all Typesense collections 37 | func (h *SearchHandler) GetTypesenseCollections(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 38 | collections, err := h.typesenseService.GetCollections(ctx) 39 | if err != nil { 40 | logrus.Errorf("failed to get collections: %v", err) 41 | return nil, fmt.Errorf("failed to get collections: %v", err) 42 | } 43 | 44 | return mcp.NewToolResultText(fmt.Sprintf("%v", collections)), nil 45 | } 46 | 47 | // Search handles the search request for any Typesense collection 48 | func (h *SearchHandler) SearchInTypesenseCollection(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 49 | // Extract collection name from arguments 50 | collection, ok := request.Params.Arguments["collection"].(string) 51 | if !ok { 52 | return nil, fmt.Errorf("collection name is required") 53 | } 54 | 55 | // Create search parameters 56 | var searchReq api.SearchCollectionParams 57 | jsonData, err := json.Marshal(request.Params.Arguments) 58 | if err != nil { 59 | logrus.Errorf("failed to marshal search arguments: %v", err) 60 | return nil, fmt.Errorf("failed to marshal arguments: %v", err) 61 | } 62 | 63 | if err := json.Unmarshal(jsonData, &searchReq); err != nil { 64 | logrus.Errorf("failed to unmarshal search request: %v", err) 65 | return nil, fmt.Errorf("failed to unmarshal search request: %v", err) 66 | } 67 | 68 | // Perform search using Typesense 69 | response, err := h.typesenseService.Search(ctx, collection, &searchReq) 70 | if err != nil { 71 | logrus.Errorf("failed to search documents in collection %s: %v", collection, err) 72 | return nil, fmt.Errorf("search failed: %v", err) 73 | } 74 | 75 | // Format the response 76 | result := formatTypesenseResults(response) 77 | 78 | // Convert to JSON string with indentation for better readability 79 | resultJSON, err := json.MarshalIndent(result, "", " ") 80 | if err != nil { 81 | logrus.Errorf("failed to marshal search results: %v", err) 82 | return nil, fmt.Errorf("failed to format results: %v", err) 83 | } 84 | 85 | return mcp.NewToolResultText(string(resultJSON)), nil 86 | } 87 | 88 | // formatTypesenseResults formats the Typesense search response 89 | func formatTypesenseResults(response *api.SearchResult) *SearchResponse { 90 | if response == nil || response.Found == nil { 91 | return &SearchResponse{ 92 | Found: 0, 93 | Page: 1, 94 | PerPage: 10, 95 | Documents: make([]map[string]interface{}, 0), 96 | } 97 | } 98 | 99 | // Format documents 100 | documents := make([]map[string]interface{}, 0) 101 | if response.Hits != nil { 102 | for _, hit := range *response.Hits { 103 | if hit.Document != nil { 104 | doc := *hit.Document 105 | // Add search score to document 106 | if hit.TextMatch != nil { 107 | doc["_text_match"] = *hit.TextMatch 108 | } 109 | if hit.Highlights != nil { 110 | doc["_highlights"] = hit.Highlights 111 | } 112 | documents = append(documents, doc) 113 | } 114 | } 115 | } 116 | 117 | return &SearchResponse{ 118 | Found: *response.Found, 119 | Page: 1, // Typesense uses offset-based pagination 120 | PerPage: len(documents), 121 | Documents: documents, 122 | FacetCount: *response.FacetCounts, 123 | } 124 | } 125 | ```