Skip to main content
Glama

Meilisearch MCP Server

Official
by meilisearch
server.py34.3 kB
import asyncio import json import os from typing import Optional, Dict, Any, List, Union from datetime import datetime import mcp.types as types from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio from .client import MeilisearchClient from .chat import ChatManager from .logging import MCPLogger logger = MCPLogger() def json_serializer(obj: Any) -> str: """Custom JSON serializer for objects not serializable by default json code""" if isinstance(obj, datetime): return obj.isoformat() # Handle Meilisearch model objects by using their __dict__ if available if hasattr(obj, "__dict__"): return obj.__dict__ return str(obj) def create_server( url: str = "http://localhost:7700", api_key: Optional[str] = None ) -> "MeilisearchMCPServer": """Create and return a configured MeilisearchMCPServer instance""" return MeilisearchMCPServer(url, api_key) class MeilisearchMCPServer: def __init__( self, url: str = "http://localhost:7700", api_key: Optional[str] = None, log_dir: Optional[str] = None, ): """Initialize MCP server for Meilisearch""" # Set up logging directory if not log_dir: log_dir = os.path.expanduser("~/.meilisearch-mcp/logs") self.logger = MCPLogger("meilisearch-mcp", log_dir) self.url = url self.api_key = api_key self.meili_client = MeilisearchClient(url, api_key) self.chat_manager = ChatManager(self.meili_client.client) self.server = Server("meilisearch") self._setup_handlers() def update_connection( self, url: Optional[str] = None, api_key: Optional[str] = None ): """Update connection settings and reinitialize client if needed""" if url: self.url = url if api_key: self.api_key = api_key self.meili_client = MeilisearchClient(self.url, self.api_key) self.chat_manager = ChatManager(self.meili_client.client) self.logger.info("Updated Meilisearch connection settings", url=self.url) def _setup_handlers(self): """Setup MCP request handlers""" @self.server.list_tools() async def handle_list_tools() -> list[types.Tool]: """List available tools""" return [ types.Tool( name="get-connection-settings", description="Get current Meilisearch connection settings", inputSchema={ "type": "object", "properties": {}, "additionalProperties": False, }, ), types.Tool( name="update-connection-settings", description="Update Meilisearch connection settings", inputSchema={ "type": "object", "properties": { "url": {"type": "string"}, "api_key": {"type": "string"}, }, "additionalProperties": False, }, ), types.Tool( name="health-check", description="Check Meilisearch server health", inputSchema={ "type": "object", "properties": {}, "additionalProperties": False, }, ), types.Tool( name="get-version", description="Get Meilisearch version information", inputSchema={ "type": "object", "properties": {}, "additionalProperties": False, }, ), types.Tool( name="get-stats", description="Get database statistics", inputSchema={ "type": "object", "properties": {}, "additionalProperties": False, }, ), types.Tool( name="create-index", description="Create a new Meilisearch index", inputSchema={ "type": "object", "properties": { "uid": {"type": "string"}, "primaryKey": {"type": "string"}, }, "required": ["uid"], "additionalProperties": False, }, ), types.Tool( name="list-indexes", description="List all Meilisearch indexes", inputSchema={ "type": "object", "properties": {}, "additionalProperties": False, }, ), types.Tool( name="delete-index", description="Delete a Meilisearch index", inputSchema={ "type": "object", "properties": {"uid": {"type": "string"}}, "required": ["uid"], "additionalProperties": False, }, ), types.Tool( name="get-documents", description="Get documents from an index", inputSchema={ "type": "object", "properties": { "indexUid": {"type": "string"}, "offset": {"type": "integer"}, "limit": {"type": "integer"}, }, "required": ["indexUid"], "additionalProperties": False, }, ), types.Tool( name="add-documents", description="Add documents to an index", inputSchema={ "type": "object", "properties": { "indexUid": {"type": "string"}, "documents": { "type": "array", "items": { "type": "object", "additionalProperties": True, }, }, "primaryKey": {"type": "string"}, }, "required": ["indexUid", "documents"], "additionalProperties": False, }, ), types.Tool( name="get-settings", description="Get current settings for an index", inputSchema={ "type": "object", "properties": {"indexUid": {"type": "string"}}, "required": ["indexUid"], "additionalProperties": False, }, ), types.Tool( name="update-settings", description="Update settings for an index", inputSchema={ "type": "object", "properties": { "indexUid": {"type": "string"}, "settings": { "type": "object", "additionalProperties": True, }, }, "required": ["indexUid", "settings"], "additionalProperties": False, }, ), types.Tool( name="search", description="Search through Meilisearch indices. If indexUid is not provided, it will search across all indices.", inputSchema={ "type": "object", "properties": { "query": {"type": "string"}, "indexUid": {"type": "string"}, "limit": {"type": "integer"}, "offset": {"type": "integer"}, "filter": {"type": "string"}, "sort": { "type": "array", "items": {"type": "string"}, }, }, "required": ["query"], "additionalProperties": False, }, ), types.Tool( name="get-task", description="Get information about a specific task", inputSchema={ "type": "object", "properties": {"taskUid": {"type": "integer"}}, "required": ["taskUid"], "additionalProperties": False, }, ), types.Tool( name="get-tasks", description="Get list of tasks with optional filters", inputSchema={ "type": "object", "properties": { "limit": {"type": "integer"}, "from": {"type": "integer"}, "reverse": {"type": "boolean"}, "batchUids": { "type": "array", "items": {"type": "string"}, }, "uids": { "type": "array", "items": {"type": "integer"}, }, "canceledBy": { "type": "array", "items": {"type": "string"}, }, "types": { "type": "array", "items": {"type": "string"}, }, "statuses": { "type": "array", "items": {"type": "string"}, }, "indexUids": { "type": "array", "items": {"type": "string"}, }, "afterEnqueuedAt": {"type": "string"}, "beforeEnqueuedAt": {"type": "string"}, "afterStartedAt": {"type": "string"}, "beforeStartedAt": {"type": "string"}, "afterFinishedAt": {"type": "string"}, "beforeFinishedAt": {"type": "string"}, }, "additionalProperties": False, }, ), types.Tool( name="cancel-tasks", description="Cancel tasks based on filters", inputSchema={ "type": "object", "properties": { "uids": {"type": "string"}, "indexUids": {"type": "string"}, "types": {"type": "string"}, "statuses": {"type": "string"}, }, "additionalProperties": False, }, ), types.Tool( name="get-keys", description="Get list of API keys", inputSchema={ "type": "object", "properties": { "offset": {"type": "integer"}, "limit": {"type": "integer"}, }, "additionalProperties": False, }, ), types.Tool( name="create-key", description="Create a new API key", inputSchema={ "type": "object", "properties": { "description": {"type": "string"}, "actions": {"type": "array", "items": {"type": "string"}}, "indexes": {"type": "array", "items": {"type": "string"}}, "expiresAt": {"type": "string"}, }, "required": ["actions", "indexes"], "additionalProperties": False, }, ), types.Tool( name="delete-key", description="Delete an API key", inputSchema={ "type": "object", "properties": {"key": {"type": "string"}}, "required": ["key"], "additionalProperties": False, }, ), types.Tool( name="get-health-status", description="Get comprehensive health status of Meilisearch", inputSchema={ "type": "object", "properties": {}, "additionalProperties": False, }, ), types.Tool( name="get-index-metrics", description="Get detailed metrics for an index", inputSchema={ "type": "object", "properties": {"indexUid": {"type": "string"}}, "required": ["indexUid"], "additionalProperties": False, }, ), types.Tool( name="get-system-info", description="Get system-level information", inputSchema={ "type": "object", "properties": {}, "additionalProperties": False, }, ), types.Tool( name="create-chat-completion", description="Create a conversational chat completion using Meilisearch's chat feature", inputSchema={ "type": "object", "properties": { "workspace_uid": { "type": "string", "description": "Unique identifier of the chat workspace", }, "messages": { "type": "array", "items": { "type": "object", "properties": { "role": { "type": "string", "enum": ["user", "assistant", "system"], }, "content": {"type": "string"}, }, "required": ["role", "content"], }, "description": "List of message objects comprising the chat history", }, "model": { "type": "string", "default": "gpt-3.5-turbo", "description": "The model to use for completion", }, "stream": { "type": "boolean", "default": True, "description": "Whether to stream the response (currently must be true)", }, }, "required": ["workspace_uid", "messages"], "additionalProperties": False, }, ), types.Tool( name="get-chat-workspaces", description="Get list of available chat workspaces", inputSchema={ "type": "object", "properties": { "offset": { "type": "integer", "description": "Number of workspaces to skip", }, "limit": { "type": "integer", "description": "Maximum number of workspaces to return", }, }, "additionalProperties": False, }, ), types.Tool( name="get-chat-workspace-settings", description="Get settings for a specific chat workspace", inputSchema={ "type": "object", "properties": { "workspace_uid": { "type": "string", "description": "Unique identifier of the chat workspace", }, }, "required": ["workspace_uid"], "additionalProperties": False, }, ), types.Tool( name="update-chat-workspace-settings", description="Update settings for a specific chat workspace", inputSchema={ "type": "object", "properties": { "workspace_uid": { "type": "string", "description": "Unique identifier of the chat workspace", }, "settings": { "type": "object", "description": "Settings to update for the workspace", "additionalProperties": True, }, }, "required": ["workspace_uid", "settings"], "additionalProperties": False, }, ), ] @self.server.call_tool() async def handle_call_tool( name: str, arguments: Optional[Dict[str, Any]] = None ) -> list[types.TextContent]: """Handle tool execution""" try: if name == "get-connection-settings": return [ types.TextContent( type="text", text=f"Current connection settings:\nURL: {self.url}\nAPI Key: {'*' * 8 if self.api_key else 'Not set'}", ) ] elif name == "update-connection-settings": self.update_connection( arguments.get("url"), arguments.get("api_key") ) return [ types.TextContent( type="text", text=f"Successfully updated connection settings to URL: {self.url}", ) ] elif name == "create-index": result = self.meili_client.indexes.create_index( arguments["uid"], arguments.get("primaryKey") ) return [ types.TextContent(type="text", text=f"Created index: {result}") ] elif name == "list-indexes": indexes = self.meili_client.get_indexes() formatted_json = json.dumps( indexes, indent=2, default=json_serializer ) return [ types.TextContent( type="text", text=f"Indexes:\n{formatted_json}" ) ] elif name == "delete-index": result = self.meili_client.indexes.delete_index(arguments["uid"]) return [ types.TextContent( type="text", text=f"Successfully deleted index: {arguments['uid']}", ) ] elif name == "get-documents": # Use default values to fix None parameter issues (related to issue #17) offset = arguments.get("offset", 0) limit = arguments.get("limit", 20) documents = self.meili_client.documents.get_documents( arguments["indexUid"], offset, limit, ) # Convert DocumentsResults object to proper JSON format (fixes issue #16) formatted_json = json.dumps( documents, indent=2, default=json_serializer ) return [ types.TextContent( type="text", text=f"Documents:\n{formatted_json}" ) ] elif name == "add-documents": result = self.meili_client.documents.add_documents( arguments["indexUid"], arguments["documents"], arguments.get("primaryKey"), ) return [ types.TextContent( type="text", text=f"Added documents: {result}" ) ] elif name == "health-check": is_healthy = self.meili_client.health_check() return [ types.TextContent( type="text", text=f"Meilisearch is {is_healthy and 'available' or 'unavailable'}", ) ] elif name == "get-version": version = self.meili_client.get_version() return [ types.TextContent(type="text", text=f"Version info: {version}") ] elif name == "get-stats": stats = self.meili_client.get_stats() return [ types.TextContent(type="text", text=f"Database stats: {stats}") ] elif name == "get-settings": settings = self.meili_client.settings.get_settings( arguments["indexUid"] ) return [ types.TextContent( type="text", text=f"Current settings: {settings}" ) ] elif name == "update-settings": result = self.meili_client.settings.update_settings( arguments["indexUid"], arguments["settings"] ) return [ types.TextContent( type="text", text=f"Settings updated: {result}" ) ] elif name == "search": search_results = self.meili_client.search( query=arguments["query"], index_uid=arguments.get("indexUid"), limit=arguments.get("limit"), offset=arguments.get("offset"), filter=arguments.get("filter"), sort=arguments.get("sort"), ) # Format the results for better readability formatted_results = json.dumps( search_results, indent=2, default=json_serializer ) return [ types.TextContent( type="text", text=f"Search results for '{arguments['query']}':\n{formatted_results}", ) ] elif name == "get-task": task = self.meili_client.tasks.get_task(arguments["taskUid"]) return [ types.TextContent(type="text", text=f"Task information: {task}") ] elif name == "get-tasks": # Filter out any invalid parameters valid_params = { "limit", "from", "reverse", "batchUids", "uids", "canceledBy", "types", "statuses", "indexUids", "afterEnqueuedAt", "beforeEnqueuedAt", "afterStartedAt", "beforeStartedAt", "afterFinishedAt", "beforeFinishedAt", } filtered_args = ( {k: v for k, v in arguments.items() if k in valid_params} if arguments else {} ) tasks = self.meili_client.tasks.get_tasks(filtered_args) return [types.TextContent(type="text", text=f"Tasks: {tasks}")] elif name == "cancel-tasks": result = self.meili_client.tasks.cancel_tasks(arguments) return [ types.TextContent( type="text", text=f"Tasks cancelled: {result}" ) ] elif name == "get-keys": keys = self.meili_client.keys.get_keys(arguments) return [types.TextContent(type="text", text=f"API keys: {keys}")] elif name == "create-key": key = self.meili_client.keys.create_key( { "description": arguments.get("description"), "actions": arguments["actions"], "indexes": arguments["indexes"], "expiresAt": arguments.get("expiresAt"), } ) return [ types.TextContent(type="text", text=f"Created API key: {key}") ] elif name == "delete-key": self.meili_client.keys.delete_key(arguments["key"]) return [ types.TextContent( type="text", text=f"Successfully deleted API key: {arguments['key']}", ) ] elif name == "get-health-status": status = self.meili_client.monitoring.get_health_status() self.logger.info("Health status checked", status=status.__dict__) return [ types.TextContent( type="text", text=f"Health status: {json.dumps(status.__dict__, default=json_serializer)}", ) ] elif name == "get-index-metrics": metrics = self.meili_client.monitoring.get_index_metrics( arguments["indexUid"] ) self.logger.info( "Index metrics retrieved", index=arguments["indexUid"], metrics=metrics.__dict__, ) return [ types.TextContent( type="text", text=f"Index metrics: {json.dumps(metrics.__dict__, default=json_serializer)}", ) ] elif name == "get-system-info": info = self.meili_client.monitoring.get_system_information() self.logger.info("System information retrieved", info=info) return [ types.TextContent( type="text", text=f"System information: {info}" ) ] elif name == "create-chat-completion": response = await self.chat_manager.create_chat_completion( workspace_uid=arguments["workspace_uid"], messages=arguments["messages"], model=arguments.get("model", "gpt-3.5-turbo"), stream=arguments.get("stream", True), ) return [ types.TextContent( type="text", text=f"Chat completion response:\n{response}", ) ] elif name == "get-chat-workspaces": workspaces = await self.chat_manager.get_chat_workspaces( offset=arguments.get("offset") if arguments else None, limit=arguments.get("limit") if arguments else None, ) formatted_json = json.dumps( workspaces, indent=2, default=json_serializer ) return [ types.TextContent( type="text", text=f"Chat workspaces:\n{formatted_json}", ) ] elif name == "get-chat-workspace-settings": settings = await self.chat_manager.get_chat_workspace_settings( workspace_uid=arguments["workspace_uid"] ) formatted_json = json.dumps( settings, indent=2, default=json_serializer ) return [ types.TextContent( type="text", text=f"Workspace settings for '{arguments['workspace_uid']}':\n{formatted_json}", ) ] elif name == "update-chat-workspace-settings": updated_settings = ( await self.chat_manager.update_chat_workspace_settings( workspace_uid=arguments["workspace_uid"], settings=arguments["settings"], ) ) formatted_json = json.dumps( updated_settings, indent=2, default=json_serializer ) return [ types.TextContent( type="text", text=f"Updated workspace settings for '{arguments['workspace_uid']}':\n{formatted_json}", ) ] raise ValueError(f"Unknown tool: {name}") except Exception as e: self.logger.error( f"Error executing tool {name}", error=str(e), tool=name, arguments=arguments, ) return [types.TextContent(type="text", text=f"Error: {str(e)}")] async def run(self): """Run the MCP server""" logger.info("Starting Meilisearch MCP server...") async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await self.server.run( read_stream, write_stream, InitializationOptions( server_name="meilisearch", server_version="0.1.0", capabilities=self.server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) def cleanup(self): """Clean shutdown""" self.logger.info("Shutting down MCP server") self.logger.shutdown() def main(): """Main entry point""" url = os.getenv("MEILI_HTTP_ADDR", "http://localhost:7700") api_key = os.getenv("MEILI_MASTER_KEY") server = create_server(url, api_key) asyncio.run(server.run()) if __name__ == "__main__": main()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/meilisearch/meilisearch-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server