# 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: -------------------------------------------------------------------------------- ``` # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib tb-mcp-server # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) vendor/ # Environment files .env # IDE specific files .idea .vscode *.swp *.swo # OS specific files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Typesense MCP Server 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. ## Features - Generic search interface for any Typesense collection - Support for all Typesense search parameters - Typo-tolerant search - Filtering and faceting support - Pagination ## Configuration The server can be configured using the following environment variables: - `TYPESENSE_HOST`: Typesense server host (default: "localhost") - `TYPESENSE_PORT`: Typesense server port (default: 8108) - `TYPESENSE_PROTOCOL`: Protocol to use (http/https) (default: "http") - `TYPESENSE_API_KEY`: Typesense API key (default: "xyz") ## Available Tools ### typesense_search Search documents in any Typesense collection. Parameters: - `collection` (required): Name of the Typesense collection to search in - `q` (required): Search query to find documents - `query_by` (optional): Comma-separated list of fields to search in (default: "*") - `filter_by` (optional): Filter expressions (e.g., "field:value", "num_field:>100") - `page` (required): Page number for pagination (1-based) - `per_page` (required): Number of results per page (default: 10, max: 100) ## Development ### Prerequisites - Go 1.23 or later - Access to a Typesense server ### Building ```bash go build -o typesense-mcp-server ``` ### Running ```bash ./typesense-mcp-server ``` ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- ```go package main import ( "fmt" "log" "os" "typesense-mcp-server/tools" "github.com/mark3labs/mcp-go/server" ) func init() { // Set up logging to file logFile, err := os.OpenFile("server.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { log.Printf("ERROR: Failed to open log file: %v", err) } else { log.SetOutput(logFile) } } func main() { // Create MCP server s := server.NewMCPServer( "Typesense MCP Server", "1.0.0", // server.WithResourceCapabilities(true, true), // server.WithToolCapabilities(true), ) // Register tools tools.RegisterTools(s) // Start the stdio server if err := server.ServeStdio(s); err != nil { fmt.Printf("Server error: %v\n", err) } } ``` -------------------------------------------------------------------------------- /config/typesense.go: -------------------------------------------------------------------------------- ```go package config import ( "fmt" "os" "strconv" ) type TypesenseConfig struct { Host string Port int Protocol string APIKey string } func NewTypesenseConfig() *TypesenseConfig { return &TypesenseConfig{ Host: getEnvOrDefault("TYPESENSE_HOST", "localhost"), Port: getEnvIntOrDefault("TYPESENSE_PORT", 8108), Protocol: getEnvOrDefault("TYPESENSE_PROTOCOL", "http"), APIKey: getEnvOrDefault("TYPESENSE_API_KEY", "xyz"), } } func (c *TypesenseConfig) URL() string { return fmt.Sprintf("%s://%s:%d", c.Protocol, c.Host, c.Port) } func getEnvOrDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func getEnvIntOrDefault(key string, defaultValue int) int { if value := os.Getenv(key); value != "" { if intValue, err := strconv.Atoi(value); err == nil { return intValue } } return defaultValue } ``` -------------------------------------------------------------------------------- /services/typesense.go: -------------------------------------------------------------------------------- ```go package services import ( "context" "fmt" "typesense-mcp-server/config" "github.com/sirupsen/logrus" "github.com/typesense/typesense-go/typesense" "github.com/typesense/typesense-go/typesense/api" ) // TypesenseService is an interface for the Typesense service type TypesenseService interface { Search(ctx context.Context, collection string, request *api.SearchCollectionParams) (*api.SearchResult, error) GetCollections(ctx context.Context) ([]*api.CollectionResponse, error) } // typesenseService is a service that provides a client for Typesense type typesenseService struct { client *typesense.Client } // NewTypesenseService creates a new Typesense service func NewTypesenseService(config *config.TypesenseConfig) TypesenseService { client := typesense.NewClient( typesense.WithServer(config.URL()), typesense.WithAPIKey(config.APIKey), ) return &typesenseService{ client: client, } } // GetCollections gets all collections from Typesense func (s *typesenseService) GetCollections(ctx context.Context) ([]*api.CollectionResponse, error) { result, err := s.client.Collections().Retrieve() if err != nil { logrus.Errorf("failed to get collections: %v", err) return nil, fmt.Errorf("failed to get collections: %v", err) } return result, nil } // Search searches the collection for the given request func (s *typesenseService) Search(ctx context.Context, collection string, request *api.SearchCollectionParams) (*api.SearchResult, error) { result, err := s.client.Collection(collection).Documents().Search(request) if err != nil { logrus.Errorf("failed to search documents in collection %s: %v", collection, err) return nil, fmt.Errorf("failed to search documents in collection %s: %v", collection, err) } return result, nil } ``` -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- ```go package tools import ( "typesense-mcp-server/config" "typesense-mcp-server/handlers" "typesense-mcp-server/services" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) // RegisterTools registers all tools with the server func RegisterTools(s *server.MCPServer) { // Initialize configuration typesenseConfig := config.NewTypesenseConfig() // Initialize services typesenseService := services.NewTypesenseService(typesenseConfig) // Initialize handlers searchHandler := handlers.NewSearchHandler(typesenseService) collectionsTool := mcp.NewTool("typesense_collections", mcp.WithDescription("Get all collections with their details such as schema etc. from Typesense"), ) s.AddTool(collectionsTool, searchHandler.GetTypesenseCollections) // Add search tool for Typesense collections searchTool := mcp.NewTool("typesense_search", mcp.WithDescription("Search documents in a Typesense collection using powerful search capabilities. "+ "Supports typo-tolerant search, filtering, faceting, and more."), mcp.WithString("collection", mcp.Required(), mcp.Description("Name of the Typesense collection to search in."), ), mcp.WithString("q", mcp.Required(), mcp.Description("Search query. Can be keywords, phrases, or natural language queries."), ), mcp.WithString("query_by", mcp.Description("Comma-separated list of fields to search in."), mcp.DefaultString("*"), ), mcp.WithString("filter_by", mcp.Description("Filter expressions. Example: field:value, num_field:>100"), ), mcp.WithNumber("page", mcp.Description("Page number for pagination (1-based)"), mcp.DefaultNumber(1), mcp.Required(), ), mcp.WithNumber("per_page", mcp.Description("Number of results per page (default: 10, max: 100)"), mcp.DefaultNumber(10), mcp.Required(), ), ) s.AddTool(searchTool, searchHandler.SearchInTypesenseCollection) } ``` -------------------------------------------------------------------------------- /handlers/search.go: -------------------------------------------------------------------------------- ```go package handlers import ( "context" "encoding/json" "fmt" "typesense-mcp-server/services" "github.com/mark3labs/mcp-go/mcp" "github.com/sirupsen/logrus" "github.com/typesense/typesense-go/typesense/api" ) // SearchHandler handles Typesense search requests type SearchHandler struct { typesenseService services.TypesenseService } // NewSearchHandler creates a new instance of SearchHandler func NewSearchHandler(typesenseService services.TypesenseService) *SearchHandler { return &SearchHandler{ typesenseService: typesenseService, } } // SearchResponse represents the formatted search response type SearchResponse struct { Found int `json:"found"` Page int `json:"page"` PerPage int `json:"per_page"` Documents []map[string]interface{} `json:"documents"` FacetCount []api.FacetCounts `json:"facet_counts,omitempty"` } // GetTypesenseCollections handles the request to get all Typesense collections func (h *SearchHandler) GetTypesenseCollections(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { collections, err := h.typesenseService.GetCollections(ctx) if err != nil { logrus.Errorf("failed to get collections: %v", err) return nil, fmt.Errorf("failed to get collections: %v", err) } return mcp.NewToolResultText(fmt.Sprintf("%v", collections)), nil } // Search handles the search request for any Typesense collection func (h *SearchHandler) SearchInTypesenseCollection(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Extract collection name from arguments collection, ok := request.Params.Arguments["collection"].(string) if !ok { return nil, fmt.Errorf("collection name is required") } // Create search parameters var searchReq api.SearchCollectionParams jsonData, err := json.Marshal(request.Params.Arguments) if err != nil { logrus.Errorf("failed to marshal search arguments: %v", err) return nil, fmt.Errorf("failed to marshal arguments: %v", err) } if err := json.Unmarshal(jsonData, &searchReq); err != nil { logrus.Errorf("failed to unmarshal search request: %v", err) return nil, fmt.Errorf("failed to unmarshal search request: %v", err) } // Perform search using Typesense response, err := h.typesenseService.Search(ctx, collection, &searchReq) if err != nil { logrus.Errorf("failed to search documents in collection %s: %v", collection, err) return nil, fmt.Errorf("search failed: %v", err) } // Format the response result := formatTypesenseResults(response) // Convert to JSON string with indentation for better readability resultJSON, err := json.MarshalIndent(result, "", " ") if err != nil { logrus.Errorf("failed to marshal search results: %v", err) return nil, fmt.Errorf("failed to format results: %v", err) } return mcp.NewToolResultText(string(resultJSON)), nil } // formatTypesenseResults formats the Typesense search response func formatTypesenseResults(response *api.SearchResult) *SearchResponse { if response == nil || response.Found == nil { return &SearchResponse{ Found: 0, Page: 1, PerPage: 10, Documents: make([]map[string]interface{}, 0), } } // Format documents documents := make([]map[string]interface{}, 0) if response.Hits != nil { for _, hit := range *response.Hits { if hit.Document != nil { doc := *hit.Document // Add search score to document if hit.TextMatch != nil { doc["_text_match"] = *hit.TextMatch } if hit.Highlights != nil { doc["_highlights"] = hit.Highlights } documents = append(documents, doc) } } } return &SearchResponse{ Found: *response.Found, Page: 1, // Typesense uses offset-based pagination PerPage: len(documents), Documents: documents, FacetCount: *response.FacetCounts, } } ```