#
tokens: 4155/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```