# 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 |
```