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