#
tokens: 2967/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# 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,
	}
}

```